Skip to content

Commit ddfa6ca

Browse files
cavcrosbyaudgirka
authored andcommitted
Ignore var-naming[pattern] to foreign role vars (#4095)
The changes to the utils.py's _rolepath were to enable the function to handle fully qualified collection names (FQCNs). Otherwise, the function would be unable to parse the role name from the FQCN returning basedir as the role_path. This can result in additional files with violations being returned by the _look_for_role_files function.
1 parent 454c94a commit ddfa6ca

File tree

7 files changed

+107
-26
lines changed

7 files changed

+107
-26
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: 884
75+
PYTEST_REQPASS: 886
7676
steps:
7777
- uses: actions/checkout@v4
7878
with:

collections/ansible_collections/local/testcollection/roles/bug4095/tasks/main.yml

Whitespace-only changes.

examples/playbooks/bug-4095.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
- name: Test task
3+
hosts: localhost
4+
connection: local
5+
tasks:
6+
- name: Reproduce bug related bug 4095
7+
ansible.builtin.include_role:
8+
name: local.testcollection.bug4095
9+
vars:
10+
var1: val1

src/ansiblelint/rules/var_naming.py

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import keyword
66
import re
77
import sys
8-
from typing import TYPE_CHECKING, Any
8+
from typing import TYPE_CHECKING, Any, NamedTuple
99

1010
from ansible.parsing.yaml.objects import AnsibleUnicode
1111
from ansible.vars.reserved import get_reserved_names
@@ -22,13 +22,20 @@
2222
from ansiblelint.rules import AnsibleLintRule, RulesCollection
2323
from ansiblelint.runner import Runner
2424
from ansiblelint.skip_utils import get_rule_skips_from_line
25-
from ansiblelint.text import has_jinja, is_fqcn_or_name
25+
from ansiblelint.text import has_jinja, is_fqcn, is_fqcn_or_name
2626
from ansiblelint.utils import parse_yaml_from_file
2727

2828
if TYPE_CHECKING:
2929
from ansiblelint.utils import Task
3030

3131

32+
class Prefix(NamedTuple):
33+
"""Prefix."""
34+
35+
value: str = ""
36+
from_fqcn: bool = False
37+
38+
3239
class VariableNamingRule(AnsibleLintRule):
3340
"""All variables should be named using only lowercase and underscores."""
3441

@@ -109,7 +116,7 @@ def get_var_naming_matcherror(
109116
self,
110117
ident: str,
111118
*,
112-
prefix: str = "",
119+
prefix: Prefix | None = None,
113120
) -> MatchError | None:
114121
"""Return a MatchError if the variable name is not valid, otherwise None."""
115122
if not isinstance(ident, str): # pragma: no cover
@@ -160,7 +167,9 @@ def get_var_naming_matcherror(
160167
rule=self,
161168
)
162169

163-
if not bool(self.re_pattern.match(ident)):
170+
if not bool(self.re_pattern.match(ident)) and (
171+
not prefix or not prefix.from_fqcn
172+
):
164173
return MatchError(
165174
tag="var-naming[pattern]",
166175
message=f"Variables names should match {self.re_pattern_str} regex. ({ident})",
@@ -169,13 +178,13 @@ def get_var_naming_matcherror(
169178

170179
if (
171180
prefix
172-
and not ident.lstrip("_").startswith(f"{prefix}_")
173-
and not has_jinja(prefix)
174-
and is_fqcn_or_name(prefix)
181+
and not ident.lstrip("_").startswith(f"{prefix.value}_")
182+
and not has_jinja(prefix.value)
183+
and is_fqcn_or_name(prefix.value)
175184
):
176185
return MatchError(
177186
tag="var-naming[no-role-prefix]",
178-
message=f"Variables names from within roles should use {prefix}_ as a prefix.",
187+
message=f"Variables names from within roles should use {prefix.value}_ as a prefix.",
179188
rule=self,
180189
)
181190
return None
@@ -204,10 +213,13 @@ def matchplay(self, file: Lintable, data: dict[str, Any]) -> list[MatchError]:
204213
if isinstance(role, AnsibleUnicode):
205214
continue
206215
role_fqcn = role.get("role", role.get("name"))
207-
prefix = role_fqcn.split("/" if "/" in role_fqcn else ".")[-1]
216+
prefix = self._parse_prefix(role_fqcn)
208217
for key in list(role.keys()):
209218
if key not in PLAYBOOK_ROLE_KEYWORDS:
210-
match_error = self.get_var_naming_matcherror(key, prefix=prefix)
219+
match_error = self.get_var_naming_matcherror(
220+
key,
221+
prefix=prefix,
222+
)
211223
if match_error:
212224
match_error.filename = str(file.path)
213225
match_error.message += f" (vars: {key})"
@@ -220,7 +232,10 @@ def matchplay(self, file: Lintable, data: dict[str, Any]) -> list[MatchError]:
220232

221233
our_vars = role.get("vars", {})
222234
for key in our_vars:
223-
match_error = self.get_var_naming_matcherror(key, prefix=prefix)
235+
match_error = self.get_var_naming_matcherror(
236+
key,
237+
prefix=prefix,
238+
)
224239
if match_error:
225240
match_error.filename = str(file.path)
226241
match_error.message += f" (vars: {key})"
@@ -250,22 +265,25 @@ def matchtask(
250265
) -> list[MatchError]:
251266
"""Return matches for task based variables."""
252267
results = []
253-
prefix = ""
268+
prefix = Prefix()
254269
filename = "" if file is None else str(file.path)
255270
if file and file.parent and file.parent.kind == "role":
256-
prefix = file.parent.path.name
271+
prefix = Prefix(file.parent.path.name)
257272
ansible_module = task["action"]["__ansible_module__"]
258273
# If the task uses the 'vars' section to set variables
259274
our_vars = task.get("vars", {})
260275
if ansible_module in ("include_role", "import_role"):
261276
action = task["action"]
262277
if isinstance(action, dict):
263278
role_fqcn = action.get("name", "")
264-
prefix = role_fqcn.split("/" if "/" in role_fqcn else ".")[-1]
279+
prefix = self._parse_prefix(role_fqcn)
265280
else:
266-
prefix = ""
281+
prefix = Prefix()
267282
for key in our_vars:
268-
match_error = self.get_var_naming_matcherror(key, prefix=prefix)
283+
match_error = self.get_var_naming_matcherror(
284+
key,
285+
prefix=prefix,
286+
)
269287
if match_error:
270288
match_error.filename = filename
271289
match_error.lineno = our_vars[LINE_NUMBER_KEY]
@@ -290,7 +308,10 @@ def matchtask(
290308
# If the task registers a variable
291309
registered_var = task.get("register", None)
292310
if registered_var:
293-
match_error = self.get_var_naming_matcherror(registered_var, prefix=prefix)
311+
match_error = self.get_var_naming_matcherror(
312+
registered_var,
313+
prefix=prefix,
314+
)
294315
if match_error:
295316
match_error.message += f" (register: {registered_var})"
296317
match_error.filename = filename
@@ -309,7 +330,7 @@ def matchyaml(self, file: Lintable) -> list[MatchError]:
309330
if str(file.kind) == "vars" and file.data:
310331
meta_data = parse_yaml_from_file(str(file.path))
311332
for key in meta_data:
312-
prefix = file.role if file.role else ""
333+
prefix = Prefix(file.role) if file.role else Prefix()
313334
match_error = self.get_var_naming_matcherror(key, prefix=prefix)
314335
if match_error:
315336
match_error.filename = filename
@@ -330,6 +351,9 @@ def matchyaml(self, file: Lintable) -> list[MatchError]:
330351
results.extend(super().matchyaml(file))
331352
return results
332353

354+
def _parse_prefix(self, fqcn: str) -> Prefix:
355+
return Prefix("" if "." in fqcn else fqcn.split("/")[-1], is_fqcn(fqcn))
356+
333357

334358
# testing code to be loaded only with pytest or when executed the rule file
335359
if "pytest" in sys.modules:
@@ -434,6 +458,17 @@ def test_var_naming_with_pattern() -> None:
434458
assert result.returncode == RC.SUCCESS
435459
assert "var-naming" not in result.stdout
436460

461+
def test_var_naming_with_pattern_foreign_role() -> None:
462+
"""Test rule matches."""
463+
role_path = "examples/playbooks/bug-4095.yml"
464+
conf_path = "examples/roles/var_naming_pattern/.ansible-lint"
465+
result = run_ansible_lint(
466+
f"--config-file={conf_path}",
467+
role_path,
468+
)
469+
assert result.returncode == RC.SUCCESS
470+
assert "var-naming" not in result.stdout
471+
437472
def test_var_naming_with_include_tasks_and_vars() -> None:
438473
"""Test with include tasks and vars."""
439474
role_path = "examples/roles/var_naming_pattern/tasks/include_task_with_vars.yml"

src/ansiblelint/text.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,10 @@ def has_glob(value: str) -> bool:
5555
def is_fqcn_or_name(value: str) -> bool:
5656
"""Return true if a string seems to be a module/filter old name or a fully qualified one."""
5757
return bool(isinstance(value, str) and RE_IS_FQCN_OR_NAME.search(value))
58+
59+
60+
@cache
61+
def is_fqcn(value: str) -> bool:
62+
"""Return true if a string seems to be a fully qualified collection name."""
63+
match = RE_IS_FQCN_OR_NAME.search(value)
64+
return bool(isinstance(value, str) and match and match.group(1))

src/ansiblelint/utils.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
from ansiblelint.errors import MatchError
7777
from ansiblelint.file_utils import Lintable, discover_lintables
7878
from ansiblelint.skip_utils import is_nested_task
79-
from ansiblelint.text import has_jinja, removeprefix
79+
from ansiblelint.text import has_jinja, is_fqcn, removeprefix
8080

8181
if TYPE_CHECKING:
8282
from ansiblelint.rules import RulesCollection
@@ -505,21 +505,40 @@ def _get_task_handler_children_for_tasks_or_playbooks(
505505

506506
def _rolepath(basedir: str, role: str) -> str | None:
507507
role_path = None
508+
namespace_name, collection_name, role_name = (
509+
role.split(".") if is_fqcn(role) else ("", "", role)
510+
)
508511

509512
possible_paths = [
510513
# if included from a playbook
511-
path_dwim(basedir, os.path.join("roles", role)),
512-
path_dwim(basedir, role),
514+
path_dwim(basedir, os.path.join("roles", role_name)),
515+
path_dwim(basedir, role_name),
513516
# if included from roles/[role]/meta/main.yml
514-
path_dwim(basedir, os.path.join("..", "..", "..", "roles", role)),
515-
path_dwim(basedir, os.path.join("..", "..", role)),
517+
path_dwim(basedir, os.path.join("..", "..", "..", "roles", role_name)),
518+
path_dwim(basedir, os.path.join("..", "..", role_name)),
516519
# if checking a role in the current directory
517-
path_dwim(basedir, os.path.join("..", role)),
520+
path_dwim(basedir, os.path.join("..", role_name)),
518521
]
519522

520523
for loc in get_app(cached=True).runtime.config.default_roles_path:
521524
loc = os.path.expanduser(loc)
522-
possible_paths.append(path_dwim(loc, role))
525+
possible_paths.append(path_dwim(loc, role_name))
526+
527+
if namespace_name and collection_name:
528+
for loc in get_app(cached=True).runtime.config.collections_paths:
529+
loc = os.path.expanduser(loc)
530+
possible_paths.append(
531+
path_dwim(
532+
loc,
533+
os.path.join(
534+
"ansible_collections",
535+
namespace_name,
536+
collection_name,
537+
"roles",
538+
role_name,
539+
),
540+
),
541+
)
523542

524543
possible_paths.append(path_dwim(basedir, ""))
525544

test/test_utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,3 +480,13 @@ def test_find_children_in_module(default_rules_collection: RulesCollection) -> N
480480
# Child correctly looks like a YAML file
481481
assert child.base_kind == "text/yaml"
482482
assert child.content.startswith("---")
483+
484+
485+
def test_find_children_in_playbook(default_rules_collection: RulesCollection) -> None:
486+
"""Verify correct function of find_children() in playbooks."""
487+
lintable = Lintable("examples/playbooks/bug-4095.yml")
488+
children = Runner(
489+
rules=default_rules_collection,
490+
).find_children(lintable)
491+
assert len(children) == 1
492+
assert children[0].role == "bug4095"

0 commit comments

Comments
 (0)