#!/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 """)