Skip to content

fix: keep files sharing a Python module name as distinct modules - #190

Merged
SizzleUnrlsd merged 18 commits into
mainfrom
fix/module-identity
Sep 29, 2026
Merged

SizzleUnrlsd merged 18 commits into
mainfrom
fix/module-identity

Conversation

@SizzleUnrlsd

Copy link
Copy Markdown
Contributor

Closes #182.

Two files with the same Python module name, a/app.py and z/app.py outside any package, overwrote each other in the module table: the file found last was the only one analysed, so the other's findings never appeared, and which one that was depended on discovery order. The regression corpus shows it: pygoat's dockerized_labs/insec_des_lab/main.py, hidden behind introduction/playground/A9/main.py, carries an insecure deserialization (pickle.loads of a posted form field, line 36) that no run reported.

  • name_modules(root, sources) names each file for the engine: its import name when unique, otherwise its path from the root as a dotted name (a.app, z.app), or the POSIX path when even those coincide; decided over the whole set, so independent of the order. module_name, nodes.Module.name and project_symbol are unchanged, so a model or a route naming python.app.main applies to every definition of that symbol.
  • build_module_graph resolves an import of a shared name to the file in the importer's own import root (the module_name_for walk) only; without exactly one there it records the name as unresolved, no edge. A unique name resolves as before, from any directory.
  • Each colliding file carries an ambiguous-module note and is covered as ambiguous; an importer whose import stayed unresolved gets a note too, since calls into it are not followed. files_analysed counts analysed files only, so a VEX statement over such a project stays under_investigation rather than not_affected (vex.py unchanged: any status but analysed already does that; a test pins it).
  • Component seeds are keyed by import name, what the component's code refers to a module by; of several files with one import name, only the one a member imports directly, resolved in its root, is registered, the others being reached through summaries that inline them already, and a name no member's root resolved is followed into neither file. The final index and the cache keys use the engine's module key; _analyse_managers registers a name two members of one component share for neither.

Regression snapshots re-recorded for DjangoGoat, httpie and pygoat, with the owner's authorization, for exactly these changes: the recovered files (+2, +1, +2), their functions (+14, +2, +8), one ambiguous-module note per colliding file, files_analysed lowered by exactly those files, and the pygoat finding above. Every colliding file is analysed in full (analysed == functions), each with its own findings; no other line changes.

Tests: tests/test_module_identity.py (16 cases, failing before the change: discovery and coverage of both files, findings whatever the discovery order and job count, coinciding dotted paths, root-scoped import resolution, unresolved imports reported and not followed, the seed of a component reaching both files, a package and a script sharing a name, index and cache keys, project plugins, JSON report, VEX status); pytest -m "not regression" 1438 passed, 2 skipped; regression corpus 19 passed on the re-recorded snapshots at the pinned revisions; ruff check . and mypy clean.

…ocessor

A TEMPLATES bound in a function, called or not, or in a class configures nothing; a function reading, mutating or rebinding the module's TEMPLATES leaves the activation uncertain; a local TEMPLATES beside module settings changes nothing.
request_processor walked every node of every module and counted any assignment to TEMPLATES, so a function-local or class-level assignment activated the request context processor. It now walks the modules with their scope tables: an assignment counts only at module scope, and a mention counts only when the name resolves to the module's binding, or to nothing, as a star import gives. ScopeTable.scope_for accepts a lambda, whose scope the builder already records.
…e flow

A block rendering {{ block.super }} extends the parent's block: its filters stay fed by the render context, with the parent template as the place of the call, however block.super is rendered; a block without it replaces the parent's whole block; other overridden blocks stay excluded; a chain of templates keeps exactly the parents each level renders, once.
A template extending another replaced every block it defined in the parent, so the parent's filter applications dropped out of the render context flow even when the child rendered {{ block.super }}, which Django executes. A template now records the blocks that render block.super, and only the blocks defined without it are passed up as replaced, level by level.
A deployment declaration names its handler as the module binds it, and a module may bind that name as an alias of the function doing the work: main = actual. A model naming the alias's project symbol now applies to the function, or to the class's methods for a class alias. Aliases are module-level assignments of one name to another, followed transitively in source order; a name bound inside a function or class, or to a call, an import or any other value, is no alias.
…-ins

A project tag library registering a filter of a built-in's name replaces it in every template loading the library before applying the filter, as Django's parser does; loads are ordered, selective loads honoured, Django's own libraries change nothing. A library the engine cannot find, read or understand, or several apps disagreeing, leaves the filter uncertain: no call is claimed and the load is listed among the unread templates, keeping the advisory under investigation (#183).
Name every module by its file: the import name when unique, otherwise its path from the root as a dotted name. Resolve an import of a shared name within the importer's root only and record the ones no root resolves. Report colliding files with an ambiguous-module note and cover them as ambiguous; key component seeds by import name, registering a shared name only for the file a member imports directly, and the final index by module key (#182).
DjangoGoat, httpie and pygoat each hold files sharing an import name that the engine used to overwrite: the recovered files, their functions and one ambiguous-module note per colliding file are now recorded, files_analysed counts analysed files only, and pygoat's dockerized_labs/insec_des_lab/main.py reports the insecure deserialization it always held (#182).
@SizzleUnrlsd SizzleUnrlsd added the bug Something isn't working label Sep 28, 2026
@SizzleUnrlsd SizzleUnrlsd self-assigned this Sep 28, 2026
@SizzleUnrlsd
SizzleUnrlsd merged commit ea13376 into main Sep 29, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P1] Preserve distinct modules when files share the same Python module name

1 participant