Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
40 changes: 37 additions & 3 deletions crates/codegraph-core/src/extractors/swift.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,9 +299,22 @@ fn match_swift_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _dept
}
"navigation_expression" => {
let last = fn_node.child(fn_node.child_count().saturating_sub(1));
let name = last
.map(|n| node_text(&n, source).to_string())
.unwrap_or_else(|| node_text(&fn_node, source).to_string());
// Swift's grammar wraps the method name in a `navigation_suffix` node
// (e.g. `.save` text), not a bare `simple_identifier`. Descend into
// navigation_suffix to get the inner simple_identifier so the resolved
// name is "save" not ".save". Mirrors WASM extractors/swift.ts fix.
let name = last.map(|n| {
if n.kind() == "navigation_suffix" {
find_child(&n, "simple_identifier")
.map(|id| node_text(&id, source).to_string())
.unwrap_or_else(|| {
let t = node_text(&n, source);
t.trim_start_matches('.').to_string()
})
} else {
node_text(&n, source).to_string()
}
}).unwrap_or_else(|| node_text(&fn_node, source).to_string());
let receiver = fn_node.child(0)
.map(|n| node_text(&n, source).to_string());
symbols.calls.push(Call {
Expand Down Expand Up @@ -376,4 +389,25 @@ mod tests {
assert_eq!(s.imports[0].source, "Foundation");
assert!(s.imports[0].swift_import.unwrap());
}

/// navigation_expression uses a `navigation_suffix` child — the method name
/// must be extracted as a bare identifier ("save"), not ".save".
#[test]
fn extracts_navigation_call_bare_name() {
let s = parse_swift("func f() { repo.save(x) }");
let call = s.calls.iter().find(|c| c.receiver.as_deref() == Some("repo")).unwrap();
assert_eq!(call.name, "save", "method name must not include leading dot");
assert_eq!(call.receiver.as_deref(), Some("repo"));
}

/// Class property with a type annotation seeds the typeMap so that
/// receiver-typed call edges can be resolved.
#[test]
fn class_property_type_annotation_seeds_type_map() {
let s = parse_swift("class Service { private let repo: Repository }");
let entry = s.type_map.iter().find(|e| e.name == "repo");
assert!(entry.is_some(), "repo should appear in type_map");
assert_eq!(entry.unwrap().type_name, "Repository");
assert_eq!(entry.unwrap().confidence, 0.9);
}
}
40 changes: 40 additions & 0 deletions src/extractors/javascript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,7 @@ function extractConstantsWalk(node: TreeSitterNode, definitions: Definition[]):
}

extractConstDeclarators(declNode, definitions);
extractLetVarObjLiteralDeclarators(declNode, definitions);

// Recurse into non-function, non-export-statement children (blocks, if-statements, etc.)
if (child.type !== 'export_statement') {
Expand Down Expand Up @@ -578,6 +579,33 @@ function extractConstDeclarators(declNode: TreeSitterNode, definitions: Definiti
}
}

/**
* Extract qualified method definitions from `let`/`var` object-literal declarations.
* Mirrors Rust match_js_objlit_qualified_method_defs which emits qualified definitions
* for method_definition (all declaration kinds) and pair+arrow/function (let/var only,
* since const is already handled by extractConstDeclarators → extractObjectLiteralFunctions).
*
* Called from extractConstantsWalk which already provides the function-scope guard.
* `var q1 = { m1() {} }` → emits Definition { name: 'q1.m1', kind: 'function' }
Comment on lines +582 to +590

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 PR description vs. actual change mismatch — clarification needed

The PR summary says "Native extractor missed object-spread property extraction — javascript.rs updated to handle { ...obj } spread patterns." However, this diff contains no changes to crates/codegraph-core/src/extractors/javascript.rs, and the actual code here handles method definitions inside let/var object literals ({ m() {} }, { k: () => {} }), not { ...obj } spread patterns. If javascript.rs already had match_js_objlit_qualified_method_defs before this PR, it would be helpful to confirm that explicitly; if it still needs to be added, that fix appears to be missing. Did crates/codegraph-core/src/extractors/javascript.rs already have match_js_objlit_qualified_method_defs for let/var object literals before this PR? The description implies javascript.rs was changed here, but no diff for that file is present. If Rust was already correct, the description is just wrong; if Rust still needs the fix, it's missing from this PR.

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.

Investigated — match_js_objlit_qualified_method_defs already exists in javascript.rs (no change needed there). The PR description's mention of spread patterns and javascript.rs changes was inaccurate. The TS function extractLetVarObjLiteralDeclarators mirrors the existing Rust equivalent. Updated the JSDoc to say exactly that: it mirrors match_js_objlit_qualified_method_defs in javascript.rs. Commit: 0301a61.

*/
function extractLetVarObjLiteralDeclarators(
declNode: TreeSitterNode,
definitions: Definition[],
): void {
const t = declNode.type;
if (t !== 'lexical_declaration' && t !== 'variable_declaration') return;
if (declNode.text.startsWith('const ')) return; // handled by extractConstDeclarators

for (let j = 0; j < declNode.childCount; j++) {
const declarator = declNode.child(j);
if (declarator?.type !== 'variable_declarator') continue;
const nameN = declarator.childForFieldName('name');
const valueN = declarator.childForFieldName('value');
if (nameN?.type !== 'identifier' || !valueN || valueN.type !== 'object') continue;
extractObjectLiteralFunctions(valueN, nameN.text, definitions);
}
}

/**
* Recursive walk to find dynamic import() calls.
* Query patterns match call_expression with identifier/member_expression/subscript_expression
Expand Down Expand Up @@ -1036,6 +1064,18 @@ function handleVariableDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
if (valueN.type === 'object') {
extractObjectLiteralFunctions(valueN, nameN.text, ctx.definitions);
}
} else if (
!isConst &&
nameN.type === 'identifier' &&
valueN.type === 'object' &&
!hasFunctionScopeAncestor(node)
) {
// `let`/`var` object literals: extract qualified method definitions so that
// `obj.method()` calls resolve correctly. Mirrors Rust match_js_objlit_qualified_method_defs
// which emits method_definition qualified names for ALL declaration kinds and
// pair+arrow/function for let/var only (const is already handled above).
// Scope guard prevents local object properties from polluting the global index.
extractObjectLiteralFunctions(valueN, nameN.text, ctx.definitions);
} else if (isConst && nameN.type === 'object_pattern' && !hasFunctionScopeAncestor(node)) {
// Destructured bindings: const { handleToken, checkPermissions } = initAuth(...)
// Each destructured property becomes a function definition so it can be
Expand Down
49 changes: 46 additions & 3 deletions src/extractors/swift.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ function walkSwiftNode(node: TreeSitterNode, ctx: ExtractorOutput): void {
handleSwiftCallExpression(node, ctx);
break;
case 'property_declaration':
seedSwiftPropertyTypeMap(node, ctx);
handleSwiftPropertyDecl(node, ctx);
break;
}
Expand Down Expand Up @@ -250,11 +251,26 @@ function handleSwiftCallExpression(node: TreeSitterNode, ctx: ExtractorOutput):
if (!funcNode) return;
const call: Call = { name: '', line: node.startPosition.row + 1 };
if (funcNode.type === 'navigation_expression') {
// obj.method(...)
// obj.method(...) — Swift's tree-sitter grammar wraps the suffix in a
// `navigation_suffix` node: navigation_expression > [simple_identifier, navigation_suffix].
// We must descend into navigation_suffix to get the bare method name.
// Mirrors Rust match_swift_node which reads node_text of the last child directly
// (which equals ".method") and the receiver from child(0).

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/inaccurate comment about Rust reference behavior

The comment says the TypeScript "Mirrors Rust match_swift_node which reads node_text of the last child directly (which equals .method)." That describes the old broken Rust behavior — but match_swift_node in swift.rs is also updated in this same PR to descend into navigation_suffix via find_child. The Rust no longer reads the last child's text directly, so framing the TypeScript fix as "mirroring" an already-fixed Rust behavior is misleading and will confuse future readers of this comment.

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 — the comment now says both TS and Rust descend into navigation_suffix via find_child to extract the inner simple_identifier, with a dot-strip fallback. Commit: b0f5fd0.

const lastChild = funcNode.child(funcNode.childCount - 1);
const firstChild = funcNode.child(0);
if (lastChild && lastChild.type === 'simple_identifier' && firstChild) {
call.name = lastChild.text;
if (lastChild && firstChild) {
// Resolve the method name: descend into navigation_suffix to find the
// simple_identifier, or fall back to stripping the leading dot from the text.
let methodName: string;
if (lastChild.type === 'simple_identifier') {
methodName = lastChild.text;
} else if (lastChild.type === 'navigation_suffix') {
const inner = findChild(lastChild, 'simple_identifier');
methodName = inner ? inner.text : lastChild.text.replace(/^\./, '');
} else {
methodName = lastChild.text;
}
call.name = methodName;
call.receiver = firstChild.text;
}
} else if (funcNode.type === 'simple_identifier') {
Expand All @@ -265,6 +281,33 @@ function handleSwiftCallExpression(node: TreeSitterNode, ctx: ExtractorOutput):
if (call.name) ctx.calls.push(call);
}

/**
* Seed the typeMap for a property_declaration with a type annotation.
* This runs for ALL property_declaration nodes (including class-body ones)
* so that `repo.method()` calls can be resolved to the correct class.
* Mirrors Rust match_swift_type_map which walks all nodes unconditionally.
*/
function seedSwiftPropertyTypeMap(node: TreeSitterNode, ctx: ExtractorOutput): void {
const typeAnn = findChild(node, 'type_annotation');
if (!typeAnn) return;
// type_annotation: ":" <user_type | simple_identifier | ...>
// The last child is the actual type node.
const lastChild = typeAnn.child(typeAnn.childCount - 1);
if (!lastChild) return;
// For "user_type > type_identifier", grab the inner identifier text;
// for a plain simple_identifier, use it directly.
const typeNode =
lastChild.type === 'user_type' ? findChild(lastChild, 'type_identifier') : lastChild;
if (!typeNode) return;
const typeName = typeNode.text;
if (!typeName) return;
const pattern = findChild(node, 'pattern');
if (!pattern) return;
const varName = findChild(pattern, 'simple_identifier')?.text ?? pattern.text;
if (!varName) return;
ctx.typeMap.set(varName, { type: typeName, confidence: 0.9 });
}

function handleSwiftPropertyDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
// Only handle top-level properties (class properties are handled inline)
if (
Expand Down
31 changes: 31 additions & 0 deletions tests/parsers/javascript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,37 @@ describe('JavaScript parser', () => {
);
});

// let/var object-literal method definitions
it('extracts qualified definitions from var object-literal arrow functions', () => {
// `var x = { a: function() {} }` — native produces `x.a`, WASM must too.
// Parity fix: extractLetVarObjLiteralDeclarators covers let/var (const already
// handled by extractConstDeclarators → extractObjectLiteralFunctions).
const symbols = parseJS(`var x = { a: function() {}, b: () => {} };`);
expect(symbols.definitions).toContainEqual(
expect.objectContaining({ name: 'x.a', kind: 'function' }),
);
expect(symbols.definitions).toContainEqual(
expect.objectContaining({ name: 'x.b', kind: 'function' }),
);
});

it('extracts qualified definitions from let object-literal shorthand methods', () => {
// `let x12 = { f13() {} }` — matches jelly-micro classes.js fixtures.
const symbols = parseJS(`let x12 = { f13() {}, f14: () => {} };`);
expect(symbols.definitions).toContainEqual(
expect.objectContaining({ name: 'x12.f13', kind: 'function' }),
);
expect(symbols.definitions).toContainEqual(
expect.objectContaining({ name: 'x12.f14', kind: 'function' }),
);
});

it('does not extract let/var object-literal definitions inside function scope', () => {
// Scope guard mirrors const path — skips object literals inside function bodies.
const symbols = parseJS(`function setup() { var local = { f() {} }; }`);
expect(symbols.definitions).not.toContainEqual(expect.objectContaining({ name: 'local.f' }));
});

// Line range verification
it('sets correct line and endLine on callback definition', () => {
const code = [
Expand Down
23 changes: 23 additions & 0 deletions tests/parsers/swift.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,27 @@ describe('Swift parser', () => {
expect(symbols.calls).toContainEqual(expect.objectContaining({ name: 'print' }));
expect(symbols.calls).toContainEqual(expect.objectContaining({ name: 'bar' }));
});

it('extracts navigation_expression calls with bare method name and receiver', () => {
// navigation_expression uses a navigation_suffix child node — method name
// must be "save" not ".save" so the call resolver can find UserRepository.save.
const symbols = parseSwift(`func f() { repo.save(x) }`);
const call = symbols.calls.find((c) => c.receiver === 'repo');
expect(call).toBeDefined();
expect(call!.name).toBe('save');
expect(call!.receiver).toBe('repo');
});

it('seeds typeMap from class property type annotations', () => {
// `private let repo: UserRepository` in a class body must seed typeMap
// so that receiver-typed call edges (repo.save → UserRepository) can resolve.
const symbols = parseSwift(`class Service {
private let repo: UserRepository
func createUser() { repo.save(x) }
}`);
const entry = symbols.typeMap.get('repo');
expect(entry).toBeDefined();
expect(entry!.type).toBe('UserRepository');
expect(entry!.confidence).toBe(0.9);
});
});
Loading