Skip to content

Commit 85ec995

Browse files
committed
fix(parity): infer var-declared local types from new-expression initializers in C#
Both the WASM and native C# extractors were skipping variable declarations with `var`/`implicit_type` type nodes entirely, so `var service = new UserService(repo)` never added `service → UserService` to the typeMap. The call-edge resolver therefore could not resolve `service.AddUser()` or `service.GetUser()` to the qualified methods on `UserService`. The dist copy of the WASM extractor already had the fix (extractVarInitType), but the source had drifted out of sync. This commit re-introduces the logic in both engines: WASM (TypeScript): add `extractVarInitType(declarator)` that walks the variable_declarator children looking for an `object_creation_expression` (or one inside an `equals_value_clause`), then reads the `type` field via `extractCSharpTypeName`. `handleCSharpVarDecl` now sets `isVar` for `implicit_type | var_keyword` and calls `extractVarInitType` in that branch. Native (Rust): mirror the same logic in `extract_var_init_type` and update `match_csharp_type_map` to drive it when the type node is `var_keyword` or `implicit_type`. Infrastructure: add CODEGRAPH_NATIVE_ADDON_PATH env-var override to `loadNative()` for local development workflows where the published npm binary can't be loaded (its dylib install-name points to the CI build path). Tests now pass with CODEGRAPH_NATIVE_ADDON_PATH set to the locally rebuilt binary. Fixes: #1418 (build-parity test failure — native vs WASM C# edge/role divergence on `var`-typed local variables instantiated with `new`).
1 parent 1b98e7f commit 85ec995

3 files changed

Lines changed: 89 additions & 22 deletions

File tree

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

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -450,26 +450,55 @@ fn extract_csharp_type_name<'a>(type_node: &Node<'a>, source: &'a [u8]) -> Optio
450450
}
451451
}
452452

