A RELATIVE import anchors a file OUTSIDE the importing file's own directory subtree — from .models import reaches every models.py in the tree, and it produced 3 of #69's 5 measured phantoms #196
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
h-dv/code-index#196
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found while inspecting #69's new binds at source. Filed separately because the mechanism is upstream of #69 and shared with every tier that reads
temp.file_keys.The measurement
#69 widened tier 1R to the member pool: +4 922 binds over the nine pinned repos, 0 lost. Every one of the 4 859 binds in the first cut was censused for an ambiguous container and the 70 that were ambiguous were read at source. Five were phantoms, and three of them share one cause:
Both referencing files import
from .models import ...— a RELATIVE import, which by Python's own semantics names a module in the importing file's OWN package and nowhere else.temp.file_keysholds each file's BASENAME STEM, so the keymodelsmatchestests/indexes/models.py,tests/model_formsets/models.pyand every othermodels.pyin the repository. The origin evidence then admits one of those, and — this is what makes it a phantom rather than an ambiguity — the CORRECT target is often not even a competitor, because it lives in amodels/package whose file stem isbaseorarticle, which the import does not name at all. So the wrong file is the unique admitted candidate and the tier'sCOUNT(DISTINCT mid) = 1uniqueness test is satisfied.Why this is not #134 or #168 again
#168's two
LineIndexphantoms are the same FAMILY — a basename stem carrying no package — but its three directions all attacked the stem anchor itself, and all three are now measured and refused (see #168; direction 3's complete form costs 5 840 correct binds on the corpus). The reason those refusals were right is that in a module-per-file language the stem IS the language's naming rule:django.utils.versionreally isdjango/utils/version.py.This issue is the narrower and structurally safer half of the same problem. It does not ask whether a stem is good evidence. It says: a relative import cannot name a file outside its own directory subtree, in any language that has relative imports. That is not a heuristic, it is what
.means.qual_infoalready carries anis_relativeflag, andREL_MARKERSalready encodes the same idea for qualifier heads.Proposed clause, unimplemented and unmeasured
Where an anchor is derived from a RELATIVE import (
is_relative = 1, or animports.modulebeginning with.), require the anchored file to be under the importing file's directory subtree. Everything else is untouched.The subtree test does not need a path GLOB: #69 already builds
temp.file_ancestor(each file mapped to every directory prefix that contains it) for exactly this shape, so it is an equality join. It exists only inside tier 1R's block today and would need hoisting.What must be measured before it lands, and why it is NOT in #69
(path, line, col, kind, name, target, rule). Position alone fans out.from .. import xand re-export chains (from .models import *in a package__init__.py) legitimately reach outside the immediate directory, and JavaScript'srequire('../lib/x')is relative but explicitly escapes the subtree — the clause must be about the RESOLVED relative target, not about the string starting with a dot.file_keysfeeds tier 1b's import rules, tier 1Q's anchors, tier 3's reachability and tier 1R's origin relation. A clause at thefile_keyslevel moves all of them; a clause at one tier's anchor moves one. The measurement should say which was done.It was deliberately kept out of #69: that change is scoped to tier 1R, and moving the corpus for a second, unrelated reason in one worktree makes the deltas uninspectable — the same reason #69 itself was deferred once.
Current containment
#69 ships a nearer-competitor gate that refuses a member-pool bind when a competing candidate sits under the referencing file's own subtree and the chosen one does not. That removes these three phantoms for member access only. The underlying anchor is unchanged and still serves every other tier.
Related
#134 (a basename stem carries no package), #168 (the same family through tier 1Q; all three of its directions measured and refused), #69 (where these three were found).
Consumer detail for the lane working this: your clause reaches ALL 18 of #69's phantoms, and its blast radius on #69 is at most 32 binds
#69 is being held behind this issue, so here is the evidence for the sequencing rather than the assumption.
Every one of the 18 travels a relative import
The 18 phantoms #69 measured (12
Article.id, 6User.email, all crossing between unrelated Django test apps) come from five referencing files. Checked at source:All five.
from .models importnormalizes to the keymodels, whichtemp.file_keysholds as the basename stem of everymodels.pyin the repository — sotests/prefetch_related/models.pyandtests/select_related_onetoone/models.pyare admitted as origins for files that can only mean their own app's. That is exactly the shape this issue describes.Four of them RETARGET rather than fall silent
Worth knowing, because it changes what your acceptance test should assert.
tests/composite_pk/models/tenant.py:20isemail = models.EmailField(unique=True)— the app's ownUserdoes declareemail. So fortests/composite_pk/test_create.py:34,49,55,56your clause does not remove a bind, it moves it to the right symbol:The other 14 have no local candidate (
Article.idis Django's implicit primary key;auth_tests'Userisdjango.contrib.auth's), so they become honest silence. Both outcomes are right, and an acceptance test that only checks "the phantom is gone" would not tell them apart.Blast radius on #69's 4,922 new binds: at most 32, all py-django
Measured — for each new bind whose target sits outside the referencing file's own directory subtree, does that file carry a relative import whose last segment equals the target file's basename stem:
18 of the 32 are the phantoms, so the collateral on #69 is at most 14 correct binds, and that 32 is an UPPER bound —
recv_originhas package-tail and non-relative-import disjuncts that may still admit some of those files after your clause.Nothing on rust-analyzer, which carries 3,231 of #69's 4,922. So the two changes are close to orthogonal, which is what makes sequencing them cheap.
Two cautions for your measurement, not for mine
temp.file_keys, which feeds tier 1b's import rules, tier 1Q's anchors, tier 3's reachability and tier 1R's origin relation — so the pre-existing bind set will move too, on all nine repos, and that is the pass this issue already calls for.from .models import *and package-shapedmodels/directories are the common Django spelling; three of the five files above import from amodels/PACKAGE rather than amodels.py. A clause written against "the target file's stem" will behave differently from one written against "the resolved relative target", which this issue's own text already warns about. Thetests/composite_pkcase is the one to build the fixture on — it is a package, and it is the one that retargets.REFUTED, and re-aimed — the real defect was elsewhere and is now fixed. Investigated in
cbeb655; the actual repair lands with #69/#203.Two of this issue's premises are stale, and the clause it proposes already exists in a stronger form:
temp.file_ancestorexists in no file undercrates/ate5775ae— index andgrepagree. It lives only in the then-unmerged #69 branch, so the "cheap, it's an equality join" route was never available on master.qual_info.is_relativeis real but is about qualifier heads (self::,static::, PHP relative namespaces), not import modules — a different population entirely.temp.import_rel(#57),crates/indexer/src/index.rs:4249-4330, built fromrelative_import_paths(:1022) andresolution_candidates(:1069). It path-RESOLVES a relative specifier to its actual target file, which strictly dominates the subtree test proposed here. Andtemp.import_key_rel's build already skips relative specifiers outright (:4611), a fact restated in tier 1b's SQL at:4906.So on master,
from .models import …cannot lend the stemmodelsto an arbitrarymodels.pythrough the import-key arm. The subtree test would have been a weaker duplicate of an exclusion already in place.Where the phantoms actually came from.
build_recv_origin(#65) enumerates import keys under a comment claiming it is "exactly the same enumerationtemp.import_key_relis built from" — and reproduces that enumeration without the exclusion sitting beside it. A prose claim the code contradicted; filed as #203.The fix is five lines: consult
temp.import_relfor a relative specifier, keep the stem lookup for absolute ones. Measured over nine repos it removes 16 of 18 phantoms while leaving the correct-bind count unchanged at 4,904, and separately removes 38 pre-existing phantoms. The alternative — skip relative specifiers with no replacement, the literal mirror of:4611— was built and refused on numbers: 48 pre-existing binds lost, 369 new ones, and ts-zod falling from 88 to 3, because TypeScript imports are almost entirely relative.One nuance recorded for whoever revisits this area: for this issue's own example,
relative_import_pathsresolves.modelstotests/foreign_object/models/__init__.py, while the correct target is reachable only through a re-export the index does not model. On the measured corpus that did not bite — py-django was the only repo whose binds moved at all — but it is the shape to watch.Closing as refuted; the work it pointed at is #203.
user.emailbinds across unrelated Django test apps via TIER1R_RECEIVER — 2 live phantoms on master, and #199's own comment says they were gone #246