Repository navigation
fix(scopes): visit expressions in parameter annotations - #267
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 925a49a06e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| self.expression(decorator, scope) | ||
| for parameter in node.parameters: | ||
| if parameter.annotation is not None: | ||
| self.expression(parameter.annotation, scope) |
There was a problem hiding this comment.
Preserve builtin fallback for postponed annotation walruses
With from __future__ import annotations on supported Python 3.11–3.13, visiting an annotation as a normal expression lets a comprehension's NamedExpr register its target as a module binding even though the annotation is stored as a string. For example, after def f(x: [(eval := i) for i in ()]): ..., a later function's eval(input()) still calls the builtin at runtime, but this binding makes SymbolTable.resolve return None because builtin fallback is only used for UNBOUND; consequently both the dangerous-eval and code-injection findings disappear. Register the nested annotation scope without treating this postponed target as a concrete module binding.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed and fixed in fix(scopes): bind nothing from postponed annotations. An annotation Python never evaluates, under from __future__ import annotations or on a local variable, still registers the scopes of its lambdas and comprehensions, but binds nothing in the scope it stands in. Evaluated annotations bind as before. Your example now reports dangerous-eval and code-injection, for a file and for a project (tests/test_annotation_bindings.py).
There was a problem hiding this comment.
Correction, in fix(scopes): keep what annotations declare in functions: a postponed annotation binds nothing at module level only. In a function, Python declares the target as a local whether the annotation is evaluated or not, so a nested function may declare it nonlocal, and the scope table now keeps that declaration. Your example is at module level and still reports dangerous-eval and code-injection.
Under from __future__ import annotations, an annotation is never evaluated, yet an assignment expression in one bound its target where the function is defined: a builtin it named, such as eval, resolved to a binding that never exists, and its calls were not reported. Such an annotation still registers the scopes of its lambdas and comprehensions, but binds nothing in the scope it stands in.
A variable annotation kept its expression in the HIR without registering its scopes, so a lambda or comprehension there raised KeyError. Register them as for parameters; a local variable's annotation is never evaluated (PEP 526) and binds nothing.
|
Commits added on top of 925a49a, which is kept as is:
Postponed annotations bound names they never assignThe scope collector visited a parameter annotation as an ordinary expression. An assignment expression in one therefore bound its target in the scope where the function is defined. Under from __future__ import annotations
def f(x: [(eval := i) for i in ()]):
pass
def run():
eval(input())At run time, The collector now treats annotations Python never evaluates differently:
The rule depends on no particular name. The module's Variable annotationsAn annotated variable without a value keeps its annotation in the HIR, for example
ResultsFindings reported,
The last row is intended:
TestsThe new file
On 925a49a, 7 of them fail: the postponed parameter cases, the two examples and three of the variable cases. The other 8 pass there and pin behaviour the change keeps. Each of these mutations of the fix makes at least one test fail:
Verification
|
Python's compiler declares the target of an assignment expression in an annotation whether the annotation is evaluated or not: in a function, the name is a local, which a nested function may declare nonlocal. Binding nothing from postponed and local variable annotations dropped that declaration, so a valid nonlocal raised ScopeError and the flow through it was lost. Only a module's names exist once assigned: only there does an annotation that is never evaluated bind nothing.
|
Three more commits, after a review of the previous ones:
Correction: annotations still declare names in functions
In a function, the name is a local of the function either way. On 3.11, 3.12 and 3.13, from __future__ import annotations
import os
def run():
def declared(x: [(cmd := i) for i in ()]):
pass
def set_cmd():
nonlocal cmd
cmd = input()
set_cmd()
os.system(cmd)The previous commits dropped that declaration:
Run with stand-ins for The rule is now:
This also corrects my earlier comment: a local variable's annotation is never evaluated (PEP 526), but it still declares its targets in the function. Results
The second row is intended. TestsIn
Two new tests cover the reproduction above, for a file and for a project:
Each of these mutations makes at least one test fail:
VerificationPython 3.12, this branch merged with
|
Closes #262.
The scope collector processes parameter default expressions, but not parameter annotations. When an annotation contains a lambda or comprehension, the engine's directory-analysis pass later asks for that nested expression's scope and raises KeyError instead of producing a report.
Visit represented parameter annotation expressions in their enclosing scope, before opening the declared function's scope. This registers scopes created inside positional, keyword-only, *args and **kwargs annotations without changing binding ownership of function parameters.
Tests: The baseline failed all 4 new tests; the corrected branch passed 6/6 targeted stdlib unittest tests in Python 3.11 under an offline, network-disabled, read-only, resource-limited Docker container. Tests cover CLI directory --check, annotation lambdas and comprehensions, and lexical free-variable resolution. Full mypy/pytest/ruff suites were not run because development tooling was unavailable in the permitted offline environment; no dependencies were installed.