Skip to content

Python: inline init_module_submodule_defn into ImportResolution#21930

Open
yoff wants to merge 2 commits into
mainfrom
yoff/python-dataflow-noop-simplifications
Open

Python: inline init_module_submodule_defn into ImportResolution#21930
yoff wants to merge 2 commits into
mainfrom
yoff/python-dataflow-noop-simplifications

Conversation

@yoff
Copy link
Copy Markdown
Contributor

@yoff yoff commented Jun 2, 2026

Factored out of #21925 (shared-CFG migration).

The new-dataflow ImportResolution module only imported semmle.python.essa.SsaDefinitions to call the 5-line helper SsaSource::init_module_submodule_defn. Inlining it locally drops the dependency on legacy SSA definitions — making new/internal/ImportResolution.qll independent of essa/.

Semantic-noop argument

  • SsaSourceVariable.getName() and Variable.getId() both project the same DB column variable(_, _, result), so the rename in the inlined body is safe.
  • The old call init_module_submodule_defn(var.getSourceVariable(), package.getEntryNode()) constrained init = package via the join init.getEntryNode() = package.getEntryNode() plus var.getScope() = init. With Scope.getEntryNode() being functional, this is equivalent to passing package directly.
  • The var instanceof GlobalVariable check in the old body is now expressed via the parameter type.

Verification

  • RA dump of python/ql/test/library-tests/dataflow/global-flow/accesses.ql shows only the expected predicate-rename shuffle (no semantic shape change in any other predicate).
  • All 70 tests under python/ql/test/library-tests/dataflow and python/ql/test/library-tests/ApiGraphs pass without reblessing.

Investigation note (negative result worth recording)

I had originally hoped to fold two other ESSA-bypass rewrites into this PR — switching definitionFlowStep from AssignmentDefinition to DefinitionNode + NameNode, and switching ModuleVariableNode.getAWrite from EssaNodeDefinition.definedBy to NameNode.defines — but they are not semantic noops on main. Old ESSA's EssaNodeDefinition (and hence AssignmentDefinition) is liveness-pruned at SsaCompute.qll:108-113, so bypassing it reintroduces dead writes. accesses.ql immediately picks up the dead g_mod = [] line. Those rewrites have to stay on the flip PR (#21925) where they're justified by the shared-SSA prune semantics.

Stacked under #21926.

yoff and others added 2 commits June 1, 2026 14:04
The internal predicates that identify `@staticmethod`, `@classmethod` and
`@property` decorators previously required the decorator's `NameNode` to
satisfy `isGlobal()` (i.e. no SSA def reaches the decorator's name use).
That filter was correct but unnecessarily indirect: these three names
are builtins, and even when a class body redefines one, the class body
has not started executing at the decorator position, so Python uses the
builtin.

Match the decorator's AST `Name` directly instead, dropping the CFG/SSA
detour. The slight semantic change — `isGlobal()` would have rejected
module-level shadowing of these builtins — is negligible in practice
and explicitly documented in the change note.

`hasContextmanagerDecorator` and `hasOverloadDecorator` keep the
`NameNode.isGlobal()` check because their target names (`contextmanager`,
`overload`) are imported, not builtin, and local shadowing is a real
concern.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The new-dataflow ImportResolution module only used
semmle.python.essa.SsaDefinitions for the 5-line helper predicate
SsaSource::init_module_submodule_defn. Inline it locally and drop the
dependency on legacy SsaDefinitions. This is the only remaining direct
import of semmle.python.essa.* in the new dataflow stack, so dropping
it makes the layering cleaner.

Semantic noop on the current SSA: SsaSourceVariable.getName() and
GlobalVariable.getId() both project the same DB column
(variable(_,_,result)), and the old call's 'init.getEntryNode() = f'
join was just constraining init = package via Scope.getEntryNode()'s
functional uniqueness. RA dump of accesses.ql confirms only the
expected predicate-rename shuffle; all 70 dataflow + ApiGraphs library
tests pass.

This factors out commit 8cab5a2 from the larger shared-CFG
migration #21925.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 2, 2026 08:24
@yoff yoff requested a review from a team as a code owner June 2, 2026 08:24
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR removes an unnecessary dependency on legacy Python ESSA definitions in the new-dataflow import resolution logic by inlining a small helper predicate, and simplifies internal decorator-detection predicates to rely on direct AST matching (with a corresponding change note).

Changes:

  • Inlined SsaSource::init_module_submodule_defn into ImportResolution.qll to avoid importing semmle.python.essa.SsaDefinitions.
  • Simplified internal predicates for detecting @staticmethod, @classmethod, and @property to match AST Name nodes directly instead of going via CFG + isGlobal().
  • Added a change note documenting the (rare) behavior change when these decorator names are shadowed.
Show a summary per file
File Description
python/ql/lib/semmle/python/dataflow/new/internal/ImportResolution.qll Inlines a small helper predicate to drop an ESSA definitions dependency from the new dataflow stack.
python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll Switches decorator detection for static/class/property decorators to direct AST Name matching with updated rationale comments.
python/ql/lib/change-notes/2026-06-01-decorator-predicate-simplification.md Documents the decorator predicate simplification and the (rare) shadowing-related semantic change.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 2

Comment on lines +259 to +263
// The decorator is *syntactically* a `Name` "staticmethod" — we don't
// care which variable it resolves to. `staticmethod` is a builtin and
// is almost never shadowed in a module-level scope; even if a class
// redefines `staticmethod` in its body, the class body has not started
// executing yet at the decorator position, so Python uses the builtin.
---
category: minorAnalysis
---
* Simplified the internal predicates that detect `@staticmethod`, `@classmethod` and `@property` decorators to match the decorator's AST `Name` directly, rather than going through the CFG and requiring the name to resolve globally. Code that shadows these three builtin decorators at the module-scope will now be classified by the decorator name alone; in practice, shadowing these names is extremely rare and the call-graph results are unchanged.
exists(NameNode id | id.getId() = "classmethod" and id.isGlobal() |
func.getADecorator() = id.getNode()
)
// See `isStaticmethod` for the rationale for matching on the AST `Name`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could refactor the functionality into a predicate hasDecorator(string id) { ...} and then transfer the comment to the re-usable implementation. This probably applies to any possible decorator

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe hasBuiltinDecorator since the comment applies for code using python features and less reliably so if somebody defines a random decorator

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants