Skip to content
Open
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
3 changes: 2 additions & 1 deletion src/domain/graph/builder/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,10 +511,11 @@ export function runChaPostPass(db: BetterSqlite3Database): number {
const qualifiedName = `${cls}.${methodSuffix}`;
const methodNodes = findMethodStmt.all(qualifiedName) as Array<{ id: number }>;
for (const methodNode of methodNodes) {
if (methodNode.id === source_id) continue; // skip self-loops
const key = `${source_id}|${methodNode.id}`;
if (seen.has(key)) continue;
seen.add(key);
newEdges.push([source_id, methodNode.id, 'calls', 0.8, 0, 'cha']);
newEdges.push([source_id, methodNode.id, 'calls', 0.8, 0, 'cha']); // TODO: align with runPostNativeCha: computeConfidence(callerFile, methodFile) - CHA_DISPATCH_PENALTY (requires extending findMethodStmt to fetch file)
}
}

Expand Down
26 changes: 18 additions & 8 deletions src/domain/graph/builder/stages/native-orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,10 @@ function runPostNativeCha(
if (row) gateAFired = true;
}

// Gate B: calls from changed-file sources to class-kind targets?
// Gate B: calls from changed-file sources to instantiable-kind targets?
// Checks the same kind set as Gate A (class/interface/trait/struct/record)
// so that future CHA extensions to struct/record kinds correctly trigger
// the full scan when RTA evidence grows in a changed file.
let gateBFired = false;
if (!gateAFired) {
for (let i = 0; i < changedFiles.length && !gateBFired; i += CHUNK_SIZE) {
Expand All @@ -534,7 +537,8 @@ function runPostNativeCha(
`SELECT 1 FROM edges e
JOIN nodes src ON e.source_id = src.id
JOIN nodes tgt ON e.target_id = tgt.id
WHERE e.kind = 'calls' AND tgt.kind = 'class'
WHERE e.kind = 'calls'
AND tgt.kind IN ('class', 'interface', 'trait', 'struct', 'record')
AND src.file IN (${ph})
LIMIT 1`,
)
Expand All @@ -556,8 +560,9 @@ function runPostNativeCha(
}

// Find existing call edges targeting qualified methods (e.g., 'IWorker.doWork').
// Include caller_file and method_file so affectedFiles can be populated for
// incremental role reclassification; confidence is hardcoded 0.8 matching runChaPostPass.
// Include the caller node's file so confidence can be computed file-pair-aware,
// using computeConfidence(callerFile, targetFile, null) - CHA_DISPATCH_PENALTY.
// (The WASM path in runChaPostPass still uses a hardcoded 0.8 — see TODO there.)
// When scopeToChangedFiles is true, restrict to call sites in the changed files
// (safe because no hierarchy or RTA evidence changed outside those files).
let callToMethods: Array<{ source_id: number; method_name: string; caller_file: string | null }>;
Expand Down Expand Up @@ -650,13 +655,17 @@ function runPostNativeCha(
method_file: string | null;
}>;
for (const methodNode of methodNodes) {
if (methodNode.id === source_id) continue; // skip self-loops
const key = `${source_id}|${methodNode.id}`;
if (seen.has(key)) continue;
seen.add(key);
// Use the same hardcoded 0.8 that runChaPostPass (helpers.ts) uses for
// DB-level CHA dispatch edges. This aligns the native orchestrator path
// with the WASM and hybrid paths, which both go through runChaPostPass.
const conf = 0.8;
// Compute confidence file-pair-aware using computeConfidence - CHA_DISPATCH_PENALTY.
// (WASM path runChaPostPass uses 0.8; alignment tracked in helpers.ts TODO.)
// Skip zero-confidence edges to match buildFileCallEdges / buildChaPostPass behaviour.
const conf =
computeConfidence(caller_file ?? '', methodNode.method_file ?? '', null) -
CHA_DISPATCH_PENALTY;
if (conf <= 0) continue;
newEdges.push([source_id, methodNode.id, 'calls', conf, 0, 'cha']);
newEdgeCount++;
if (caller_file) affectedFiles.add(caller_file);
Expand Down Expand Up @@ -900,6 +909,7 @@ async function runPostNativeThisDispatch(
);

for (const t of targets) {
if (t.id === callerRow.id) continue; // skip self-loops
const key = `${callerRow.id}|${t.id}`;
if (seen.has(key)) continue;
seen.add(key);
Expand Down
1 change: 1 addition & 0 deletions src/domain/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ const COMMON_QUERY_PATTERNS: string[] = [
'(variable_declarator name: (identifier) @varfn_name value: (generator_function) @varfn_value)',
'(method_definition name: (property_identifier) @meth_name) @meth_node',
'(method_definition name: (private_property_identifier) @meth_name) @meth_node',
'(method_definition name: (computed_property_name) @meth_name) @meth_node',
'(import_statement source: (string) @imp_source) @imp_node',
'(export_statement) @exp_node',
'(call_expression function: (identifier) @callfn_name) @callfn_node',
Expand Down
11 changes: 9 additions & 2 deletions src/domain/wasm-worker-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ const COMMON_QUERY_PATTERNS: string[] = [
'(variable_declarator name: (identifier) @varfn_name value: (generator_function) @varfn_value)',
'(method_definition name: (property_identifier) @meth_name) @meth_node',
'(method_definition name: (private_property_identifier) @meth_name) @meth_node',
'(method_definition name: (computed_property_name) @meth_name) @meth_node',
'(import_statement source: (string) @imp_source) @imp_node',
'(export_statement) @exp_node',
'(call_expression function: (identifier) @callfn_name) @callfn_node',
Expand All @@ -125,11 +126,17 @@ const COMMON_QUERY_PATTERNS: string[] = [
'(expression_statement (assignment_expression left: (member_expression) @assign_left right: (_) @assign_right)) @assign_node',
];

const JS_CLASS_PATTERN: string = '(class_declaration name: (identifier) @cls_name) @cls_node';
const JS_CLASS_PATTERNS: string[] = [
'(class_declaration name: (identifier) @cls_name) @cls_node',
// class expressions: `return class Foo extends Bar { ... }` or `const X = class Foo { ... }`
'(class name: (identifier) @cls_name) @cls_node',
];

const TS_EXTRA_PATTERNS: string[] = [
'(class_declaration name: (type_identifier) @cls_name) @cls_node',
'(abstract_class_declaration name: (type_identifier) @cls_name) @cls_node',
// class expressions: `return class Foo extends Bar { ... }`
'(class name: (type_identifier) @cls_name) @cls_node',
'(interface_declaration name: (type_identifier) @iface_name) @iface_node',
'(type_alias_declaration name: (type_identifier) @type_name) @type_node',
];
Expand Down Expand Up @@ -433,7 +440,7 @@ async function loadLanguageLazy(entry: LanguageRegistryEntry): Promise<Parser |
const isTS = entry.id === 'typescript' || entry.id === 'tsx';
const patterns = isTS
? [...COMMON_QUERY_PATTERNS, ...TS_EXTRA_PATTERNS]
: [...COMMON_QUERY_PATTERNS, JS_CLASS_PATTERN];
: [...COMMON_QUERY_PATTERNS, ...JS_CLASS_PATTERNS];
_queries.set(entry.id, new Query(lang, patterns.join('\n')));
}
return parser;
Expand Down
21 changes: 21 additions & 0 deletions src/extractors/javascript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,15 @@ function extractDestructuredBindingsWalk(node: TreeSitterNode, definitions: Defi
nodeEndLine(declNode),
definitions,
);
} else if (nameN && nameN.type === 'array_pattern') {
// `const [x, y] = ...` — emit a single constant node whose name is the
// full array pattern text (e.g. `[x, y]`), matching native engine behaviour.
definitions.push({
name: nameN.text,
kind: 'constant',
line: nodeStartLine(declNode),
endLine: nodeEndLine(declNode),
});
}
}
}
Expand Down Expand Up @@ -1017,6 +1026,16 @@ function handleVariableDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
nodeEndLine(node),
ctx.definitions,
);
} else if (isConst && nameN.type === 'array_pattern' && !hasFunctionScopeAncestor(node)) {
// Array destructuring: `const [x, y] = ...` — emit a single constant node
// whose name is the full array pattern text (e.g. `[x, y]`), matching
// native engine behaviour. Scope guard mirrors the object_pattern branch above.
ctx.definitions.push({
name: nameN.text,
kind: 'constant',
line: nodeStartLine(node),
endLine: nodeEndLine(node),
});
}
}
}
Expand Down Expand Up @@ -3359,11 +3378,13 @@ function emitPrototypeMethod(
): void {
const fullName = `${className}.${methodName}`;
if (rhs.type === 'function_expression' || rhs.type === 'arrow_function') {
const params = extractParameters(rhs);
definitions.push({
name: fullName,
kind: 'method',
line: nodeStartLine(rhs),
endLine: nodeEndLine(rhs),
children: params.length > 0 ? params : undefined,
});
} else if (rhs.type === 'identifier' && !BUILTIN_GLOBALS.has(rhs.text)) {
// Prototype alias: `A.prototype.t = f` → typeMap['A.t'] = { type: 'f' }
Expand Down
114 changes: 114 additions & 0 deletions tests/parsers/javascript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1265,4 +1265,118 @@ describe('JavaScript parser', () => {
);
});
});

