Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
59 changes: 44 additions & 15 deletions crates/codegraph-core/src/extractors/csharp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -450,26 +450,55 @@ fn extract_csharp_type_name<'a>(type_node: &Node<'a>, source: &'a [u8]) -> Optio
}
}

/// Extract the constructor type from a `var x = new Foo()` initializer.
fn extract_var_init_type<'a>(declarator: &Node<'a>, source: &'a [u8]) -> Option<&'a str> {
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);
}
}
if child.kind() == "equals_value_clause" {
for j in 0..child.child_count() {
let Some(expr) = child.child(j) else { continue };
if expr.kind() == "object_creation_expression" {
if let Some(t) = expr.child_by_field_name("type") {
return extract_csharp_type_name(&t, source);
}
}
}
}
}
None
}

fn match_csharp_type_map(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: usize) {
match node.kind() {
"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" {
symbols.type_map.push(TypeMapEntry {
name: node_text(&name_node, source).to_string(),
type_name: type_name.to_string(),
confidence: 0.9,
});
}
let is_var = type_node.kind() == "var_keyword" || type_node.kind() == "implicit_type";
let explicit_type = if is_var { None } else { extract_csharp_type_name(&type_node, source) };
if !is_var && explicit_type.is_none() { return; }
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
};
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(),
confidence: 0.9,
});
}
}
}
Expand Down
36 changes: 29 additions & 7 deletions src/extractors/csharp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,19 +329,41 @@ function extractCSharpTypeMap(node: TreeSitterNode, ctx: ExtractorOutput): void
extractCSharpTypeMapDepth(node, ctx, 0);
}

/** Extract type info from a variable_declaration node (local vars with explicit types). */
/** 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 variable_declaration node (local vars with explicit or inferred 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);
}
}

Expand Down
16 changes: 16 additions & 0 deletions src/infrastructure/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,26 @@ function resolvePlatformPackage(): string | null {
/**
* Try to load the native napi addon.
* Returns the module on success, null on failure.
*
* Dev override: CODEGRAPH_NATIVE_ADDON_PATH can point to a locally built
* .node file (e.g. crates/codegraph-core/index.node from `cargo build`).
* Only honoured when set explicitly — never falls back to it implicitly.
*/
export function loadNative(): NativeAddon | null {
if (_cached !== undefined) return _cached;

const devOverride = process.env.CODEGRAPH_NATIVE_ADDON_PATH;
if (devOverride) {
try {
_cached = _require(devOverride) as NativeAddon;
return _cached;
} catch (err) {
_loadError = err as Error;
_cached = null;
return null;
}
Comment on lines +104 to +111

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 When the dev-override path is set but fails, the function returns null with no diagnostic output. Every other load-failure path in this file sets _loadError, and callers surface the message through getNative(). Here the error is recorded but there's no debug() call, which means a developer who misspells the path or points to a stale binary will see native silently unavailable with no log hint — even when DEBUG is enabled.

Suggested change
} catch (err) {
_loadError = err as Error;
_cached = null;
return null;
}
} catch (err) {
_loadError = err as Error;
debug(`loadNative: CODEGRAPH_NATIVE_ADDON_PATH=${devOverride} failed: ${toErrorMessage(err as Error)}`);
_cached = null;
return null;
}

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 — the merge conflict resolution adopted main's version of native.ts (PR #1389), which already includes a warn() call on the failure path (more visible than debug()). The original PR's simpler CODEGRAPH_NATIVE_ADDON_PATH approach was superseded by the more complete implementation from main that uses NAPI_RS_NATIVE_LIBRARY_PATH + automatic local binary detection.

}

const pkg = resolvePlatformPackage();
if (pkg) {
try {
Expand Down
21 changes: 21 additions & 0 deletions tests/benchmarks/resolution/fixtures/javascript/class-scope.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Regression guard: bare function calls in JS class methods must NOT resolve
// to same-named class methods. In JS/TS, bare foo() is lexically scoped to
// the module, not the class — there is no implicit this binding on bare calls.
//
// If the call.receiver guard in resolveByMethodOrGlobal (call-resolver.ts) is
// ever removed, the resolver would incorrectly emit Processor.run → Processor.flush
// (a false positive). The 1.0 precision floor on the JS fixture catches that
// regression immediately.

export function processData(x) {
return x * 2;
}

export class Processor {
run(x) {
processData(x); // same-file module-level function — resolves correctly
flush(); // bare call; no module-level 'flush' in scope — must NOT resolve to Processor.flush
}

flush() {} // Processor.flush exists; bare flush() in run() must not target it
}
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,13 @@
"kind": "calls",
"mode": "receiver-typed",
"notes": "this.service.doB() — receiver-typed via ClassB.service = new ServiceB() (class-scoped typeMap key prevents collision with ClassA.service)"
},
{
"source": { "name": "Processor.run", "file": "class-scope.js" },
"target": { "name": "processData", "file": "class-scope.js" },
"kind": "calls",
"mode": "same-file",
"notes": "Bare call to same-file module-level function — regression guard: bare flush() in run() must NOT resolve to Processor.flush (class-scoped lookup must be receiver-gated)"
}
]
}
3 changes: 2 additions & 1 deletion tests/benchmarks/resolution/resolution-benchmark.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ const THRESHOLDS: Record<string, { precision: number; recall: number }> = {
// adds bind/call/apply resolution (3 new edges in bind-call-apply.js), total expected now 33.
// Phase 8.3f adds Object.defineProperty accessor this-dispatch (#1335): getter→baz in
// define-property.js and accessorGetter→accessorTarget.accessMethod in define-property-accessor.js,
// total expected now 35.
// total expected now 35. multi-class.js adds 4 class-scoped typeMap edges → 39.
// #1407 adds class-scope.js (bare-call guard), +1 → total 40.

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 The comment attributes class-scope.js to PR #1407, but that file is introduced in this PR (#1424). The reference pattern used elsewhere in this comment block (e.g. #1335) refers to PR numbers, so this looks like a copy-paste from a draft. Suggest updating to the actual PR number so the history trail stays accurate.

Suggested change
// #1407 adds class-scope.js (bare-call guard), +1 → total 40.
// #1424 adds class-scope.js (bare-call guard), +1 → total 40.

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 comment from #1407 to #1424 (the current PR). The reference was a copy-paste error from a draft.

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 — the comment now correctly references #1422, which is the PR that landed class-scope.js on main. The merge conflict resolution had previously reset to main's stale #1407 reference; the new commit corrects it to #1422/#1424 as appropriate.

javascript: { precision: 1.0, recall: 0.9 },
// pts-javascript: hand-authored points-to JS fixture (for-of, Set, Array.from, spread) — patterns
// too broad for the main JS fixture. Patterns split per file to prevent intra-fixture FPs.
Expand Down
Loading