#175 residual: 13 phantoms where a PROJECT module shadows a STDLIB module name (django/utils/{inspect,html}.py) #251

Closed
opened 2026-09-10 07:39:51 +02:00 by buildagent · 2 comments
Member

Disclosed by #175 itself ("THE RESIDUAL IS NOT ZERO", 13 phantoms added) and confirmed
independently by a bind-for-bind census of py-django between 1d3228e and bd1c4e2.
Filing it so the class has a home, with the exact sites.

The shape

import <mod> binds <mod> to a module, and #175 correctly requalifies
<mod>.func(...) into a module-qualified call. Where the project happens to contain a
file with the SAME BASENAME as the stdlib module, the qualifier now anchors on that file.

py-django ships four such collisions:

django/utils/inspect.py     shadows stdlib inspect
django/utils/html.py        shadows stdlib html
django/utils/copy.py        shadows stdlib copy
django/utils/json.py        shadows stdlib json

The 13 sites, all gained (unresolved → resolved, wrongly)

site call now binds
django/utils/inspect.py:149,151, tests/basic/tests.py:842, tests/deprecation/test_deprecate_posargs.py:421 ×2, tests/template_tests/syntax_tests/i18n/test_blocktranslate.py:38, .../test_translate.py:34 inspect.signature(...) django/utils/inspect.py#signature
django/utils/inspect.py:133,138,140 inspect.getfullargspec(...) django/utils/inspect.py#getfullargspec
django/test/html.py:177,196, django/utils/html.py:62 html.escape(...) django/utils/html.py#escape

django/utils/inspect.py line 2 is a plain import inspect, so inside that very file
inspect.signature means the STDLIB — and it now binds the file's own def signature
at line 143. A self-shadowing phantom.

Why #168's gate does not catch it

Deliberately, and the exclusion is documented in qual_root_scope_sql: "A qualifier
whose first segment names no package. No root evidence, no gate."
inspect names a
stdlib module, which has no package root in this tree, so NOT EXISTS (temp.qual_root)
passes and the stem anchor is ungated rather than refused.

That exclusion is right — refusing on absence of evidence cost recall when it was
measured. So this is not a bug in #168; it is a gap between the two rules.

Possible direction (not a decision)

The distinguishing fact is available: the call's qualifier was bound by a plain import <name> whose module string has no dotted prefix inside this project — i.e. the name
resolves to nothing the walker ever indexed. A ref whose qualifier names a module the
index does not contain is a candidate for "leave unresolved" rather than "anchor on a
basename collision". Worth measuring bind-for-bind before building; the same predicate
would also cover copy, json, os, re and every other stdlib name.

Counted and bounded, not urgent: 13 phantoms against 150 removed in the same change.

Disclosed by #175 itself ("THE RESIDUAL IS NOT ZERO", 13 phantoms added) and confirmed independently by a bind-for-bind census of py-django between `1d3228e` and `bd1c4e2`. Filing it so the class has a home, with the exact sites. ## The shape `import <mod>` binds `<mod>` to a module, and #175 correctly requalifies `<mod>.func(...)` into a module-qualified call. Where the project happens to contain a file with the SAME BASENAME as the stdlib module, the qualifier now anchors on that file. py-django ships four such collisions: ``` django/utils/inspect.py shadows stdlib inspect django/utils/html.py shadows stdlib html django/utils/copy.py shadows stdlib copy django/utils/json.py shadows stdlib json ``` ## The 13 sites, all gained (unresolved → resolved, wrongly) | site | call | now binds | |---|---|---| | `django/utils/inspect.py:149,151`, `tests/basic/tests.py:842`, `tests/deprecation/test_deprecate_posargs.py:421` ×2, `tests/template_tests/syntax_tests/i18n/test_blocktranslate.py:38`, `.../test_translate.py:34` | `inspect.signature(...)` | `django/utils/inspect.py#signature` | | `django/utils/inspect.py:133,138,140` | `inspect.getfullargspec(...)` | `django/utils/inspect.py#getfullargspec` | | `django/test/html.py:177,196`, `django/utils/html.py:62` | `html.escape(...)` | `django/utils/html.py#escape` | `django/utils/inspect.py` line 2 is a plain `import inspect`, so inside that very file `inspect.signature` means the STDLIB — and it now binds the file's own `def signature` at line 143. A self-shadowing phantom. ## Why #168's gate does not catch it Deliberately, and the exclusion is documented in `qual_root_scope_sql`: *"A qualifier whose first segment names no package. No root evidence, no gate."* `inspect` names a stdlib module, which has no package root in this tree, so `NOT EXISTS (temp.qual_root)` passes and the stem anchor is ungated rather than refused. That exclusion is right — refusing on absence of evidence cost recall when it was measured. So this is not a bug in #168; it is a gap between the two rules. ## Possible direction (not a decision) The distinguishing fact is available: the call's qualifier was bound by a plain `import <name>` whose module string has **no dotted prefix inside this project** — i.e. the name resolves to nothing the walker ever indexed. A ref whose qualifier names a module the index does not contain is a candidate for "leave unresolved" rather than "anchor on a basename collision". Worth measuring bind-for-bind before building; the same predicate would also cover `copy`, `json`, `os`, `re` and every other stdlib name. Counted and bounded, not urgent: 13 phantoms against 150 removed in the same change.
Author
Member

