Skip to content

Commit 3d07f99

Browse files
authored
Improve jinja error line number identification (#3044)
1 parent 591fe7d commit 3d07f99

File tree

3 files changed

+59
-25
lines changed

3 files changed

+59
-25
lines changed

examples/playbooks/rule-jinja-invalid.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
- name: Fixture
33
hosts: localhost
44
tasks:
5-
- name: Foo # <-- this is valid jinja
6-
ansible.builtin.debug:
7-
msg: "{{ 'a' b }}" # <-- jinja2[invalid]
5+
- name: A block used to check that we do not identify error at correct level
6+
block:
7+
- name: Foo # <-- this is valid jinja2
8+
ansible.builtin.debug:
9+
foo: "{{ 1 }}" # <-- jinja2[spacing]
10+
msg: "{{ 'a' b }}" # <-- jinja2[invalid]
811
# It should be noted that even ansible --syntax-check fails to spot the jinja
912
# error above, but ansible will throw a runtime error when running

src/ansiblelint/rules/jinja.py

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,16 @@ def _msg(self, tag: str, value: str, reformatted: str) -> str:
6464
"""Generate error message."""
6565
return self._tag2msg[tag].format(value=value, reformatted=reformatted)
6666

67-
# pylint: disable=too-many-branches
67+
# pylint: disable=too-many-branches,too-many-locals
6868
def matchtask( # noqa: C901
6969
self, task: dict[str, Any], file: Lintable | None = None
70-
) -> bool | str | MatchError:
70+
) -> list[MatchError]:
71+
result = []
7172
try:
72-
for key, v, _ in nested_items_path(task):
73+
for key, v, path in nested_items_path(
74+
task,
75+
ignored_keys=("block", "ansible.builtin.block", "ansible.legacy.block"),
76+
):
7377
if isinstance(v, str):
7478
try:
7579
template(
@@ -122,29 +126,34 @@ def matchtask( # noqa: C901
122126
# AnsibleError: template error while templating string: expected token ':', got '}'. String: {{ {{ '1' }} }}
123127
# AnsibleError: template error while templating string: unable to locate collection ansible.netcommon. String: Foo {{ buildset_registry.host | ipwrap }}
124128
if not bypass:
125-
return self.create_matcherror(
126-
message=str(exc),
127-
linenumber=task[LINE_NUMBER_KEY],
128-
filename=file,
129-
tag=f"{self.id}[invalid]",
129+
result.append(
130+
self.create_matcherror(
131+
message=str(exc),
132+
linenumber=_get_error_line(task, path),
133+
filename=file,
134+
tag=f"{self.id}[invalid]",
135+
)
130136
)
137+
continue
131138
reformatted, details, tag = self.check_whitespace(
132139
v, key=key, lintable=file
133140
)
134141
if reformatted != v:
135-
return self.create_matcherror(
136-
message=self._msg(
137-
tag=tag, value=v, reformatted=reformatted
138-
),
139-
linenumber=task[LINE_NUMBER_KEY],
140-
details=details,
141-
filename=file,
142-
tag=f"{self.id}[{tag}]",
142+
result.append(
143+
self.create_matcherror(
144+
message=self._msg(
145+
tag=tag, value=v, reformatted=reformatted
146+
),
147+
linenumber=_get_error_line(task, path),
148+
details=details,
149+
filename=file,
150+
tag=f"{self.id}[{tag}]",
151+
)
143152
)
144153
except Exception as exc:
145154
_logger.info("Exception in JinjaRule.matchtask: %s", exc)
146155
raise
147-
return False
156+
return result
148157

149158
def matchyaml(self, file: Lintable) -> list[MatchError]:
150159
"""Return matches for variables defined in vars files."""
@@ -344,7 +353,7 @@ def blacken(text: str) -> str:
344353
@pytest.fixture(name="error_expected_lines")
345354
def fixture_error_expected_lines() -> list[int]:
346355
"""Return list of expected error lines."""
347-
return [31, 34, 37, 40, 43, 46, 72]
356+
return [33, 36, 39, 42, 45, 48, 74]
348357

349358
# 21 68
350359
@pytest.fixture(name="lint_error_lines")
@@ -636,9 +645,13 @@ def test_jinja_invalid() -> None:
636645
collection.register(JinjaRule())
637646
success = "examples/playbooks/rule-jinja-invalid.yml"
638647
errs = Runner(success, rules=collection).run()
639-
assert len(errs) == 1
640-
assert errs[0].tag == "jinja[invalid]"
648+
assert len(errs) == 2
649+
assert errs[0].tag == "jinja[spacing]"
641650
assert errs[0].rule.id == "jinja"
651+
assert errs[0].linenumber == 9
652+
assert errs[1].tag == "jinja[invalid]"
653+
assert errs[1].rule.id == "jinja"
654+
assert errs[1].linenumber == 9
642655

643656
def test_jinja_valid() -> None:
644657
"""Tests our ability to parse jinja, even when variables may not be defined."""
@@ -647,3 +660,16 @@ def test_jinja_valid() -> None:
647660
success = "examples/playbooks/rule-jinja-valid.yml"
648661
errs = Runner(success, rules=collection).run()
649662
assert len(errs) == 0
663+
664+
665+
def _get_error_line(task: dict[str, Any], path: list[str | int]) -> int:
666+
"""Return error line number."""
667+
line = task[LINE_NUMBER_KEY]
668+
ctx = task
669+
for _ in path:
670+
ctx = ctx[_]
671+
if LINE_NUMBER_KEY in ctx:
672+
line = ctx[LINE_NUMBER_KEY]
673+
if not isinstance(line, int):
674+
raise RuntimeError("Line number is not an integer")
675+
return line

src/ansiblelint/yaml_utils.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
Dict,
1515
Iterator,
1616
Pattern,
17+
Sequence,
1718
Tuple,
1819
Union,
1920
cast,
@@ -170,6 +171,7 @@ def iter_tasks_in_file(
170171

171172
def nested_items_path(
172173
data_collection: dict[Any, Any] | list[Any],
174+
ignored_keys: Sequence[str] = (),
173175
) -> Iterator[tuple[Any, Any, list[str | int]]]:
174176
"""Iterate a nested data structure, yielding key/index, value, and parent_path.
175177
@@ -230,12 +232,15 @@ def nested_items_path(
230232
# valid data, we better ignore NoneType
231233
if data_collection is None:
232234
return
233-
yield from _nested_items_path(data_collection=data_collection, parent_path=[])
235+
yield from _nested_items_path(
236+
data_collection=data_collection, parent_path=[], ignored_keys=ignored_keys
237+
)
234238

235239

236240
def _nested_items_path(
237241
data_collection: dict[Any, Any] | list[Any],
238242
parent_path: list[str | int],
243+
ignored_keys: Sequence[str] = (),
239244
) -> Iterator[tuple[Any, Any, list[str | int]]]:
240245
"""Iterate through data_collection (internal implementation of nested_items_path).
241246
@@ -260,7 +265,7 @@ def _nested_items_path(
260265
f"of type '{type(data_collection)}'"
261266
)
262267
for key, value in convert_data_collection_to_tuples():
263-
if key in (SKIPPED_RULES_KEY, "__file__", "__line__"):
268+
if key in (SKIPPED_RULES_KEY, "__file__", "__line__", *ignored_keys):
264269
continue
265270
yield key, value, parent_path
266271
if isinstance(value, (dict, list)):

0 commit comments

Comments
 (0)