diff --git a/src/djlint/rules.yaml b/src/djlint/rules.yaml index ad12c3cf..79035659 100644 --- a/src/djlint/rules.yaml +++ b/src/djlint/rules.yaml @@ -276,4 +276,4 @@ message: Duplicate attribute found. flags: re.I patterns: - - <\w[^>]*?\s([a-z][a-z-]*?)(?==.+?\1=[^>]*?>) + - <\w[^>]*?\s\K([a-z][a-z-]*?)(?==[^>]+?\1=[^>]*?>) diff --git a/tests/conftest.py b/tests/conftest.py index 871a35ba..e212a65c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -134,24 +134,17 @@ def printer(expected, source, actual): print(f"{ color.get(diff[:1], Style.RESET_ALL)}{diff}{Style.RESET_ALL}") -def lint_printer(source, expected, excluded, actual): +def lint_printer(source, expected, actual): width, _ = shutil.get_terminal_size() expected_text = "Expected Rules" - excluded_text = "Excluded Rules" actual_text = "Actual" source_text = "Source" expected_width = int((width - len(expected_text) - 2) / 2) - excluded_width = int((width - len(excluded_text) - 2) / 2) actual_width = int((width - len(actual_text) - 2) / 2) source_width = int((width - len(source_text) - 2) / 2) - def padder(value, width): - if len(value) < width: - return str(value) + " " * (width - len(value)) - return value[:20] - print() print( f"{Fore.BLUE}{Style.BRIGHT}{'─' * source_width} {source_text} {'─' * source_width}{Style.RESET_ALL}" @@ -160,42 +153,34 @@ def padder(value, width): print(source) print() - if expected != (): - print( - f"{Fore.BLUE}{Style.BRIGHT}{'─' * expected_width} {expected_text} {'─' * expected_width}{Style.RESET_ALL}" - ) - print() - for x in expected: - if isinstance(x, tuple): - print(f"{x[0]}, line #{x[1]}") - else: - print(x) - print() - if excluded != (): + print( + f"{Fore.BLUE}{Style.BRIGHT}{'─' * expected_width} {expected_text} {'─' * expected_width}{Style.RESET_ALL}" + ) + print() + for x in expected: print( - f"{Fore.BLUE}{Style.BRIGHT}{'─' * excluded_width} {excluded_text} {'─' * excluded_width}{Style.RESET_ALL}" + f"{Fore.RED}{Style.BRIGHT}{x['code']}{Style.RESET_ALL} {x['line']} {x['match']}" ) + print(f' {x["message"]}') print() - for x in excluded: - if isinstance(x, tuple): - print(f"{x[0]}, line #{x[1]}") - else: - print(x) - print() + print( f"{Fore.BLUE}{Style.BRIGHT}{'─' * actual_width} {actual_text} {'─' * actual_width}{Style.RESET_ALL}" ) print() - if actual: - max_code = max(len(x["code"]) for x in actual) - max_line = max(len(x["line"]) for x in actual) - max_match = min(max(len(x["match"]) for x in actual), 20) - for x in actual: - print( - f'{padder(x["code"],max_code)} {padder(x["line"], max_line)} {padder(x["match"],max_match)} >> {x["message"]}' - ) + for x in actual: + print( + f"{Fore.RED}{Style.BRIGHT}{x['code']}{Style.RESET_ALL} {x['line']} {x['match']}" + ) + print(f' {x["message"]}') + print() + if len(actual) == 0: + print(f"{Fore.YELLOW}No codes found.{Style.RESET_ALL}") + print() + else: + print(f"{Fore.YELLOW}{actual}{Style.RESET_ALL}") print() diff --git a/tests/test_linter/test_django_linter.py b/tests/test_linter/test_django_linter.py index 65146d8c..ce083a1b 100644 --- a/tests/test_linter/test_django_linter.py +++ b/tests/test_linter/test_django_linter.py @@ -1,22 +1,8 @@ -"""Djlint linter rule tests. - -run:: - - pytest tests/test_linter.py --cov=src/djlint --cov-branch \ - --cov-report xml:coverage.xml --cov-report term-missing - - # for a single test - - pytest tests/test_linter/test_linter.py::test_random - -Test setup - -(html, (list of codes that should file, plus optional line number)) +"""Djlint linter rule tests for django. +poetry run pytest tests/test_linter/test_django_linter.py """ -# pylint: disable=C0116,C0103,C0302 - import pytest @@ -26,54 +12,117 @@ test_data = [ pytest.param( ("{{test }}\n{% test%}"), - ([("T001", 1), ("T001", 2)]), - (), + ( + [ + { + "code": "T001", + "line": "1:0", + "match": "{{test", + "message": "Variables should be wrapped in a single whitespace.", + }, + { + "code": "T001", + "line": "2:3", + "match": "test%}", + "message": "Variables should be wrapped in a single whitespace.", + }, + { + "code": "H025", + "line": "2:9", + "match": "", + "message": "Tag seems to be an orphan.", + }, + ] + ), id="T001", ), pytest.param( ("{% extends 'this' %}"), ( [ - ("T002", 1), + { + "code": "T002", + "line": "1:0", + "match": "{% extends 'this' %}", + "message": "Double quotes should be used in tags.", + } ] ), - (), id="T002", ), pytest.param( ("{% extends this %}"), - (), - (["T002"]), + ([]), id="T002_unquoted_var_names", ), pytest.param( ("{% with a='this' %}"), - (["T002"]), - (), + ( + [ + { + "code": "T002", + "line": "1:0", + "match": "{% with a='this' %}", + "message": "Double quotes should be used in tags.", + } + ] + ), id="T002_with", ), pytest.param( ("{% trans 'this' %}"), - (["T002"]), - (), + ( + [ + { + "code": "T002", + "line": "1:0", + "match": "{% trans 'this' %}", + "message": "Double quotes should be used in tags.", + } + ] + ), id="T002_trans", ), pytest.param( ("{% translate 'this' %}"), - (["T002"]), - (), + ( + [ + { + "code": "T002", + "line": "1:0", + "match": "{% translate 'this' ", + "message": "Double quotes should be used in tags.", + } + ] + ), id="T002_translate", ), pytest.param( ("{% include 'this' %}"), - (["T002"]), - (), + ( + [ + { + "code": "T002", + "line": "1:0", + "match": "{% include 'this' %}", + "message": "Double quotes should be used in tags.", + } + ] + ), id="T002_include", ), pytest.param( ("{% now 'Y-m-d G:i:s' %}"), - (["T002"]), - (), + ( + [ + { + "code": "T002", + "line": "1:0", + "match": "{% now 'Y-m-d G:i:s'", + "message": "Double quotes should be used in tags.", + } + ] + ), id="T002_now", ), pytest.param( @@ -83,14 +132,21 @@ '{% include "template.html" %}\n' "
" ), - (), - (["T002"]), + ( + [ + { + "code": "H025", + "line": "2:0", + "match": '
'), - ([("D004", 1)]), - (), + ( + [ + { + "code": "D004", + "line": "1:0", + "match": '\n' '
' ), - ([("D018", 1), ("D018", 2)]), - (), + ( + [ + { + "code": "D018", + "line": "1:0", + "match": '\n' '
'), - (), - ([("D018", 1), ("D018", 2)]), + ( + [ + { + "code": "H019", + "line": "1:0", + "match": '\n
\n' '
' ), - (), - ([("D018", 1), ("D018", 2)]), + ([]), id="DJ018_has_urls", ), pytest.param( ('
'), - ([("D018", 1)]), - (), + ( + [ + { + "code": "D018", + "line": "1:0", + "match": '
'), - (), - (["D018"]), + ([]), id="DJ018_mailto", ), pytest.param( (''), - (), - (["D018"]), + ([]), id="DJ018_data", ), pytest.param( ('
'), - (), - (["D018"]), + ([]), id="DJ018_attribute_names", ), pytest.param( ('
'), - (), - (["D018"]), + ( + [ + { + "code": "H037", + "line": "1:6", + "match": "action", + "message": "Duplicate attribute found.", + } + ] + ), id="DJ018_data_action", ), pytest.param( ("{% blah 'asdf %}"), - (["T027"]), - (), + ( + [ + { + "code": "T027", + "line": "1:0", + "match": "{% blah 'asdf %}", + "message": "Unclosed string found in template syntax.", + } + ] + ), id="T027", ), pytest.param( ("{% blah 'asdf' %}{{ blah \"asdf\" }}"), - (), - (["T027"]), + ([]), id="T027_no", ), pytest.param( ("{% blah 'asdf' 'blah %}"), - (["T027"]), - (), + ( + [ + { + "code": "T027", + "line": "1:0", + "match": "{% blah 'asdf' 'blah", + "message": "Unclosed string found in template syntax.", + } + ] + ), id="T027_long_name", ), pytest.param( ('{% trans "Check box if you\'re interested in this location." %}'), - (), - (["T027"]), + ([]), id="T027_trans", ), pytest.param( ( "{% macro rendersubmit(buttons=[], class=\"\", index='', url='', that=\"\" , test='') -%}" ), - (), - (["T027"]), + ([]), id="T027_mixed_quotes", ), pytest.param( (""), - (), - (["T028"]), + ( + [ + { + "code": "H025", + "line": "1:0", + "match": ""), - (), - (["T028"]), + ( + [ + { + "code": "H025", + "line": "1:0", + "match": ""), - (), - (["T028"]), + ( + [ + { + "code": "H025", + "line": "1:0", + "match": "'), - (), - (["T028"]), + ([]), id="T028_no_7", ), pytest.param( ("{% static '' \" \" 'foo/bar.min.css' %}"), - (["T032"]), - (), + ( + [ + { + "code": "T032", + "line": "1:0", + "match": "{% static ''", + "message": "Extra whitespace found in template tags.", + } + ] + ), id="T032", ), pytest.param( ("{% static '' %}"), - (["T032"]), - (), + ( + [ + { + "code": "T001", + "line": "1:12", + "match": "' %}", + "message": "Variables should be wrapped in a single whitespace.", + }, + { + "code": "T032", + "line": "1:0", + "match": "{% static", + "message": "Extra whitespace found in template tags.", + }, + ] + ), id="T032_2", ), pytest.param( ("{% static '' \" \" 'foo/bar.min.css' %}"), - (), - (["T032"]), + ([]), id="T032_no", ), pytest.param( ("{{ static '' \" \" 'foo/bar.min.css' }}"), - (["T032"]), - (), + ( + [ + { + "code": "T032", + "line": "1:0", + "match": "{{ static ''", + "message": "Extra whitespace found in template tags.", + } + ] + ), id="T032_3", ), pytest.param( ("{{ static '' }}"), - (["T032"]), - (), + ( + [ + { + "code": "T001", + "line": "1:12", + "match": "' }}", + "message": "Variables should be wrapped in a single whitespace.", + }, + { + "code": "T032", + "line": "1:0", + "match": "{{ static", + "message": "Extra whitespace found in template tags.", + }, + ] + ), id="T032_4", ), pytest.param( ("{{ static '' \" \" 'foo/bar.min.css' }}"), - (), - (["T032"]), + ([]), id="T032_no_2", ), ] -@pytest.mark.parametrize(("source", "expected", "excluded"), test_data) -def test_django_linter(source, expected, excluded, django_config) -> None: - filename = "file" - filepath = filename - lint = linter(django_config, source, filename, filepath)[filename] - - lint_printer(source, expected, excluded, lint) +@pytest.mark.parametrize(("source", "expected"), test_data) +def test_base(source, expected, django_config): + filename = "test.html" + output = linter(django_config, source, filename, filename) - def check_rule(rule, lint): - if isinstance(rule, tuple): - return ( - any( - x["code"] == rule[0] and int(x["line"].split(":")[0]) == rule[1] - for x in lint - ) - is True - ) - else: - return any(x["code"] == rule for x in lint) is True + lint_printer(source, expected, output[filename]) - for rule in expected: - assert check_rule(rule, lint) is True + mismatch = list(filter(lambda x: x not in expected, output[filename])) + list( + filter(lambda x: x not in output[filename], expected) + ) - for rule in excluded: - assert check_rule(rule, lint) is False + assert len(mismatch) == 0 diff --git a/tests/test_linter/test_h037.py b/tests/test_linter/test_h037.py new file mode 100644 index 00000000..c6ae2db7 --- /dev/null +++ b/tests/test_linter/test_h037.py @@ -0,0 +1,65 @@ +"""Test twig comment tags. + +poetry run pytest tests/test_linter/test_h037.py +""" +import pytest + +from src.djlint.lint import linter +from tests.conftest import lint_printer + +test_data = [ + pytest.param( + ('
'), + ( + [ + { + "code": "H037", + "line": "1:4", + "match": "class", + "message": "Duplicate attribute found.", + } + ] + ), + id="one", + ), + pytest.param( + ('
'), + ( + [ + { + "code": "H037", + "line": "1:5", + "match": "data-class", + "message": "Duplicate attribute found.", + } + ] + ), + id="two", + ), + pytest.param( + ('
'), + ([]), + id="mismatch names", + ), + pytest.param( + ( + '
' + ), + ([]), + id="repeating tags", + ), +] + + +@pytest.mark.parametrize(("source", "expected"), test_data) +def test_base(source, expected, nunjucks_config): + filename = "test.html" + output = linter(nunjucks_config, source, filename, filename) + + lint_printer(source, expected, output[filename]) + + mismatch = list(filter(lambda x: x not in expected, output[filename])) + list( + filter(lambda x: x not in output[filename], expected) + ) + + assert len(mismatch) == 0 diff --git a/tests/test_linter/test_jinja_linter.py b/tests/test_linter/test_jinja_linter.py index 9b6842dd..96e57a1d 100644 --- a/tests/test_linter/test_jinja_linter.py +++ b/tests/test_linter/test_jinja_linter.py @@ -1,22 +1,8 @@ -"""Djlint linter rule tests. - -run:: - - pytest tests/test_linter.py --cov=src/djlint --cov-branch \ - --cov-report xml:coverage.xml --cov-report term-missing - - # for a single test - - pytest tests/test_linter/test_linter.py::test_random - -Test setup - -(html, (list of codes that should file, plus optional line number)) +"""Djlint linter tests for jinja. +poetry run pytest tests/test_linter/test_jinja_linter.py """ -# pylint: disable=C0116,C0103,C0302 - import pytest @@ -26,108 +12,157 @@ test_data = [ pytest.param( ("{#-test -#}"), - (), - (["T001"]), + ([]), id="T001", ), pytest.param( ("{#- test -#}"), - (), - (["T001"]), + ([]), id="T001_2", ), pytest.param( ("
\n" " {%\n" ' ("something", "1"),\n' " %}\n" "
"), - (), - (["T001"]), + ([]), id="T001_3", ), pytest.param( ("{{- foo }}{{+ bar }}{{ biz -}}{{ baz +}}"), - (), - (["T001"]), + ([]), id="T001_4", ), pytest.param( (''), - ([("J004", 1)]), - (), + ( + [ + { + "code": "J004", + "line": "1:0", + "match": '\n
' ), - ([("J018", 1), ("J018", 2)]), - (), + ( + [ + { + "code": "J018", + "line": "1:0", + "match": '\n
'), - (), - ([("J018", 1), ("J018", 2)]), + ( + [ + { + "code": "H019", + "line": "1:0", + "match": '\n
'), - (), - ([("J018", 1), ("J018", 2)]), + ([]), id="J018_on_events", ), pytest.param( ('
'), - ([("J018", 1)]), - (), + ( + [ + { + "code": "J018", + "line": "1:0", + "match": '
'), - (), - (["J018"]), + ([]), id="J018_mailto", ), pytest.param( (''), - (), - (["J018"]), + ([]), id="J018_data", ), pytest.param( ('
'), - (), - (["J018"]), + ([]), id="J018_attributes", ), pytest.param( ( "{% macro rendersubmit(buttons=[], class=\"\", index='', url='', that=\"\" , test='') -%}" ), - (), - (["T027"]), + ([]), id="T027", ), pytest.param( - (""), - (), - (["T028"]), + (""), + ([]), id="T028", ), pytest.param( - (""), - (["T028"]), - (), + (""), + ( + [ + { + "code": "T028", + "line": "1:0", + "match": ""), - (["T028"]), - (), + (""), + ( + [ + { + "code": "T028", + "line": "1:0", + "match": ""), - (), - (["T028"]), + (""), + ([]), id="T028_4", ), pytest.param( @@ -144,20 +179,26 @@ " ('https://example.com', 'plum', 'Three'),\n" " ] %}" ), - (), - (["T032"]), + ([]), id="T032", ), pytest.param( ("{% not ok }%"), - (["T034"]), - (), + ( + [ + { + "code": "T034", + "line": "1:0", + "match": "{% not ok }%", + "message": "Did you intend to use {% ... %} instead of {% ... }%?", + } + ] + ), id="T034", ), pytest.param( ("{% not ok \n%}"), - (), - (["T034"]), + ([]), id="T034", ), pytest.param( @@ -166,35 +207,21 @@ "; e.g. for a ISO8601 formatted timestring, use: %{%Y-%m-%dT%H:%M:%S%z}t\n" "{% endraw %}" ), - (), - (["T034"]), + ([]), id="raw", ), ] -@pytest.mark.parametrize(("source", "expected", "excluded"), test_data) -def test_jinja_linter(source, expected, excluded, jinja_config) -> None: - filename = "file" - filepath = filename - lint = linter(jinja_config, source, filename, filepath)[filename] - - lint_printer(source, expected, excluded, lint) +@pytest.mark.parametrize(("source", "expected"), test_data) +def test_base(source, expected, jinja_config): + filename = "test.html" + output = linter(jinja_config, source, filename, filename) - def check_rule(rule, lint): - if isinstance(rule, tuple): - return ( - any( - x["code"] == rule[0] and int(x["line"].split(":")[0]) == rule[1] - for x in lint - ) - is True - ) - else: - return any(x["code"] == rule for x in lint) is True + lint_printer(source, expected, output[filename]) - for rule in expected: - assert check_rule(rule, lint) is True + mismatch = list(filter(lambda x: x not in expected, output[filename])) + list( + filter(lambda x: x not in output[filename], expected) + ) - for rule in excluded: - assert check_rule(rule, lint) is False + assert len(mismatch) == 0 diff --git a/tests/test_linter/test_linter.py b/tests/test_linter/test_linter.py index 35241653..6e8a4234 100644 --- a/tests/test_linter/test_linter.py +++ b/tests/test_linter/test_linter.py @@ -648,20 +648,6 @@ def test_H036(runner: CliRunner, tmp_file: TextIO) -> None: assert "H036" in result.output -def test_H037(runner: CliRunner, tmp_file: TextIO) -> None: - write_to_file(tmp_file.name, b"
") - result = runner.invoke(djlint, [tmp_file.name]) - assert "H037" in result.output - - write_to_file(tmp_file.name, b"
") - result = runner.invoke(djlint, [tmp_file.name]) - assert "H037" in result.output - - write_to_file(tmp_file.name, b"
") - result = runner.invoke(djlint, [tmp_file.name]) - assert "H037" not in result.output - - def test_rules_not_matched_in_ignored_block( runner: CliRunner, tmp_file: TextIO ) -> None: diff --git a/tests/test_linter/test_nunjucks_linter.py b/tests/test_linter/test_nunjucks_linter.py index 9c5cf495..b2d0f723 100644 --- a/tests/test_linter/test_nunjucks_linter.py +++ b/tests/test_linter/test_nunjucks_linter.py @@ -1,23 +1,8 @@ -"""Djlint linter rule tests. - -run:: - - pytest tests/test_linter.py --cov=src/djlint --cov-branch \ - --cov-report xml:coverage.xml --cov-report term-missing - - # for a single test - - pytest tests/test_linter/test_linter.py::test_random - -Test setup - -(html, (list of codes that should file, plus optional line number)) +"""Djlint linter rule tests for nunjucks. +poetry run pytest tests/test_linter/test_nunjucks_linter.py """ -# pylint: disable=C0116,C0103,C0302 - - import pytest from src.djlint.lint import linter @@ -26,60 +11,49 @@ test_data = [ pytest.param( ("{%- test-%}"), - ([("T001", 1)]), - (), + ( + [ + { + "code": "T001", + "line": "1:4", + "match": "test-%}", + "message": "Variables should be wrapped in a single whitespace.", + } + ] + ), id="T001", ), pytest.param( ("{%-test -%}"), - ([("T001", 1)]), - (), + ( + [ + { + "code": "T001", + "line": "1:0", + "match": "{%-test", + "message": "Variables should be wrapped in a single whitespace.", + } + ] + ), id="T001_2", ), pytest.param( ("{%- test -%}"), - (), - (["T001"]), + ([]), id="T001_3", ), ] -@pytest.mark.parametrize(("source", "expected", "excluded"), test_data) -def test_jinja_linter(source, expected, excluded, nunjucks_config) -> None: - filename = "file" - filepath = filename - lint = linter(nunjucks_config, source, filename, filepath)[filename] - - lint_printer(source, expected, excluded, lint) - - def check_rule(rule, lint): - if isinstance(rule, tuple): - return ( - any( - x["code"] == rule[0] and int(x["line"].split(":")[0]) == rule[1] - for x in lint - ) - is True - ) - else: - return any(x["code"] == rule for x in lint) is True - - for rule in expected: - assert check_rule(rule, lint) is True - - for rule in excluded: - assert check_rule(rule, lint) is False - - -# def test_T001(runner: CliRunner, tmp_file: TextIO) -> None: +@pytest.mark.parametrize(("source", "expected"), test_data) +def test_base(source, expected, nunjucks_config): + filename = "test.html" + output = linter(nunjucks_config, source, filename, filename) + lint_printer(source, expected, output[filename]) -# write_to_file(tmp_file.name, b"{%-test -%}") -# result = runner.invoke(djlint, [tmp_file.name, "--profile", "nunjucks"]) -# assert result.exit_code == 1 -# assert "T001 1:" in result.output + mismatch = list(filter(lambda x: x not in expected, output[filename])) + list( + filter(lambda x: x not in output[filename], expected) + ) -# write_to_file(tmp_file.name, b"{%- test -%}") -# result = runner.invoke(djlint, [tmp_file.name, "--profile", "nunjucks"]) -# assert result.exit_code == 0 + assert len(mismatch) == 0