453+
/// Extract the constructor type from a `var x = new Foo()` initializer.
454+
fn extract_var_init_type<'a>(declarator: &Node<'a>, source: &'a [u8]) -> Option<&'a str> {
455+
for i in 0..declarator.child_count() {
456+
let Some(child) = declarator.child(i) else { continue };
457+
if child.kind() == "object_creation_expression" {
458+
if let Some(t) = child.child_by_field_name("type") {
459+
return extract_csharp_type_name(&t, source);
460+
}
461+
}
462+
if child.kind() == "equals_value_clause" {
463+
for j in 0..child.child_count() {
464+
let Some(expr) = child.child(j) else { continue };
465+
if expr.kind() == "object_creation_expression" {
466+
if let Some(t) = expr.child_by_field_name("type") {
467+
return extract_csharp_type_name(&t, source);
468+
}
469+
}
470+
}
471+
}
472+
}
473+
None
474+
}
475+
453476
fn match_csharp_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: usize) {
454477
match node.kind() {
455478
"variable_declaration" => {
456479
let type_node = node.child_by_field_name("type").or_else(|| node.child(0));
457480
if let Some(type_node) = type_node {
458-
if type_node.kind() != "var_keyword" && type_node.kind() != "implicit_type" {
459-
if let Some(type_name) = extract_csharp_type_name(&type_node, source) {
460-
for i in 0..node.child_count() {
461-
if let Some(child) = node.child(i) {
462-
if child.kind() == "variable_declarator" {
463-
let name_node = child.child_by_field_name("name")
464-
.or_else(|| child.child(0));
465-
if let Some(name_node) = name_node {
466-
if name_node.kind() == "identifier" {
467-
symbols.type_map.push(TypeMapEntry {
468-
name: node_text(&name_node, source).to_string(),
469-
type_name: type_name.to_string(),
470-
confidence: 0.9,
471-
});
472-
}
481+
let is_var = type_node.kind() == "var_keyword" || type_node.kind() == "implicit_type";
482+
let explicit_type = if is_var { None } else { extract_csharp_type_name(&type_node, source) };
483+
if !is_var && explicit_type.is_none() { return; }
484+
for i in 0..node.child_count() {
485+
if let Some(child) = node.child(i) {
486+
if child.kind() == "variable_declarator" {
487+
let name_node = child.child_by_field_name("name")
488+
.or_else(|| child.child(0));
489+
if let Some(name_node) = name_node {
490+
if name_node.kind() == "identifier" {
491+
let type_name = if is_var {
492+
extract_var_init_type(&child, source)
493+
} else {
494+
explicit_type
495+
};
496+
if let Some(type_name) = type_name {
497+
symbols.type_map.push(TypeMapEntry {
498+
name: node_text(&name_node, source).to_string(),
499+
type_name: type_name.to_string(),
500+
confidence: 0.9,
501+
});
473502
}
474503
}
475504
}

src/extractors/csharp.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -329,19 +329,41 @@ function extractCSharpTypeMap(node: TreeSitterNode, ctx: ExtractorOutput): void
329329
extractCSharpTypeMapDepth(node, ctx, 0);
330330
}
331331

332-
/** Extract type info from a variable_declaration node (local vars with explicit types). */
332+
/** Extract the constructor type from a `var x = new Foo()` initializer. */
333+
function extractVarInitType(declarator: TreeSitterNode): string | null {
334+
for (let i = 0; i < declarator.childCount; i++) {
335+
const child = declarator.child(i);
336+
if (child?.type === 'object_creation_expression') {
337+
const tNode = child.childForFieldName('type');
338+
if (tNode) return extractCSharpTypeName(tNode);
339+
}
340+
if (child?.type === 'equals_value_clause') {
341+
for (let j = 0; j < child.childCount; j++) {
342+
const expr = child.child(j);
343+
if (expr?.type === 'object_creation_expression') {
344+
const tNode = expr.childForFieldName('type');
345+
if (tNode) return extractCSharpTypeName(tNode);
346+
}
347+
}
348+
}
349+
}
350+
return null;
351+
}
352+
353+
/** Extract type info from a variable_declaration node (local vars with explicit or inferred types). */
333354
function handleCSharpVarDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
334355
const typeNode = node.childForFieldName('type') || node.child(0);
335-
if (!typeNode || typeNode.type === 'var_keyword') return;
336-
const typeName = extractCSharpTypeName(typeNode);
337-
if (!typeName) return;
356+
if (!typeNode) return;
357+
const isVar = typeNode.type === 'implicit_type' || typeNode.type === 'var_keyword';
358+
const explicitTypeName = isVar ? null : extractCSharpTypeName(typeNode);
359+
if (!isVar && !explicitTypeName) return;
338360
for (let i = 0; i < node.childCount; i++) {
339361
const child = node.child(i);
340362
if (child?.type !== 'variable_declarator') continue;
341363
const nameNode = child.childForFieldName('name') || child.child(0);
342-
if (nameNode && nameNode.type === 'identifier' && ctx.typeMap) {
343-
setTypeMapEntry(ctx.typeMap, nameNode.text, typeName, 0.9);
344-
}
364+
if (nameNode?.type !== 'identifier' || !ctx.typeMap) continue;
365+
const typeName = isVar ? extractVarInitType(child) : explicitTypeName;
366+
if (typeName) setTypeMapEntry(ctx.typeMap, nameNode.text, typeName, 0.9);
345367
}
346368
}
347369

src/infrastructure/native.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,26 @@ function resolvePlatformPackage(): string | null {
5858
/**
5959
* Try to load the native napi addon.
6060
* Returns the module on success, null on failure.
61+
*
62+
* Dev override: CODEGRAPH_NATIVE_ADDON_PATH can point to a locally built
63+
* .node file (e.g. crates/codegraph-core/index.node from `cargo build`).
64+
* Only honoured when set explicitly — never falls back to it implicitly.
6165
*/
6266
export function loadNative(): NativeAddon | null {
6367
if (_cached !== undefined) return _cached;
6468

69+
const devOverride = process.env.CODEGRAPH_NATIVE_ADDON_PATH;
70+
if (devOverride) {
71+
try {
72+
_cached = _require(devOverride) as NativeAddon;
73+
return _cached;
74+
} catch (err) {
75+
_loadError = err as Error;
76+
_cached = null;
77+
return null;
78+
}
79+
}
80+
6581
const pkg = resolvePlatformPackage();
6682
if (pkg) {
6783
try {

0 commit comments

Comments
 (0)