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
1 change: 1 addition & 0 deletions crates/codegraph-core/src/domain/graph/builder/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2120,6 +2120,7 @@ fn build_and_insert_call_edges(
object_rest_param_bindings: non_empty(&symbols.object_rest_param_bindings),
object_prop_bindings: non_empty(&symbols.object_prop_bindings),
computed_dispatch_table_evidence: non_empty(&symbols.computed_dispatch_table_evidence),
new_expressions: non_empty(&symbols.new_expressions),
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@ pub struct FileEdgeInput {
/// `src/types.ts` — see its doc comment for the full rationale.
#[napi(js_name = "computedDispatchTableEvidence")]
pub computed_dispatch_table_evidence: Option<Vec<String>>,
/// RTA instantiation evidence (issue #2346): every constructor type name
/// that appears in ANY `new X()` expression in this file, regardless of
/// assignment shape. Mirrors `ExtractorOutput.newExpressions` in
/// `src/types.ts` — see `collect_cha_instantiated_types`'s doc comment.
#[napi(js_name = "newExpressions")]
pub new_expressions: Option<Vec<String>>,
}

#[napi(object)]
Expand Down Expand Up @@ -380,14 +386,26 @@ fn add_to_file_scoped<'a>(
}
}

/// RTA: collect instantiated class names from every file's typeMap, keeping
/// only high-confidence (>= 0.9) entries — mirrors the typeMap fallback
/// branch of `collectInstantiatedTypes` in `cha.ts` (constructor-confidence
/// 1.0 and type-annotation-confidence 0.9 entries both qualify; native has no
/// dedicated `newExpressions` list, so this is the only RTA evidence source).
/// RTA: collect instantiated class names from every file, unioning two
/// sources — mirrors `collectInstantiatedTypes` in `cha.ts` exactly:
/// (a) the dedicated `new_expressions` list (issue #2346): every constructor
/// type name that appears in ANY `new X()` expression in the file, regardless
/// of assignment shape (object-literal property value, bare non-`this.`
/// assignment, etc.) — no confidence threshold applies to this source, same
/// as the TS side; and
/// (b) the typeMap fallback: high-confidence (>= 0.9) entries only
/// (constructor-confidence 1.0 and type-annotation-confidence 0.9 entries
/// both qualify) — covers instantiation evidence inferred indirectly (e.g.
/// cross-file return-type propagation) that never produces a literal
/// `new X()` in this file.
fn collect_cha_instantiated_types(files: &[FileEdgeInput]) -> HashSet<&str> {
let mut instantiated = HashSet::new();
for file in files {
if let Some(new_expressions) = &file.new_expressions {
for type_name in new_expressions {
instantiated.insert(type_name.as_str());
}
}
for tm in &file.type_map {
if tm.confidence >= 0.9 {
instantiated.insert(tm.type_name.as_str());
Expand Down Expand Up @@ -4598,6 +4616,7 @@ mod call_edge_tests {
object_rest_param_bindings: None,
object_prop_bindings: None,
computed_dispatch_table_evidence: None,
new_expressions: None,
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,7 @@ mod tests {
object_rest_param_bindings: vec![],
object_prop_bindings: vec![],
computed_dispatch_table_evidence: vec![],
new_expressions: vec![],
}
}

Expand Down
11 changes: 11 additions & 0 deletions crates/codegraph-core/src/extractors/javascript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2887,6 +2887,17 @@ fn handle_call_expr(
}

fn handle_new_expr(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
// RTA instantiation evidence (issue #2346): record every constructor type
// name that appears in a `new X()` expression, regardless of whether the
// result is ever assigned to anything — mirrors the WASM engine's
// unconditional `newExpressions` collection in `src/extractors/javascript.ts`,
// and gives `collect_cha_instantiated_types` (build_edges.rs) coverage for
// instantiation shapes (e.g. object-literal property values, bare
// non-`this.` assignments) that never produce a confidence>=0.9 typeMap
// entry.
if let Some(type_name) = extract_new_expr_type_name(node, source) {
symbols.new_expressions.push(type_name.to_string());
}
let ctor = node
.child_by_field_name("constructor")
.or_else(|| node.child(1));
Expand Down
9 changes: 9 additions & 0 deletions crates/codegraph-core/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,14 @@ pub struct FileSymbols {
/// `src/types.ts`.
#[napi(js_name = "computedDispatchTableEvidence")]
pub computed_dispatch_table_evidence: Vec<String>,
/// Every constructor type name that appears in ANY `new X()` expression in
/// this file, regardless of whether the result is assigned to anything —
/// RTA (Rapid Type Analysis) instantiation evidence for CHA dispatch
/// (issue #2346). Mirrors `ExtractorOutput.newExpressions` in
/// `src/types.ts` / `symbols.newExpressions` populated by
/// `new_expression` handling in `src/extractors/javascript.ts`.
#[napi(js_name = "newExpressions")]
pub new_expressions: Vec<String>,
}

impl FileSymbols {
Expand Down Expand Up @@ -603,6 +611,7 @@ impl FileSymbols {
object_rest_param_bindings: Vec::new(),
object_prop_bindings: Vec::new(),
computed_dispatch_table_evidence: Vec::new(),
new_expressions: Vec::new(),
}
}
}
Expand Down
29 changes: 29 additions & 0 deletions tests/fixtures/cha-dispatch/ObjWorker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { IWorker } from './IWorker.js';

// Issue #2346: instantiated ONLY as an object-literal property value — never
// as `const x = new ObjWorker()` (every other IWorker implementor in this
// fixture set already uses that variable-declarator shape, which both engines'
// typeMap seeding has always recognized as confidence-1.0 instantiation
// evidence). The WASM engine's `newExpressions` list (Phase 8.5) already
// captures every `new X()` in a file regardless of assignment shape, so RTA
// already treated ObjWorker as instantiated there. The native engine's RTA
// evidence, before the #2346 fix, came ONLY from typeMap confidence>=0.9
// entries — and constructor typeMap seeding only fires when a `new_expression`
// is the direct value of a variable declarator or a `this.prop = ` assignment,
// so a `new X()` buried inside an object-literal property value was invisible
// to native's CHA/RTA filter even though `dispatch(worker: IWorker)` in
// Dispatcher.ts should CHA-expand to it just like ConcreteWorker/MockWorker.
export class ObjWorker implements IWorker {
doWork(): string {
return 'obj';
}
}

// Object-literal-property-value instantiation — NOT a variable declarator.
const objWorkerTable: Record<string, IWorker> = { w: new ObjWorker() };

// Keep the table referenced so it isn't dead code — mirrors the real-world
// object-literal dispatch-table shape this RTA gap was found in.
export function describeObjWorkerTable(): string {
return Object.keys(objWorkerTable).join(',');
}
21 changes: 21 additions & 0 deletions tests/integration/phase-8.5-cha-dispatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,27 @@ describe.each(ENGINES)('Phase 8.5 CHA dispatch (%s)', (engine) => {
expect(edge?.technique).toBe('cha');
});

// ── RTA evidence from non-typeMap instantiation shapes (issue #2346) ───
// ObjWorker is instantiated ONLY as an object-literal property value
// (`{ w: new ObjWorker() }`), never as `const x = new ObjWorker()`. The
// WASM engine's `newExpressions` list already covers this; the native
// engine's RTA evidence, before #2346, came only from typeMap
// confidence>=0.9 entries, which this instantiation shape never produces.

it('CHA: emits dispatch → ObjWorker.doWork (instantiated only via object-literal-property-value, issue #2346)', () => {
const edge = callEdges.find(
(e) =>
e.caller_name === 'dispatch' &&
e.callee_name === 'ObjWorker.doWork' &&
e.callee_file === 'ObjWorker.ts',
);
expect(
edge,
`Expected dispatch → ObjWorker.doWork edge (RTA must treat a new-expression used only as an object-literal property value as instantiation evidence, not just typeMap-tracked variable declarators).\nActual edges:\n${JSON.stringify(callEdges, null, 2)}`,
).toBeDefined();
expect(edge?.technique).toBe('cha');
});

// ── RTA filter ─────────────────────────────────────────────────────────

it('RTA: does NOT emit dispatch → GhostWorker.doWork (never instantiated)', () => {
Expand Down
Loading