Skip to content

Commit 5ef11f6

Browse files
committed
fix(resolver): seed all get/set accessors per descriptor and use qualified names for object literal defs (#1351)
Two fixes: 1. findDescriptorAccessors (TS) / find_descriptor_accessors (Rust): changed from returning the first accessor to returning all of them. For { get: getter, set: setter }, both 'getter:this = obj' and 'setter:this = obj' are now seeded so both functions can resolve this-dispatch via Phase 8.3f. 2. extractObjectLiteralFunctions (TS) / extract_object_literal_functions (Rust): definitions are now emitted under qualified names ('obj.baz' instead of bare 'baz') to avoid polluting the global definition index with common property names that could produce false-positive edges via the broad exact-name fallback. The typeMap value for function/arrow entries is also updated to the qualified name so the resolver calls lookup.byName('obj.baz'). Also adds complexity metrics for method_definition nodes in the Rust branch (parity with the pair branch which already computed them).
1 parent d2b55b6 commit 5ef11f6

2 files changed

Lines changed: 70 additions & 38 deletions

File tree

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

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,8 @@ fn seed_define_property_entries(node: &Node, source: &[u8], symbols: &mut FileSy
242242
confidence: 0.85,
243243
});
244244
}
245-
// Phase 8.3f: { get: getter } or { set: setter } → this inside getter/setter === obj
246-
if let Some(accessor) = find_descriptor_accessor(&args[2], source) {
245+
// Phase 8.3f: { get: getter } and/or { set: setter } → this inside each accessor === obj
246+
for accessor in find_descriptor_accessors(&args[2], source) {
247247
symbols.type_map.push(TypeMapEntry {
248248
name: format!("{}:this", accessor),
249249
type_name: obj_name.to_string(),
@@ -357,10 +357,12 @@ fn find_descriptor_value<'a>(node: &Node<'a>, source: &'a [u8]) -> Option<&'a st
357357
None
358358
}
359359

360-
/// Phase 8.3f: return the identifier text of a `get` or `set` accessor in a property descriptor.
361-
/// `{ get: getter }` → Some("getter"); `{ set: setter }` → Some("setter").
362-
fn find_descriptor_accessor<'a>(node: &Node<'a>, source: &'a [u8]) -> Option<&'a str> {
363-
if node.kind() != "object" { return None; }
360+
/// Phase 8.3f: return the identifier texts of all `get` and `set` accessors in a property
361+
/// descriptor. `{ get: getter, set: setter }` → ["getter", "setter"].
362+
/// Returns all accessors so that each one gets a `callerName:this = obj` typeMap entry.
363+
fn find_descriptor_accessors<'a>(node: &Node<'a>, source: &'a [u8]) -> Vec<&'a str> {
364+
if node.kind() != "object" { return Vec::new(); }
365+
let mut result = Vec::new();
364366
for i in 0..node.child_count() {
365367
let Some(child) = node.child(i) else { continue };
366368
if child.kind() != "pair" { continue; }
@@ -369,18 +371,23 @@ fn find_descriptor_accessor<'a>(node: &Node<'a>, source: &'a [u8]) -> Option<&'a
369371
if key_text != "get" && key_text != "set" { continue; }
370372
let Some(val) = child.child_by_field_name("value") else { continue };
371373
if val.kind() == "identifier" {
372-
return Some(node_text(&val, source));
374+
result.push(node_text(&val, source));
373375
}
374376
}
375-
None
377+
result
376378
}
377379

378380
/// Phase 8.3f: extract function/arrow properties from an object literal as standalone definitions
379381
/// and seed composite typeMap keys so that `this.method()` inside Object.defineProperty accessors
380382
/// can resolve them.
381383
///
382-
/// `const obj = { baz: () => {} }` → Definition { name: "baz", kind: "function" }
383-
/// + TypeMapEntry { name: "obj.baz", type_name: "baz" }
384+
/// Definitions are emitted under qualified names (`obj.baz`) to avoid polluting the global
385+
/// definition index with common property names like `init`, `run`, or `render`. The typeMap
386+
/// value for function/arrow properties also uses the qualified name so the resolver calls
387+
/// `lookup.byName("obj.baz")` rather than `lookup.byName("baz")`.
388+
///
389+
/// `const obj = { baz: () => {} }` → Definition { name: "obj.baz", kind: "function" }
390+
/// + TypeMapEntry { name: "obj.baz", type_name: "obj.baz" }
384391
/// `const obj = { baz }` (shorthand) → TypeMapEntry { name: "obj.baz", type_name: "baz" }
385392
fn extract_object_literal_functions(
386393
obj_node: &Node,
@@ -408,10 +415,13 @@ fn extract_object_literal_functions(
408415
Some(node_text(&key_n, source).to_string())
409416
};
410417
let Some(key) = key else { continue };
418+
let qualified = format!("{}.{}", var_name, key);
411419
match val_n.kind() {
412420
"arrow_function" | "function_expression" | "function" => {
421+
// Use qualified name for the definition so it doesn't collide with
422+
// unrelated top-level functions sharing the same property name.
413423
symbols.definitions.push(Definition {
414-
name: key.clone(),
424+
name: qualified.clone(),
415425
kind: "function".to_string(),
416426
line: start_line(&child),
417427
end_line: Some(end_line(&val_n)),
@@ -420,16 +430,17 @@ fn extract_object_literal_functions(
420430
cfg: build_function_cfg(&val_n, "javascript", source),
421431
children: None,
422432
});
433+
// Store qualified name as value so resolver looks up the qualified def.
423434
symbols.type_map.push(TypeMapEntry {
424-
name: format!("{}.{}", var_name, key),
425-
type_name: key,
435+
name: qualified.clone(),
436+
type_name: qualified,
426437
confidence: 0.85,
427438
});
428439
}
429440
"identifier" => {
430441
let target = node_text(&val_n, source);
431442
symbols.type_map.push(TypeMapEntry {
432-
name: format!("{}.{}", var_name, key),
443+
name: qualified,
433444
type_name: target.to_string(),
434445
confidence: 0.85,
435446
});
@@ -439,14 +450,16 @@ fn extract_object_literal_functions(
439450
}
440451
"method_definition" => {
441452
let Some(name_n) = child.child_by_field_name("name") else { continue };
453+
let qualified = format!("{}.{}", var_name, node_text(&name_n, source));
454+
let body = child.child_by_field_name("body");
442455
symbols.definitions.push(Definition {
443-
name: node_text(&name_n, source).to_string(),
456+
name: qualified,
444457
kind: "function".to_string(),
445458
line: start_line(&child),
446459
end_line: Some(end_line(&child)),
447460
decorators: None,
448-
complexity: None,
449-
cfg: None,
461+
complexity: body.and_then(|b| compute_all_metrics(&b, source, "javascript")),
462+
cfg: body.and_then(|b| build_function_cfg(&b, "javascript", source)),
450463
children: None,
451464
});
452465
}

src/extractors/javascript.ts

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,7 @@ function extractConstDeclarators(declNode: TreeSitterNode, definitions: Definiti
499499
});
500500
// Phase 8.3f: extract function/arrow properties from object literals.
501501
if (valueN.type === 'object') {
502-
extractObjectLiteralFunctions(valueN, definitions);
502+
extractObjectLiteralFunctions(valueN, nameN.text, definitions);
503503
}
504504
}
505505
}
@@ -841,7 +841,7 @@ function handleVariableDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
841841
// Phase 8.3f: extract function/arrow properties from object literals so that
842842
// this.method() calls inside Object.defineProperty accessors can resolve them.
843843
if (valueN.type === 'object') {
844-
extractObjectLiteralFunctions(valueN, ctx.definitions);
844+
extractObjectLiteralFunctions(valueN, nameN.text, ctx.definitions);
845845
}
846846
} else if (isConst && nameN.type === 'object_pattern' && !hasFunctionScopeAncestor(node)) {
847847
// Destructured bindings: const { handleToken, checkPermissions } = initAuth(...)
@@ -866,11 +866,20 @@ function handleVariableDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
866866
/**
867867
* Phase 8.3f: extract function/arrow function properties from an object literal as standalone
868868
* definitions so that `this.method()` calls inside Object.defineProperty accessor functions can
869-
* resolve them via the normal same-file definition lookup.
869+
* resolve them via the same-file definition lookup.
870870
*
871-
* `const obj = { baz: () => {} }` → emits Definition { name: 'baz', kind: 'function' }
871+
* Definitions are emitted as qualified names (`obj.baz` rather than bare `baz`) to avoid
872+
* polluting the global definition index with common property names like `init`, `run`, or
873+
* `render`. The typeMap value stored by the caller also uses the qualified name so the resolver
874+
* looks up `lookup.byName('obj.baz')` rather than `lookup.byName('baz')`.
875+
*
876+
* `const obj = { baz: () => {} }` → emits Definition { name: 'obj.baz', kind: 'function' }
872877
*/
873-
function extractObjectLiteralFunctions(objNode: TreeSitterNode, definitions: Definition[]): void {
878+
function extractObjectLiteralFunctions(
879+
objNode: TreeSitterNode,
880+
varName: string,
881+
definitions: Definition[],
882+
): void {
874883
for (let i = 0; i < objNode.childCount; i++) {
875884
const child = objNode.child(i);
876885
if (!child) continue;
@@ -887,7 +896,7 @@ function extractObjectLiteralFunctions(objNode: TreeSitterNode, definitions: Def
887896
valueNode.type === 'function'
888897
) {
889898
definitions.push({
890-
name: keyName,
899+
name: `${varName}.${keyName}`,
891900
kind: 'function',
892901
line: nodeStartLine(child),
893902
endLine: nodeEndLine(valueNode),
@@ -897,7 +906,7 @@ function extractObjectLiteralFunctions(objNode: TreeSitterNode, definitions: Def
897906
const nameNode = child.childForFieldName('name');
898907
if (nameNode) {
899908
definitions.push({
900-
name: nameNode.text,
909+
name: `${varName}.${nameNode.text}`,
901910
kind: 'function',
902911
line: nodeStartLine(child),
903912
endLine: nodeEndLine(child),
@@ -1637,10 +1646,15 @@ function handleVarDeclaratorTypeMap(
16371646
}
16381647

16391648
// Phase 8.3f: seed composite pts keys for object literal properties.
1640-
// `const obj = { baz: () => {} }` → typeMap['obj.baz'] = 'baz'
1641-
// `const obj = { baz }` (shorthand) → typeMap['obj.baz'] = 'baz'
1649+
// `const obj = { baz: () => {} }` → typeMap['obj.baz'] = 'obj.baz'
1650+
// `const obj = { baz }` (shorthand) → typeMap['obj.baz'] = 'baz' (bare identifier target)
1651+
// `const obj = { baz: otherFn }` → typeMap['obj.baz'] = 'otherFn' (identifier alias)
1652+
//
1653+
// For function/arrow values, the value is the qualified name ('obj.baz') because
1654+
// extractObjectLiteralFunctions now registers definitions under that qualified name to avoid
1655+
// polluting the global index with bare property names like 'init', 'run', or 'render'.
16421656
// Enables accessor this-dispatch: when typeMap['getter:this'] = 'obj',
1643-
// resolving this.baz() inside getter → typeMap['obj.baz'] → 'baz'.
1657+
// resolving this.baz() inside getter → typeMap['obj.baz'] → 'obj.baz' → lookup.byName('obj.baz').
16441658
if (valueN.type === 'object') {
16451659
for (let i = 0; i < valueN.childCount; i++) {
16461660
const child = valueN.child(i);
@@ -1654,14 +1668,16 @@ function handleVarDeclaratorTypeMap(
16541668
const keyName =
16551669
keyNode.type === 'string' ? keyNode.text.replace(/^['"]|['"]$/g, '') : keyNode.text;
16561670
if (!keyName) continue;
1671+
const qualifiedKey = `${nameN.text}.${keyName}`;
16571672
if (
16581673
valNode.type === 'arrow_function' ||
16591674
valNode.type === 'function_expression' ||
16601675
valNode.type === 'function'
16611676
) {
1662-
setTypeMapEntry(typeMap, `${nameN.text}.${keyName}`, keyName, 0.85);
1677+
// Store the qualified name so the resolver finds the qualified definition.
1678+
setTypeMapEntry(typeMap, qualifiedKey, qualifiedKey, 0.85);
16631679
} else if (valNode.type === 'identifier') {
1664-
setTypeMapEntry(typeMap, `${nameN.text}.${keyName}`, valNode.text, 0.85);
1680+
setTypeMapEntry(typeMap, qualifiedKey, valNode.text, 0.85);
16651681
}
16661682
}
16671683
}
@@ -1759,9 +1775,8 @@ function handleDefinePropertyTypeMap(
17591775
if (target) {
17601776
setTypeMapEntry(typeMap, `${arg0.text}.${key}`, target, 0.85);
17611777
}
1762-
// Phase 8.3f: { get: getter } or { set: setter } → this inside getter/setter is arg0 (obj)
1763-
const accessor = findDescriptorAccessor(arg2);
1764-
if (accessor) {
1778+
// Phase 8.3f: { get: getter } and/or { set: setter } → this inside each accessor is arg0 (obj)
1779+
for (const accessor of findDescriptorAccessors(arg2)) {
17651780
setTypeMapEntry(typeMap, `${accessor}:this`, arg0.text, 0.85);
17661781
}
17671782
} else {
@@ -1799,19 +1814,23 @@ function findDescriptorValue(desc: TreeSitterNode): string | undefined {
17991814
}
18001815

18011816
/**
1802-
* Phase 8.3f: return the identifier text of a `get` or `set` accessor in a property descriptor.
1803-
* `{ get: getter }` → 'getter'; `{ set: setter }` → 'setter'.
1817+
* Phase 8.3f: return the identifier texts of all `get` and `set` accessors in a property
1818+
* descriptor. `{ get: getter, set: setter }` → ['getter', 'setter'].
1819+
* Returns all accessors so that each one gets a `callerName:this = obj` typeMap entry.
18041820
*/
1805-
function findDescriptorAccessor(desc: TreeSitterNode): string | undefined {
1806-
if (desc.type !== 'object') return undefined;
1821+
function findDescriptorAccessors(desc: TreeSitterNode): string[] {
1822+
if (desc.type !== 'object') return [];
1823+
const result: string[] = [];
18071824
for (let i = 0; i < desc.childCount; i++) {
18081825
const pair = desc.child(i);
18091826
if (pair?.type !== 'pair') continue;
18101827
const key = pair.childForFieldName('key');
18111828
const val = pair.childForFieldName('value');
1812-
if ((key?.text === 'get' || key?.text === 'set') && val?.type === 'identifier') return val.text;
1829+
if ((key?.text === 'get' || key?.text === 'set') && val?.type === 'identifier') {
1830+
result.push(val.text);
1831+
}
18131832
}
1814-
return undefined;
1833+
return result;
18151834
}
18161835

18171836
/** Seed composite pts keys for each property in a prototype object literal. */

0 commit comments

Comments
 (0)