Skip to content
Merged
54 changes: 44 additions & 10 deletions crates/codegraph-core/src/extractors/csharp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,29 @@ fn extract_csharp_base_types(

// ── Type map extraction ─────────────────────────────────────────────────────

fn extract_var_init_type(declarator: &Node, source: &[u8]) -> Option<String> {
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());
}
}
Comment on lines +442 to +448

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Early-return ? in outer loop silently truncates the search

declarator.child(i)? uses the ? operator, so if any child slot returns None the entire function returns None immediately, abandoning all remaining children. In contrast, the inner loop on line 450 correctly uses if let Some(expr) to continue past missing slots. The TypeScript mirror of this function also uses optional-chaining (child?.type) for the same reason. If tree-sitter ever produces a gap in the child list (which the Option return type explicitly allows), this outer loop would silently stop and return no type for a valid initializer that appears later in the declarator.

Suggested change
for i in 0..declarator.child_count() {
let child = declarator.child(i)?;
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());
}
}
for i in 0..declarator.child_count() {
let Some(child) = declarator.child(i) else { continue };
if child.kind() == "object_creation_expression" {

Fix in Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d8a329d — replaced declarator.child(i)? with let Some(child) = declarator.child(i) else { continue }, matching the inner loop's pattern. Now skips None child slots instead of aborting the entire search.

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)),
Expand All @@ -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<String> = 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,
});
}
Expand Down
15 changes: 9 additions & 6 deletions src/domain/graph/builder/call-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
32 changes: 27 additions & 5 deletions src/extractors/csharp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
19 changes: 19 additions & 0 deletions src/extractors/javascript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
Comment on lines +2679 to +2697

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Stale comment in call-resolver.ts now misleads future readers

The block at src/domain/graph/builder/call-resolver.ts lines 85–93 still says "extractReceiverName returns the raw node text for non-identifier nodes, so (new A).t() produces receiver='(new A)'" — but that is no longer true after this change. The accompanying regex fallback is now dead code for the new_expression / parenthesized_expression(new_expression) cases it was designed to cover. The comment should be updated (or the fallback removed) to keep the two sites consistent; otherwise the next developer who hits a resolution issue and reads call-resolver.ts will be chasing a phantom.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — updated the stale comment in call-resolver.ts (commit ca5065b). The comment now correctly describes the regex as a belt-and-suspenders fallback for AST node types not yet handled by extractReceiverName, rather than the primary handler for inline-new receivers. The regex is kept as a safety net for future unhandled cases.

return objNode.text;
}

Expand Down
4 changes: 3 additions & 1 deletion tests/benchmarks/resolution/resolution-benchmark.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,9 @@ const THRESHOLDS: Record<string, { precision: number; recall: number }> = {
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 },
Expand Down
22 changes: 22 additions & 0 deletions tests/parsers/csharp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
});
});
15 changes: 13 additions & 2 deletions tests/search/embedding-regression.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Loading