From 35474de863e74ee81e3934d54a6dcbe813052d29 Mon Sep 17 00:00:00 2001 From: Christopher Pickering Date: Mon, 4 Oct 2021 15:26:22 +0200 Subject: [PATCH] added option for custom rules file --- docs/djlint/changelog.rst | 1 + docs/djlint/rules.rst | 16 +++++ src/djlint/lint.py | 14 +---- src/djlint/settings.py | 72 +++++++++++++++++++++++ tests/custom_rules/.djlint_rules.yaml | 6 ++ tests/custom_rules/html.html | 1 + tests/custom_rules/pyproject.toml | 1 + tests/custom_rules_bad/.djlint_rules.yaml | 24 ++++++++ tests/custom_rules_bad/html.html | 1 + tests/custom_rules_bad/pyproject.toml | 1 + tests/test_linter.py | 18 +++++- 11 files changed, 141 insertions(+), 14 deletions(-) create mode 100644 tests/custom_rules/.djlint_rules.yaml create mode 100644 tests/custom_rules/html.html create mode 100644 tests/custom_rules/pyproject.toml create mode 100644 tests/custom_rules_bad/.djlint_rules.yaml create mode 100644 tests/custom_rules_bad/html.html create mode 100644 tests/custom_rules_bad/pyproject.toml diff --git a/docs/djlint/changelog.rst b/docs/djlint/changelog.rst index 4ca915b..5953f13 100644 --- a/docs/djlint/changelog.rst +++ b/docs/djlint/changelog.rst @@ -4,6 +4,7 @@ Changelog Next Release ------------ - Split ``alt`` requirement from H006 to H013 +- Added optional custom rules file 0.5.1 ----- diff --git a/docs/djlint/rules.rst b/docs/djlint/rules.rst index c1afd04..698f7f7 100644 --- a/docs/djlint/rules.rst +++ b/docs/djlint/rules.rst @@ -73,3 +73,19 @@ The first letter of a code follows the pattern: - J: applies specifically to Jinja - N: applies specifically to Nunjucks - M: applies specifically to Handlebars + +Custom Rules +~~~~~~~~~~~~ + +Create a file ``.djlint_rules.yaml`` alongside your ``pyproject.toml``. Rules can be added to this files and djLint will pick them up. + +A good rule follows this pattern: + +.. code:: yaml + + - rule: + name: T001 + message: Find Trichotillomania + flags: re.DOTALL|re.I + patterns: + - Trichotillomania diff --git a/src/djlint/lint.py b/src/djlint/lint.py index c1f36c5..6c17fd3 100644 --- a/src/djlint/lint.py +++ b/src/djlint/lint.py @@ -3,14 +3,9 @@ from pathlib import Path from typing import Dict, List import regex as re -import yaml from .settings import Config -rules = yaml.load( - (Path(__file__).parent / "rules.yaml").read_text(encoding="utf8"), - Loader=yaml.SafeLoader, -) flags = { "re.A": re.A, "re.ASCII": re.ASCII, @@ -69,14 +64,7 @@ def lint_file(config: Config, this_file: Path) -> Dict: for m in re.finditer(r"(?:.*\n)|(?:[^\n]+$)", html) ] - for rule in list( - filter( - lambda x: x["rule"]["name"] not in config.ignore.split(",") - and x["rule"]["name"][0] not in config.profile_code - and config.profile not in x["rule"].get("exclude", []), - rules, - ) - ): + for rule in config.linter_rules: rule = rule["rule"] for pattern in rule["patterns"]: diff --git a/src/djlint/settings.py b/src/djlint/settings.py index 7ef1931..52ed4cf 100644 --- a/src/djlint/settings.py +++ b/src/djlint/settings.py @@ -11,6 +11,9 @@ from pathlib import Path from typing import Dict, List, Optional, Union import tomlkit +import yaml +from click import echo +from colorama import Fore logger = logging.getLogger(__name__) @@ -28,6 +31,19 @@ def find_pyproject(src: Path) -> Optional[Path]: return None +def find_djlint_rules(src: Path) -> Optional[Path]: + """Search upstream for a pyprojec.toml file.""" + + for directory in [src, *src.resolve().parents]: + + candidate = directory / ".djlint_rules.yaml" + + if candidate.is_file(): + return candidate + + return None + + def load_pyproject_settings(src: Path) -> Dict: """Load djlint config from pyproject.toml.""" @@ -44,6 +60,44 @@ def load_pyproject_settings(src: Path) -> Dict: return djlint_content +def validate_rules(rules: List) -> List: + clean_rules = [] + + for rule in rules: + # check for name + warning = 0 + name = rule["rule"].get("name", "undefined") + if "name" not in rule["rule"]: + warning += 1 + echo(Fore.RED + f"Warning: A rule is missing a name! 😢") + if "patterns" not in rule["rule"]: + warning += 1 + echo(Fore.RED + f"Warning: Rule {name} is missing a pattern! 😢") + if "message" not in rule["rule"]: + warning += 1 + echo(Fore.RED + f"Warning: Rule {name} is missing a message! 😢") + + if warning == 0: + clean_rules.append(rule) + + return clean_rules + + +def load_custom_rules(src: Path) -> List: + """Load djlint config from pyproject.toml.""" + + djlint_content: List = [] + djlint_rules_file = find_djlint_rules(src) + + if djlint_rules_file: + djlint_content = yaml.load( + Path(djlint_rules_file).read_text(encoding="utf8"), + Loader=yaml.SafeLoader, + ) + + return djlint_content + + def build_custom_blocks(custom_blocks: Union[str, None]) -> Optional[str]: """Build regex string for custom template blocks.""" if custom_blocks: @@ -91,6 +145,24 @@ class Config: profile or djlint_settings.get("profile", "all") ).lower() + # load linter rules + rule_set = validate_rules( + yaml.load( + (Path(__file__).parent / "rules.yaml").read_text(encoding="utf8"), + Loader=yaml.SafeLoader, + ) + + load_custom_rules(Path(src)) + ) + + self.linter_rules = list( + filter( + lambda x: x["rule"]["name"] not in self.ignore.split(",") + and x["rule"]["name"][0] not in self.profile_code + and self.profile not in x["rule"].get("exclude", []), + rule_set, + ) + ) + # base options self.indent: str = (indent or int(djlint_settings.get("indent", 4))) * " " diff --git a/tests/custom_rules/.djlint_rules.yaml b/tests/custom_rules/.djlint_rules.yaml new file mode 100644 index 0000000..5eea89b --- /dev/null +++ b/tests/custom_rules/.djlint_rules.yaml @@ -0,0 +1,6 @@ +- rule: + name: T001 + message: Find Trichotillomania + flags: re.DOTALL|re.I + patterns: + - Trichotillomania diff --git a/tests/custom_rules/html.html b/tests/custom_rules/html.html new file mode 100644 index 0000000..c005965 --- /dev/null +++ b/tests/custom_rules/html.html @@ -0,0 +1 @@ +This is trichotillomania. diff --git a/tests/custom_rules/pyproject.toml b/tests/custom_rules/pyproject.toml new file mode 100644 index 0000000..9aed255 --- /dev/null +++ b/tests/custom_rules/pyproject.toml @@ -0,0 +1 @@ +[tool] diff --git a/tests/custom_rules_bad/.djlint_rules.yaml b/tests/custom_rules_bad/.djlint_rules.yaml new file mode 100644 index 0000000..611c255 --- /dev/null +++ b/tests/custom_rules_bad/.djlint_rules.yaml @@ -0,0 +1,24 @@ +- rule: + name: T001 + message: Find Trichotillomania + flags: re.DOTALL|re.I + patterns: + - Trichotillomania + +- rule: + name: T002 + flags: re.DOTALL|re.I + patterns: + - Trichotillomania + +- rule: + name: T003 + patterns: + - Trichotillomania + +- rule: + name: T004 + +- rule: + patterns: + - Trichotillomania diff --git a/tests/custom_rules_bad/html.html b/tests/custom_rules_bad/html.html new file mode 100644 index 0000000..c005965 --- /dev/null +++ b/tests/custom_rules_bad/html.html @@ -0,0 +1 @@ +This is trichotillomania. diff --git a/tests/custom_rules_bad/pyproject.toml b/tests/custom_rules_bad/pyproject.toml new file mode 100644 index 0000000..9aed255 --- /dev/null +++ b/tests/custom_rules_bad/pyproject.toml @@ -0,0 +1 @@ +[tool] diff --git a/tests/test_linter.py b/tests/test_linter.py index 16dadc8..3aa2633 100644 --- a/tests/test_linter.py +++ b/tests/test_linter.py @@ -7,7 +7,7 @@ run:: # for a single test - pytest tests/test_linter.py::test_DJ018 --cov=src/djlint --cov-branch \ + pytest tests/test_linter.py::test_custom_rules_bad_config --cov=src/djlint --cov-branch \ --cov-report xml:coverage.xml --cov-report term-missing """ @@ -254,3 +254,19 @@ def test_rules_not_matched_in_ignored_block( print(result.output) assert result.exit_code == 0 assert "H011 1:" not in result.output + + +def test_custom_rules(runner: CliRunner, tmp_file: TextIO) -> None: + result = runner.invoke(djlint, ["tests/custom_rules"]) + assert """Linting""" in result.output + assert """1/1""" in result.output + assert """T001 1:""" in result.output + assert result.exit_code == 1 + + +def test_custom_rules_bad_config(runner: CliRunner, tmp_file: TextIO) -> None: + result = runner.invoke(djlint, ["tests/custom_rules_bad"]) + assert """Linting""" in result.output + assert """1/1""" in result.output + assert """T001 1:""" in result.output + assert result.exit_code == 1