Skip to content

Commit d1ff289

Browse files
authored
Improve logic of find_children (#4161)
1 parent 983d251 commit d1ff289

File tree

15 files changed

+130
-18
lines changed

15 files changed

+130
-18
lines changed

.github/workflows/tox.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ jobs:
7272
env:
7373
# Number of expected test passes, safety measure for accidental skip of
7474
# tests. Update value if you add/remove tests.
75-
PYTEST_REQPASS: 879
75+
PYTEST_REQPASS: 882
7676
steps:
7777
- uses: actions/checkout@v4
7878
with:

examples/playbooks/common-include-1.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@
88
- name: Some include_tasks with file and jinja2
99
ansible.builtin.include_tasks:
1010
file: "{{ 'tasks/included-with-lint.yml' }}"
11+
- name: Some include 3
12+
ansible.builtin.include_tasks: file=tasks/included-with-lint.yml
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
- name: Fixture for test coverage
3+
hosts: localhost
4+
gather_facts: false
5+
tasks:
6+
- name: Some include with invalid syntax
7+
ansible.builtin.include_tasks: "file="
8+
- name: Some include with invalid syntax
9+
ansible.builtin.include_tasks: other=tasks/included-with-lint.yml
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
- name: Fixture for test coverage
3+
hosts: localhost
4+
gather_facts: false
5+
tasks:
6+
- name: Some include with invalid syntax
7+
ansible.builtin.include_tasks:
8+
file: null
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
- name: Fixture
3+
hosts: localhost
4+
tasks:
5+
- name: Fixture
6+
ansible.builtin.include_role:
7+
name: include_wrong_syntax

examples/playbooks/include.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
tasks:
1212
- ansible.builtin.include_tasks: tasks/x.yml
1313
- ansible.builtin.include_tasks: tasks/x.yml y=z
14+
- ansible.builtin.include_tasks: file=tasks/x.yml
1415

1516
handlers:
1617
- ansible.builtin.include_tasks: handlers/y.yml
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
- name: Invalid syntax for import (coverage)
3+
ansible.builtin.import_tasks: wrong=imported_tasks.yml

examples/roles/var_naming_pattern/tasks/include_task_with_vars.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
---
2-
- name: include_task_with_vars | Foo
2+
- name: include_task_with_vars | Var1
3+
ansible.builtin.include_tasks: file=../tasks/included-task-with-vars.yml
4+
5+
- name: include_task_with_vars | Var2
36
ansible.builtin.include_tasks: ../tasks/included-task-with-vars.yml
47
vars:
58
var_naming_pattern_1: bar
69
_var_naming_pattern_2: ... # we allow _ before the prefix
710
__var_naming_pattern_3: ... # we allow __ before the prefix
811

9-
- name: include_task_with_vars | Foo
12+
- name: include_task_with_vars | Var3
1013
ansible.builtin.include_role:
1114
name: bobbins
1215
vars:

playbook.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
- name: Example
33
hosts: localhost
4+
gather_facts: false
45
tasks:
56
- name: include extra tasks
67
ansible.builtin.include_tasks:

src/ansiblelint/rules/role_name.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ def _infer_role_name(meta: Path, default: str) -> str:
164164
if meta_data:
165165
try:
166166
return str(meta_data["galaxy_info"]["role_name"])
167-
except KeyError:
167+
except (KeyError, TypeError):
168168
pass
169169
return default
170170

src/ansiblelint/rules/syntax_check.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ class KnownError:
2727
re.MULTILINE | re.S | re.DOTALL,
2828
),
2929
),
30+
KnownError(
31+
tag="no-file",
32+
regex=re.compile(
33+
r"^ERROR! (?P<title>No file specified for [^\n]*)",
34+
re.MULTILINE | re.S | re.DOTALL,
35+
),
36+
),
3037
KnownError(
3138
tag="empty-playbook",
3239
regex=re.compile(

src/ansiblelint/runner.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,9 @@ def _emit_matches(self, files: list[Lintable]) -> Generator[MatchError, None, No
455455
visited: set[Lintable] = set()
456456
while visited != self.lintables:
457457
for lintable in self.lintables - visited:
458+
visited.add(lintable)
459+
if not lintable.path.exists():
460+
continue
458461
try:
459462
children = self.find_children(lintable)
460463
for child in children:
@@ -468,8 +471,10 @@ def _emit_matches(self, files: list[Lintable]) -> Generator[MatchError, None, No
468471
exc.rule = self.rules["load-failure"]
469472
yield exc
470473
except AttributeError:
471-
yield MatchError(lintable=lintable, rule=self.rules["load-failure"])
472-
visited.add(lintable)
474+
yield MatchError(
475+
lintable=lintable,
476+
rule=self.rules["load-failure"],
477+
)
473478

474479
def find_children(self, lintable: Lintable) -> list[Lintable]:
475480
"""Traverse children of a single file or folder."""
@@ -490,7 +495,6 @@ def find_children(self, lintable: Lintable) -> list[Lintable]:
490495
except AnsibleError as exc:
491496
msg = f"Loading {lintable.filename} caused an {type(exc).__name__} exception: {exc}, file was ignored."
492497
logging.exception(msg)
493-
# raise SystemExit(exc) from exc
494498
return []
495499
results = []
496500
# playbook_ds can be an AnsibleUnicode string, which we consider invalid

src/ansiblelint/utils.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
import logging
2929
import os
3030
import re
31-
from collections.abc import ItemsView, Iterator, Mapping, Sequence
31+
from collections.abc import ItemsView, Iterable, Iterator, Mapping, Sequence
3232
from dataclasses import _MISSING_TYPE, dataclass, field
3333
from functools import cache, lru_cache
3434
from pathlib import Path
@@ -290,7 +290,7 @@ class HandleChildren:
290290
rules: RulesCollection = field(init=True, repr=False)
291291
app: App
292292

293-
def include_children(
293+
def include_children( # pylint: disable=too-many-return-statements
294294
self,
295295
lintable: Lintable,
296296
k: str,
@@ -325,14 +325,21 @@ def include_children(
325325
return []
326326

327327
# handle include: filename.yml tags=blah
328-
(args, _) = tokenize(v)
328+
(args, kwargs) = tokenize(v)
329329

330-
result = path_dwim(basedir, args[0])
330+
if args:
331+
file = args[0]
332+
elif "file" in kwargs:
333+
file = kwargs["file"]
334+
else:
335+
return []
336+
337+
result = path_dwim(basedir, file)
331338
while basedir not in ["", "/"]:
332339
if os.path.exists(result):
333340
break
334341
basedir = os.path.dirname(basedir)
335-
result = path_dwim(basedir, args[0])
342+
result = path_dwim(basedir, file)
336343

337344
return [Lintable(result, kind=parent_type)]
338345

@@ -430,7 +437,7 @@ def roles_children(
430437
# pylint: disable=unused-argument # parent_type)
431438
basedir = str(lintable.path.parent)
432439
results: list[Lintable] = []
433-
if not v:
440+
if not v or not isinstance(v, Iterable):
434441
# typing does not prevent junk from being passed in
435442
return results
436443
for role in v:
@@ -467,10 +474,24 @@ def _get_task_handler_children_for_tasks_or_playbooks(
467474
if not task_handler or isinstance(task_handler, str): # pragma: no branch
468475
continue
469476

470-
file_name = task_handler[task_handler_key]
471-
if isinstance(file_name, Mapping) and file_name.get("file", None):
472-
file_name = file_name["file"]
477+
file_name = ""
478+
action_args = task_handler[task_handler_key]
479+
if isinstance(action_args, str):
480+
(args, kwargs) = tokenize(action_args)
481+
if len(args) == 1:
482+
file_name = args[0]
483+
elif kwargs.get("file", None):
484+
file_name = kwargs["file"]
485+
else:
486+
# ignore invalid data (syntax check will outside the scope)
487+
continue
488+
489+
if isinstance(action_args, Mapping) and action_args.get("file", None):
490+
file_name = action_args["file"]
473491

492+
if not file_name:
493+
# ignore invalid data (syntax check will outside the scope)
494+
continue
474495
f = path_dwim(basedir, file_name)
475496
while basedir not in ["", "/"]:
476497
if os.path.exists(f):

test/test_runner.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,52 @@ def test_files_not_scanned_twice(default_rules_collection: RulesCollection) -> N
178178
assert len(run2) == 0
179179

180180

181+
@pytest.mark.parametrize(
182+
("filename", "failures", "checked_files_no"),
183+
(
184+
pytest.param(
185+
"examples/playbooks/common-include-wrong-syntax.yml",
186+
1,
187+
1,
188+
id="1",
189+
),
190+
pytest.param(
191+
"examples/playbooks/common-include-wrong-syntax2.yml",
192+
1,
193+
1,
194+
id="2",
195+
),
196+
pytest.param(
197+
"examples/playbooks/common-include-wrong-syntax3.yml",
198+
0,
199+
2,
200+
id="3",
201+
),
202+
),
203+
)
204+
def test_include_wrong_syntax(
205+
filename: str,
206+
failures: int,
207+
checked_files_no: int,
208+
default_rules_collection: RulesCollection,
209+
) -> None:
210+
"""Ensure that lintables aren't double-checked."""
211+
checked_files: set[Lintable] = set()
212+
213+
path = Path(filename).resolve()
214+
runner = Runner(
215+
path,
216+
rules=default_rules_collection,
217+
verbosity=0,
218+
checked_files=checked_files,
219+
)
220+
result = runner.run()
221+
assert len(runner.checked_files) == checked_files_no
222+
assert len(result) == failures, result
223+
for item in result:
224+
assert item.tag == "syntax-check[no-file]"
225+
226+
181227
def test_runner_not_found(default_rules_collection: RulesCollection) -> None:
182228
"""Ensure that lintables aren't double-checked."""
183229
checked_files: set[Lintable] = set()

test/test_yaml_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -768,12 +768,12 @@ def test_get_path_to_play(
768768
pytest.param(
769769
"examples/playbooks/include.yml",
770770
14,
771-
[0, "tasks", 1],
771+
[0, "tasks", 2],
772772
id="playbook-multi_tasks_blocks-tasks_last_task_before_handlers",
773773
),
774774
pytest.param(
775775
"examples/playbooks/include.yml",
776-
16,
776+
17,
777777
[0, "handlers", 0],
778778
id="playbook-multi_tasks_blocks-handlers_task",
779779
),

0 commit comments

Comments
 (0)