A sharper distinguishing fact than "the index does not contain it"

The filed direction reads: "a ref whose qualifier names a module the
index does not contain is a candidate for leave-unresolved."

That predicate does not hold on these sites, and it is worth saying why
before anyone builds it: django/utils/inspect.py IS in the index.
The qualifier inspect names something the walker indexed — that is
exactly how it acquired a wrong anchor. A rule phrased on "not in the
index" would leave all 13 sites bound.

The fact that actually separates them is a language rule:

Since PEP 328, a plain import X in Python 3 is an absolute
import of a top-level module. It can never mean a sibling file.

So inside django/utils/inspect.py, line 2's import inspect means the
stdlib, and it cannot mean the file it is written in — regardless of what
the index holds. django/utils/inspect.py is the module
django.utils.inspect; its tail is inspect, its full path is
not. The anchor is matching the tail where the import's semantics demand
the full path.

That reframes this from "absence of evidence" (which #168's
qual_root_scope_sql documents as having cost recall, correctly) to a
positive structural test, which is a much better place to stand.

The machinery is already there

Nothing new is needed to express it:

  • index::package_root_of_in (crates/indexer/src/index.rs:2513) and
    package_root_of (:2492)
  • temp.file_pkg already carries pkg_root, pkg_dir and pkg_tail
    side by side (index.rs:~4639-4705)
  • tier-1Q already compares a tail against a qualifier segment by LENGTH
    — LENGTH(sp.pkg_tail) = LENGTH(sp.seg) — and
    crates/indexer/tests/tier1q_root_scope.rs:157,320 carries a RUN
    mutation for dropping it

The candidate clause is therefore one predicate on an existing relation:
for a single-segment qualifier bound by a plain import X,
require the anchor file to be a top-level module of its package root
(pkg_dir = pkg_root), not merely a file whose stem is X. A project's
own top-level inspect.py still binds; django/utils/inspect.py is
refused.

One clause, resting on a structural fact, covering copy, json, os,
re and every other stdlib name at once — rather than a fix per
collision.

Two things that must be measured before it is built

  1. It is a PYTHON rule and must be language-scoped. Ruby, PHP and
    JavaScript do not share Python's absolute-import semantics, and this
    repository has already measured what an unscoped variant of a
    neighbouring clause costs: tier1q_root_scope.rs:19 records deriving
    pkg_tail from pkg_dir at −1244 on rust-analyzer. An unscoped
    version of this would move corpora that have nothing to do with the
    defect.

  2. Bind-for-bind, split by resolved_by, before blessing anything.
    The failure mode to look for is a project that legitimately imports
    its own top-level module sharing a stdlib name — there the new
    predicate must still bind, and a census is the only thing that shows
    it. The installed binary is a free "before" for that census, joined
    on (path, start_line, start_col, name) with kind excluded from
    the key.

