Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixes

- In Rust, a function that uses an enum only through its variants, like `Mode::A`, `mode::Mode::B` or a `Mode::C(x) =>` match arm, now counts as referencing the enum, so it shows up in the enum's callers and impact; reading a type's associated constant, like `Limits::MAX`, references that type too. Re-index Rust projects after upgrading. (#2328)


## [1.6.2] - 2026-10-03

Expand Down
23 changes: 23 additions & 0 deletions __tests__/kernel-rustlang-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,29 @@ fn next() {}
assertParity('commands.rs', source.replace(/\n/g, '\r\n'));
});

it('variant and associated-const paths reference their type (#2328)', () => {
const source = `
use crate::mode::{self, Mode};
const DEFAULT: Mode = Mode::A;
impl Mode {
fn flip(&self) -> u8 {
let _s = "ü"; let _x = Self::B;
match self { Self::A => 1, Mode::C(x) => *x, mode::Mode::D { x } => *x, _ => 0 }
}
}
trait Tr { fn t(&self) { let _ = Self::K; } }
fn uses() {
use crate::mode::Mode::A;
let _a = crate::mode::Mode::A; let _b = Mode::C(1); let _c = Mode::D { x: 2 };
let _d = Mode::new(); let _e = util::take(3); let _f = u8::MAX; let _g = Limits::MAX;
let _h = xs.map(Mode::C); let _i = Option::<u8>::None; let _j = Mode::A.flip();
if let Mode::C(x) | Mode::B = m {}
}
`;
assertParity('variants.rs', source);
assertParity('variants.rs', source.replace(/\n/g, '\r\n'));
});

