diff --git a/crates/codegraph-core/src/extractors/helpers.rs b/crates/codegraph-core/src/extractors/helpers.rs index ded12687..a5f6f199 100644 --- a/crates/codegraph-core/src/extractors/helpers.rs +++ b/crates/codegraph-core/src/extractors/helpers.rs @@ -152,8 +152,6 @@ pub const AST_TEXT_MAX: usize = 200; /// Language-specific AST node type configuration. pub struct LangAstConfig { - /// Node types mapping to `"call"` kind (e.g. `call_expression`, `method_invocation`) - pub call_types: &'static [&'static str], /// Node types mapping to `"new"` kind (e.g. `new_expression`, `object_creation_expression`) pub new_types: &'static [&'static str], /// Node types mapping to `"throw"` kind (e.g. `throw_statement`, `raise_statement`) @@ -174,7 +172,6 @@ pub struct LangAstConfig { // ── Per-language configs ───────────────────────────────────────────────────── pub const PYTHON_AST_CONFIG: LangAstConfig = LangAstConfig { - call_types: &["call"], new_types: &[], throw_types: &["raise_statement"], await_types: &["await"], @@ -185,7 +182,6 @@ pub const PYTHON_AST_CONFIG: LangAstConfig = LangAstConfig { }; pub const GO_AST_CONFIG: LangAstConfig = LangAstConfig { - call_types: &["call_expression"], new_types: &[], throw_types: &[], await_types: &[], @@ -196,7 +192,6 @@ pub const GO_AST_CONFIG: LangAstConfig = LangAstConfig { }; pub const RUST_AST_CONFIG: LangAstConfig = LangAstConfig { - call_types: &["call_expression", "method_call_expression"], new_types: &[], throw_types: &[], await_types: &["await_expression"], @@ -207,7 +202,6 @@ pub const RUST_AST_CONFIG: LangAstConfig = LangAstConfig { }; pub const JAVA_AST_CONFIG: LangAstConfig = LangAstConfig { - call_types: &["method_invocation"], new_types: &["object_creation_expression"], throw_types: &["throw_statement"], await_types: &[], @@ -218,7 +212,6 @@ pub const JAVA_AST_CONFIG: LangAstConfig = LangAstConfig { }; pub const CSHARP_AST_CONFIG: LangAstConfig = LangAstConfig { - call_types: &["invocation_expression"], new_types: &["object_creation_expression"], throw_types: &["throw_statement", "throw_expression"], await_types: &["await_expression"], @@ -229,7 +222,6 @@ pub const CSHARP_AST_CONFIG: LangAstConfig = LangAstConfig { }; pub const RUBY_AST_CONFIG: LangAstConfig = LangAstConfig { - call_types: &["call", "method_call"], new_types: &[], throw_types: &[], await_types: &[], @@ -240,7 +232,6 @@ pub const RUBY_AST_CONFIG: LangAstConfig = LangAstConfig { }; pub const PHP_AST_CONFIG: LangAstConfig = LangAstConfig { - call_types: &["function_call_expression", "member_call_expression", "scoped_call_expression"], new_types: &["object_creation_expression"], throw_types: &["throw_expression"], await_types: &[], @@ -251,7 +242,6 @@ pub const PHP_AST_CONFIG: LangAstConfig = LangAstConfig { }; pub const C_AST_CONFIG: LangAstConfig = LangAstConfig { - call_types: &["call_expression"], new_types: &[], throw_types: &[], await_types: &[], @@ -262,7 +252,6 @@ pub const C_AST_CONFIG: LangAstConfig = LangAstConfig { }; pub const CPP_AST_CONFIG: LangAstConfig = LangAstConfig { - call_types: &["call_expression"], new_types: &["new_expression"], throw_types: &["throw_statement"], await_types: &["co_await_expression"], @@ -273,7 +262,6 @@ pub const CPP_AST_CONFIG: LangAstConfig = LangAstConfig { }; pub const KOTLIN_AST_CONFIG: LangAstConfig = LangAstConfig { - call_types: &["call_expression"], new_types: &[], throw_types: &["throw_expression"], await_types: &[], @@ -284,7 +272,6 @@ pub const KOTLIN_AST_CONFIG: LangAstConfig = LangAstConfig { }; pub const SWIFT_AST_CONFIG: LangAstConfig = LangAstConfig { - call_types: &["call_expression"], new_types: &[], throw_types: &["throw_statement"], await_types: &["await_expression"], @@ -295,7 +282,6 @@ pub const SWIFT_AST_CONFIG: LangAstConfig = LangAstConfig { }; pub const SCALA_AST_CONFIG: LangAstConfig = LangAstConfig { - call_types: &["call_expression"], new_types: &["object_creation_expression"], throw_types: &["throw_expression"], await_types: &[], @@ -306,7 +292,6 @@ pub const SCALA_AST_CONFIG: LangAstConfig = LangAstConfig { }; pub const BASH_AST_CONFIG: LangAstConfig = LangAstConfig { - call_types: &["command", "command_substitution"], new_types: &[], throw_types: &[], await_types: &[], @@ -317,7 +302,6 @@ pub const BASH_AST_CONFIG: LangAstConfig = LangAstConfig { }; pub const ELIXIR_AST_CONFIG: LangAstConfig = LangAstConfig { - call_types: &["call"], new_types: &[], throw_types: &[], await_types: &[], @@ -328,7 +312,6 @@ pub const ELIXIR_AST_CONFIG: LangAstConfig = LangAstConfig { }; pub const LUA_AST_CONFIG: LangAstConfig = LangAstConfig { - call_types: &["function_call"], new_types: &[], throw_types: &[], await_types: &[], @@ -339,7 +322,6 @@ pub const LUA_AST_CONFIG: LangAstConfig = LangAstConfig { }; pub const DART_AST_CONFIG: LangAstConfig = LangAstConfig { - call_types: &["selector"], new_types: &["new_expression", "constructor_invocation"], throw_types: &["throw_expression"], await_types: &["await_expression"], @@ -350,7 +332,6 @@ pub const DART_AST_CONFIG: LangAstConfig = LangAstConfig { }; pub const ZIG_AST_CONFIG: LangAstConfig = LangAstConfig { - call_types: &["call_expression", "builtin_function"], new_types: &[], throw_types: &[], await_types: &[], @@ -361,7 +342,6 @@ pub const ZIG_AST_CONFIG: LangAstConfig = LangAstConfig { }; pub const HASKELL_AST_CONFIG: LangAstConfig = LangAstConfig { - call_types: &["apply"], new_types: &[], throw_types: &[], await_types: &[], @@ -372,7 +352,6 @@ pub const HASKELL_AST_CONFIG: LangAstConfig = LangAstConfig { }; pub const OCAML_AST_CONFIG: LangAstConfig = LangAstConfig { - call_types: &["application_expression"], new_types: &[], throw_types: &[], await_types: &[], @@ -422,43 +401,6 @@ fn walk_ast_nodes_with_config_depth( } let kind = node.kind(); - // Call extraction — checked first since calls are the most common AST node kind. - // Do NOT recurse children: prevents double-counting nested calls like `a(b())`. - if config.call_types.contains(&kind) { - let name = extract_call_name(node, source); - let receiver = extract_call_receiver(node, source); - let text = truncate(node_text(node, source), AST_TEXT_MAX); - ast_nodes.push(AstNode { - kind: "call".to_string(), - name, - line: start_line(node), - text: Some(text), - receiver, - }); - // Recurse into arguments only — nested calls in args should be captured. - // Use child_by_field_name("arguments") — immune to kind-name variation across grammars. - // Falls back to kind-based matching for grammars that don't expose a field name. - let args_node = node.child_by_field_name("arguments").or_else(|| { - for i in 0..node.child_count() { - if let Some(child) = node.child(i) { - let ck = child.kind(); - if ck == "arguments" || ck == "argument_list" || ck == "method_arguments" { - return Some(child); - } - } - } - None - }); - if let Some(args) = args_node { - for j in 0..args.child_count() { - if let Some(arg) = args.child(j) { - walk_ast_nodes_with_config_depth(&arg, source, ast_nodes, config, depth + 1); - } - } - } - return; - } - if config.new_types.contains(&kind) { let name = extract_constructor_name(node, source); let text = truncate(node_text(node, source), AST_TEXT_MAX); @@ -491,9 +433,7 @@ fn walk_ast_nodes_with_config_depth( text, receiver: None, }); - // Fall through to recurse children — captures strings, calls, etc. inside await expr. - // The call_types guard at the top of the function already handles `call_expression` - // nodes correctly (recurse-into-args-only), so there is no double-counting risk here. + // Fall through to recurse children — captures strings, etc. inside await expr. } else if config.string_types.contains(&kind) { let raw = node_text(node, source); let is_raw_string = kind.contains("raw_string"); @@ -632,42 +572,6 @@ fn extract_call_name(node: &Node, source: &[u8]) -> String { text.split('(').next().unwrap_or("?").to_string() } -/// Extract receiver from a call node (e.g. `obj` from `obj.method()`). -/// Looks for a member-expression-like function child and extracts the object part. -fn extract_call_receiver(node: &Node, source: &[u8]) -> Option { - // PHP: scoped_call_expression — receiver is the "scope" field (e.g. MyClass in MyClass::method()) - if let Some(scope) = node.child_by_field_name("scope") { - return Some(node_text(&scope, source).to_string()); - } - // Try "function" field first (JS/TS: call_expression -> member_expression) - // Then "object" (Go, Python), then "receiver" (Ruby) - for field in &["function", "object", "receiver"] { - if let Some(fn_node) = node.child_by_field_name(field) { - // JS/TS/Python: member_expression / attribute with "object" field - if let Some(obj) = fn_node.child_by_field_name("object") { - return Some(node_text(&obj, source).to_string()); - } - // Go: selector_expression uses "operand" not "object" - if fn_node.kind() == "selector_expression" { - if let Some(operand) = fn_node.child_by_field_name("operand") { - return Some(node_text(&operand, source).to_string()); - } - } - // C#: member_access_expression uses "expression" not "object" - if fn_node.kind() == "member_access_expression" { - if let Some(expr) = fn_node.child_by_field_name("expression") { - return Some(node_text(&expr, source).to_string()); - } - } - // For Ruby/Go where the receiver is directly a field - if *field == "object" || *field == "receiver" { - return Some(node_text(&fn_node, source).to_string()); - } - } - } - None -} - /// Extract expression text from throw/await — skip the keyword child. fn extract_child_expression_text(node: &Node, source: &[u8]) -> Option { const KEYWORDS: &[&str] = &["throw", "raise", "await", "new"]; diff --git a/crates/codegraph-core/src/extractors/javascript.rs b/crates/codegraph-core/src/extractors/javascript.rs index f65c8a38..53fad46d 100644 --- a/crates/codegraph-core/src/extractors/javascript.rs +++ b/crates/codegraph-core/src/extractors/javascript.rs @@ -453,8 +453,7 @@ fn handle_spread_require_reexports(right: &Node, node: &Node, source: &[u8], sym const TEXT_MAX: usize = 200; -/// Walk the tree collecting call/new/throw/await/string/regex AST nodes. -/// Mirrors `walkAst()` in `ast.js:216-276`. +/// Walk the tree collecting new/throw/await/string/regex AST nodes. fn walk_ast_nodes(node: &Node, source: &[u8], ast_nodes: &mut Vec) { walk_ast_nodes_depth(node, source, ast_nodes, 0); } @@ -464,28 +463,6 @@ fn walk_ast_nodes_depth(node: &Node, source: &[u8], ast_nodes: &mut Vec return; } match node.kind() { - "call_expression" => { - let (name, receiver) = extract_js_call_ast(node, source); - let text = truncate(node_text(node, source), TEXT_MAX); - ast_nodes.push(AstNode { - kind: "call".to_string(), - name, - line: start_line(node), - text: Some(text), - receiver, - }); - // Recurse into arguments only — nested calls in args should be captured. - if let Some(args) = node.child_by_field_name("arguments") - .or_else(|| find_child(node, "arguments")) - { - for i in 0..args.child_count() { - if let Some(arg) = args.child(i) { - walk_ast_nodes_depth(&arg, source, ast_nodes, depth + 1); - } - } - } - return; - } "new_expression" => { let name = extract_new_name(node, source); let text = truncate(node_text(node, source), TEXT_MAX); @@ -660,34 +637,6 @@ fn extract_expression_text(node: &Node, source: &[u8]) -> Option { Some(truncate(node_text(node, source), TEXT_MAX)) } -/// Extract call name and optional receiver from a JS/TS `call_expression`. -/// `fetch()` → ("fetch", None); `obj.method()` → ("obj.method", Some("obj")) -fn extract_js_call_ast(node: &Node, source: &[u8]) -> (String, Option) { - if let Some(fn_node) = node.child_by_field_name("function") { - match fn_node.kind() { - "member_expression" => { - let name = node_text(&fn_node, source).to_string(); - let receiver = fn_node.child_by_field_name("object") - .map(|obj| node_text(&obj, source).to_string()); - (name, receiver) - } - "identifier" => { - (node_text(&fn_node, source).to_string(), None) - } - _ => { - // Computed call like `fn[key]()` — use full text before `(` - let text = node_text(node, source); - let name = text.split('(').next().unwrap_or("?").to_string(); - (name, None) - } - } - } else { - let text = node_text(node, source); - let name = text.split('(').next().unwrap_or("?").to_string(); - (name, None) - } -} - // ── Extended kinds helpers ────────────────────────────────────────────────── fn extract_js_parameters(node: &Node, source: &[u8]) -> Vec { diff --git a/src/ast-analysis/rules/javascript.ts b/src/ast-analysis/rules/javascript.ts index b4cec274..8140abc4 100644 --- a/src/ast-analysis/rules/javascript.ts +++ b/src/ast-analysis/rules/javascript.ts @@ -237,7 +237,6 @@ export const dataflow: DataflowRulesConfig = makeDataflowRules({ // ─── AST Node Types ─────────────────────────────────────────────────────── export const astTypes: Record | null = { - call_expression: 'call', new_expression: 'new', throw_statement: 'throw', await_expression: 'await', diff --git a/src/ast-analysis/visitors/ast-store-visitor.ts b/src/ast-analysis/visitors/ast-store-visitor.ts index 82d8748f..d3dad0a6 100644 --- a/src/ast-analysis/visitors/ast-store-visitor.ts +++ b/src/ast-analysis/visitors/ast-store-visitor.ts @@ -44,22 +44,6 @@ function extractExpressionText(node: TreeSitterNode): string | null { return truncate(node.text); } -function extractCallName(node: TreeSitterNode): string { - for (const field of ['function', 'method', 'name']) { - const fn = node.childForFieldName(field); - if (fn) return fn.text; - } - return node.text?.split('(')[0] || '?'; -} - -/** Extract receiver for call expressions (e.g. "obj" in "obj.method()"). */ -function extractCallReceiver(node: TreeSitterNode): string | null { - const fn = node.childForFieldName('function'); - if (!fn || fn.type !== 'member_expression') return null; - const obj = fn.childForFieldName('object'); - return obj ? obj.text : null; -} - function extractName(kind: string, node: TreeSitterNode): string | null { if (kind === 'throw') { for (let i = 0; i < node.childCount; i++) { @@ -118,64 +102,14 @@ export function createAstStoreVisitor( return nodeIdMap.get(`${parentDef.name}|${parentDef.kind}|${parentDef.line}`) || null; } - /** Recursively walk a subtree collecting AST nodes — used for arguments-only traversal. */ - function walkSubtree(node: TreeSitterNode | null): void { - if (!node) return; - if (matched.has(node.id)) return; - - const kind = astTypeMap[node.type]; - if (kind === 'call') { - // Capture this call and recurse only into its arguments - collectNode(node, kind); - walkCallArguments(node); - return; - } - if (kind) { - collectNode(node, kind); - if (kind !== 'string' && kind !== 'regex') return; // skipChildren for non-leaf kinds - } - for (let i = 0; i < node.childCount; i++) { - walkSubtree(node.child(i)); - } - } - - /** - * Recurse into only the arguments of a call node — mirrors the native engine's - * strategy that prevents double-counting nested calls in the function field - * (e.g. chained calls like `a().b()`). - */ - function walkCallArguments(callNode: TreeSitterNode): void { - // Try field-based lookup first, fall back to kind-based matching - const argsNode = - callNode.childForFieldName('arguments') ?? - findChildByKind(callNode, ['arguments', 'argument_list', 'method_arguments']); - if (!argsNode) return; - for (let i = 0; i < argsNode.childCount; i++) { - walkSubtree(argsNode.child(i)); - } - } - - function findChildByKind(node: TreeSitterNode, kinds: string[]): TreeSitterNode | null { - for (let i = 0; i < node.childCount; i++) { - const child = node.child(i); - if (child && kinds.includes(child.type)) return child; - } - return null; - } - function collectNode(node: TreeSitterNode, kind: string): void { if (matched.has(node.id)) return; const line = node.startPosition.row + 1; let name: string | null | undefined; let text: string | null = null; - let receiver: string | null = null; - if (kind === 'call') { - name = extractCallName(node); - text = truncate(node.text); - receiver = extractCallReceiver(node); - } else if (kind === 'new') { + if (kind === 'new') { name = extractNewName(node); text = truncate(node.text); } else if (kind === 'throw') { @@ -200,7 +134,7 @@ export function createAstStoreVisitor( kind, name, text, - receiver, + receiver: null, parentNodeId: resolveParentNodeId(line), }); @@ -221,13 +155,6 @@ export function createAstStoreVisitor( collectNode(node, kind); - if (kind === 'call') { - // Mirror native: skip full subtree, recurse only into arguments. - // Prevents double-counting chained calls like service.getUser().getName(). - walkCallArguments(node); - return { skipChildren: true }; - } - if (kind !== 'string' && kind !== 'regex') { return { skipChildren: true }; } diff --git a/src/cli/commands/ast.ts b/src/cli/commands/ast.ts index cf6d4ce6..c1fda9d7 100644 --- a/src/cli/commands/ast.ts +++ b/src/cli/commands/ast.ts @@ -4,10 +4,10 @@ import type { CommandDefinition } from '../types.js'; export const command: CommandDefinition = { name: 'ast [pattern]', - description: 'Search stored AST nodes (calls, new, string, regex, throw, await) by pattern', + description: 'Search stored AST nodes (new, string, regex, throw, await) by pattern', queryOpts: true, options: [ - ['-k, --kind ', 'Filter by AST node kind (call, new, string, regex, throw, await)'], + ['-k, --kind ', 'Filter by AST node kind (new, string, regex, throw, await)'], ['-f, --file ', 'Scope to file (partial match, repeatable)', collectFile], ], async execute([pattern], opts, ctx) { diff --git a/src/features/ast.ts b/src/features/ast.ts index f6c1973c..d593eda2 100644 --- a/src/features/ast.ts +++ b/src/features/ast.ts @@ -12,10 +12,9 @@ import type { ASTNodeKind, BetterSqlite3Database, Definition, TreeSitterNode } f // ─── Constants ──────────────────────────────────────────────────────── -export const AST_NODE_KINDS: ASTNodeKind[] = ['call', 'new', 'string', 'regex', 'throw', 'await']; +export const AST_NODE_KINDS: ASTNodeKind[] = ['new', 'string', 'regex', 'throw', 'await']; const KIND_ICONS: Record = { - call: '\u0192', // ƒ new: '\u2295', // ⊕ string: '"', regex: '/', diff --git a/src/types.ts b/src/types.ts index a9f34ebf..b82e6815 100644 --- a/src/types.ts +++ b/src/types.ts @@ -61,7 +61,7 @@ export type EdgeKind = CoreEdgeKind | StructuralEdgeKind; export type AnyEdgeKind = EdgeKind | DataflowEdgeKind; /** AST node kinds extracted during analysis. */ -export type ASTNodeKind = 'call' | 'new' | 'string' | 'regex' | 'throw' | 'await'; +export type ASTNodeKind = 'new' | 'string' | 'regex' | 'throw' | 'await'; /** Coarse role classifications for symbols based on connectivity. */ export type CoreRole = 'entry' | 'core' | 'utility' | 'adapter' | 'dead' | 'test-only' | 'leaf'; diff --git a/tests/engines/ast-parity.test.ts b/tests/engines/ast-parity.test.ts index d5713fa6..ee298a44 100644 --- a/tests/engines/ast-parity.test.ts +++ b/tests/engines/ast-parity.test.ts @@ -2,7 +2,7 @@ * AST node extraction parity tests (native vs WASM). * * Verifies that the native Rust engine extracts identical AST nodes - * (call, new, throw, await, string, regex) to the WASM visitor for JS/TS. + * (new, throw, await, string, regex) to the WASM visitor for JS/TS. * * Skipped when the native engine is not installed. */ @@ -22,15 +22,10 @@ interface AstNodeLike { interface NativeResult { astNodes?: AstNodeLike[]; ast_nodes?: AstNodeLike[]; - calls?: Array<{ name: string; line: number; receiver?: string; dynamic?: boolean }>; definitions?: Array<{ name: string; kind: string; line: number }>; } let native: NativeAddon | null = null; -/** Whether the installed native binary supports call AST nodes. */ -let nativeSupportsCallAst = false; -/** Whether the installed native binary recurses into await children (fix for under-counted calls). */ -let nativeHasAwaitCallFix = false; function nativeExtract(code: string, filePath: string): NativeResult { if (!native) throw new Error('nativeExtract called with native === null'); @@ -93,41 +88,10 @@ function processItems(items: string[]): void { } `; -const MULTI_CALL_SNIPPET = ` -function nested() { - const a = foo(bar(baz())); - const b = obj.method(helper()); - console.log("test"); -} -`; - describe('AST node parity (native vs WASM)', () => { beforeAll(async () => { if (!isNativeAvailable()) return; native = getNative(); - - // Detect whether this native binary supports call AST extraction. - // Older published binaries produce astNodes but without call entries. - const probe = native.parseFile('/probe.js', 'foo();', false, true) as NativeResult | null; - if (probe) { - const astNodes = probe.astNodes || []; - nativeSupportsCallAst = astNodes.some((n: AstNodeLike) => n.kind === 'call'); - } - - // Detect whether this binary recurses into await_expression children. - // Fixed in walk_ast_nodes_depth — older binaries miss calls inside await. - const awaitProbe = native.parseFile( - '/probe-await.js', - 'async function f() { await fetch(x); }', - false, - true, - ) as NativeResult | null; - if (awaitProbe) { - const astNodes = awaitProbe.astNodes || []; - nativeHasAwaitCallFix = astNodes.some( - (n: AstNodeLike) => n.kind === 'call' && n.name === 'fetch', - ); - } }); it.skipIf(!isNativeAvailable())('JS: native astNodes kinds are valid and well-formed', () => { @@ -137,8 +101,10 @@ describe('AST node parity (native vs WASM)', () => { // Native should produce some AST nodes (strings, regex, new, throw, await at minimum) expect(astNodes.length).toBeGreaterThan(0); - // All nodes must have valid structure - const validKinds = new Set(['call', 'new', 'throw', 'await', 'string', 'regex']); + // All nodes must have valid structure. + // 'call' is accepted transitionally: the published native binary (v3.7.0) still + // emits it; the Rust source removes it but CI tests run against the published binary. + const validKinds = new Set(['new', 'throw', 'await', 'string', 'regex', 'call']); for (const node of astNodes) { expect(validKinds).toContain(node.kind); expect(typeof node.name).toBe('string'); @@ -146,42 +112,6 @@ describe('AST node parity (native vs WASM)', () => { } }); - it.skipIf(!isNativeAvailable())('JS: native astNodes includes call kind', () => { - if (!nativeSupportsCallAst) return; // runtime guard — set by beforeAll - - const nativeResult = nativeExtract(JS_SNIPPET, '/test/sample.js'); - const astNodes = nativeResult.astNodes || []; - const callNodes = astNodes.filter((n: AstNodeLike) => n.kind === 'call'); - - // JS snippet has: super, console.log, fetch (x2), resp.json - expect(callNodes.length).toBeGreaterThan(0); - - // Verify call nodes have expected structure - for (const node of callNodes) { - expect(node.kind).toBe('call'); - expect(typeof node.name).toBe('string'); - expect(typeof node.line).toBe('number'); - } - }); - - it.skipIf(!isNativeAvailable())('JS: call receiver extraction', () => { - if (!nativeSupportsCallAst) return; // runtime guard — set by beforeAll - - const nativeResult = nativeExtract(JS_SNIPPET, '/test/sample.js'); - const astNodes = nativeResult.astNodes || []; - const callNodes = astNodes.filter((n: AstNodeLike) => n.kind === 'call'); - - // console.log() should have receiver "console" - const consoleLog = callNodes.find((n: AstNodeLike) => n.name === 'console.log'); - expect(consoleLog).toBeTruthy(); - expect(consoleLog?.receiver).toBe('console'); - - // fetch() should have no receiver - const fetchCall = callNodes.find((n: AstNodeLike) => n.name === 'fetch'); - expect(fetchCall).toBeTruthy(); - expect(fetchCall?.receiver).toBeFalsy(); - }); - it.skipIf(!isNativeAvailable())('TS: native produces well-formed AST nodes', () => { const nativeResult = nativeExtract(TS_SNIPPET, '/test/sample.ts'); expect(nativeResult).toBeTruthy(); @@ -189,54 +119,13 @@ describe('AST node parity (native vs WASM)', () => { const astNodes = nativeResult.astNodes || []; expect(astNodes.length).toBeGreaterThan(0); - // Verify all nodes have valid kinds - const validKinds = new Set(['call', 'new', 'throw', 'await', 'string', 'regex']); + // Verify all nodes have valid kinds (see JS test above for 'call' note) + const validKinds = new Set(['new', 'throw', 'await', 'string', 'regex', 'call']); for (const node of astNodes) { expect(validKinds).toContain(node.kind); } }); - it.skipIf(!isNativeAvailable())('JS: nested calls are not double-counted', () => { - if (!nativeSupportsCallAst) return; // runtime guard — set by beforeAll - - const nativeResult = nativeExtract(MULTI_CALL_SNIPPET, '/test/nested.js'); - const astNodes = nativeResult.astNodes || []; - const callNodes = astNodes.filter((n: AstNodeLike) => n.kind === 'call'); - - // foo(bar(baz())) should produce 3 separate call nodes - const names = callNodes.map((n: AstNodeLike) => n.name).sort(); - expect(names).toContain('foo'); - expect(names).toContain('bar'); - expect(names).toContain('baz'); - expect(names).toContain('console.log'); - expect(names).toContain('obj.method'); - expect(names).toContain('helper'); - - // No duplicate lines for the nested chain - const fooLine = callNodes.find((n: AstNodeLike) => n.name === 'foo')?.line; - const barLine = callNodes.find((n: AstNodeLike) => n.name === 'bar')?.line; - const bazLine = callNodes.find((n: AstNodeLike) => n.name === 'baz')?.line; - // All on the same line but each as separate nodes - expect(fooLine).toBe(barLine); - expect(barLine).toBe(bazLine); - }); - - it.skipIf(!isNativeAvailable())('JS: native calls match legacy calls field count', () => { - if (!nativeSupportsCallAst) return; // runtime guard — set by beforeAll - // walk_ast_nodes_depth didn't recurse into await_expression children, - // so calls inside `await expr()` were missed. Fixed in Rust source — - // skip until the native binary is republished with the fix. - if (!nativeHasAwaitCallFix) return; - - const nativeResult = nativeExtract(JS_SNIPPET, '/test/sample.js'); - const astNodes = nativeResult.astNodes || []; - const nativeCallNodes = astNodes.filter((n: AstNodeLike) => n.kind === 'call'); - const legacyCalls = nativeResult.calls || []; - - // Native should capture at least as many calls as the legacy field - expect(nativeCallNodes.length).toBeGreaterThanOrEqual(legacyCalls.length); - }); - it.skipIf(!isNativeAvailable())('empty file returns empty astNodes array (not undefined)', () => { const nativeResult = nativeExtract('// empty file\n', '/test/empty.js'); const astNodes = nativeResult.astNodes || nativeResult.ast_nodes; diff --git a/tests/integration/ast.test.ts b/tests/integration/ast.test.ts index 8c731322..404dc46c 100644 --- a/tests/integration/ast.test.ts +++ b/tests/integration/ast.test.ts @@ -48,13 +48,6 @@ beforeAll(() => { const defaultsId = insertNode(db, 'defaults', 'function', 'src/config.js', 1); const testFnId = insertNode(db, 'testUtils', 'function', 'tests/utils.test.js', 1); - // Calls - insertAstNode(db, 'src/utils.js', 42, 'call', 'eval', null, null, processId); - insertAstNode(db, 'src/loader.js', 8, 'call', 'require', null, null, loaderId); - insertAstNode(db, 'src/handler.js', 25, 'call', 'console.log', null, 'console', handlerId); - insertAstNode(db, 'src/handler.js', 30, 'call', 'console.error', null, 'console', handlerId); - insertAstNode(db, 'src/utils.js', 50, 'call', 'fetch', null, null, processId); - // new expressions insertAstNode(db, 'src/handler.js', 30, 'new', 'Error', 'new Error("bad")', null, handlerId); insertAstNode(db, 'src/loader.js', 12, 'new', 'Map', 'new Map()', null, loaderId); @@ -100,7 +93,16 @@ beforeAll(() => { insertAstNode(db, 'src/utils.js', 60, 'regex', '/\\d+/g', '/\\d+/g', null, processId); // Test file nodes (should be excluded by noTests) - insertAstNode(db, 'tests/utils.test.js', 5, 'call', 'eval', null, null, testFnId); + insertAstNode( + db, + 'tests/utils.test.js', + 5, + 'new', + 'TestError', + 'new TestError()', + null, + testFnId, + ); db.close(); }); @@ -113,7 +115,7 @@ afterAll(() => { describe('AST_NODE_KINDS', () => { test('exports all expected kinds', () => { - expect(AST_NODE_KINDS).toEqual(['call', 'new', 'string', 'regex', 'throw', 'await']); + expect(AST_NODE_KINDS).toEqual(['new', 'string', 'regex', 'throw', 'await']); }); }); @@ -125,16 +127,16 @@ describe('astQueryData', () => { }); test('substring pattern match', () => { - const data = astQueryData('eval', dbPath); - // Should match 'eval' in src/utils.js and tests/utils.test.js + const data = astQueryData('Error', dbPath); + // Should match 'Error' in handler (new) and handler (throw) and test file (new TestError) expect(data.results.length).toBeGreaterThanOrEqual(2); - expect(data.results.every((r) => r.name.includes('eval'))).toBe(true); + expect(data.results.every((r) => r.name.includes('Error'))).toBe(true); }); test('glob wildcard pattern', () => { - const data = astQueryData('console.*', dbPath); - expect(data.results.length).toBe(2); - expect(data.results.every((r) => r.name.startsWith('console.'))).toBe(true); + const data = astQueryData('*host*', dbPath); + expect(data.results.length).toBe(1); + expect(data.results.every((r) => r.name.includes('host'))).toBe(true); }); test('exact pattern with star', () => { @@ -142,12 +144,6 @@ describe('astQueryData', () => { expect(data.count).toBeGreaterThan(0); }); - test('kind filter — call', () => { - const data = astQueryData(undefined, dbPath, { kind: 'call' }); - expect(data.results.every((r) => r.kind === 'call')).toBe(true); - expect(data.results.length).toBeGreaterThanOrEqual(5); - }); - test('kind filter — string', () => { const data = astQueryData(undefined, dbPath, { kind: 'string' }); expect(data.results.every((r) => r.kind === 'string')).toBe(true); @@ -157,7 +153,7 @@ describe('astQueryData', () => { test('kind filter — new', () => { const data = astQueryData(undefined, dbPath, { kind: 'new' }); expect(data.results.every((r) => r.kind === 'new')).toBe(true); - expect(data.results.length).toBe(2); + expect(data.results.length).toBe(3); }); test('kind filter — throw', () => { @@ -185,9 +181,9 @@ describe('astQueryData', () => { }); test('noTests excludes test files', () => { - const withTests = astQueryData('eval', dbPath); - const noTests = astQueryData('eval', dbPath, { noTests: true }); - expect(noTests.results.length).toBeLessThan(withTests.results.length); + const withTests = astQueryData('*Error*', dbPath, { kind: 'new' }); + const noTests = astQueryData('*Error*', dbPath, { kind: 'new', noTests: true }); + expect(withTests.results.length).toBeGreaterThan(noTests.results.length); expect(noTests.results.every((r) => !r.file.includes('.test.'))).toBe(true); }); @@ -206,20 +202,14 @@ describe('astQueryData', () => { }); test('parent node resolution', () => { - const data = astQueryData('eval', dbPath, { noTests: true }); + const data = astQueryData('Error', dbPath, { kind: 'new', file: 'handler' }); expect(data.results.length).toBe(1); const r = data.results[0]; expect(r.parent).toBeDefined(); - expect(r.parent.name).toBe('processInput'); + expect(r.parent.name).toBe('handleRequest'); expect(r.parent.kind).toBe('function'); }); - test('receiver field for calls', () => { - const data = astQueryData('console.log', dbPath); - expect(data.results.length).toBe(1); - expect(data.results[0].receiver).toBe('console'); - }); - test('empty results for non-matching pattern', () => { const data = astQueryData('nonexistent_xyz', dbPath); expect(data.results.length).toBe(0); @@ -227,8 +217,8 @@ describe('astQueryData', () => { }); test('combined kind + file filter', () => { - const data = astQueryData(undefined, dbPath, { kind: 'call', file: 'handler' }); - expect(data.results.every((r) => r.kind === 'call' && r.file.includes('handler'))).toBe(true); - expect(data.results.length).toBe(2); + const data = astQueryData(undefined, dbPath, { kind: 'new', file: 'handler' }); + expect(data.results.every((r) => r.kind === 'new' && r.file.includes('handler'))).toBe(true); + expect(data.results.length).toBe(1); }); }); diff --git a/tests/integration/build-parity.test.ts b/tests/integration/build-parity.test.ts index e6febfa5..4476fa9c 100644 --- a/tests/integration/build-parity.test.ts +++ b/tests/integration/build-parity.test.ts @@ -114,18 +114,25 @@ describeOrSkip('Build parity: native vs WASM', () => { it('produces identical ast_nodes', () => { const wasmGraph = readGraph(path.join(wasmDir, '.codegraph', 'graph.db')); const nativeGraph = readGraph(path.join(nativeDir, '.codegraph', 'graph.db')); + // Filter out 'call' kind transitionally: the WASM side no longer emits call AST + // nodes, but the published native binary (v3.7.0) still does. Once the next native + // binary is published with call removal, this filter can be dropped. + const filterCall = (nodes: unknown[]) => + (nodes as { kind: string }[]).filter((n) => n.kind !== 'call'); + const wasmFiltered = filterCall(wasmGraph.astNodes); + const nativeFiltered = filterCall(nativeGraph.astNodes); // Diagnostic: log counts to help debug CI-only parity failures - if (nativeGraph.astNodes.length !== wasmGraph.astNodes.length) { + if (nativeFiltered.length !== wasmFiltered.length) { console.error( - `[parity-diag] native astNodes: ${nativeGraph.astNodes.length}, wasm astNodes: ${wasmGraph.astNodes.length}`, + `[parity-diag] native astNodes: ${nativeFiltered.length}, wasm astNodes: ${wasmFiltered.length}`, ); console.error( - `[parity-diag] native kinds: ${JSON.stringify([...new Set((nativeGraph.astNodes as any[]).map((n: any) => n.kind))])}`, + `[parity-diag] native kinds: ${JSON.stringify([...new Set(nativeFiltered.map((n) => n.kind))])}`, ); console.error( - `[parity-diag] wasm kinds: ${JSON.stringify([...new Set((wasmGraph.astNodes as any[]).map((n: any) => n.kind))])}`, + `[parity-diag] wasm kinds: ${JSON.stringify([...new Set(wasmFiltered.map((n) => n.kind))])}`, ); } - expect(nativeGraph.astNodes).toEqual(wasmGraph.astNodes); + expect(nativeFiltered).toEqual(wasmFiltered); }); }); diff --git a/tests/parsers/ast-all-langs.test.ts b/tests/parsers/ast-all-langs.test.ts index 28e2d2b7..8c067919 100644 --- a/tests/parsers/ast-all-langs.test.ts +++ b/tests/parsers/ast-all-langs.test.ts @@ -147,7 +147,7 @@ describe('buildAstNodes — non-JS language astNodes', () => { test('all inserted nodes have valid kinds', async () => { const all = queryAll(db); - const validKinds = new Set(['call', 'new', 'string', 'regex', 'throw', 'await']); + const validKinds = new Set(['new', 'string', 'regex', 'throw', 'await']); for (const node of all) { expect(validKinds.has(node.kind)).toBe(true); } @@ -507,7 +507,8 @@ describe.skipIf(!canTestMultiLang)('native AST nodes — multi-language', () => test('all nodes have valid kinds', () => { const all = queryAll(db); - const validKinds = new Set(['call', 'new', 'string', 'regex', 'throw', 'await']); + // 'call' accepted transitionally: published native binary (v3.7.0) still emits it + const validKinds = new Set(['new', 'string', 'regex', 'throw', 'await', 'call']); for (const node of all) { expect(validKinds.has(node.kind)).toBe(true); } diff --git a/tests/parsers/ast-nodes.test.ts b/tests/parsers/ast-nodes.test.ts index ca3c27cb..4ebd79f7 100644 --- a/tests/parsers/ast-nodes.test.ts +++ b/tests/parsers/ast-nodes.test.ts @@ -100,15 +100,9 @@ function queryAllAstNodes() { // ─── Tests ──────────────────────────────────────────────────────────── describe('buildAstNodes — JS extraction', () => { - test('captures call_expression as kind:call', () => { + test('does not extract call_expression as AST nodes', () => { const calls = queryAstNodes('call'); - // eval(input), result.set('data', data), console.log(result) - // Note: fetch('/api/data') is inside await — captured as kind:await, not kind:call - expect(calls.length).toBe(3); - const names = calls.map((n) => n.name); - expect(names).toContain('eval'); - expect(names).toContain('result.set'); - expect(names).toContain('console.log'); + expect(calls.length).toBe(0); }); test('captures new_expression as kind:new', () => { @@ -172,7 +166,7 @@ describe('buildAstNodes — JS extraction', () => { test('all inserted nodes have valid kinds', () => { const all = queryAllAstNodes(); - const validKinds = new Set(['call', 'new', 'string', 'regex', 'throw', 'await']); + const validKinds = new Set(['new', 'string', 'regex', 'throw', 'await']); for (const node of all) { expect(validKinds.has(node.kind)).toBe(true); } @@ -301,7 +295,8 @@ describe.skipIf(!canTestNative)('buildAstNodes — native engine', () => { test('all nodes have valid kinds', () => { const all = queryAllNativeAstNodes(); - const validKinds = new Set(['call', 'new', 'string', 'regex', 'throw', 'await']); + // 'call' accepted transitionally: published native binary (v3.7.0) still emits it + const validKinds = new Set(['new', 'string', 'regex', 'throw', 'await', 'call']); for (const node of all) { expect(validKinds.has(node.kind)).toBe(true); }