Filed as analysis, not as a decision — the "counted and bounded, not
urgent" framing above still stands: 13 phantoms against 150 removed.

## A sharper distinguishing fact than "the index does not contain it" The filed direction reads: *"a ref whose qualifier names a module the index does not contain is a candidate for leave-unresolved."* That predicate does not hold on these sites, and it is worth saying why before anyone builds it: **`django/utils/inspect.py` IS in the index.** The qualifier `inspect` names something the walker indexed — that is exactly how it acquired a wrong anchor. A rule phrased on "not in the index" would leave all 13 sites bound. The fact that actually separates them is a language rule: > Since PEP 328, a plain `import X` in Python 3 is an **absolute** > import of a **top-level** module. It can never mean a sibling file. So inside `django/utils/inspect.py`, line 2's `import inspect` means the stdlib, and it cannot mean the file it is written in — regardless of what the index holds. `django/utils/inspect.py` is the module `django.utils.inspect`; its **tail** is `inspect`, its **full path** is not. The anchor is matching the tail where the import's semantics demand the full path. That reframes this from "absence of evidence" (which #168's `qual_root_scope_sql` documents as having cost recall, correctly) to a **positive structural test**, which is a much better place to stand. ## The machinery is already there Nothing new is needed to express it: - `index::package_root_of_in` (`crates/indexer/src/index.rs:2513`) and `package_root_of` (`:2492`) - `temp.file_pkg` already carries `pkg_root`, `pkg_dir` and `pkg_tail` side by side (`index.rs:~4639-4705`) - tier-1Q already compares a tail against a qualifier segment by LENGTH — `LENGTH(sp.pkg_tail) = LENGTH(sp.seg)` — and `crates/indexer/tests/tier1q_root_scope.rs:157,320` carries a RUN mutation for dropping it The candidate clause is therefore one predicate on an existing relation: for a **single-segment** qualifier bound by a **plain `import X`**, require the anchor file to be a top-level module of its package root (`pkg_dir = pkg_root`), not merely a file whose stem is `X`. A project's own top-level `inspect.py` still binds; `django/utils/inspect.py` is refused. One clause, resting on a structural fact, covering `copy`, `json`, `os`, `re` and every other stdlib name at once — rather than a fix per collision. ## Two things that must be measured before it is built 1. **It is a PYTHON rule and must be language-scoped.** Ruby, PHP and JavaScript do not share Python's absolute-import semantics, and this repository has already measured what an unscoped variant of a neighbouring clause costs: `tier1q_root_scope.rs:19` records deriving `pkg_tail` from `pkg_dir` at **−1244 on rust-analyzer**. An unscoped version of this would move corpora that have nothing to do with the defect. 2. **Bind-for-bind, split by `resolved_by`, before blessing anything.** The failure mode to look for is a project that legitimately imports its own top-level module sharing a stdlib name — there the new predicate must still bind, and a census is the only thing that shows it. The installed binary is a free "before" for that census, joined on `(path, start_line, start_col, name)` with **kind excluded** from the key. Filed as analysis, not as a decision — the "counted and bounded, not urgent" framing above still stands: 13 phantoms against 150 removed.
Author
Member

Released in v0.28.1, build 340a75a, via #264 and #265. Release CI passed, including the Windows archive round-trip smoke test. Using the actual published CLI to fresh-index pinned Django, all 13 reported inspect/html call occurrences remain present and no longer bind to the project basename collisions. The full nine-repository prior projection also reproduced exactly. This is a source-site check of the reported phantoms, separate from the synthetic precision gate. Closing this residual.

Released in [v0.28.1](https://git.h-dv.de/h-dv/code-index/releases/tag/v0.28.1), build `340a75a`, via #264 and #265. [Release CI](https://git.h-dv.de/h-dv/code-index/actions/runs/752) passed, including the Windows archive round-trip smoke test. Using the actual published CLI to fresh-index pinned Django, all 13 reported inspect/html call occurrences remain present and no longer bind to the project basename collisions. The full nine-repository prior projection also reproduced exactly. This is a source-site check of the reported phantoms, separate from the synthetic precision gate. Closing this residual.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
h-dv/code-index#251
No description provided.