it('torture fixture: impl/trait quirks, use bindings, chains, fn-refs, value-refs, route macros', () => {
const file = path.join(FIXTURE_DIR, 'torture.rs');
assertParity('fixtures/torture.rs', fs.readFileSync(file, 'utf8'), 20);
Expand Down
277 changes: 277 additions & 0 deletions __tests__/rust-enum-variant-reference.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
/**
* A Rust enum used only through its variants is a dependency of the code that
* uses them (#2328). `mode::Mode::A` in an expression, `Mode::B => …` in a
* `match`, a `Mode::C(x)` / `Mode::D { .. }` pattern and `Self::A` in the
* enum's impl reference the enum the path names — the same `references` edge a
* type annotation (`fn takes(_m: Mode)`) produces.
*/
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { CodeGraph } from '../src';

let root: string;
let cg: CodeGraph | undefined;
beforeEach(() => { root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-rust-variant-ref-')); });
afterEach(() => { cg?.close(); cg = undefined; fs.rmSync(root, { recursive: true, force: true }); });

async function index(files: Record<string, string>) {
fs.mkdirSync(path.join(root, 'src'));
fs.writeFileSync(path.join(root, 'Cargo.toml'), '[package]\nname = "repro"\nversion = "0.1.0"\nedition = "2021"\n');
for (const [file, text] of Object.entries(files)) fs.writeFileSync(path.join(root, 'src', file), text);
cg = await CodeGraph.init(root, { index: true });
}

/** The enum named `name` declared in `src/<file>`. */
function enumIn(file: string, name = 'Mode') {
const node = cg!.getNodesByKind('enum').find((n) => n.filePath === `src/${file}` && n.name === name);
expect(node, `enum ${name} in ${file}`).toBeDefined();
return node!;
}

/** `file:name` of every symbol with a `references` edge to the node. */
function referencers(nodeId: string): string[] {
const names = cg!.getIncomingEdges(nodeId)
.filter((e) => e.kind === 'references')
.map((e) => cg!.getNode(e.source)!)
.map((n) => `${n.filePath.replace(/^src\//, '')}:${n.name}`);
return [...new Set(names)].sort();
}

const MODE = `pub enum Mode {
A,
B,
C(u8),
D { x: u8 },
}

pub fn takes(_m: Mode) {}

pub fn current() -> Mode { Mode::B }
`;

describe('Rust enum variant paths reference their enum (#2328)', () => {
it('the issue: a variant path in an expression and in match arms, through the module', async () => {
await index({
'mode.rs': MODE,
'variant_use.rs': `use crate::mode;

pub fn code(flag: bool) -> u8 {
let m = if flag { mode::Mode::A } else { mode::Mode::B };
match m {
mode::Mode::A => 1,
mode::Mode::B => 2,
_ => 3,
}
}
`,
'main.rs': `mod mode;
mod variant_use;

fn main() {
mode::takes(mode::Mode::A);
let _ = variant_use::code(true);
}
`,
});
expect(referencers(enumIn('mode.rs').id)).toEqual(['main.rs:main', 'mode.rs:current', 'mode.rs:takes', 'variant_use.rs:code']);
});

it('a bare `Mode::A` after `use crate::mode::Mode;`', async () => {
await index({
'lib.rs': 'pub mod mode;\npub mod bare;\n',
'mode.rs': MODE,
'bare.rs': `use crate::mode::{self, Mode};

pub fn pick() -> u8 {
let _m = Mode::B;
0
}

pub fn score() -> u8 {
match mode::current() {
Mode::A => 1,
_ => 0,
}
}
`,
});
expect(referencers(enumIn('mode.rs').id)).toEqual(['bare.rs:pick', 'bare.rs:score', 'mode.rs:current', 'mode.rs:takes']);
const pick = cg!.getNodesByKind('function').find((n) => n.name === 'pick')!;
const at = cg!.getOutgoingEdges(pick.id).filter((e) => e.kind === 'references').map((e) => `${e.line}:${e.column}`);
expect(at).toEqual(['4:13']);
});

it('tuple and struct variant patterns, and a variant read as a value', async () => {
await index({
'lib.rs': 'pub mod mode;\npub mod pat;\n',
'mode.rs': MODE,
'pat.rs': `use crate::mode::{self, Mode};

pub fn tuple() -> u8 {
match mode::current() {
Mode::C(x) => x,
_ => 0,
}
}

pub fn structure() -> u8 {
if let crate::mode::Mode::D { x } = mode::current() { x } else { 0 }
}

pub fn ctor_value(xs: Vec<u8>) -> usize {
xs.into_iter().map(Mode::C).count()
}
`,
});
expect(referencers(enumIn('mode.rs').id)).toEqual([
'mode.rs:current', 'mode.rs:takes', 'pat.rs:ctor_value', 'pat.rs:structure', 'pat.rs:tuple',
]);
});

it('constructing a tuple or struct variant stays linked to the variant itself', async () => {
await index({
'lib.rs': 'pub mod mode;\npub mod build;\n',
'mode.rs': MODE,
'build.rs': `use crate::mode::Mode;

pub fn build() {
let _c = Mode::C(3);
let _d = Mode::D { x: 2 };
}
`,
});
const build = cg!.getNodesByKind('function').find((n) => n.name === 'build')!;
const out = cg!.getOutgoingEdges(build.id).map((e) => `${e.kind}:${cg!.getNode(e.target)!.qualifiedName}`).sort();
expect(out).toEqual(['calls:Mode::C', 'instantiates:Mode::D']);
});

it('`Self::A` inside `impl Mode`, in the enum\'s file and in another one', async () => {
await index({
'lib.rs': 'pub mod mode;\npub mod ops;\n',
'mode.rs': `${MODE}
impl Mode {
pub fn flip(&self) -> u8 {
match self {
Self::A => 1,
_ => 0,
}
}
}
`,
'ops.rs': `use crate::mode::Mode;

impl Mode {
pub fn is_b(&self) -> bool {
if let Self::B = self { true } else { false }
}
}
`,
});
expect(referencers(enumIn('mode.rs').id)).toEqual(['mode.rs:current', 'mode.rs:flip', 'mode.rs:takes', 'ops.rs:is_b']);
});

it('a variant imported by name and used bare has no path to read the enum from', async () => {
await index({
'lib.rs': 'pub mod mode;\npub mod other;\npub mod glob;\n',
'mode.rs': MODE,
'other.rs': 'pub enum Mode {\n A,\n Z,\n}\n',
'glob.rs': `use crate::mode::Mode::*;

pub fn bare(m: u8) -> u8 {
match m {
0 => { let _ = A; 1 }
_ => 0,
}
}
`,
});
// Out of scope (#2328 is about paths): a bare name gives no receiver, and
// it must never land on the other module's same-named enum either.
expect(referencers(enumIn('mode.rs').id)).toEqual(['mode.rs:current', 'mode.rs:takes']);
expect(referencers(enumIn('other.rs').id)).toEqual([]);
});

it('the path picks the type that declares the variant, never a same-named type without it', async () => {
await index({
'lib.rs': 'pub mod matching;\npub mod other;\npub mod user;\n',
'matching.rs': 'pub enum Match {\n None,\n Ignore,\n}\n',
'other.rs': 'pub struct Match;\n',
'user.rs': `use crate::matching::Match;

pub enum ColorChoice {
Auto,
Ansi,
}

pub fn first() -> u8 {
let _m = Match::None;
0
}

pub fn color() -> u8 {
let _c = termcolor::ColorChoice::AlwaysAnsi;
0
}
`,
});
expect(referencers(enumIn('matching.rs', 'Match').id)).toEqual(['user.rs:first']);
const struct = cg!.getNodesByKind('struct').find((n) => n.filePath === 'src/other.rs')!;
expect(referencers(struct.id)).toEqual([]);
// `termcolor`'s ColorChoice, not the file's own enum, which has no `AlwaysAnsi`.
expect(referencers(enumIn('user.rs', 'ColorChoice').id)).toEqual([]);
});

it('an associated const read through its type references the type', async () => {
await index({
'lib.rs': 'pub mod limits;\npub mod user;\n',
'limits.rs': 'pub struct Limits;\n\nimpl Limits {\n pub const MAX: u8 = 3;\n}\n',
'user.rs': 'use crate::limits::Limits;\n\npub fn cap() -> u8 {\n Limits::MAX\n}\n',
});
const limits = cg!.getNodesByKind('struct').find((n) => n.name === 'Limits')!;
expect(referencers(limits.id)).toEqual(['user.rs:cap']);
});

it('a module path, an associated function and a same-named enum elsewhere are not references to the enum', async () => {
await index({
'lib.rs': 'pub mod mode;\npub mod other;\npub mod util;\npub mod paths;\n',
'mode.rs': `${MODE}
impl Mode {
pub fn new() -> Mode { Mode::A }
}
`,
'other.rs': 'pub enum Mode {\n A,\n Z,\n}\n',
'util.rs': 'pub fn take(x: u8) -> u8 { x }\n',
'paths.rs': `use crate::mode;
use crate::other;
use crate::util;

pub fn via_module() -> u8 {
util::take(3)
}

pub fn via_ctor() {
let _m = mode::Mode::new();
}

pub fn via_other() -> u8 {
match other::Mode::A {
other::Mode::Z => 1,
_ => 0,
}
}

pub fn via_mode() {
let _m = mode::Mode::A;
}
`,
});
const viaModule = cg!.getNodesByKind('function').find((n) => n.name === 'via_module')!;
expect(cg!.getOutgoingEdges(viaModule.id).filter((e) => e.kind === 'references')).toEqual([]);
expect(referencers(enumIn('other.rs').id)).toEqual(['paths.rs:via_other']);
// `new`'s own `Mode::A` and return type; `via_ctor` calls an associated
// function, which links the function, not the enum.
expect(referencers(enumIn('mode.rs').id)).toEqual(['mode.rs:current', 'mode.rs:new', 'mode.rs:takes', 'paths.rs:via_mode']);
});
});
Loading