diff --git a/crates/codegraph-core/src/extractors/csharp.rs b/crates/codegraph-core/src/extractors/csharp.rs index 444109e6..de6de19b 100644 --- a/crates/codegraph-core/src/extractors/csharp.rs +++ b/crates/codegraph-core/src/extractors/csharp.rs @@ -438,6 +438,29 @@ fn extract_csharp_base_types( // ── Type map extraction ───────────────────────────────────────────────────── +fn extract_var_init_type(declarator: &Node, source: &[u8]) -> Option { + for i in 0..declarator.child_count() { + let Some(child) = declarator.child(i) else { continue }; + if child.kind() == "object_creation_expression" { + if let Some(t) = child.child_by_field_name("type") { + return extract_csharp_type_name(&t, source).map(|s| s.to_string()); + } + } + if child.kind() == "equals_value_clause" { + for j in 0..child.child_count() { + if let Some(expr) = child.child(j) { + if expr.kind() == "object_creation_expression" { + if let Some(t) = expr.child_by_field_name("type") { + return extract_csharp_type_name(&t, source).map(|s| s.to_string()); + } + } + } + } + } + } + None +} + fn extract_csharp_type_name<'a>(type_node: &Node<'a>, source: &'a [u8]) -> Option<&'a str> { match type_node.kind() { "identifier" | "qualified_name" => Some(node_text(type_node, source)), @@ -455,18 +478,29 @@ fn match_csharp_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, "variable_declaration" => { let type_node = node.child_by_field_name("type").or_else(|| node.child(0)); if let Some(type_node) = type_node { - if type_node.kind() != "var_keyword" && type_node.kind() != "implicit_type" { - if let Some(type_name) = extract_csharp_type_name(&type_node, source) { - for i in 0..node.child_count() { - if let Some(child) = node.child(i) { - if child.kind() == "variable_declarator" { - let name_node = child.child_by_field_name("name") - .or_else(|| child.child(0)); - if let Some(name_node) = name_node { - if name_node.kind() == "identifier" { + let is_var = type_node.kind() == "implicit_type" || type_node.kind() == "var_keyword"; + let explicit_type_name: Option = if is_var { + None + } else { + extract_csharp_type_name(&type_node, source).map(|s| s.to_string()) + }; + if is_var || explicit_type_name.is_some() { + for i in 0..node.child_count() { + if let Some(child) = node.child(i) { + if child.kind() == "variable_declarator" { + let name_node = child.child_by_field_name("name") + .or_else(|| child.child(0)); + if let Some(name_node) = name_node { + if name_node.kind() == "identifier" { + let type_name = if is_var { + extract_var_init_type(&child, source) + } else { + explicit_type_name.clone() + }; + if let Some(type_name) = type_name { symbols.type_map.push(TypeMapEntry { name: node_text(&name_node, source).to_string(), - type_name: type_name.to_string(), + type_name, confidence: 0.9, }); } diff --git a/src/domain/graph/builder/call-resolver.ts b/src/domain/graph/builder/call-resolver.ts index 4b5402db..7a11b7f0 100644 --- a/src/domain/graph/builder/call-resolver.ts +++ b/src/domain/graph/builder/call-resolver.ts @@ -94,12 +94,15 @@ export function resolveByMethodOrGlobal( : (typeEntry as { type?: string }).type : null; - // Handle inline new-expression receivers: `(new Foo).bar()` or `(new Foo()).bar()`. - // extractReceiverName returns the raw node text for non-identifier nodes, so `(new A).t()` - // produces receiver='(new A)'. Extract the constructor name directly. - // The regex intentionally restricts to uppercase-initial names ([A-Z_$]) as a heuristic - // to distinguish constructors (PascalCase) from regular functions — avoiding false positives - // on `(new xmlParser()).parse()` style calls which are rare in practice. + // Belt-and-suspenders fallback for inline new-expression receivers that + // extractReceiverName did not normalise (e.g. raw text leaked from an + // unhandled AST node type). extractReceiverName already handles the common + // `new_expression` / `parenthesized_expression(new_expression)` shapes by + // returning the constructor name directly, so this branch is exercised only + // by future node types or constructs that fall through to the raw-text path. + // The uppercase-initial restriction ([A-Z_$]) is a heuristic to distinguish + // constructors (PascalCase) from regular functions and avoids false positives + // on `(new xmlParser()).parse()` style calls. if (!typeName && call.receiver) { const m = /^\(?\s*new\s+([A-Z_$][A-Za-z0-9_$]*)/.exec(call.receiver); if (m?.[1]) typeName = m[1]; diff --git a/src/extractors/csharp.ts b/src/extractors/csharp.ts index 52f47cb4..97385bb5 100644 --- a/src/extractors/csharp.ts +++ b/src/extractors/csharp.ts @@ -332,17 +332,39 @@ function extractCSharpTypeMap(node: TreeSitterNode, ctx: ExtractorOutput): void /** Extract type info from a variable_declaration node (local vars with explicit types). */ function handleCSharpVarDecl(node: TreeSitterNode, ctx: ExtractorOutput): void { const typeNode = node.childForFieldName('type') || node.child(0); - if (!typeNode || typeNode.type === 'var_keyword') return; - const typeName = extractCSharpTypeName(typeNode); - if (!typeName) return; + if (!typeNode) return; + const isVar = typeNode.type === 'implicit_type' || typeNode.type === 'var_keyword'; + const explicitTypeName = isVar ? null : extractCSharpTypeName(typeNode); + if (!isVar && !explicitTypeName) return; for (let i = 0; i < node.childCount; i++) { const child = node.child(i); if (child?.type !== 'variable_declarator') continue; const nameNode = child.childForFieldName('name') || child.child(0); - if (nameNode && nameNode.type === 'identifier' && ctx.typeMap) { - setTypeMapEntry(ctx.typeMap, nameNode.text, typeName, 0.9); + if (nameNode?.type !== 'identifier' || !ctx.typeMap) continue; + const typeName = isVar ? extractVarInitType(child) : explicitTypeName; + if (typeName) setTypeMapEntry(ctx.typeMap, nameNode.text, typeName, 0.9); + } +} + +/** Extract the constructor type from a `var x = new Foo()` initializer. */ +function extractVarInitType(declarator: TreeSitterNode): string | null { + for (let i = 0; i < declarator.childCount; i++) { + const child = declarator.child(i); + if (child?.type === 'object_creation_expression') { + const tNode = child.childForFieldName('type'); + if (tNode) return extractCSharpTypeName(tNode); + } + if (child?.type === 'equals_value_clause') { + for (let j = 0; j < child.childCount; j++) { + const expr = child.child(j); + if (expr?.type === 'object_creation_expression') { + const tNode = expr.childForFieldName('type'); + if (tNode) return extractCSharpTypeName(tNode); + } + } } } + return null; } /** Extract type info from a parameter node. */ diff --git a/src/extractors/javascript.ts b/src/extractors/javascript.ts index 27f05b80..3ef4ab2f 100644 --- a/src/extractors/javascript.ts +++ b/src/extractors/javascript.ts @@ -2676,6 +2676,25 @@ function extractReceiverName(objNode: TreeSitterNode | null): string | undefined if (!objNode) return undefined; const t = objNode.type; if (t === 'identifier' || t === 'this' || t === 'super') return objNode.text; + // `(new Foo(...)).method()` — extract the constructor name so the resolver can + // look up `Foo.method` directly without relying on a text-based regex heuristic. + if (t === 'new_expression') { + const name = extractNewExprTypeName(objNode); + if (name) return name; + } + if (t === 'parenthesized_expression') { + // Only one level of parentheses is unwrapped here. Doubly-nested parens + // (e.g. `((new Dog())).bark()`) and cast expressions inside parens + // (e.g. `(new Dog() as Animal).bark()`) fall through to raw-text handling + // below and are caught by the regex fallback in call-resolver.ts. + for (let i = 0; i < objNode.childCount; i++) { + const child = objNode.child(i); + if (child?.type === 'new_expression') { + const name = extractNewExprTypeName(child); + if (name) return name; + } + } + } return objNode.text; } diff --git a/tests/benchmarks/resolution/resolution-benchmark.test.ts b/tests/benchmarks/resolution/resolution-benchmark.test.ts index 2d0c6063..ff460d22 100644 --- a/tests/benchmarks/resolution/resolution-benchmark.test.ts +++ b/tests/benchmarks/resolution/resolution-benchmark.test.ts @@ -148,7 +148,9 @@ const THRESHOLDS: Record = { python: { precision: 0.7, recall: 0.3 }, go: { precision: 0.7, recall: 0.3 }, java: { precision: 0.7, recall: 0.3 }, - csharp: { precision: 1.0, recall: 0.8 }, + // csharp 1.0/0.9: static receiver fix (#1395) ensures precision; var-declared instance typeMap + // (implicit_type) lifts receiver-typed recall from 0/4 → 4/4 (#1396). + csharp: { precision: 1.0, recall: 0.9 }, kotlin: { precision: 0.6, recall: 0.2 }, // Lower bars — resolution still maturing rust: { precision: 0.6, recall: 0.2 }, diff --git a/tests/parsers/csharp.test.ts b/tests/parsers/csharp.test.ts index 8f9f4e0c..20170765 100644 --- a/tests/parsers/csharp.test.ts +++ b/tests/parsers/csharp.test.ts @@ -151,4 +151,26 @@ public class Service : BaseService, IDisposable { expect.objectContaining({ name: 'User.Name', kind: 'property' }), ); }); + + it('populates typeMap for var-declared instances (implicit type)', () => { + const symbols = parseCSharp(`public class Program { + void Run() { + var service = new UserService(); + var repo = new UserRepository(); + service.AddUser(null); + } +}`); + expect(symbols.typeMap.get('service')).toEqual({ type: 'UserService', confidence: 0.9 }); + expect(symbols.typeMap.get('repo')).toEqual({ type: 'UserRepository', confidence: 0.9 }); + }); + + it('populates typeMap for explicitly-typed local variables', () => { + const symbols = parseCSharp(`public class Foo { + void Bar() { + UserService svc = new UserService(); + svc.DoWork(); + } +}`); + expect(symbols.typeMap.get('svc')).toEqual({ type: 'UserService', confidence: 0.9 }); + }); }); diff --git a/tests/search/embedding-regression.test.ts b/tests/search/embedding-regression.test.ts index 38f2e99e..6ec5df0c 100644 --- a/tests/search/embedding-regression.test.ts +++ b/tests/search/embedding-regression.test.ts @@ -68,12 +68,23 @@ describe.skipIf(!hasTransformers)('embedding regression (real model)', () => { dbPath = path.join(tmpDir, '.codegraph', 'graph.db'); // Build embeddings with the smallest/fastest model. - // Skip gracefully when HuggingFace rate-limits the model download (HTTP 429). + // Skip gracefully when HuggingFace rate-limits the model download (HTTP 429) + // or when the network is unavailable (ECONNRESET, ETIMEDOUT, ENOTFOUND, + // ECONNREFUSED, ERR_HTTP2_STREAM_CANCEL, ERR_HTTP2_SESSION_ERROR). try { await buildEmbeddings(tmpDir, 'minilm', dbPath); } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); - if (msg.includes('429')) { + const code = (err as NodeJS.ErrnoException).code ?? ''; + const isNetworkError = + msg.includes('429') || + code === 'ECONNRESET' || + code === 'ETIMEDOUT' || + code === 'ENOTFOUND' || + code === 'ECONNREFUSED' || + code === 'ERR_HTTP2_STREAM_CANCEL' || + code === 'ERR_HTTP2_SESSION_ERROR'; + if (isNetworkError) { rateLimited = true; return; }