From 2c03f8cfe18aeb7fc5c3aadb57a15f2772fc5360 Mon Sep 17 00:00:00 2001 From: Olivier Philippon Date: Fri, 29 Apr 2022 10:28:30 +0100 Subject: [PATCH] [css] Add a "did you mean" suggestion when an unknown CSS property is spotted So using "bckgroundu: red" in a CSS file will report _"unknown declaration 'bckgroundu'; did you mean 'background'?"_ --- src/textual/css/_styles_builder.py | 45 +++++++++++++++++++++--------- tests/css/test_stylesheet.py | 37 ++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 13 deletions(-) diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index 54b1a2d46..9d764a4d3 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -1,5 +1,7 @@ from __future__ import annotations +from difflib import get_close_matches +from functools import lru_cache from typing import cast, Iterable, NoReturn import rich.repr @@ -84,24 +86,35 @@ class StylesBuilder: process_method = getattr(self, f"process_{rule_name}", None) if process_method is None: + error_message = f"unknown declaration {declaration.name!r}" + did_you_mean_rule_name = self._did_you_mean_for_rule_name(declaration.name) + if did_you_mean_rule_name: + error_message += f"; did you mean {did_you_mean_rule_name!r}?" self.error( declaration.name, declaration.token, - f"unknown declaration {declaration.name!r}", + error_message, ) - else: - tokens = declaration.tokens + return - important = tokens[-1].name == "important" - if important: - tokens = tokens[:-1] - self.styles.important.add(rule_name) - try: - process_method(declaration.name, tokens) - except DeclarationError: - raise - except Exception as error: - self.error(declaration.name, declaration.token, str(error)) + tokens = declaration.tokens + + important = tokens[-1].name == "important" + if important: + tokens = tokens[:-1] + self.styles.important.add(rule_name) + try: + process_method(declaration.name, tokens) + except DeclarationError: + raise + except Exception as error: + self.error(declaration.name, declaration.token, str(error)) + + @lru_cache(maxsize=None) + def _processable_rule_names(self) -> frozenset[str]: + return frozenset( + [attr[8:] for attr in dir(self) if attr.startswith("process_")] + ) def _process_enum_multiple( self, name: str, tokens: list[Token], valid_values: set[str], count: int @@ -717,3 +730,9 @@ class StylesBuilder: process_content_align = process_align process_content_align_horizontal = process_align_horizontal process_content_align_vertical = process_align_vertical + + def _did_you_mean_for_rule_name(self, rule_name: str) -> str | None: + possible_matches = get_close_matches( + rule_name, self._processable_rule_names(), n=1 + ) + return None if not possible_matches else possible_matches[0] diff --git a/tests/css/test_stylesheet.py b/tests/css/test_stylesheet.py index 4544d1e1d..5d4978405 100644 --- a/tests/css/test_stylesheet.py +++ b/tests/css/test_stylesheet.py @@ -53,3 +53,40 @@ def test_color_property_parsing(css_value, expectation, expected_color): if expected_color: css_rule = stylesheet.rules[0] assert css_rule.styles.background == expected_color + + +@pytest.mark.parametrize( + "css_property_name,expected_property_name_suggestion", + [ + ["backgroundu", "background"], + ["bckgroundu", "background"], + ["colr", "color"], + ["colour", "color"], + ["wdth", "width"], + ["wth", "width"], + ["wh", None], + ["xkcd", None], + ], +) +def test_did_you_mean_for_css_property_names( + css_property_name, expected_property_name_suggestion +): + stylesheet = Stylesheet() + css = """ + * { + border: blue; + ${PROPERTY}: red; + } + """.replace( + "${PROPERTY}", css_property_name + ) + + with pytest.raises(StylesheetParseError) as err: + stylesheet.parse(css) + + error_token, error_message = err.value.errors.stylesheet.rules[0].errors[0] + if expected_property_name_suggestion is None: + assert "did you mean" not in error_message + else: + expected_did_you_mean_error_message = f"unknown declaration '{css_property_name}'; did you mean '{expected_property_name_suggestion}'?" + assert expected_did_you_mean_error_message == error_message