Resolved references can name unrelated methods, global names and function-local types #531
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.
Blocks
Reference
h-dv/code-index#531
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?
Scope (consolidated 2026-11-12)
Caller and impact results can name a project declaration that the source cannot reach. A matching method name, file stem or qualifier is insufficient evidence of receiver identity. Generic qualifiers can also be mistaken for concrete types, exposing declarations local to another function. This ticket covers wrong targets; unresolved uses and missing extraction are tracked in #536 and #540.
Priority: Critical · Risk: wrong answer · Size: L
Merged into this ticket: #566 (Residual audited wrong binds), #579 (Generic qualifier binds local type)
Done
crates/indexer/src/index/name_only.rsandcargo_scope.rs(e1420994; released v0.41.48).rooted_paths_require_exact_global_identityanda_global_receiver_keeps_its_proven_inherited_memberincrates/indexer/tests/global_roots.rscover identity and inheritance.crates/indexer/src/index/qualifier_modules.rs;an_imported_qualifier_never_binds_what_its_file_cannot_reach,a_namespace_import_never_names_a_class_memberandanother_types_impl_block_is_on_no_chainincrates/indexer/tests/qualifier_scope.rscover the v0.41.50 subset.Visincrates/indexer/tests/configuration_reference.rs; this avoids the gate failure and does not fix the resolver.Remaining
Receiver identity
ENFORCE_EVIDENCELESS_STD_SURFACE = falseinname_only.rs); reproduce the remaining receiver cases in the audit. (#531, #566)Namespace and lexical identity
::Logger, Rust exact module/type-path and private-alias rows, and residual file-isolation/receiver rows individually; reproduction and audit table in #566. Coordinate shared scope mechanisms with #536. (#566)Regression qualification
Acceptance
Suggested order
Original report
Summary
Name-only tiers bind method calls whose real target is a builtin, std or global API to the only project method of the same name. These are wrong binds. They show up in
find_callersandchange_impact, including callers in other crates and in test-only code. Reproduced in Rust and TypeScript on pinned corpus repositories (installed build2dcfc615, which is behind master only by XAML/CI commits).Rust: rust-ripgrep and rust-analyzer
crates/cli/src/decompress.rs:137,139:program.as_ref()/a.as_ref()on a genericP: AsRef<OsStr>binds toimpl AsRef<Glob> for Globin another crate (crates/globset/src/glob.rs:84), withresolved_by: tier1b_container_import. All 15 resolved refs of that impl are phantoms.ignore::Error::clone(crates/ignore/src/lib.rs:123): 73 binds, mostly.clone()onIgnore/Arc, viatier1b_file_key_import.label.as_bytes()on&str(searcher/mod.rs:138) binds to a#[cfg(test)] KitchenSink::as_bytes.err.kind()onio::Errorbinds toSinkContext::kind.ent.file_type()onfs::DirEntrybinds viatier1r_receiver.projections.pushon aVecbinds toCaptureSourceStack::push, andcrate_ids.iter()toMemDocs::iter.tier3_import_booston method calls: RA 9/20, ripgrep (non-test) 9/15.method_callbinds by name-only rules with a receiver other thanself): ripgrep 182, RA 1,903.TypeScript: ts-zod
JSON.parse(...)(v4/core/json-schema-processors.ts:501,509,to-json-schema.ts:507,classic/from-json-schema.ts:641) binds to$ZodTypeInternals.parse/ZodType.parseviatier3_import_boost.Object.defineProperty(this, ...)×4 andReflect.defineProperty(v4/core/util.ts:230,261,274,293,562) bind to a Proxy-handler literal method viatier1a_unique_own_file.Not seen
C#, Python, Ruby, JS and PHP samples showed 0 wrong binds of this shape (C#: 318 checked; Python: ~360; Ruby: 617; JS: 29).
Why this is one defect
Every case has the same shape: a
method_callwhose receiver evidence does not name the candidate's container, with a name that also exists on the language's builtin/prelude/global surface, bound by a tier that proves only "the name is unique here" or "the file is imported".tier1b_file_key_import/tier1b_container_importaccept an import of the candidate's file or container module, not of its self type.Proposal (one generic rule)
method_callwhen either of these holds:JSON,Object,Reflect,Math, …); the profile declares the global set, extendingis_ts_builtin_globalbeyond type heads; orclone,as_ref,iter,push,kind,len, …). Theinfluence: builtin_onlycensus already exists for this.self/this, a proven receiver) may still decide.tier1b_*_importfor a method requires the import to name the method's impl self type, not merely its file.Acceptance
(path,line,col,kind,occurrence)and split byresolved_by(per CLAUDE.md), shows only removals of this shape. Sample the unchanged binds too.precision_gategains decoy sites:JSON.parseagainst a projectparse, a generic.as_ref()against a projectAsRefimpl, and.clone()against a projectCloneimpl.Related: #270 (receiver-binding phantom: the same family, a different tier). Evidence:
lang-out/rust/ph_*.json,sample_mc.txt,ra_sample_rule30.txt;lang-out/typescript/builtin_phantoms.txt.Filed from the per-language review on 2026-11-09: 8 agents, one per language plus one for Vue/Svelte/XAML/timeline, run against copies of the pinned corpus repos (
tests/corpus/corpus.toml) using installed build2dcfc615, which differs from master only by XAML/CI commits. The top claim of every issue was spot-checked in the main session. Artifacts live under the review session's scratchpadlang-out/<lang>/and are available on request.Wrong binds: name-only tiers bind builtin/std/global method calls to project methods (Rust .as_ref/.clone/.push, TS JSON.parse/Object.defineProperty)to Wrong binds: builtin/std/global-named method calls (Rust, TS), PHP parent:: and \Foo, residual django bindsreadofpkg.digestbinds a same-named pub field in another crate, and localising the receiver does not move it #270Field case (dogfood 2026-11-09):
tier3_import_boostbinds stdWrite::flushto a test-local impl in ANOTHER test cratefind_callers(<Capture::flush in crates/daemon/tests/activation_offer_licence.rs:172>)returns 4 resolved callers:builtin_displacement_e2e.rs:725input.flush()on aChildStdinbuiltin_displacement_e2e.rs:737doctor_path_claims_e2e.rs:156stdin.flush()release_gate_e2e.rs:796All four are
resolved_by: tier3_import_boost,influence.class: builtin_only, qualifierinput/stdin, and the importing file hasuse std::io::Write;. A worker found it (it cost three source reads to disprove); the lead reproduced it. It appeared the moment a test addedimpl Write for Capture { fn flush }.This adds two generic constraints to this issue's rule:
flushis on Rust's std/prelude method surface (io::Write), so per the rule above a name-only tier must not decide it without receiver evidence naming the candidate's container.tests/*.rs(andexamples/*.rs,benches/*.rs,src/bin/*.rs) is its own crate root. A ref in one can bind to an item in another only through a sharedmodfile (e.g.tests/common/mod.rs). Name-only tiers must never cross crate roots. Precision decoy: two integration test files, one with a localimpl Write for X { fn flush }, the other callingstdin.flush().Second field case from the lead's review (2026-11-09):
ReadPool::gethas 43 "resolved" callers, almost all wrongfind_callers(ReadPool::get)(crates/daemon/src/read_pool.rs:112) → 43 resolved rows, including:crates/daemon/src/server.rs:585,587,593,598,600:p.get(k)onserde_json::Valueparams;crates/daemon/tests/content_snapshot.rs:10,31,36,…:r.get(0)on a rusqliteRow.getis the canonical std/prelude-surface name (HashMap/Value/Row/Option…). A worker's caller census for #512 was wrong because of this and had to be rebuilt from compiler errors. Add it to this issue's acceptance: after the fix,ReadPool::get's resolved callers are onlyself.conn.get()/pool receivers.Recall audit and owner decision, 2026-11-09.
codex-531 implemented the full rule. The bind diff ran on all 9 repos (shipped/shipped), and every removal was audited:
JSON.parse,Object.defineProperty)Every correct bind that was lost has no receiver fact yet: closure and macro-closure parameters, enum/Option patterns, captures, match chains. On ripgrep, all 28 sites whose RECORDED receiver binding contradicted the old target were wrong.
Decision (operator): ship the loss-free subset now.
The std/prelude method-surface rule for evidence-less receivers is DEFERRED. It stays in the code, switched off, until the guests emit receiver facts for those shapes, and that is the follow-up here. Acceptance for the subset is zero correct binds removed, audited per guard. A test pins that the evidence-less
dir.remove(...)call stays bound.Loss-free subset merged into
integration-1109(e1420994, branch commitb56e06ac). Nothing is pushed yet.decide_typed)name_only.rsambient_globals)cargo_scope.rsrefuse_name_roots)globset/src/lib.rs:343(path: P: AsRef<Path>),ignore/src/walk.rs:331(fs::DirEntry) and zodv3/types.ts:1439(Number.isFinite) are all wrong binds and correctly removed.Capture::flushgoes from 4 resolved callers to 0.precision_gate7/7, phantom 0.dir.removestays bound).Still open:
ENFORCE_EVIDENCELESS_STD_SURFACE = false. It needs guest receiver facts first: closure and macro-closure params, enum/Option patterns, captures, match/builder chains.decompress.rs:139and theError::cloneguesses stay until then.Status (local
integration-1109@60368c0d): py-django tier-3 resolved count is −42 intier3-baseline.json.Released in v0.41.48 (source PR #564). Shipped: refusal of method binds contradicted by recorded Rust receiver evidence, undeclared JS/TS runtime globals, and name-only cross-crate decisions.
Still open here: the evidence-less std-surface rule (kept disabled) and the PHP
parent::and leading-backslash items.🤖 Generated with Claude Code
Status with v0.41.49 (released, PHP package 0.1.6 bundled)
parent::binds to the superclass declaration instead of the overriding method, and a leading-backslash name keeps its global identity for calls and constructors.Stays open for: the refusal of evidence-less std/prelude method binds, which remains disabled until the receiver facts it needs are available.
Wrong binds: builtin/std/global-named method calls (Rust, TS), PHP parent:: and \Foo, residual django bindsto Resolved references can name unrelated methods, global names and function-local typesV::Valuewith a generic parameterVbinds to a function-localstruct Vin another crate #579Step 1, lexical containment: done on a lane, not yet merged (2026-11-13)
Merged into the train branch after v0.41.52 (
integration-1114); nothing of it is in v0.41.52.Rule. A declaration written inside a function, method or lambda body (function, class, struct, enum, interface, trait, type alias, const, static, module, namespace, and in Rust the
implblock of a body-local type with its associated items) is a resolved target only for references inside the enclosing declaration's span. A reference outside loses the target and stays unresolved; it does not fall to another same-named candidate. Methods and fields of a local type are not covered, because a value of a local type can escape and be used elsewhere.Coverage, decided by language semantics. Rust, C# (local functions), JavaScript and TypeScript, through a new profile field
body_local_names. Not PHP (a function declared inside a function becomes global when the outer one runs) and not Ruby (adefinside adefdefines a method on the enclosing class). Python is not covered yet: nested functions are returned and called elsewhere, and the package emits no fact that separates that from a wrong bind.Measured before the fix (nine pinned repositories plus this one): references that resolve to a body-local declaration from outside its body: Rust 16, Python 124, TypeScript 2, others 0. Judged from the source: all 16 Rust binds first judged wrong (for the eight method calls among them that judgement is not certain, see below); of 20 sampled Python binds 13 correct (calls of returned or yielded functions) and 7 wrong; both TypeScript binds correct (methods of an escaping object, which the rule leaves alone).
Bind account, base against lane, joined on (path, line, col, kind, occurrence) over 1,172,552 reference keys: 1 removed (rust-analyzer: a callback parameter
fbound to a local helper of another function, judged wrong), 0 added, 0 retargeted, 334,292 unchanged; 50 unchanged binds sampled per repository. No corpus baseline moves.What the rule clears and what it leaves (all 16 Rust rows re-checked on the final tree, this repository included): 8 are cleared (1 in rust-analyzer, 7 in this repository; all targets are functions declared inside another function). 8 survive, and all 8 are METHODS of a body-local type reached by a method call from elsewhere (
span_forof a localFakeSpanMap,try_fold_regionof a localErrorReplacer<_>,idof a localProbein another crate). The rule deliberately leaves methods alone: a value of a local type can escape (returned asimpl Trait, passed to a generic parameter) and its methods are then called outside, which a fixture test now pins. Whether such a call really reaches the local type is a receiver-identity question; it stays under "Receiver identity" below. The final account over the nine repositories plus this one: 10 binds removed (all judged wrong, two of them the new decoys), 0 added, 0 retargeted, 445,133 unchanged.Cost. The first version raised full-scan steps by 6–10 % on every tier-1 repository and was sent back. The final statement starts from the body-local declarations and reaches references through
idx_refs_target: all seven tier-1 records are within 0.1 % ofcost-baseline.jsonon all four measures.Checked by the lead on the shipped build shape with the released language packages:
V::Valuewith a generic parameterVno longer resolves to the associated type of a function-localstruct V(the #579 case); a Rust local type and a C# local function are resolved inside their body and unresolved outside.Gates added: five fixture tests and four decoys in the precision gate (Rust, C#, JavaScript, TypeScript), each with its mutation run; populations grew, none shrank.
Still open under this ticket: the residual row ledger of #566; Python nested declarations and the generic-parameter qualifier rule, which need facts from the packages (step 2); receiver identity and the disabled std/prelude guard (step 3).
Declarations inside function bodies in Rust, C#, JavaScript and TypeScript are no longer resolved as targets of references outside those bodies. Method calls on function-local types, Python nested declarations and receiver identity remain.