Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
21 changes: 21 additions & 0 deletions crates/codegraph-core/src/edge_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,27 @@ fn resolve_call_targets<'a>(
|| call.receiver.as_deref() == Some("self")
|| call.receiver.as_deref() == Some("super")
{
// Phase 8.3f: accessor this-dispatch via Object.defineProperty.
// When a plain function (no class prefix in caller_name) is registered as a get/set
// accessor for `obj`, typeMap seeds 'callerName:this' = 'obj'. Resolve this.method()
// via typeMap['obj.method'] → the concrete definition. Runs before the broad exact-name
// lookup to avoid false positives from unrelated same-file definitions.
if call.receiver.as_deref() == Some("this") && !caller_name.is_empty() && !caller_name.contains('.') {
let accessor_key = format!("{}:this", caller_name);
if let Some(&(obj_name, _)) = type_map.get(accessor_key.as_str()) {
let obj_method_key = format!("{}.{}", obj_name, call.name);
if let Some(&(target_fn, _)) = type_map.get(obj_method_key.as_str()) {
let accessor_resolved: Vec<&NodeInfo> = ctx.nodes_by_name
.get(target_fn)
.map(|v| v.iter()
.filter(|n| import_resolution::compute_confidence(rel_path, &n.file, None) >= 0.5)
.copied().collect())
.unwrap_or_default();
if !accessor_resolved.is_empty() { return accessor_resolved; }
}
}
}

// First try exact name match (e.g. an unqualified function named "area").
let exact: Vec<&NodeInfo> = ctx.nodes_by_name
.get(call.name.as_str())
Expand Down
149 changes: 142 additions & 7 deletions crates/codegraph-core/src/extractors/javascript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,17 +231,27 @@ fn seed_define_property_entries(node: &Node, source: &[u8], symbols: &mut FileSy
}

if method == "defineProperty" {
// Object.defineProperty(obj, "key", { value: fn })
// Object.defineProperty(obj, "key", { value: fn }) or { get: getter }
if args.len() < 3 { return; }
if args[0].kind() != "identifier" { return; }
let obj_name = node_text(&args[0], source);
let Some(key) = extract_string_fragment(&args[1], source) else { return };
let Some(target) = find_descriptor_value(&args[2], source) else { return };
symbols.type_map.push(TypeMapEntry {
name: format!("{}.{}", obj_name, key),
type_name: target.to_string(),
confidence: 0.85,
});
// Phase 8.3e: { value: fn } → obj.key pts to fn
if let Some(target) = find_descriptor_value(&args[2], source) {
symbols.type_map.push(TypeMapEntry {
name: format!("{}.{}", obj_name, key),
type_name: target.to_string(),
confidence: 0.85,
});
}
// Phase 8.3f: { get: getter } and/or { set: setter } → this inside each accessor === obj
for accessor in find_descriptor_accessors(&args[2], source) {
symbols.type_map.push(TypeMapEntry {
name: format!("{}:this", accessor),
type_name: obj_name.to_string(),
confidence: 0.85,
});
}
} else {
// Object.defineProperties(obj, { "key": { value: fn }, ... })
if args.len() < 2 { return; }
Expand Down Expand Up @@ -349,6 +359,124 @@ fn find_descriptor_value<'a>(node: &Node<'a>, source: &'a [u8]) -> Option<&'a st
None
}

/// Phase 8.3f: return the identifier texts of all `get` and `set` accessors in a property
/// descriptor. `{ get: getter, set: setter }` → ["getter", "setter"].
/// Returns all accessors so that each one gets a `callerName:this = obj` typeMap entry.
fn find_descriptor_accessors<'a>(node: &Node<'a>, source: &'a [u8]) -> Vec<&'a str> {
if node.kind() != "object" { return Vec::new(); }
let mut result = Vec::new();
for i in 0..node.child_count() {
let Some(child) = node.child(i) else { continue };
if child.kind() != "pair" { continue; }
let Some(key) = child.child_by_field_name("key") else { continue };
let key_text = node_text(&key, source);
if key_text != "get" && key_text != "set" { continue; }
let Some(val) = child.child_by_field_name("value") else { continue };
if val.kind() == "identifier" {
result.push(node_text(&val, source));
}
}
result
}

/// Phase 8.3f: extract function/arrow properties from an object literal as standalone definitions
/// and seed composite typeMap keys so that `this.method()` inside Object.defineProperty accessors
/// can resolve them.
///
/// Definitions are emitted under qualified names (`obj.baz`) to avoid polluting the global
/// definition index with common property names like `init`, `run`, or `render`. The typeMap
/// value for function/arrow properties also uses the qualified name so the resolver calls
/// `lookup.byName("obj.baz")` rather than `lookup.byName("baz")`.
///
/// `const obj = { baz: () => {} }` → Definition { name: "obj.baz", kind: "function" }
/// + TypeMapEntry { name: "obj.baz", type_name: "obj.baz" }
/// `const obj = { baz }` (shorthand) → TypeMapEntry { name: "obj.baz", type_name: "baz" }
fn extract_object_literal_functions(
obj_node: &Node,
source: &[u8],
var_name: &str,
symbols: &mut FileSymbols,
) {
for i in 0..obj_node.child_count() {
let Some(child) = obj_node.child(i) else { continue };
match child.kind() {
"shorthand_property_identifier" => {
let prop_name = node_text(&child, source);
symbols.type_map.push(TypeMapEntry {
name: format!("{}.{}", var_name, prop_name),
type_name: prop_name.to_string(),
confidence: 0.85,
});
}
"pair" => {
let Some(key_n) = child.child_by_field_name("key") else { continue };
let Some(val_n) = child.child_by_field_name("value") else { continue };
let key = if key_n.kind() == "string" {
extract_string_fragment(&key_n, source).map(|s| s.to_string())
} else {
Some(node_text(&key_n, source).to_string())
};
let Some(key) = key else { continue };
let qualified = format!("{}.{}", var_name, key);
match val_n.kind() {
"arrow_function" | "function_expression" | "function" => {
// Use qualified name for the definition so it doesn't collide with
// unrelated top-level functions sharing the same property name.
symbols.definitions.push(Definition {
name: qualified.clone(),
kind: "function".to_string(),
line: start_line(&child),
end_line: Some(end_line(&val_n)),
decorators: None,
complexity: compute_all_metrics(&val_n, source, "javascript"),
cfg: build_function_cfg(&val_n, "javascript", source),
children: None,
});
// Store qualified name as value so resolver looks up the qualified def.
symbols.type_map.push(TypeMapEntry {
name: qualified.clone(),
type_name: qualified,
confidence: 0.85,
});
}
"identifier" => {
let target = node_text(&val_n, source);
symbols.type_map.push(TypeMapEntry {
name: qualified,
type_name: target.to_string(),
confidence: 0.85,
});
}
_ => {}
}
}
"method_definition" => {
let Some(name_n) = child.child_by_field_name("name") else { continue };
let qualified = format!("{}.{}", var_name, node_text(&name_n, source));
let body = child.child_by_field_name("body");
symbols.definitions.push(Definition {
name: qualified.clone(),
kind: "function".to_string(),
line: start_line(&child),
end_line: Some(end_line(&child)),
decorators: None,
complexity: body.and_then(|b| compute_all_metrics(&b, source, "javascript")),
cfg: body.and_then(|b| build_function_cfg(&b, "javascript", source)),
children: None,
});
// Seed typeMap so the two-step accessor dispatch can find the qualified def.
// `const obj = { baz() {} }` → typeMap['obj.baz'] = 'obj.baz'
symbols.type_map.push(TypeMapEntry {
name: qualified.clone(),
type_name: qualified,
confidence: 0.85,
});
}
_ => {}
}
}
}

// ── Return-type map extraction (Phase 8.2 parity) ───────────────────────────

/// Walk the AST collecting function/method return types into `symbols.return_type_map`.
Expand Down Expand Up @@ -837,6 +965,13 @@ fn handle_var_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
cfg: None,
children: None,
});
// Phase 8.3f: extract function/arrow properties from object literals and seed
// typeMap composite keys so that this.method() inside Object.defineProperty
// accessor functions can resolve them.
if value_n.kind() == "object" && name_n.kind() == "identifier" {
let var_name = node_text(&name_n, source);
extract_object_literal_functions(&value_n, source, var_name, symbols);
}
} else if name_n.kind() == "identifier" && value_n.kind() == "identifier" {
// Phase 8.3: `const alias = handler` — record for pts analysis.
// Mirror the JS BUILTIN_GLOBALS guard: skip well-known JS globals so
Expand Down
29 changes: 29 additions & 0 deletions src/domain/graph/builder/call-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,35 @@ export function resolveByMethodOrGlobal(
call.receiver === 'self' ||
call.receiver === 'super'
) {
// Phase 8.3f: accessor this-dispatch via Object.defineProperty.
// When a plain function (no class prefix) is registered as a get/set accessor for `obj`
// via Object.defineProperty, typeMap seeds 'callerName:this' = 'obj'.
// We then resolve this.method() → typeMap['obj.method'] → the concrete definition.
// This runs before the broad exact-name lookup to avoid false positives from
// unrelated same-file definitions.
if (call.receiver === 'this' && callerName && !callerName.includes('.')) {
const accessorThisEntry = typeMap.get(`${callerName}:this`);
const objName = accessorThisEntry
? typeof accessorThisEntry === 'string'
? accessorThisEntry
: (accessorThisEntry as { type?: string }).type
: null;
if (objName) {
const objMethodEntry = typeMap.get(`${objName}.${call.name}`);
const targetFn = objMethodEntry
? typeof objMethodEntry === 'string'
? objMethodEntry
: (objMethodEntry as { type?: string }).type
: null;
if (targetFn) {
const resolved = lookup
.byName(targetFn)
.filter((t) => computeConfidence(relPath, t.file, null) >= 0.5);
if (resolved.length > 0) return resolved;
}
}
}

const exact = lookup
.byName(call.name)
.filter((t) => computeConfidence(relPath, t.file, null) >= 0.5);
Expand Down
Loading
Loading