Skip to content

Commit ed2bf33

Browse files
authored
refactor: decompose monolithic language extractors into per-category handlers (#490)
* chore: remove dead exports and un-export internal constant - Remove dead `truncate` function from ast-analysis/shared.js (0 consumers) - Remove dead `truncStart` function from presentation/table.js (0 consumers) - Un-export `BATCH_CHUNK` in builder/helpers.js (only used internally) Skipped sync.json targets that were false positives: - BUILTIN_RECEIVERS: used by incremental.js + build-edges.js - TRANSIENT_CODES/RETRY_DELAY_MS: internal to readFileSafe - MAX_COL_WIDTH: internal to printAutoTable - findFunctionNode: re-exported from index.js, used in tests Impact: 1 functions changed, 32 affected * refactor: extract shared findNodes utility from cfg and dataflow features Impact: 5 functions changed, 7 affected * fix: replace empty catch blocks in db connection and migrations connection.js: add debug() logging to all 8 catch-with-fallback blocks so failures are observable without changing behavior. migrations.js: replace 14 try/catch blocks in initSchema with hasColumn() and hasTable() guards. CREATE INDEX calls use IF NOT EXISTS directly. getBuildMeta uses hasTable() check instead of try/catch. Impact: 10 functions changed, 19 affected * fix: replace empty catch blocks in domain analysis layer Add debug() logging to 10 empty catch blocks across context.js, symbol-lookup.js, exports.js, impact.js, and module-map.js. All catches retain their fallback behavior but failures are now observable via debug logging. Impact: 6 functions changed, 18 affected * fix: replace empty catch blocks in parser.js Add debug() logging to 6 empty catch blocks: 3 in disposeParsers() for WASM resource cleanup, 2 in ensureWasmTrees() for file read and parse failures, and 1 in getActiveEngine() for version lookup. Impact: 3 functions changed, 0 affected * fix: replace empty catch blocks in features layer Add debug() logging to 9 empty catch blocks across complexity.js (5), cfg.js (2), and dataflow.js (2). All catches for file read and parse failures now log the error message before continuing. Impact: 4 functions changed, 2 affected * refactor: decompose extractSymbolsWalk into per-category handlers Split the monolithic walkJavaScriptNode switch (13 cases, cognitive 228) into 11 focused handler functions. The dispatcher is now a thin switch that delegates to handleFunctionDecl, handleClassDecl, handleMethodDef, handleInterfaceDecl, handleTypeAliasDecl, handleVariableDecl, handleEnumDecl, handleCallExpr, handleImportStmt, handleExportStmt, and handleExpressionStmt. The expression_statement case now reuses the existing handleCommonJSAssignment helper, eliminating ~50 lines of duplication. Worst handler complexity: handleVariableDecl (cognitive 20), down from the original monolithic function (cognitive 279). Impact: 13 functions changed, 3 affected * refactor: decompose extractPythonSymbols into per-category handlers Split walkPythonNode switch into 7 focused handlers: handlePyFunctionDef, handlePyClassDef, handlePyCall, handlePyImport, handlePyExpressionStmt, handlePyImportFrom, plus the decorated_definition inline dispatch. Moved extractPythonParameters, extractPythonClassProperties, walkInitBody, and findPythonParentClass from closures to module-scope functions. Impact: 12 functions changed, 5 affected * refactor: decompose extractJavaSymbols into per-category handlers Split walkJavaNode switch into 8 focused handlers plus an extractJavaInterfaces helper. Moved findJavaParentClass to module scope. The class_declaration case (deepest nesting in the file) is now split between handleJavaClassDecl and extractJavaInterfaces. Impact: 12 functions changed, 5 affected * refactor: decompose remaining language extractors Apply the same per-category handler decomposition to all remaining language extractors: Go (6 handlers), Ruby (8 handlers), PHP (11 handlers), C# (11 handlers), Rust (9 handlers), HCL (4 handlers). Each extractor now follows the template established by the JS extractor: - Thin entry function creates ctx, delegates to walkXNode - walkXNode is a thin dispatcher switch - Each case is a named handler function at module scope - Helper functions (findParentClass, etc.) moved to module scope Impact: 66 functions changed, 23 affected * fix: use early-return guard in handleGoFuncDecl for consistency All other handlers use `if (!nameNode) return;` style. Align handleGoFuncDecl to match. * fix: skip duplicate definitions for trait/interface methods in Rust, C#, and PHP extractors Trait/interface handlers already emit qualified method definitions. Without this guard, the recursive walker also fires the method handler, producing bare-name duplicates. Skip methods whose parent is a trait_item/interface_declaration body. * fix: add interface-method guard to handleJavaMethodDecl to prevent duplicate definitions * fix: format parity test deduplication filter
1 parent c9f3460 commit ed2bf33

10 files changed

Lines changed: 1860 additions & 1804 deletions

File tree

src/extractors/csharp.js

Lines changed: 224 additions & 207 deletions
Large diffs are not rendered by default.

src/extractors/go.js

Lines changed: 176 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -4,196 +4,200 @@ import { findChild, goVisibility, nodeEndLine } from './helpers.js';
44
* Extract symbols from Go files.
55
*/
66
export function extractGoSymbols(tree, _filePath) {
7-
const definitions = [];
8-
const calls = [];
9-
const imports = [];
10-
const classes = [];
11-
const exports = [];
7+
const ctx = {
8+
definitions: [],
9+
calls: [],
10+
imports: [],
11+
classes: [],
12+
exports: [],
13+
};
1214

13-
function walkGoNode(node) {
14-
switch (node.type) {
15-
case 'function_declaration': {
16-
const nameNode = node.childForFieldName('name');
17-
if (nameNode) {
18-
const params = extractGoParameters(node.childForFieldName('parameters'));
19-
definitions.push({
20-
name: nameNode.text,
21-
kind: 'function',
22-
line: node.startPosition.row + 1,
23-
endLine: nodeEndLine(node),
24-
children: params.length > 0 ? params : undefined,
25-
visibility: goVisibility(nameNode.text),
26-
});
27-
}
28-
break;
29-
}
15+
walkGoNode(tree.rootNode, ctx);
16+
return ctx;
17+
}
3018

31-
case 'method_declaration': {
32-
const nameNode = node.childForFieldName('name');
33-
const receiver = node.childForFieldName('receiver');
34-
if (nameNode) {
35-
let receiverType = null;
36-
if (receiver) {
37-
// receiver is a parameter_list like (r *Foo) or (r Foo)
38-
for (let i = 0; i < receiver.childCount; i++) {
39-
const param = receiver.child(i);
40-
if (!param) continue;
41-
const typeNode = param.childForFieldName('type');
42-
if (typeNode) {
43-
receiverType =
44-
typeNode.type === 'pointer_type'
45-
? typeNode.text.replace(/^\*/, '')
46-
: typeNode.text;
47-
break;
48-
}
49-
}
50-
}
51-
const fullName = receiverType ? `${receiverType}.${nameNode.text}` : nameNode.text;
52-
const params = extractGoParameters(node.childForFieldName('parameters'));
53-
definitions.push({
54-
name: fullName,
55-
kind: 'method',
56-
line: node.startPosition.row + 1,
57-
endLine: nodeEndLine(node),
58-
children: params.length > 0 ? params : undefined,
59-
visibility: goVisibility(nameNode.text),
60-
});
61-
}
62-
break;
63-
}
19+
function walkGoNode(node, ctx) {
20+
switch (node.type) {
21+
case 'function_declaration':
22+
handleGoFuncDecl(node, ctx);
23+
break;
24+
case 'method_declaration':
25+
handleGoMethodDecl(node, ctx);
26+
break;
27+
case 'type_declaration':
28+
handleGoTypeDecl(node, ctx);
29+
break;
30+
case 'import_declaration':
31+
handleGoImportDecl(node, ctx);
32+
break;
33+
case 'const_declaration':
34+
handleGoConstDecl(node, ctx);
35+
break;
36+
case 'call_expression':
37+
handleGoCallExpr(node, ctx);
38+
break;
39+
}
6440

65-
case 'type_declaration': {
66-
for (let i = 0; i < node.childCount; i++) {
67-
const spec = node.child(i);
68-
if (!spec || spec.type !== 'type_spec') continue;
69-
const nameNode = spec.childForFieldName('name');
70-
const typeNode = spec.childForFieldName('type');
71-
if (nameNode && typeNode) {
72-
if (typeNode.type === 'struct_type') {
73-
const fields = extractStructFields(typeNode);
74-
definitions.push({
75-
name: nameNode.text,
76-
kind: 'struct',
77-
line: node.startPosition.row + 1,
78-
endLine: nodeEndLine(node),
79-
children: fields.length > 0 ? fields : undefined,
80-
});
81-
} else if (typeNode.type === 'interface_type') {
82-
definitions.push({
83-
name: nameNode.text,
84-
kind: 'interface',
85-
line: node.startPosition.row + 1,
86-
endLine: nodeEndLine(node),
87-
});
88-
for (let j = 0; j < typeNode.childCount; j++) {
89-
const member = typeNode.child(j);
90-
if (member && member.type === 'method_elem') {
91-
const methName = member.childForFieldName('name');
92-
if (methName) {
93-
definitions.push({
94-
name: `${nameNode.text}.${methName.text}`,
95-
kind: 'method',
96-
line: member.startPosition.row + 1,
97-
endLine: member.endPosition.row + 1,
98-
});
99-
}
100-
}
101-
}
102-
} else {
103-
definitions.push({
104-
name: nameNode.text,
105-
kind: 'type',
106-
line: node.startPosition.row + 1,
107-
endLine: nodeEndLine(node),
108-
});
109-
}
110-
}
111-
}
41+
for (let i = 0; i < node.childCount; i++) walkGoNode(node.child(i), ctx);
42+
}
43+
44+
// ── Walk-path per-node-type handlers ────────────────────────────────────────
45+
46+
function handleGoFuncDecl(node, ctx) {
47+
const nameNode = node.childForFieldName('name');
48+
if (!nameNode) return;
49+
const params = extractGoParameters(node.childForFieldName('parameters'));
50+
ctx.definitions.push({
51+
name: nameNode.text,
52+
kind: 'function',
53+
line: node.startPosition.row + 1,
54+
endLine: nodeEndLine(node),
55+
children: params.length > 0 ? params : undefined,
56+
visibility: goVisibility(nameNode.text),
57+
});
58+
}
59+
60+
function handleGoMethodDecl(node, ctx) {
61+
const nameNode = node.childForFieldName('name');
62+
const receiver = node.childForFieldName('receiver');
63+
if (!nameNode) return;
64+
let receiverType = null;
65+
if (receiver) {
66+
for (let i = 0; i < receiver.childCount; i++) {
67+
const param = receiver.child(i);
68+
if (!param) continue;
69+
const typeNode = param.childForFieldName('type');
70+
if (typeNode) {
71+
receiverType =
72+
typeNode.type === 'pointer_type' ? typeNode.text.replace(/^\*/, '') : typeNode.text;
11273
break;
11374
}
75+
}
76+
}
77+
const fullName = receiverType ? `${receiverType}.${nameNode.text}` : nameNode.text;
78+
const params = extractGoParameters(node.childForFieldName('parameters'));
79+
ctx.definitions.push({
80+
name: fullName,
81+
kind: 'method',
82+
line: node.startPosition.row + 1,
83+
endLine: nodeEndLine(node),
84+
children: params.length > 0 ? params : undefined,
85+
visibility: goVisibility(nameNode.text),
86+
});
87+
}
11488

115-
case 'import_declaration': {
116-
for (let i = 0; i < node.childCount; i++) {
117-
const child = node.child(i);
118-
if (!child) continue;
119-
if (child.type === 'import_spec') {
120-
const pathNode = child.childForFieldName('path');
121-
if (pathNode) {
122-
const importPath = pathNode.text.replace(/"/g, '');
123-
const nameNode = child.childForFieldName('name');
124-
const alias = nameNode ? nameNode.text : importPath.split('/').pop();
125-
imports.push({
126-
source: importPath,
127-
names: [alias],
128-
line: child.startPosition.row + 1,
129-
goImport: true,
89+
function handleGoTypeDecl(node, ctx) {
90+
for (let i = 0; i < node.childCount; i++) {
91+
const spec = node.child(i);
92+
if (!spec || spec.type !== 'type_spec') continue;
93+
const nameNode = spec.childForFieldName('name');
94+
const typeNode = spec.childForFieldName('type');
95+
if (nameNode && typeNode) {
96+
if (typeNode.type === 'struct_type') {
97+
const fields = extractStructFields(typeNode);
98+
ctx.definitions.push({
99+
name: nameNode.text,
100+
kind: 'struct',
101+
line: node.startPosition.row + 1,
102+
endLine: nodeEndLine(node),
103+
children: fields.length > 0 ? fields : undefined,
104+
});
105+
} else if (typeNode.type === 'interface_type') {
106+
ctx.definitions.push({
107+
name: nameNode.text,
108+
kind: 'interface',
109+
line: node.startPosition.row + 1,
110+
endLine: nodeEndLine(node),
111+
});
112+
for (let j = 0; j < typeNode.childCount; j++) {
113+
const member = typeNode.child(j);
114+
if (member && member.type === 'method_elem') {
115+
const methName = member.childForFieldName('name');
116+
if (methName) {
117+
ctx.definitions.push({
118+
name: `${nameNode.text}.${methName.text}`,
119+
kind: 'method',
120+
line: member.startPosition.row + 1,
121+
endLine: member.endPosition.row + 1,
130122
});
131123
}
132124
}
133-
if (child.type === 'import_spec_list') {
134-
for (let j = 0; j < child.childCount; j++) {
135-
const spec = child.child(j);
136-
if (spec && spec.type === 'import_spec') {
137-
const pathNode = spec.childForFieldName('path');
138-
if (pathNode) {
139-
const importPath = pathNode.text.replace(/"/g, '');
140-
const nameNode = spec.childForFieldName('name');
141-
const alias = nameNode ? nameNode.text : importPath.split('/').pop();
142-
imports.push({
143-
source: importPath,
144-
names: [alias],
145-
line: spec.startPosition.row + 1,
146-
goImport: true,
147-
});
148-
}
149-
}
150-
}
151-
}
152-
}
153-
break;
154-
}
155-
156-
case 'const_declaration': {
157-
for (let i = 0; i < node.childCount; i++) {
158-
const spec = node.child(i);
159-
if (!spec || spec.type !== 'const_spec') continue;
160-
const constName = spec.childForFieldName('name');
161-
if (constName) {
162-
definitions.push({
163-
name: constName.text,
164-
kind: 'constant',
165-
line: spec.startPosition.row + 1,
166-
endLine: spec.endPosition.row + 1,
167-
});
168-
}
169125
}
170-
break;
126+
} else {
127+
ctx.definitions.push({
128+
name: nameNode.text,
129+
kind: 'type',
130+
line: node.startPosition.row + 1,
131+
endLine: nodeEndLine(node),
132+
});
171133
}
134+
}
135+
}
136+
}
172137

173-
case 'call_expression': {
174-
const fn = node.childForFieldName('function');
175-
if (fn) {
176-
if (fn.type === 'identifier') {
177-
calls.push({ name: fn.text, line: node.startPosition.row + 1 });
178-
} else if (fn.type === 'selector_expression') {
179-
const field = fn.childForFieldName('field');
180-
if (field) {
181-
const operand = fn.childForFieldName('operand');
182-
const call = { name: field.text, line: node.startPosition.row + 1 };
183-
if (operand) call.receiver = operand.text;
184-
calls.push(call);
185-
}
186-
}
138+
function handleGoImportDecl(node, ctx) {
139+
for (let i = 0; i < node.childCount; i++) {
140+
const child = node.child(i);
141+
if (!child) continue;
142+
if (child.type === 'import_spec') {
143+
extractGoImportSpec(child, ctx);
144+
}
145+
if (child.type === 'import_spec_list') {
146+
for (let j = 0; j < child.childCount; j++) {
147+
const spec = child.child(j);
148+
if (spec && spec.type === 'import_spec') {
149+
extractGoImportSpec(spec, ctx);
187150
}
188-
break;
189151
}
190152
}
153+
}
154+
}
155+
156+
function extractGoImportSpec(spec, ctx) {
157+
const pathNode = spec.childForFieldName('path');
158+
if (pathNode) {
159+
const importPath = pathNode.text.replace(/"/g, '');
160+
const nameNode = spec.childForFieldName('name');
161+
const alias = nameNode ? nameNode.text : importPath.split('/').pop();
162+
ctx.imports.push({
163+
source: importPath,
164+
names: [alias],
165+
line: spec.startPosition.row + 1,
166+
goImport: true,
167+
});
168+
}
169+
}
191170

192-
for (let i = 0; i < node.childCount; i++) walkGoNode(node.child(i));
171+
function handleGoConstDecl(node, ctx) {
172+
for (let i = 0; i < node.childCount; i++) {
173+
const spec = node.child(i);
174+
if (!spec || spec.type !== 'const_spec') continue;
175+
const constName = spec.childForFieldName('name');
176+
if (constName) {
177+
ctx.definitions.push({
178+
name: constName.text,
179+
kind: 'constant',
180+
line: spec.startPosition.row + 1,
181+
endLine: spec.endPosition.row + 1,
182+
});
183+
}
193184
}
185+
}
194186

195-
walkGoNode(tree.rootNode);
196-
return { definitions, calls, imports, classes, exports };
187+
function handleGoCallExpr(node, ctx) {
188+
const fn = node.childForFieldName('function');
189+
if (!fn) return;
190+
if (fn.type === 'identifier') {
191+
ctx.calls.push({ name: fn.text, line: node.startPosition.row + 1 });
192+
} else if (fn.type === 'selector_expression') {
193+
const field = fn.childForFieldName('field');
194+
if (field) {
195+
const operand = fn.childForFieldName('operand');
196+
const call = { name: field.text, line: node.startPosition.row + 1 };
197+
if (operand) call.receiver = operand.text;
198+
ctx.calls.push(call);
199+
}
200+
}
197201
}
198202

199203
// ── Child extraction helpers ────────────────────────────────────────────────

0 commit comments

Comments
 (0)