Skip to content

Commit ad651d3

Browse files
committed
fix(rust): require a same-file struct definition for the unit-struct heuristic
The `^[A-Z]` guard on a bare-identifier let-value (`let x = TypeName;`) matched unit enum variants too — `None`/`Ok` (Option/Result, always in scope) and any custom fieldless variant brought in via `use Enum::Variant` parse identically to a unit-struct reference. `x` was typed as `None` rather than recognized as untyped, since nothing distinguished the two shapes. Now requires a matching `kind === 'struct'` definition in the same file before seeding the typeMap entry — mirrored in both the WASM (rust.ts) and native (rust_lang.rs) extractors, with a regression test in each mirroring the existing seeds_unit_struct_value_type_map coverage. Impact: 2 functions changed, 3 affected
1 parent 03a0d2a commit ad651d3

3 files changed

Lines changed: 48 additions & 2 deletions

File tree

crates/codegraph-core/src/extractors/rust_lang.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,11 @@ fn extract_scoped_use_list(node: &Node, source: &[u8]) -> Vec<(String, Vec<Strin
397397
vec![(prefix, names)]
398398
}
399399

400+
/// True if `name` matches a struct defined in this file (match_rust_node runs before this).
401+
fn is_known_unit_struct(name: &str, symbols: &FileSymbols) -> bool {
402+
symbols.definitions.iter().any(|d| d.kind == "struct" && d.name == name)
403+
}
404+
400405
fn extract_rust_type_name<'a>(type_node: &Node<'a>, source: &'a [u8]) -> Option<&'a str> {
401406
match type_node.kind() {
402407
"type_identifier" | "identifier" | "scoped_type_identifier" => Some(node_text(type_node, source)),
@@ -432,9 +437,17 @@ fn match_rust_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _d
432437
// let x = TypeName; — a bare capitalized identifier value binds
433438
// a unit-struct instance (e.g. `let v = NameValidator;` for
434439
// `struct NameValidator;`), not a reference to another variable (#1876).
440+
// Requiring a same-file `struct` definition excludes unit enum variants
441+
// like `None`/`Ok` (Option/Result, always in scope) and any custom
442+
// fieldless variant brought into scope via `use Enum::Variant` — those
443+
// also parse as a bare capitalized identifier but are values, not types
444+
// (Greptile review). A struct defined elsewhere in the crate is missed,
445+
// same as every other same-file-only heuristic in this extractor.
435446
if value_node.kind() == "identifier" {
436447
let type_name = node_text(&value_node, source);
437-
if type_name.starts_with(|c: char| c.is_uppercase()) {
448+
if type_name.starts_with(|c: char| c.is_uppercase())
449+
&& is_known_unit_struct(type_name, symbols)
450+
{
438451
symbols.type_map.push(TypeMapEntry {
439452
name: node_text(&pattern, source).to_string(),
440453
type_name: type_name.to_string(),
@@ -649,6 +662,15 @@ mod tests {
649662
assert_eq!(entry.type_name, "NameValidator");
650663
}
651664

665+
#[test]
666+
fn does_not_type_unit_enum_variant_as_unit_struct() {
667+
// `None` (Option::None) parses identically to a unit-struct reference — a bare
668+
// capitalized identifier — but is an enum variant, not a struct. Without a same-file
669+
// `struct` definition for the name, it must not be typed (#1876 review).
670+
let s = parse_rust("fn f() { let x = None; }");
671+
assert!(s.type_map.iter().all(|e| e.name != "x"));
672+
}
673+
652674
#[test]
653675
fn does_not_type_lowercase_bare_identifier_binding() {
654676
let s = parse_rust("fn f() { let a = 1; let b = a; }");

src/extractors/rust.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,11 @@ function extractRustTypeMap(node: TreeSitterNode, ctx: ExtractorOutput): void {
295295
extractRustTypeMapDepth(node, ctx, 0);
296296
}
297297

298+
/** True if `name` matches a struct defined in this file (walkRustNode runs before this). */
299+
function isKnownUnitStruct(name: string, ctx: ExtractorOutput): boolean {
300+
return ctx.definitions.some((d) => d.kind === 'struct' && d.name === name);
301+
}
302+
298303
function extractRustTypeMapDepth(node: TreeSitterNode, ctx: ExtractorOutput, depth: number): void {
299304
if (depth >= MAX_WALK_DEPTH) return;
300305

@@ -309,8 +314,19 @@ function extractRustTypeMapDepth(node: TreeSitterNode, ctx: ExtractorOutput, dep
309314
// let x = TypeName; — a bare capitalized identifier value binds a
310315
// unit-struct instance (e.g. `let v = NameValidator;` for `struct
311316
// NameValidator;`), not a reference to another variable (#1876).
317+
// Requiring a same-file `struct` definition excludes unit enum variants
318+
// like `None`/`Ok` (Option/Result, always in scope) and any custom
319+
// fieldless variant brought into scope via `use Enum::Variant` — those
320+
// also parse as a bare capitalized identifier but are values, not types
321+
// (Greptile review). A struct defined elsewhere in the crate is missed,
322+
// same as every other same-file-only heuristic in this extractor.
312323
const valueNode = node.childForFieldName('value');
313-
if (valueNode?.type === 'identifier' && /^[A-Z]/.test(valueNode.text) && ctx.typeMap) {
324+
if (
325+
valueNode?.type === 'identifier' &&
326+
/^[A-Z]/.test(valueNode.text) &&
327+
ctx.typeMap &&
328+
isKnownUnitStruct(valueNode.text, ctx)
329+
) {
314330
setTypeMapEntry(ctx.typeMap, pattern.text, valueNode.text, 0.7);
315331
}
316332
}

tests/parsers/rust.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,14 @@ impl Display for Foo {}`);
119119
expect(symbols.typeMap?.get('v')).toEqual(expect.objectContaining({ type: 'NameValidator' }));
120120
});
121121

122+
it('does not type a unit enum variant as a unit struct (Greptile review)', () => {
123+
// `None` (Option::None) parses identically to a unit-struct reference — a bare
124+
// capitalized identifier — but is an enum variant, not a struct. Without a
125+
// same-file `struct` definition for the name, it must not be typed.
126+
const symbols = parseRust(`fn f() { let x = None; }`);
127+
expect(symbols.typeMap?.has('x')).toBe(false);
128+
});
129+
122130
it('does not type a lowercase bare identifier assignment', () => {
123131
const symbols = parseRust(`fn f() { let a = 1; let b = a; }`);
124132
expect(symbols.typeMap?.has('b')).toBe(false);

0 commit comments

Comments
 (0)