143 lines
4.3 KiB
Python
143 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Explore Jinja2's AST to understand variable detection in conditionals"""
|
|
|
|
from jinja2 import Environment, meta, nodes
|
|
|
|
# Test different template patterns
|
|
templates = [
|
|
# Simple required variable
|
|
("simple", "Hello {{ name }}!"),
|
|
|
|
# Variable in conditional (optional)
|
|
("conditional", """{% if error %}
|
|
Error: {{ error }}
|
|
{% endif %}"""),
|
|
|
|
# Mixed required and optional
|
|
("mixed", """Status: {{ status }}
|
|
{% if error %}
|
|
Error: {{ error }}
|
|
{% endif %}"""),
|
|
|
|
# Nested conditionals
|
|
("nested", """{% if user %}
|
|
Name: {{ user.name }}
|
|
{% if user.email %}
|
|
Email: {{ user.email }}
|
|
{% endif %}
|
|
{% endif %}"""),
|
|
|
|
# Variable with default filter (makes it optional)
|
|
("default_filter", "Hello {{ name | default('Guest') }}!"),
|
|
|
|
# Variable in for loop
|
|
("for_loop", """{% for item in items %}
|
|
- {{ item }}
|
|
{% endfor %}"""),
|
|
|
|
# Complex with multiple contexts
|
|
("complex", """Required: {{ required }}
|
|
{% if optional1 %}
|
|
Optional1: {{ optional1 }}
|
|
{% endif %}
|
|
Default: {{ optional2 | default('N/A') }}
|
|
{% for item in items %}
|
|
- {{ item }}
|
|
{% endfor %}""")
|
|
]
|
|
|
|
env = Environment()
|
|
|
|
print("=" * 70)
|
|
print("Analyzing Jinja2 AST for Variable Detection")
|
|
print("=" * 70)
|
|
|
|
for name, template in templates:
|
|
print(f"\n{name.upper()} TEMPLATE:")
|
|
print("-" * 40)
|
|
print(template)
|
|
print("-" * 40)
|
|
|
|
# Parse the template
|
|
ast = env.parse(template)
|
|
|
|
# Get undeclared variables (what we currently use)
|
|
undeclared = meta.find_undeclared_variables(ast)
|
|
print(f"Undeclared variables: {undeclared}")
|
|
|
|
# Try to analyze the AST more deeply
|
|
print("\nAST Analysis:")
|
|
|
|
def analyze_node(node, indent=0, context="root"):
|
|
"""Recursively analyze AST nodes"""
|
|
prefix = " " * indent
|
|
|
|
if isinstance(node, nodes.Name):
|
|
print(f"{prefix}Variable '{node.name}' in context: {context}")
|
|
|
|
elif isinstance(node, nodes.If):
|
|
print(f"{prefix}If block:")
|
|
print(f"{prefix} Test expression:")
|
|
analyze_node(node.test, indent + 2, "if_test")
|
|
print(f"{prefix} Body:")
|
|
for child in node.body:
|
|
analyze_node(child, indent + 2, "if_body")
|
|
if node.elif_:
|
|
print(f"{prefix} Elif:")
|
|
for child in node.elif_:
|
|
analyze_node(child, indent + 2, "elif")
|
|
if node.else_:
|
|
print(f"{prefix} Else:")
|
|
for child in node.else_:
|
|
analyze_node(child, indent + 2, "else")
|
|
|
|
elif isinstance(node, nodes.For):
|
|
print(f"{prefix}For loop:")
|
|
print(f"{prefix} Target: {node.target}")
|
|
print(f"{prefix} Iter:")
|
|
analyze_node(node.iter, indent + 2, "for_iter")
|
|
print(f"{prefix} Body:")
|
|
for child in node.body:
|
|
analyze_node(child, indent + 2, "for_body")
|
|
|
|
elif isinstance(node, nodes.Filter):
|
|
print(f"{prefix}Filter: {node.name}")
|
|
if node.name == "default":
|
|
print(f"{prefix} (Makes variable optional!)")
|
|
analyze_node(node.node, indent + 1, f"filter_{node.name}")
|
|
|
|
elif isinstance(node, nodes.Getattr):
|
|
print(f"{prefix}Attribute access: .{node.attr}")
|
|
analyze_node(node.node, indent + 1, "getattr")
|
|
|
|
elif isinstance(node, nodes.Output):
|
|
print(f"{prefix}Output:")
|
|
for child in node.nodes:
|
|
analyze_node(child, indent + 1, "output")
|
|
|
|
elif isinstance(node, nodes.TemplateData):
|
|
# Skip raw text
|
|
pass
|
|
|
|
else:
|
|
# Recurse into child nodes
|
|
for child in node.iter_child_nodes():
|
|
analyze_node(child, indent, context)
|
|
|
|
analyze_node(ast)
|
|
|
|
print("\n" + "=" * 70)
|
|
print("INSIGHTS:")
|
|
print("-" * 70)
|
|
print("""
|
|
1. Jinja2's `find_undeclared_variables()` returns ALL variables, regardless of context
|
|
2. Variables in {% if %} conditions could be considered optional
|
|
3. Variables with | default() filter are definitely optional
|
|
4. We can walk the AST to identify context for each variable
|
|
|
|
Possible solution:
|
|
- Walk the AST to categorize variables by context
|
|
- Mark variables in if_test contexts as optional (unless also used elsewhere)
|
|
- Mark variables with default filter as optional
|
|
- Consider all others as required
|
|
""") |