Skip to content

Do not report names assigned by every if branch as undeclared - #2237

Closed
rulfox wants to merge 1 commit into
pallets:mainfrom
rulfox:fix-2069-find-undeclared-variables-if-branches
Closed

Do not report names assigned by every if branch as undeclared#2237
rulfox wants to merge 1 commit into
pallets:mainfrom
rulfox:fix-2069-find-undeclared-variables-if-branches

Conversation

@rulfox

@rulfox rulfox commented Aug 13, 2026

Copy link
Copy Markdown

meta.find_undeclared_variables reports a name that is {% set %} in every branch of an {% if %} chain ending in {% else %}, even though such a name is always defined afterwards and is never read from the context:

env.parse("{% if a %}{% set x = 1 %}{% else %}{% set x = 2 %}{% endif %}{{ x }}")
# reports {"a", "x"}; "x" cannot come from the context

Why it regressed

Symbols.branch_update used to skip the context load for a name stored by every branch. #1665 removed that optimisation to fix #1253, because it also skipped the load when a branch read the name before assigning it.

This change

Restores the optimisation behind two guards:

  1. Every branch must introduce the name as a fresh local, that is, its load is undefined rather than resolve/alias. This is the case What's wrong with this Jinja template when using {% elif %} #1253 needed.
  2. The name must not be read inside a scope FrameSymbolVisitor does not enter. Those reads are invisible to the frame's symbols but still read the frame's variables, so FrameSymbolVisitor now records them and visit_If consults them. Every field the compiler visits with a child frame is tracked: For.body/else_/test, Macro.body/defaults, CallBlock.body/defaults, FilterBlock.body/filter, With.body, AssignBlock.body/filter, Scope.body, Block.body, OverlayScope.context/body.

visit_If also tracks each {% elif %} body as its own branch, so plain {% if %}/{% else %} is recognised too. That case was reported in #2069 as failing on 3.1.4 as well.

The scan of nested scopes is lazy, so a frame with no exhaustive {% if %} chain does no extra work.

Verification

  • The three snippets in meta.find_undeclared_variables returning variables set in if blocks as of 3.1.5 #2069 (with elif, without elif, and the no-else control case) all behave as the reporter expected.
  • Differential run over ~1260 generated templates x 8 contexts covering for, for recursive, macro, call, filter, with, set/endset, block, extends, include, import, namespaces, tuple targets and nested chains: no rendered output changes, only the intended reduction in find_undeclared_variables results.
  • Compile time measured at roughly +2% on a 200-unit page template.

Tests

  • tests/test_api.py — parametrized find_undeclared_variables cases, including the negative ones: no else, a branch that does not assign, read-before-assign, read in an elif test, read in a nested scope.
  • tests/test_regression.py — rendering tests beside the existing What's wrong with this Jinja template when using {% elif %} #1253 test, covering a read in each child-frame field.
  • tests/test_idtracking.py — symbol-level assertions for an exhaustive chain and for OverlayScope.context.

Full suite: 936 passed. ruff clean; mypy reports the same pre-existing errors as main.

fixes #2069

`meta.find_undeclared_variables` reported a name that is `{% set %}` in
every branch of an `{% if %}` chain ending in `{% else %}`, even though
such a name is always defined afterwards and never read from the context.

The optimisation that recognised this was removed in pallets#1665 because it
also skipped the load when a branch read the name before assigning it,
which broke pallets#1253. Restore it with two guards: the name must be a fresh
local in every branch, and it must not be read in a nested scope, which
the frame's symbols do not see.

Track each `{% elif %}` body as its own branch as well, so `{% if %}` /
`{% else %}` without an `{% elif %}` is recognised too.

fixes pallets#2069

Co-authored-by: Claude <noreply@anthropic.com>
@davidism

Copy link
Copy Markdown
Member

@davidism davidism closed this Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

meta.find_undeclared_variables returning variables set in if blocks as of 3.1.5 What's wrong with this Jinja template when using {% elif %}

2 participants