unexpanded_supertypes does not hold supertypes: it holds any TYPE_POSITION ref in the container span, so a Rust enum ships its variant payload types #240
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#240
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 by the #205 lane while bounding this field, and deliberately not folded into that fix — a behaviour change smuggled into a cap would have been unattributable, which is the same reason #205's reporter refused to ship the cap on its own.
The fact
MemberCensusBasis::unexpanded_supertypesis populated by:That is not what the name says, and not what the doc promises. The predicate is "a type mentioned somewhere in this container's body that no child symbol claims" — supertypes satisfy it, and so does a great deal else.
Two measured instances
Rust enums declare zero child symbols. So every type named in every variant is inside the container span and outside every child span.
Exprinrust-analyzer'scrates/hir-def/src/hir.rsproduces 27 entries — all of them variant payload types, none of them supertypes. Before #205's cap, all 27 shipped in asafe_deletereply under a field namedunexpanded_supertypes.JavaScript
static {}blocks land the same way. Anew A2()inside a static initialiser block is aTYPE_POSITIONref in the container span with no child claiming it. Note this is currently depended on: the JS fixture added in #205 relies on this behaviour and says so in its own comment — it will go RED when this is fixed, which is intended and is the marker for whoever fixes it.Why it matters beyond the name
unexpanded_supertypesis read as evidence about a hierarchy — its whole role in the census is to let a reader tell "one implicit root object" from "a real inheritance chain". A Rust enum with 27 payload types reads as a deep hierarchy and is not one. The field is not merely mis-named; it is mis-informative at exactly the question it exists to answer.The #205 cap bounds the damage (8 entries, with
unexpanded_supertypes_totalsaying how many were cut) and does not touch the correctness of what is in the list. That was the right split.What must NOT be done
Shape of a fix
The extractors know the difference — a Rust supertrait bound, a Python base list, a C#/TS interface list, a PHP
extends/implements, a Rubyincludeare all syntactically distinct from an incidental type mention. The honest fix is a ref kind or role that says "this type ref is a declared supertype", emitted by the plugins that can see it, rather than a span-containment heuristic in the daemon.That is a wire/plugin change, so it is not small. An intermediate step worth costing: keep the span predicate but exclude containers whose language+kind declares supertypes syntactically elsewhere (a Rust
enumdeclares none, so it should contribute none), which fixes the measured instance without a new ref kind.What a fix must prove
enumwith typed variants reports zero unexpanded supertypes, and a Rusttraitwith supertrait bounds still reports them.#205JS fixture's dependence on the current behaviour is addressed explicitly — updated with its comment, not silently.deepcolumn onCENSUS_ROWS) is the pattern.Related
#205 (the bounding fix, closed — this is the correctness residual it deliberately left), #199 (the census this field belongs to, and the inherited/implicit member class it interacts with).
Filed 2026-09-09 against
master5ac45d9.Fixed on
masteratefe6f52(merged8a8ea5c). First: the headline instance in this issue is wrong, and I wrote it twice.My error
I claimed
Expr's 27 entries "shipped in asafe_deletereply" — here, and again when closing #205. They did not, and could not.A Rust
enumemits zero child symbols (measured: 0 of 825 enums in rust-analyzer), andmember_census_basisonly runs for a symbol whose parent is a member container. So no enum has ever been reachable through the shipped tool. The 27 strings were measured by running the predicate over every container — which is also how #205's ownmax 37 / over-8 34table was taken, so that number is the same artefact.The defect in the predicate was real and worth fixing. That particular consequence was not, and I asserted it as measured. The enum half is now graded over refs, with the reachability claim pinned so a plugin that starts emitting variants as children fails the test rather than outliving its reason.
The fix, and the measurement that rejected the cheap option
raw::ref_role::SUPERTYPE(bit 20), emitted at declared-supertype slots by all six builtin plugins and the packaged Ruby guest. The daemon predicate is(r.roles & SUPERTYPE) != 0— one clause, seven languages.I briefed the issue's intermediate (exclude containers whose kind declares no supertypes) as worth costing. Costed over the population the census can actually reach — a container with ≥1 child, since only such a container is somebody's parent:
0.5%. And it cannot touch the dominant case structurally: a Rust
impl Trait for Tdoes declare a supertype, and its self type sits in the same slot — and 8,755 of rust-analyzer's 9,015 reachable rows areimplheaders. It also leaves C#/PHP field annotations, generic bounds, where-clauses and trait associated types untouched.Cost of the honest path: core bit + abi registry, 6 plugins + wasm guest (rebuilt, digest re-recorded,
de.h-dv.ruby→ 0.6.0), andm0065_supertype_roles(schema v65, a forced re-parse — the bit comes out of the grammar walk, so no SQL can derive it).The corpus delta, read at source
19,893 reachable rows → 14,295. 5,598 removed, ZERO added across nine pinned repos — so containment holds empirically, not just by doc.
Every removed row adjudicated by class:
impl Tr for Tself type or generic argimpl T— the container's own typemetaclass=/ C# attributewhere T : I, TS abstract member sig,mod-leveltype, TS interface constraintNot one was a declared supertype. Documented residual, recorded on the constant rather than hidden: a generic argument of a declared supertype still takes the bit (
Structinimpl From<Struct> for VariantDef).tests/corpus/baseline.jsonmd5 unmoved (4b8dad0f…), verified by me post-merge.package-baseline.jsonmoved on conditions only — schema 64→65, ruby 0.5.0→0.6.0 — with everycount./gen./rule./stage./influence.value byte-identical.Mutations — and two that are more interesting than a RED
I re-ran M4 (widen the daemon clause back to
TYPE_POSITION) myself. RED on three tests, and it reproduces the exact leak:Restored by
cp, md5 verified, re-run 34 passed.Two entries worth more than their verdict:
AND 1 = 0), which compiles and is RED.Some(1)assertion, which fired before the post-loopreached_the_capcount — so the count itself was ungraded. A table-level guard was added asserting the exempted set is exactly["javascript"], and M9b is RED on it. A red that does not prove what you think is the failure mode this repo keeps finding, and catching it inside one's own new work is the good version.tp_rolesdirectly and is RED on it. Reported as a survivor, then covered.ROLE_NAMESassertion and the wasm rebuild byte-compare) and is reported as such rather than as one clean kill.Three things this issue and #205 got wrong
TYPE_POSITION's doc overclaims PHP. It lists "PHPuse <Trait>" among its slots; probed onclass Thing extends Base { use Helper; }the PHP plugin emits no ref of any kind there. Measurement recorded on the new constant; not fixed, because emitting there would add refs and move binds.SUPERTYPE_LIST_CAPis now unexercised. Post-fix the largest declared supertype list anywhere on the pinned corpus is 5. #205's entire justification for 8 rested on a population that turns out to have been payloads and bounds — so the cap is back in exactly the state #205 refused to ship one in. Kept (the list has no structural bound and the cut is graded at 12 by fixture), but its doc no longer claims to be live on real code.The #205 JS fixture
Went RED exactly as its own comment predicted, and is addressed explicitly rather than deleted. JavaScript
extendstakes one expression and has no interface list, so the row carries a recordeddeep_ungradeableexemption with its measurement in it. The exempted arm is not a skip: it assertsSome(1)over a fixture that still holds the elevennew A_n()calls, so it is RED at 12 if the leak returns and RED at 0 if the one real base is lost. Both arms run.The old
child_exclusion_ungradeablecolumn is gone by measurement, not convenience: withSUPERTYPEas the clause all seven languages can put a supertype ref inside a member's span, so its recorded reason no longer describes anything.Unrelated finding
code-index-test-support --test fork_windowfailed in a concurrent run withinstall the stand-in: NotFoundand passes in isolation — it builds its stand-in at a fixedtemp_dir()/cosi-fork-window-standin/, so parallel lanes race on one binary. Not caused by this change.Verification
fmtclean ·clippy --workspace --all-targets -D warningsclean ·cargo test --workspace --no-fail-fast332/332, 0 failed ·precision_gate --nocapture7/7, phantom_count 0 with populations printed and unshrunk (re-run by me post-merge: js 1, php 3, csharp 3, ruby 3, ts 4, rust 6, python 7) ·corpus_ratchetwithCOSI_CORPUS_REQUIRE=1green, baseline unmoved · rustdoc-D warningsclean.Closing.
MemberCensusBasis::unexpanded_supertypesis an unboundedVec<String>in a reply — no LIMIT, no cap, no truncation disclosure #205