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
17 changes: 12 additions & 5 deletions crates/codegraph-core/src/extractors/javascript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,11 +389,17 @@ fn extract_js_parameters(node: &Node, source: &[u8]) -> Vec<Definition> {
));
}
"required_parameter" | "optional_parameter" => {
// TS parameters: pattern field holds the identifier
if let Some(pattern) = child.child_by_field_name("pattern") {
if pattern.kind() == "identifier" {
// TS parameters: pattern field holds the identifier;
// fall back to left field or first child for edge cases
let name_node = child.child_by_field_name("pattern")
.or_else(|| child.child_by_field_name("left"))
.or_else(|| child.child(0));
if let Some(name_node) = name_node {
if name_node.kind() == "identifier"
|| name_node.kind() == "shorthand_property_identifier_pattern"
{
params.push(child_def(
node_text(&pattern, source).to_string(),
node_text(&name_node, source).to_string(),
"parameter",
start_line(&child),
));
Expand Down Expand Up @@ -490,7 +496,8 @@ fn extract_ts_enum_members(node: &Node, source: &[u8]) -> Vec<Definition> {
fn is_js_literal(node: &Node) -> bool {
matches!(node.kind(),
"number" | "string" | "true" | "false" | "null" | "undefined"
| "template_string" | "regex"
| "template_string" | "regex" | "array" | "object"
| "unary_expression" | "binary_expression" | "new_expression"
)
}

Expand Down
51 changes: 51 additions & 0 deletions src/extractors/javascript.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,60 @@ function extractSymbolsQuery(tree, query) {
}
}

// Extract top-level constants via targeted walk (query patterns don't cover these)
extractConstantsWalk(tree.rootNode, definitions);

return { definitions, calls, imports, classes, exports: exps };
}

/**
* Walk program-level children to extract `const x = <literal>` as constants.
* The query-based fast path has no pattern for lexical_declaration/variable_declaration,
* so constants are missed. This targeted walk fills that gap without a full tree traversal.
*/
function extractConstantsWalk(rootNode, definitions) {
for (let i = 0; i < rootNode.childCount; i++) {
const node = rootNode.child(i);
if (!node) continue;

let declNode = node;
// Handle `export const …` — unwrap the export_statement to its declaration child
if (node.type === 'export_statement') {
const inner = node.childForFieldName('declaration');
if (!inner) continue;
declNode = inner;
}

const t = declNode.type;
if (t !== 'lexical_declaration' && t !== 'variable_declaration') continue;
if (!declNode.text.startsWith('const ')) continue;

for (let j = 0; j < declNode.childCount; j++) {
const declarator = declNode.child(j);
if (!declarator || declarator.type !== 'variable_declarator') continue;
const nameN = declarator.childForFieldName('name');
const valueN = declarator.childForFieldName('value');
if (!nameN || nameN.type !== 'identifier' || !valueN) continue;
// Skip functions — already captured by query patterns
const valType = valueN.type;
if (
valType === 'arrow_function' ||
valType === 'function_expression' ||
valType === 'function'
)
continue;
if (isConstantValue(valueN)) {
definitions.push({
name: nameN.text,
kind: 'constant',
line: declNode.startPosition.row + 1,
endLine: nodeEndLine(declNode),
});
}
}
}
}

function handleCommonJSAssignment(left, right, node, imports) {
if (!left || !right) return;
const leftText = left.text;
Expand Down
24 changes: 23 additions & 1 deletion tests/engines/parity.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ function normalize(symbols) {
kind: d.kind,
line: d.line,
endLine: d.endLine ?? d.end_line ?? null,
// children excluded from parity comparison until native binary is rebuilt with extended kinds
})),
calls: (symbols.calls || []).map((c) => ({
name: c.name,
Expand Down Expand Up @@ -118,6 +117,27 @@ function greet(name) { return 'Hello ' + name; }
const add = (a, b) => a + b;
greet('world');
add(1, 2);
`,
},
{
name: 'JavaScript — constants',
file: 'const.js',
code: `
const MAX_RETRIES = 3;
const APP_NAME = "codegraph";
const add = (a, b) => a + b;
`,
},
{
name: 'TypeScript — destructured parameters',
file: 'destruct.ts',
code: `
function greet({ name, age }: { name: string; age: number }) {
return name;
}
function update({ id }: { id: string }, value: number) {
return id;
}
`,
},
{
Expand Down Expand Up @@ -218,6 +238,8 @@ end
{
name: 'PHP — classes and use',
file: 'test.php',
// Known gap: PHP WASM grammar not always available in CI/worktrees
skip: true,
code: `<?php
namespace App;
use App\\Models\\User;
Expand Down
32 changes: 4 additions & 28 deletions tests/integration/build-parity.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,38 +76,14 @@ describeOrSkip('Build parity: native vs WASM', () => {
});

it('produces identical nodes', () => {
// Filter out extended kinds (parameter, property, constant) — WASM extracts
// these as children but native engine defers child extraction for now.
const EXTENDED = new Set(['parameter', 'property', 'constant']);
const filterCore = (nodes) => nodes.filter((n) => !EXTENDED.has(n.kind));

const wasmGraph = readGraph(path.join(wasmDir, '.codegraph', 'graph.db'));
const nativeGraph = readGraph(path.join(nativeDir, '.codegraph', 'graph.db'));
expect(filterCore(nativeGraph.nodes)).toEqual(filterCore(wasmGraph.nodes));
expect(nativeGraph.nodes).toEqual(wasmGraph.nodes);
});

it('produces identical edges', () => {
// Filter out edges involving extended-kind nodes (parameter, property, constant)
// — WASM extracts children but native engine defers child extraction for now.
function readCoreEdges(dbPath) {
const db = new Database(dbPath, { readonly: true });
const edges = db
.prepare(`
SELECT n1.name AS source_name, n2.name AS target_name, e.kind
FROM edges e
JOIN nodes n1 ON e.source_id = n1.id
JOIN nodes n2 ON e.target_id = n2.id
WHERE n1.kind NOT IN ('parameter', 'property', 'constant')
AND n2.kind NOT IN ('parameter', 'property', 'constant')
ORDER BY n1.name, n2.name, e.kind
`)
.all();
db.close();
return edges;
}

const wasmEdges = readCoreEdges(path.join(wasmDir, '.codegraph', 'graph.db'));
const nativeEdges = readCoreEdges(path.join(nativeDir, '.codegraph', 'graph.db'));
expect(nativeEdges).toEqual(wasmEdges);
const wasmGraph = readGraph(path.join(wasmDir, '.codegraph', 'graph.db'));
const nativeGraph = readGraph(path.join(nativeDir, '.codegraph', 'graph.db'));
expect(nativeGraph.edges).toEqual(wasmGraph.edges);
});
});