describe('computed method name extraction (#1471)', () => {
it('extracts computed getter method from object literal', () => {
const symbols = parseJS(`const obj = { get ['property7']() {} };`);
expect(symbols.definitions).toContainEqual(
expect.objectContaining({ name: "['property7']", kind: 'method' }),
);
});

it('extracts computed setter method with parameter from object literal', () => {
const symbols = parseJS(`const obj = { set ['property8'](value) {} };`);
const def = symbols.definitions.find((d) => d.name === "['property8']");
expect(def).toBeDefined();
expect(def).toMatchObject({ kind: 'method' });
expect(def!.children).toContainEqual(
expect.objectContaining({ name: 'value', kind: 'parameter' }),
);
});

it('extracts computed regular method with parameter from object literal', () => {
const symbols = parseJS(`const obj = { ['property9'](parameters) {} };`);
const def = symbols.definitions.find((d) => d.name === "['property9']");
expect(def).toBeDefined();
expect(def!.children).toContainEqual(
expect.objectContaining({ name: 'parameters', kind: 'parameter' }),
);
});

it('extracts computed generator method from object literal', () => {
const symbols = parseJS(`const obj = { *['generator10'](parameters) {} };`);
expect(symbols.definitions).toContainEqual(
expect.objectContaining({ name: "['generator10']", kind: 'method' }),
);
});

it('extracts computed async method from object literal', () => {
const symbols = parseJS(`const obj = { async ['property11'](parameters) {} };`);
expect(symbols.definitions).toContainEqual(
expect.objectContaining({ name: "['property11']", kind: 'method' }),
);
});
});

