Skip to content

fix(rust): a variant path references the enum that declares it (#2328) - #2354

Merged
colbymchenry merged 1 commit into
mainfrom
fix/2328-rust-enum-variant-refs
Oct 5, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
fix/2328-rust-enum-variant-refs

Conversation

@colbymchenry

@colbymchenry colbymchenry commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

Summary

Issue #2328: a Rust enum used only through its variants (mode::Mode::A in an expression, mode::Mode::A => 1 / Mode::B => … in a match, a Mode::C(x) or Mode::D { .. } pattern) recorded no reference to the enum. Code that builds or matches variants was missing from codegraph callers Mode and from impact. Only a type annotation (fn takes(_m: Mode)) produced a reference.

With this branch the issue's repro lists takes, code (src/variant_use.rs) and main (src/main.rs) as callers of Mode, through both the native kernel and the wasm extractor.

Cause

Rust was not part of the static-member pass. That pass turns Enum.value / Type::CONST reads into references edges for Java, C#, Kotlin, Swift, Scala, Dart, PHP and C++. A Rust path to a variant reaches no other extractor unless it is a call or a struct literal.

Fix

Extraction (wasm extractor and native kernel, mirrored). A path whose last segment is capitalized emits a references ref named by its receiver. The receiver is the segment before the member; Self is read as the impl's type, and inside a trait the path emits nothing. The ref sits at the receiver, so a mode:: prefix scopes it exactly as it scopes a type annotation. This covers:

  • value reads
  • match arms, | and if let patterns
  • tuple-variant and struct-variant patterns
  • a variant passed as a value (.map(Mode::C))

A call's callee (Mode::C(1), Foo::new()), a struct literal, the prefix of a longer path, a use tree, and a lowercase receiver or member emit nothing.

Resolution (isRustNameInScope). A reference written Name::Variant or Self::Variant lands only on an enum that declares that variant. It goes through the same Rust scoping as any other path: module path, use, glob, prelude, and std/dependency imports. As a result:

  • ripgrep's Match::None reaches ignore's enum, not the matcher's struct Match.
  • std variants (Ordering::Less, cmp::Ordering::Greater, Option::Some, Option::None) never reach a same-named project enum.
  • Associated consts and functions (Limits::MAX, Mode::ALL, Self::ALL, Foo::new()) reference no type.
  • Self::X is read as the impl's type, never through a path written elsewhere on its line.

Relation to #2330 (@danusha2345)

This adapts #2330. The extraction half (both walkers) and most of the tests come from it, and the commit credits the author.

What changed:

  • Variants only. The resolver accepts a declared variant and nothing else. fix(rust): a variant path references its enum (#2328) #2330 also accepted any type whose file holds a variable or const named like the item. In the graph, impl consts are file-level variables with no link to their type, so that check cannot tell a type's associated const from another type's or from a top-level const. Associated-const reads therefore stay unlinked, as they are on main. On ripgrep / tokio the enum edge counts below match the ones fix(rust): a variant path references its enum (#2328) #2330 reports (822 / 665). The 3 / 222 struct edges it also reports are the associated-const reads this branch leaves out.
  • Self::X skips the "path written elsewhere on the line" lookup.
  • Extraction speed. Both walkers now check the node kind before looking at the node's parent, and the body walk hands the parent down. fix(rust): a variant path references its enum (#2328) #2330 called Node::parent(), which walks down from the tree's root, for every node of every function body. With the native kernel on tokio's 808 files, Rust extraction went from 2.1 s to 3.9 s (interleaved, same dist, min of 5 passes). With this change it is within noise of main (2.1–2.6 s vs 2.1–2.4 s). The wasm path showed no measurable difference either way.
  • No EXTRACTION_VERSION bump. It is already 28 on main.
  • Docs. docs/design/rust-lang-kernel-port-checklist.md no longer says Rust is outside the static-member pass.

Verification

  • __tests__/rust-enum-variant-reference.test.ts has 10 tests. Its fixtures are real temp Cargo projects indexed into SQLite. It covers:
    • the issue
    • bare and pathed paths, with the edge's line and column
    • patterns
    • Self::A in the enum's file and in another file
    • a same-named struct
    • an enum without the variant
    • associated consts and functions
    • std and prelude enums against same-named project enums
    • module paths
  • Fails on main: 6/10 fail in wasm mode (CODEGRAPH_KERNEL=0), in kernel mode with main's kernel binary, and with this TypeScript over main's kernel binary, so the kernel half is exercised. The 4 that pass on main are guards against new false edges.
  • Passes with the fix: 10/10 in both modes.
  • kernel-rustlang-parity has a new case with every shape plus a non-ASCII column and CRLF. It also asserts the refs are emitted, so the parity check is not vacuous. It passes under CODEGRAPH_KERNEL_EXPECT=1.
  • scripts/kernel-parity.mjs over ripgrep + alacritty + tokio: 1004/1006 files byte-parity, 0 diffs. The 2 parse-error files defer to wasm by policy.
  • npx tsc --noEmit is clean and npm run build succeeds.
  • The Rust, kernel, resolution, extraction and framework suites (32 files, 1355 tests) pass with the kernel. The wasm subset (15 files, 1138 tests) passes too.
  • Full suite (final code, rebased on current main): 6034 passed, 10 failed, in 3 files: function-ref, git-index-currency and sync.
    • All three pass when rerun alone.
    • On the first run, before the rebase, sync and git-index-currency also failed alone, with 5 s git timeouts on this loaded Windows box. They failed the same way on main then.

Validation

Native kernel loaded. Edges are keyed by source QN / kind / target QN / file / line, comparing main to this branch.

repo edges added / removed kind new symbol→enum pairs enums that gain their first referencer
BurntSushi/ripgrep @3fce3b5 +822 / −0 all references → enum +220 31
alacritty/alacritty @29dc553 +917 / −0 all references → enum +253 13
tokio-rs/tokio @b263675 +665 / −0 all references → enum +254 31
  • Node counts are unchanged.
  • Every added edge was checked by script: the source at the edge's column reads Name::Variant or Self::Variant, and the target enum declares that variant. There are no std enum targets.
  • Manual spot-checks: 20 random edges and 6 Self:: edges, all correct.
  • Name collisions: where another enum shares both the name and the variant, every case is either a same-file enum or follows the file's use. The one exception is below.
  • One wrong target, on tokio: stress-test/examples/simple_echo_tcp.rs oneshot::error::TryRecvError::Empty links to mpsc's TryRecvError. The existing path rule checks only the last module segment (error). Type annotations share that rule. The next line, oneshot::error::TryRecvError::Closed, gets no edge, because mpsc's enum has no Closed.
  • Index time on tokio is within noise of main: interleaved codegraph init medians were 21.9 s vs 20.5 s, with main's own runs spanning 18.7–25.5 s on this shared box. Resolution time summed over workers was 27.7 s vs 31.2 s.
  • The final build, rebased on current main and including the walker change, produces the same nodes and edges on all three repos as the build measured above (0 added, 0 removed by natural key).

Not in this PR

  • A variant brought in by use Mode::* / use Mode::A and used bare (A =>). There is no path to read the enum from.
  • Paths inside macro token trees (matches!(m, Mode::A)).
  • A same-named struct elsewhere can still win a plain type annotation (-> Mode) through the Rust framework resolver's PascalCase struct heuristic, because a use binding is not tied to its module. This is pre-existing; fix(rust): resolve bare calls through exact use bindings #2308 works on module-aware use bindings for calls.

Thanks @mg-mg-mg for the report and @danusha2345 for #2330.

🤖 Generated with Claude Code

@colbymchenry
colbymchenry force-pushed the fix/2328-rust-enum-variant-refs branch 2 times, most recently from 7c72016 to bf05b13 Compare October 5, 2026 21:52
Using an enum only through its variants (`mode::Mode::A` in an expression,
`mode::Mode::A => 1` / `Mode::B => ...` in a match, a `Mode::C(x)` or
`Mode::D { .. }` pattern) recorded no reference to the enum, so code that
builds or matches variants was missing from the enum's callers and impact.
Only a type annotation (`fn takes(_m: Mode)`) produced one.

Cause: Rust was not in the static-member pass, which turns `Enum.value` /
`Type::CONST` reads into references for Java, C#, Kotlin, Swift, Scala,
Dart, PHP and C++, and a path to a variant that is not a call or a struct
literal reaches no other extractor.

Fix:
- Extraction (wasm extractor and native kernel, mirrored): a path whose
  last segment is capitalized emits a `references` ref named by its
  receiver (the segment before it; `Self` read as the impl's type, nothing
  in a trait), positioned at the receiver, so a `mode::` prefix scopes it
  exactly as it scopes a type annotation. Covers value reads, match arms,
  `|` / `if let` patterns, tuple- and struct-variant patterns and a variant
  passed as a value. A call's callee (`Mode::C(1)`, `Foo::new()`), a struct
  literal, a longer path's prefix, a `use` tree and a lowercase receiver or
  member emit nothing.
- Resolution (isRustNameInScope): a reference written `Name::Variant` or
  `Self::Variant` lands only on an enum that declares that variant, after
  the usual Rust scoping (module path, `use`, glob, prelude, std and
  dependency imports). ripgrep's `Match::None` reaches `ignore`'s enum, not
  the matcher's `struct Match`; std variants (`Ordering::Less`,
  `Option::Some`) never reach a same-named project enum; associated consts
  and functions (`Limits::MAX`, `Mode::ALL`, `Foo::new()`) reference no
  type. `Self::X` is read as the impl's type, never through a path written
  elsewhere on its line.

Adapted from #2330 by @danusha2345: the extraction half (both walkers) and
most of the tests are theirs. Changed here:
- The resolver accepts a declared variant only. #2330 also accepted any type
  whose file holds a variable or const of the item's name; impl consts are
  file-level variables in the graph, so that cannot tell a type's associated
  const from another type's or a top-level one.
- The walkers check the node kind before looking at its parent, and the
  body walk hands the parent down: #2330 called `parent()`, which walks down
  from the tree's root, for every node of every body, which nearly doubled
  the kernel's Rust extraction time on tokio (2.1 s -> 3.9 s); now within
  noise of main.
- EXTRACTION_VERSION is not bumped again (already 28 on main).

Verification: __tests__/rust-enum-variant-reference.test.ts (10 tests):
6 fail on main in wasm mode, in kernel mode, and with this TypeScript over
main's kernel binary; 10/10 pass in both modes. The kernel parity case
covers every shape and asserts the refs exist; the kernel-parity sweep over
ripgrep, alacritty and tokio is 1004/1006 files byte-parity, 0 diffs (2
parse-error files defer to wasm by policy).

Validation (native kernel loaded, main -> branch, edges keyed by
QN/kind/file/line): ripgrep +822/-0, alacritty +917/-0, tokio +665/-0, all
`references -> enum`, node counts unchanged. Every added edge's source
reads `Name::Variant` / `Self::Variant` at its column and its target enum
declares that variant; no std enum targets. New symbol->enum pairs:
ripgrep +220, alacritty +253, tokio +254; enums with no referencer before
and one now: 31 / 13 / 31. One wrong target on tokio
(`oneshot::error::TryRecvError::Empty` -> mpsc's `TryRecvError`) comes
from the existing path rule that checks only the last module segment, which
type annotations share. Index time on tokio stays within noise of main
(interleaved runs; resolveOne time 27.7 s vs 31.2 s summed over workers).

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant