Fix variable definition test to detect set_fact and ansible.builtin.set_fact (both block and inline forms)

- Support fully qualified ansible.builtin.set_fact
- Parse inline set_fact mappings (e.g. set_fact: { a: 1, b: 2 })
- Continue scanning inside vars/set_fact blocks for Jinja {% set %}, {% for %}, and {% macro %}
- Ensures variables defined by set_fact are correctly recognized as defined
This commit is contained in:
Kevin Veen-Birkenbach 2025-08-16 23:51:27 +02:00
parent 2620ee088e
commit 1126765da2
No known key found for this signature in database
GPG Key ID: 44D8F11FD62F878E

View File

@ -41,24 +41,28 @@ class TestVariableDefinitions(unittest.TestCase):
# Simple {{ var }} usage with optional Jinja filters after a pipe
self.simple_var_pattern = re.compile(r"{{\s*([a-zA-Z_]\w*)\s*(?:\|[^}]*)?}}")
# {% set var = ... %}
# {% set var = ... %} (allow trimmed variants)
self.jinja_set_def = re.compile(r'{%\s*-?\s*set\s+([a-zA-Z_]\w*)\s*=')
# {% for x in ... %} or {% for k, v in ... %}
# {% for x in ... %} or {% for k, v in ... %} (allow trimmed variants)
self.jinja_for_def = re.compile(
r'{%\s*-?\s*for\s+([a-zA-Z_]\w*)(?:\s*,\s*([a-zA-Z_]\w*))?\s+in'
)
# {% macro name(param1, param2=..., *varargs, **kwargs) %}
# {% macro name(param1, param2=..., *varargs, **kwargs) %} (allow trimmed variants)
self.jinja_macro_def = re.compile(
r'{%\s*-?\s*macro\s+[a-zA-Z_]\w*\s*\((.*?)\)\s*-?%}'
)
# Ansible YAML anchors for inline var declarations
self.ansible_set_fact = re.compile(r'^(?:\s*[-]\s*)?set_fact\s*:\s*$')
# Support short and FQCN forms, plus inline dict after colon
self.ansible_set_fact = re.compile(
r'^(?:\s*-\s*)?(?:ansible\.builtin\.)?set_fact\s*:\s*(\{[^}]*\})?\s*$'
)
self.ansible_vars_block = re.compile(r'^(?:\s*[-]\s*)?vars\s*:\s*$')
self.ansible_loop_var = re.compile(r'^\s*loop_var\s*:\s*([a-zA-Z_]\w*)')
self.mapping_key = re.compile(r'^\s*([a-zA-Z_]\w*)\s*:\s*')
self.register_pat = re.compile(r'^\s*register\s*:\s*([a-zA-Z_]\w*)')
# -----------------------
# Collect "defined" names
@ -85,6 +89,7 @@ class TestVariableDefinitions(unittest.TestCase):
path = os.path.join(root, fn)
# Track when we're inside set_fact:/vars: blocks to also extract mapping keys.
in_set_fact = False
set_fact_indent = 0
in_vars_block = False
@ -96,63 +101,75 @@ class TestVariableDefinitions(unittest.TestCase):
stripped = line.lstrip()
indent = len(line) - len(stripped)
# --- set_fact block keys
if self.ansible_set_fact.match(stripped):
# --- set_fact (short and FQCN), supports inline and block forms
m_sf = self.ansible_set_fact.match(stripped)
if m_sf:
inline_map = m_sf.group(1)
if inline_map:
# Inline mapping: set_fact: { a: 1, b: 2 }
try:
data = yaml.safe_load(inline_map)
if isinstance(data, dict):
self.defined.update(
k for k in data.keys() if isinstance(k, str)
)
except Exception:
pass
# do not enter block mode if inline present
in_set_fact = False
else:
# Block mapping: keys on subsequent indented lines
in_set_fact = True
set_fact_indent = indent
# continue to next iteration to avoid double-processing this line
continue
if in_set_fact:
# Still inside set_fact child mapping?
if indent > set_fact_indent and stripped.strip():
m = self.mapping_key.match(stripped)
if m:
self.defined.add(m.group(1))
continue
# do not continue; still scan for Jinja defs below
else:
# Leaving the block when indentation decreases or a new key at same level appears
if indent <= set_fact_indent and stripped:
in_set_fact = False
# --- vars: block keys
# --- vars: block (collect mapping keys)
if self.ansible_vars_block.match(stripped):
in_vars_block = True
vars_block_indent = indent
# continue to next line to avoid double-processing this line
continue
if in_vars_block:
# Ignore blank lines inside vars block
if not stripped.strip():
continue
# Still inside vars child mapping?
if indent > vars_block_indent:
# Inside vars: collect top-level mapping keys
if indent > vars_block_indent and stripped.strip():
m = self.mapping_key.match(stripped)
if m:
self.defined.add(m.group(1))
continue
# do not continue; still scan for Jinja defs below
else:
# Leaving vars block
if indent <= vars_block_indent and stripped:
in_vars_block = False
# --- loop_var
m_loop = self.ansible_loop_var.match(stripped)
if m_loop:
self.defined.add(m_loop.group(1))
# --- Always scan every line (including inside blocks) for Jinja definitions
# --- register: name
m_reg = re.match(r'^\s*register\s*:\s*([a-zA-Z_]\w*)', stripped)
if m_reg:
self.defined.add(m_reg.group(1))
# --- {% set var = ... %}
# {% set var = ... %}
for m in self.jinja_set_def.finditer(line):
self.defined.add(m.group(1))
# --- {% for x [ , y ] in ... %}
# {% for x [, y] in ... %}
for m in self.jinja_for_def.finditer(line):
self.defined.add(m.group(1))
if m.group(2):
self.defined.add(m.group(2))
# --- {% macro name(params...) %} -> collect parameter names
# {% macro name(params...) %}
for m in self.jinja_macro_def.finditer(line):
params_blob = m.group(1)
# Split by comma at top level (macros don't support nested tuples in params)
params = [p.strip() for p in params_blob.split(',')]
for p in params:
if not p:
@ -163,6 +180,16 @@ class TestVariableDefinitions(unittest.TestCase):
name = p.split('=', 1)[0].strip()
if re.match(r'^[a-zA-Z_]\w*$', name):
self.defined.add(name)
# --- loop_var and register names
m_loop = self.ansible_loop_var.match(stripped)
if m_loop:
self.defined.add(m_loop.group(1))
m_reg = self.register_pat.match(stripped)
if m_reg:
self.defined.add(m_reg.group(1))
except Exception:
# Ignore unreadable files
pass