describe('class expression inside function extraction (#1471)', () => {
it('extracts named class expression returned from a function', () => {
const symbols = parseJS(
`function mixin() { return class PostMixin extends A { constructor() { super(); } }; }`,
);
expect(symbols.definitions).toContainEqual(
expect.objectContaining({ name: 'PostMixin', kind: 'class' }),
);
});

it('records extends relationship for class expression inside function', () => {
const symbols = parseJS(`function mixin() { return class PostMixin extends A { m() {} }; }`);
expect(symbols.classes).toContainEqual(
expect.objectContaining({ name: 'PostMixin', extends: 'A' }),
);
});

it('extracts class field properties as children of class expression', () => {
const symbols = parseJS(
`function mixin() { return class PostMixin extends A { w = 1; eee = this; }; }`,
);
const pm = symbols.definitions.find((d) => d.name === 'PostMixin');
expect(pm).toBeDefined();
expect(pm!.children).toContainEqual(expect.objectContaining({ name: 'w', kind: 'property' }));
expect(pm!.children).toContainEqual(
expect.objectContaining({ name: 'eee', kind: 'property' }),
);
});
});

describe('array destructuring constant extraction (#1471)', () => {
it('extracts const array pattern as a single constant node', () => {
const symbols = parseJS(`const [x, y] = new Set([() => {}, () => {}]);`);
expect(symbols.definitions).toContainEqual(
expect.objectContaining({ name: '[x, y]', kind: 'constant' }),
);
});

it('does not extract let or var array destructuring', () => {
const symbols = parseJS(`let [a, b] = [1, 2];`);
expect(symbols.definitions.every((d) => d.name !== '[a, b]')).toBe(true);
});
});

describe('prototype method parameter extraction (#1471)', () => {
it('extracts parameters from Foo.prototype.bar = (x, y) => arrow', () => {
const symbols = parseJS(`function Arit() {}\nArit.prototype.sum = (x, y) => x + y;`);
const def = symbols.definitions.find((d) => d.name === 'Arit.sum');
expect(def).toBeDefined();
expect(def!.children).toContainEqual(
expect.objectContaining({ name: 'x', kind: 'parameter' }),
);
expect(def!.children).toContainEqual(
expect.objectContaining({ name: 'y', kind: 'parameter' }),
);
});

it('extracts parameters from Foo.prototype.bar = function(key, value)', () => {
const symbols = parseJS(
`function Foo() {}\nFoo.prototype.add = function(key, value) { this[key] = value; };`,
);
const def = symbols.definitions.find((d) => d.name === 'Foo.add');
expect(def).toBeDefined();
expect(def!.children).toContainEqual(
expect.objectContaining({ name: 'key', kind: 'parameter' }),
);
expect(def!.children).toContainEqual(
expect.objectContaining({ name: 'value', kind: 'parameter' }),
);
});
});
});
Loading