55import keyword
66import re
77import sys
8- from typing import TYPE_CHECKING , Any
8+ from typing import TYPE_CHECKING , Any , NamedTuple
99
1010from ansible .parsing .yaml .objects import AnsibleUnicode
1111from ansible .vars .reserved import get_reserved_names
2222from ansiblelint .rules import AnsibleLintRule , RulesCollection
2323from ansiblelint .runner import Runner
2424from 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
2626from ansiblelint .utils import parse_yaml_from_file
2727
2828if 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+
3239class 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
335359if "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"
0 commit comments