@lifeaitools/rdc-skills 0.34.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +284 -1
- package/VALIDATOR-ARCHITECTURE.md +534 -0
- package/commands/analyze-tests.md +11 -0
- package/commands/check-clean-code.md +11 -0
- package/commands/check-packages.md +10 -0
- package/commands/compare-compliance.md +14 -0
- package/commands/full-analysis.md +50 -0
- package/commands/get-refactoring-plan.md +13 -0
- package/commands/quick-check.md +13 -0
- package/commands/recover.md +149 -0
- package/commands/review-arch.md +12 -0
- package/commands/review.md +12 -113
- package/commands/suggest-patterns.md +11 -0
- package/commands/validate-solid.md +11 -0
- package/package.json +14 -2
- package/scripts/architecture-score.mjs +157 -0
- package/scripts/clean-code-score.mjs +177 -0
- package/scripts/duplication-score.mjs +66 -0
- package/scripts/lib/architecture-scoring.mjs +695 -0
- package/scripts/lib/clean-code-scoring.mjs +258 -0
- package/scripts/lib/duplication-scoring.mjs +238 -0
- package/scripts/lib/language-plugin.mjs +82 -0
- package/scripts/lib/package-metrics.mjs +439 -0
- package/scripts/lib/pattern-scoring.mjs +351 -0
- package/scripts/lib/plugins/treesitter.mjs +1182 -0
- package/scripts/lib/plugins/typescript.mjs +672 -0
- package/scripts/lib/refactoring-scoring.mjs +307 -0
- package/scripts/lib/solid-scoring.mjs +101 -0
- package/scripts/lib/test-smell-scoring.mjs +581 -0
- package/scripts/lib/vendor/codeflow-parser/.source-commit +1 -0
- package/scripts/lib/vendor/codeflow-parser/grammars.d.ts +23 -0
- package/scripts/lib/vendor/codeflow-parser/grammars.js +57 -0
- package/scripts/lib/vendor/codeflow-parser/memberFacts.d.ts +274 -0
- package/scripts/lib/vendor/codeflow-parser/memberFacts.js +1117 -0
- package/scripts/lib/vendor/codeflow-parser/nativeParser.d.ts +115 -0
- package/scripts/lib/vendor/codeflow-parser/nativeParser.js +759 -0
- package/scripts/lib/vendor/codeflow-parser/package.json +3 -0
- package/scripts/lib/vendor/codeflow-parser/xmlParser.d.ts +77 -0
- package/scripts/lib/vendor/codeflow-parser/xmlParser.js +400 -0
- package/scripts/package-metrics-cli.mjs +112 -0
- package/scripts/pattern-score.mjs +143 -0
- package/scripts/refactoring-score.mjs +253 -0
- package/scripts/solid-score.mjs +337 -0
- package/skills/architecture-reviewer/SKILL.md +287 -0
- package/skills/clean-code-analyzer/SKILL.md +147 -0
- package/skills/package-design/SKILL.md +118 -0
- package/skills/pattern-advisor/SKILL.md +237 -0
- package/skills/pattern-refactoring-guide/SKILL.md +262 -0
- package/skills/review/SKILL.md +29 -0
- package/skills/solid-validator/SKILL.md +92 -0
- package/skills/testing-strategy/SKILL.md +132 -0
- package/tests/lib/architecture-scoring.test.mjs +335 -0
- package/tests/lib/clean-code-scoring.test.mjs +241 -0
- package/tests/lib/duplication-scoring.test.mjs +144 -0
- package/tests/lib/fixtures.mjs +58 -0
- package/tests/lib/package-metrics.test.mjs +241 -0
- package/tests/lib/pattern-scoring.test.mjs +251 -0
- package/tests/lib/refactoring-scoring.test.mjs +264 -0
- package/tests/lib/solid-scoring.test.mjs +291 -0
- package/tests/lib/test-smell-scoring.test.mjs +281 -0
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nativeParser.ts — LanguageParser implementation via pre-built tree-sitter WASM.
|
|
3
|
+
*
|
|
4
|
+
* Implements the LanguageParser contract from @regen/codeflow using the native
|
|
5
|
+
* web-tree-sitter bindings. Extracts symbols, interfaces, calls, and imports
|
|
6
|
+
* from TypeScript, JavaScript, Python, C, C++, and C# source files.
|
|
7
|
+
*
|
|
8
|
+
* Design decisions:
|
|
9
|
+
* D2 — parser topology: native node-tree-sitter in PM2 service
|
|
10
|
+
* D3 — rich CALLS edge (call_line, receiver, resolved, arity, count)
|
|
11
|
+
* D10 — types live in @regen/codeflow (no new shared package)
|
|
12
|
+
* A2 — resolution_confidence scale per phase-f
|
|
13
|
+
* A3 — parse_status enum for false-RED guard
|
|
14
|
+
*/
|
|
15
|
+
import { loadGrammar } from './grammars.js';
|
|
16
|
+
import { buildReferenceGraph, countIdentifiers, extractMembers, hasLanguageProfile, resolveOverrideShapes, } from './memberFacts.js';
|
|
17
|
+
import { extractXml, isXmlLanguage, XML_LANGUAGES } from './xmlParser.js';
|
|
18
|
+
const DEFAULT_MAX_CALLS = 200;
|
|
19
|
+
// The tree-sitter node that represents "a call" is NAMED DIFFERENTLY IN EVERY
|
|
20
|
+
// GRAMMAR. This map is the whole reason python and csharp had 0 CALLS edges
|
|
21
|
+
// while carrying 20,871 and 10,076 symbols respectively: extractCallsFromBody
|
|
22
|
+
// tested `node.type === 'call_expression'` — true only for the C-like grammars —
|
|
23
|
+
// so the python and csharp walks ran to completion and matched nothing.
|
|
24
|
+
//
|
|
25
|
+
// All four shapes expose the SAME two fields (`function`, `arguments`), which is
|
|
26
|
+
// why only the type test varies below and the extraction body is shared.
|
|
27
|
+
const CALL_NODE_TYPES = {
|
|
28
|
+
typescript: ['call_expression'],
|
|
29
|
+
javascript: ['call_expression'],
|
|
30
|
+
python: ['call'],
|
|
31
|
+
c: ['call_expression'],
|
|
32
|
+
cpp: ['call_expression'],
|
|
33
|
+
csharp: ['invocation_expression'],
|
|
34
|
+
};
|
|
35
|
+
function callNodeTypesFor(language) {
|
|
36
|
+
return CALL_NODE_TYPES[language] ?? ['call_expression'];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Create a native tree-sitter LanguageParser.
|
|
40
|
+
*/
|
|
41
|
+
export function createNativeParser(opts) {
|
|
42
|
+
const maxCalls = opts?.maxCallsPerFunction ?? DEFAULT_MAX_CALLS;
|
|
43
|
+
let initialized = false;
|
|
44
|
+
let initializationFailed = false;
|
|
45
|
+
let ParserClass = null;
|
|
46
|
+
let Language = null;
|
|
47
|
+
const languageCache = new Map();
|
|
48
|
+
async function ensureRuntime() {
|
|
49
|
+
if (initialized)
|
|
50
|
+
return !initializationFailed;
|
|
51
|
+
initialized = true;
|
|
52
|
+
try {
|
|
53
|
+
const moduleName = 'web-tree-sitter';
|
|
54
|
+
const mod = await import(/* @vite-ignore */ moduleName);
|
|
55
|
+
ParserClass = mod.default ?? mod.Parser;
|
|
56
|
+
if (!ParserClass?.init)
|
|
57
|
+
throw new Error('web-tree-sitter runtime is unavailable');
|
|
58
|
+
await ParserClass.init();
|
|
59
|
+
Language = mod.Language ?? ParserClass.Language;
|
|
60
|
+
return Boolean(Language);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
initializationFailed = true;
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function getLanguage(language) {
|
|
68
|
+
const cached = languageCache.get(language);
|
|
69
|
+
if (cached)
|
|
70
|
+
return cached;
|
|
71
|
+
const grammar = loadGrammar(language);
|
|
72
|
+
if (!grammar.available || !grammar.grammar || !Language)
|
|
73
|
+
return null;
|
|
74
|
+
try {
|
|
75
|
+
const loaded = await Language.load(grammar.grammar);
|
|
76
|
+
languageCache.set(language, loaded);
|
|
77
|
+
return loaded;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
id: 'tree-sitter',
|
|
85
|
+
languages: ['typescript', 'javascript', 'python', 'c', 'cpp', 'csharp', ...XML_LANGUAGES],
|
|
86
|
+
async parse(files) {
|
|
87
|
+
const results = [];
|
|
88
|
+
const ready = await ensureRuntime();
|
|
89
|
+
if (!ready || !ParserClass) {
|
|
90
|
+
return files.map(file => emptyResult(file, 'parse_error'));
|
|
91
|
+
}
|
|
92
|
+
// Per-file identifier frequencies + exported names, collected during the
|
|
93
|
+
// main loop so the cross-file reference pass costs one extra sweep of
|
|
94
|
+
// already-parsed data rather than re-parsing anything.
|
|
95
|
+
const referenceInputs = [];
|
|
96
|
+
const factsByPath = new Map();
|
|
97
|
+
for (const file of files) {
|
|
98
|
+
try {
|
|
99
|
+
// XML-family documents are handled BEFORE grammar loading: there is
|
|
100
|
+
// no tree-sitter grammar for them, so the grammar path would return
|
|
101
|
+
// `no_grammar` and drop a BPMN process — a program with real nodes
|
|
102
|
+
// and real edges — on the floor.
|
|
103
|
+
if (isXmlLanguage(file.language)) {
|
|
104
|
+
const xml = extractXml(file.content, file.language);
|
|
105
|
+
const hasXmlContent = xml.symbols.length > 0;
|
|
106
|
+
factsByPath.set(file.path, { members: xml.members, units: xml.units });
|
|
107
|
+
referenceInputs.push({
|
|
108
|
+
path: file.path,
|
|
109
|
+
exportedNames: exportedNamesOf(xml.symbols, xml.interfaces),
|
|
110
|
+
identifierCounts: countXmlIdentifiers(xml),
|
|
111
|
+
});
|
|
112
|
+
results.push({
|
|
113
|
+
path: file.path,
|
|
114
|
+
language: file.language,
|
|
115
|
+
symbols: xml.symbols,
|
|
116
|
+
interfaces: xml.interfaces,
|
|
117
|
+
calls: xml.calls,
|
|
118
|
+
imports: xml.imports,
|
|
119
|
+
parse_status: hasXmlContent ? 'parsed' : 'legitimately_empty',
|
|
120
|
+
calls_truncated: false,
|
|
121
|
+
members: xml.members,
|
|
122
|
+
units: xml.units,
|
|
123
|
+
references: [],
|
|
124
|
+
profile_complete: true,
|
|
125
|
+
});
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const grammar = await getLanguage(file.language);
|
|
129
|
+
if (!grammar) {
|
|
130
|
+
results.push(emptyResult(file, 'no_grammar'));
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const parser = new ParserClass();
|
|
134
|
+
parser.setLanguage(grammar);
|
|
135
|
+
const tree = parser.parse(file.content);
|
|
136
|
+
if (!tree) {
|
|
137
|
+
results.push(emptyResult(file, 'parse_error'));
|
|
138
|
+
parser.delete?.();
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const symbols = [];
|
|
142
|
+
const interfaces = [];
|
|
143
|
+
const calls = [];
|
|
144
|
+
const imports = [];
|
|
145
|
+
const rootNode = tree.rootNode;
|
|
146
|
+
// Declarations first: `calls` resolution tests a callee against the
|
|
147
|
+
// known-symbol set, so the roster has to be complete before any body
|
|
148
|
+
// is walked.
|
|
149
|
+
if (file.language === 'typescript' || file.language === 'javascript') {
|
|
150
|
+
extractTsJsSymbols(rootNode, file, symbols, interfaces, imports);
|
|
151
|
+
}
|
|
152
|
+
else if (file.language === 'python') {
|
|
153
|
+
extractPythonSymbols(rootNode, file, symbols, interfaces, imports);
|
|
154
|
+
}
|
|
155
|
+
else if (file.language === 'c' || file.language === 'cpp' || file.language === 'csharp') {
|
|
156
|
+
extractCFamilySymbols(rootNode, file, symbols, interfaces, imports);
|
|
157
|
+
}
|
|
158
|
+
// THE FIX. Calls used to be pulled only from bodies that were both
|
|
159
|
+
// exported and a top-level `function_declaration`, which excluded
|
|
160
|
+
// every class method, every arrow, every non-exported helper and
|
|
161
|
+
// every nested function — the majority of real code. Now one member
|
|
162
|
+
// walk decides what a callable body is, and both the fact surface
|
|
163
|
+
// and the call surface are driven from it.
|
|
164
|
+
const callNodeTypes = callNodeTypesFor(file.language);
|
|
165
|
+
let callsTruncated = false;
|
|
166
|
+
const facts = extractMembers(rootNode, file.language, (member, body) => {
|
|
167
|
+
// `caller` stays the BARE member name on purpose. CodeFlow's graph
|
|
168
|
+
// resolves a call edge by matching this against the symbol roster,
|
|
169
|
+
// and qualifying it (`Widget.render`) would leave every edge from a
|
|
170
|
+
// method unresolvable against a symbol recorded as `render`. The
|
|
171
|
+
// owner is not lost — `members[].owner` carries it for the
|
|
172
|
+
// validation engine, which is the consumer that needs the
|
|
173
|
+
// distinction between two same-named methods.
|
|
174
|
+
callsTruncated = extractCallsFromBody(member.name, body, symbols, calls, maxCalls, callNodeTypes) || callsTruncated;
|
|
175
|
+
});
|
|
176
|
+
// Class members were absent from `symbols` entirely in this parser —
|
|
177
|
+
// a class read as one opaque symbol with no methods. Add them, since
|
|
178
|
+
// a graph that cannot name a method cannot resolve a call to it.
|
|
179
|
+
appendMemberSymbols(facts.members, symbols);
|
|
180
|
+
factsByPath.set(file.path, facts);
|
|
181
|
+
referenceInputs.push({
|
|
182
|
+
path: file.path,
|
|
183
|
+
exportedNames: exportedNamesOf(symbols, interfaces),
|
|
184
|
+
identifierCounts: countIdentifiers(rootNode),
|
|
185
|
+
});
|
|
186
|
+
const hasContent = symbols.length > 0 || interfaces.length > 0;
|
|
187
|
+
const parseStatus = hasContent ? 'parsed' : 'legitimately_empty';
|
|
188
|
+
results.push({
|
|
189
|
+
path: file.path,
|
|
190
|
+
language: file.language,
|
|
191
|
+
symbols,
|
|
192
|
+
interfaces,
|
|
193
|
+
calls,
|
|
194
|
+
imports,
|
|
195
|
+
parse_status: parseStatus,
|
|
196
|
+
calls_truncated: callsTruncated,
|
|
197
|
+
members: facts.members,
|
|
198
|
+
units: facts.units,
|
|
199
|
+
references: [],
|
|
200
|
+
profile_complete: hasLanguageProfile(file.language),
|
|
201
|
+
});
|
|
202
|
+
tree.delete?.();
|
|
203
|
+
parser.delete?.();
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
results.push(emptyResult(file, 'parse_error'));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// Batch passes. Both are deliberately AFTER the per-file loop: an
|
|
210
|
+
// override's base class and a symbol's callers live in other files, and
|
|
211
|
+
// resolving them mid-loop would make a file's output depend on the order
|
|
212
|
+
// it happened to appear in — the exact non-determinism this parser must
|
|
213
|
+
// not have.
|
|
214
|
+
resolveOverrideShapes([...factsByPath.values()]);
|
|
215
|
+
const referencesByPath = buildReferenceGraph(referenceInputs);
|
|
216
|
+
for (const result of results) {
|
|
217
|
+
result.references = referencesByPath.get(result.path) ?? [];
|
|
218
|
+
}
|
|
219
|
+
return results;
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* The one shape an unparseable file takes.
|
|
225
|
+
*
|
|
226
|
+
* Written once because five hand-rolled copies is how `calls_truncated: false`
|
|
227
|
+
* ended up duplicated on the same object literal — every future field on
|
|
228
|
+
* `ParseFileResult` would otherwise need five identical edits, and a missed
|
|
229
|
+
* one is a type error at best and a silently absent surface at worst.
|
|
230
|
+
*/
|
|
231
|
+
function emptyResult(file, status) {
|
|
232
|
+
return {
|
|
233
|
+
path: file.path,
|
|
234
|
+
language: file.language,
|
|
235
|
+
symbols: [],
|
|
236
|
+
interfaces: [],
|
|
237
|
+
calls: [],
|
|
238
|
+
imports: [],
|
|
239
|
+
parse_status: status,
|
|
240
|
+
calls_truncated: false,
|
|
241
|
+
members: [],
|
|
242
|
+
units: [],
|
|
243
|
+
references: [],
|
|
244
|
+
profile_complete: hasLanguageProfile(file.language),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Add class members to the symbol roster.
|
|
249
|
+
*
|
|
250
|
+
* Only members that BELONG to a unit are added. A nested helper or a callback
|
|
251
|
+
* inside a function body is a real member for scoring purposes but is not a
|
|
252
|
+
* declaration anything outside the file can reference, and recording it as a
|
|
253
|
+
* symbol produces the "nine symbols named genId from one file" problem — a
|
|
254
|
+
* bigger index that is worse to search.
|
|
255
|
+
*/
|
|
256
|
+
function appendMemberSymbols(members, symbols) {
|
|
257
|
+
const seen = new Set(symbols.map(s => `${s.name}:${s.start_line}`));
|
|
258
|
+
for (const member of members) {
|
|
259
|
+
if (!member.owner)
|
|
260
|
+
continue;
|
|
261
|
+
const key = `${member.name}:${member.start_line}`;
|
|
262
|
+
if (seen.has(key))
|
|
263
|
+
continue;
|
|
264
|
+
seen.add(key);
|
|
265
|
+
symbols.push({
|
|
266
|
+
name: member.name,
|
|
267
|
+
kind: member.kind === 'function' ? 'method' : member.kind,
|
|
268
|
+
exported: member.exported,
|
|
269
|
+
start_line: member.start_line,
|
|
270
|
+
end_line: member.end_line,
|
|
271
|
+
return_type: member.return_type ?? undefined,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Name frequencies for an XML document, so its symbols participate in the same
|
|
277
|
+
* cross-file reference graph as code.
|
|
278
|
+
*
|
|
279
|
+
* A BPMN flow node referenced from another document (a call activity naming a
|
|
280
|
+
* process, a DMN decision cited by a rule task) is a genuine cross-file
|
|
281
|
+
* reference; excluding XML from the graph would report every process as a dead
|
|
282
|
+
* export.
|
|
283
|
+
*/
|
|
284
|
+
function countXmlIdentifiers(xml) {
|
|
285
|
+
const counts = new Map();
|
|
286
|
+
const bump = (name) => {
|
|
287
|
+
if (name)
|
|
288
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
289
|
+
};
|
|
290
|
+
for (const symbol of xml.symbols)
|
|
291
|
+
bump(symbol.name);
|
|
292
|
+
for (const call of xml.calls) {
|
|
293
|
+
bump(call.caller);
|
|
294
|
+
bump(call.callee);
|
|
295
|
+
}
|
|
296
|
+
return counts;
|
|
297
|
+
}
|
|
298
|
+
/** Names this file exposes to other files — the reference-graph subjects. */
|
|
299
|
+
function exportedNamesOf(symbols, interfaces) {
|
|
300
|
+
const names = new Set();
|
|
301
|
+
for (const symbol of symbols)
|
|
302
|
+
if (symbol.exported)
|
|
303
|
+
names.add(symbol.name);
|
|
304
|
+
for (const iface of interfaces)
|
|
305
|
+
if (iface.exported)
|
|
306
|
+
names.add(iface.name);
|
|
307
|
+
return [...names].sort();
|
|
308
|
+
}
|
|
309
|
+
// ── TS/JS extraction ──────────────────────────────────────────────────────────
|
|
310
|
+
function isExported(node) {
|
|
311
|
+
const parent = node.parent;
|
|
312
|
+
if (!parent)
|
|
313
|
+
return false;
|
|
314
|
+
return parent.type === 'export_statement';
|
|
315
|
+
}
|
|
316
|
+
function getDeclarationNode(node) {
|
|
317
|
+
if (node.type === 'export_statement') {
|
|
318
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
319
|
+
const child = node.namedChild(i);
|
|
320
|
+
if (child && child.type !== 'comment')
|
|
321
|
+
return child;
|
|
322
|
+
}
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
return node;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Top-level declarations for the coarse symbol roster.
|
|
329
|
+
*
|
|
330
|
+
* Deliberately still top-level-only: this produces the FILE's public shape.
|
|
331
|
+
* Members inside those declarations are produced by `extractMembers` and
|
|
332
|
+
* merged in by `appendMemberSymbols`, so the two concerns stay separable and
|
|
333
|
+
* neither has to know the other's traversal rules.
|
|
334
|
+
*/
|
|
335
|
+
function extractTsJsSymbols(rootNode, file, symbols, interfaces, imports) {
|
|
336
|
+
for (let i = 0; i < rootNode.namedChildCount; i++) {
|
|
337
|
+
const topNode = rootNode.namedChild(i);
|
|
338
|
+
if (!topNode)
|
|
339
|
+
continue;
|
|
340
|
+
const exported = topNode.type === 'export_statement';
|
|
341
|
+
const declNode = getDeclarationNode(topNode);
|
|
342
|
+
if (!declNode)
|
|
343
|
+
continue;
|
|
344
|
+
switch (declNode.type) {
|
|
345
|
+
case 'function_declaration': {
|
|
346
|
+
const nameNode = declNode.childForFieldName('name');
|
|
347
|
+
if (nameNode) {
|
|
348
|
+
const returnTypeNode = declNode.childForFieldName('return_type');
|
|
349
|
+
symbols.push({
|
|
350
|
+
name: nameNode.text,
|
|
351
|
+
kind: 'function',
|
|
352
|
+
exported,
|
|
353
|
+
start_line: declNode.startPosition.row + 1,
|
|
354
|
+
end_line: declNode.endPosition.row + 1,
|
|
355
|
+
return_type: returnTypeNode?.text?.replace(/^:\s*/, ''),
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
case 'class_declaration': {
|
|
361
|
+
const nameNode = declNode.childForFieldName('name');
|
|
362
|
+
if (nameNode) {
|
|
363
|
+
symbols.push({
|
|
364
|
+
name: nameNode.text,
|
|
365
|
+
kind: 'class',
|
|
366
|
+
exported,
|
|
367
|
+
start_line: declNode.startPosition.row + 1,
|
|
368
|
+
end_line: declNode.endPosition.row + 1,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
case 'lexical_declaration': {
|
|
374
|
+
for (let j = 0; j < declNode.namedChildCount; j++) {
|
|
375
|
+
const declarator = declNode.namedChild(j);
|
|
376
|
+
if (!declarator || declarator.type !== 'variable_declarator')
|
|
377
|
+
continue;
|
|
378
|
+
const nameNode = declarator.childForFieldName('name');
|
|
379
|
+
const valueNode = declarator.childForFieldName('value');
|
|
380
|
+
if (!nameNode)
|
|
381
|
+
continue;
|
|
382
|
+
let kind = 'const';
|
|
383
|
+
if (valueNode) {
|
|
384
|
+
const vt = valueNode.type;
|
|
385
|
+
if (vt === 'arrow_function' || vt === 'function_expression' || vt === 'function') {
|
|
386
|
+
kind = 'function';
|
|
387
|
+
}
|
|
388
|
+
else if (vt === 'class_expression' || vt === 'class') {
|
|
389
|
+
kind = 'class';
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
kind = 'variable';
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
symbols.push({
|
|
396
|
+
name: nameNode.text,
|
|
397
|
+
kind,
|
|
398
|
+
exported,
|
|
399
|
+
start_line: declNode.startPosition.row + 1,
|
|
400
|
+
end_line: declNode.endPosition.row + 1,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
case 'interface_declaration': {
|
|
406
|
+
const nameNode = declNode.childForFieldName('name');
|
|
407
|
+
if (nameNode) {
|
|
408
|
+
interfaces.push({
|
|
409
|
+
name: nameNode.text,
|
|
410
|
+
kind: 'interface',
|
|
411
|
+
exported,
|
|
412
|
+
start_line: declNode.startPosition.row + 1,
|
|
413
|
+
end_line: declNode.endPosition.row + 1,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
case 'type_alias_declaration': {
|
|
419
|
+
const nameNode = declNode.childForFieldName('name');
|
|
420
|
+
if (nameNode) {
|
|
421
|
+
interfaces.push({
|
|
422
|
+
name: nameNode.text,
|
|
423
|
+
kind: 'type_alias',
|
|
424
|
+
exported,
|
|
425
|
+
start_line: declNode.startPosition.row + 1,
|
|
426
|
+
end_line: declNode.endPosition.row + 1,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
extractTsJsImports(rootNode, imports);
|
|
434
|
+
}
|
|
435
|
+
function extractCallsFromBody(callerName, bodyNode, symbols, calls, maxCalls, callNodeTypes = ['call_expression']) {
|
|
436
|
+
const knownSymbols = new Set(symbols.map(s => s.name));
|
|
437
|
+
const callMap = new Map();
|
|
438
|
+
function walkForCalls(node, count) {
|
|
439
|
+
if (count >= maxCalls)
|
|
440
|
+
return count;
|
|
441
|
+
if (callNodeTypes.includes(node.type)) {
|
|
442
|
+
count++;
|
|
443
|
+
const fnNode = node.childForFieldName('function');
|
|
444
|
+
if (fnNode) {
|
|
445
|
+
const fnText = fnNode.text;
|
|
446
|
+
// Member access is spelled `.` in TS/JS/python/C#, but `->` and `::` in
|
|
447
|
+
// C/C++. Split on all three so a C++ `obj->method()` yields callee
|
|
448
|
+
// `method` with receiver `obj`, not one opaque `obj->method` callee.
|
|
449
|
+
const parts = fnText.split(/->|::|\./);
|
|
450
|
+
const calleeName = parts[parts.length - 1];
|
|
451
|
+
const receiver = parts.length > 1 ? parts.slice(0, -1).join('.') : undefined;
|
|
452
|
+
const argsNode = node.childForFieldName('arguments');
|
|
453
|
+
const arity = argsNode ? argsNode.namedChildCount : 0;
|
|
454
|
+
const callLine = node.startPosition.row + 1;
|
|
455
|
+
const resolved = knownSymbols.has(calleeName);
|
|
456
|
+
const resolution_confidence = resolved ? 0.9 : 0.0;
|
|
457
|
+
const aggKey = `${callerName}→${calleeName}`;
|
|
458
|
+
const existing = callMap.get(aggKey);
|
|
459
|
+
if (existing) {
|
|
460
|
+
existing.count = (existing.count ?? 1) + 1;
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
callMap.set(aggKey, {
|
|
464
|
+
caller: callerName,
|
|
465
|
+
callee: calleeName,
|
|
466
|
+
call_line: callLine,
|
|
467
|
+
receiver,
|
|
468
|
+
resolved,
|
|
469
|
+
resolution_confidence,
|
|
470
|
+
arity,
|
|
471
|
+
count: 1,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
477
|
+
const child = node.child(i);
|
|
478
|
+
if (child)
|
|
479
|
+
count = walkForCalls(child, count);
|
|
480
|
+
}
|
|
481
|
+
return count;
|
|
482
|
+
}
|
|
483
|
+
const visited = walkForCalls(bodyNode, 0);
|
|
484
|
+
for (const call of callMap.values()) {
|
|
485
|
+
calls.push(call);
|
|
486
|
+
}
|
|
487
|
+
return visited >= maxCalls;
|
|
488
|
+
}
|
|
489
|
+
function extractTsJsImports(rootNode, imports) {
|
|
490
|
+
for (let i = 0; i < rootNode.namedChildCount; i++) {
|
|
491
|
+
const node = rootNode.namedChild(i);
|
|
492
|
+
if (!node || node.type !== 'import_statement')
|
|
493
|
+
continue;
|
|
494
|
+
const sourceNode = node.childForFieldName('source');
|
|
495
|
+
if (!sourceNode)
|
|
496
|
+
continue;
|
|
497
|
+
const source = sourceNode.text.replace(/^['"]|['"]$/g, '');
|
|
498
|
+
const specifiers = [];
|
|
499
|
+
for (let j = 0; j < node.namedChildCount; j++) {
|
|
500
|
+
const child = node.namedChild(j);
|
|
501
|
+
if (!child)
|
|
502
|
+
continue;
|
|
503
|
+
if (child.type === 'import_clause') {
|
|
504
|
+
for (let k = 0; k < child.namedChildCount; k++) {
|
|
505
|
+
const clauseChild = child.namedChild(k);
|
|
506
|
+
if (!clauseChild)
|
|
507
|
+
continue;
|
|
508
|
+
if (clauseChild.type === 'identifier') {
|
|
509
|
+
specifiers.push(clauseChild.text);
|
|
510
|
+
}
|
|
511
|
+
else if (clauseChild.type === 'named_imports') {
|
|
512
|
+
for (let m = 0; m < clauseChild.namedChildCount; m++) {
|
|
513
|
+
const specNode = clauseChild.namedChild(m);
|
|
514
|
+
if (specNode && specNode.type === 'import_specifier') {
|
|
515
|
+
const nameNode = specNode.childForFieldName('name');
|
|
516
|
+
if (nameNode)
|
|
517
|
+
specifiers.push(nameNode.text);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
else if (clauseChild.type === 'namespace_import') {
|
|
522
|
+
const nameNode = clauseChild.namedChild(0);
|
|
523
|
+
if (nameNode)
|
|
524
|
+
specifiers.push(`* as ${nameNode.text}`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
imports.push({ source, specifiers, line: node.startPosition.row + 1 });
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
// ── Python extraction ─────────────────────────────────────────────────────────
|
|
533
|
+
function extractPythonSymbols(rootNode, file, symbols, interfaces, imports) {
|
|
534
|
+
// RECURSIVE, deliberately. This walk was previously a single pass over
|
|
535
|
+
// rootNode.namedChild(i) — top level only — so every method inside a class
|
|
536
|
+
// was invisible: not a symbol, and therefore not a possible caller either.
|
|
537
|
+
function walk(node) {
|
|
538
|
+
switch (node.type) {
|
|
539
|
+
case 'function_definition': {
|
|
540
|
+
const nameNode = node.childForFieldName('name');
|
|
541
|
+
if (nameNode) {
|
|
542
|
+
// Python exported = not underscore-prefixed (per assignment)
|
|
543
|
+
const name = nameNode.text;
|
|
544
|
+
symbols.push({
|
|
545
|
+
name,
|
|
546
|
+
kind: 'function',
|
|
547
|
+
exported: !name.startsWith('_'),
|
|
548
|
+
start_line: node.startPosition.row + 1,
|
|
549
|
+
end_line: node.endPosition.row + 1,
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
case 'class_definition': {
|
|
555
|
+
const nameNode = node.childForFieldName('name');
|
|
556
|
+
if (nameNode) {
|
|
557
|
+
const name = nameNode.text;
|
|
558
|
+
symbols.push({
|
|
559
|
+
name,
|
|
560
|
+
kind: 'class',
|
|
561
|
+
exported: !name.startsWith('_'),
|
|
562
|
+
start_line: node.startPosition.row + 1,
|
|
563
|
+
end_line: node.endPosition.row + 1,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
case 'expression_statement': {
|
|
569
|
+
const child = node.namedChild(0);
|
|
570
|
+
if (child && child.type === 'assignment') {
|
|
571
|
+
const leftNode = child.childForFieldName('left');
|
|
572
|
+
if (leftNode && leftNode.type === 'identifier') {
|
|
573
|
+
const name = leftNode.text;
|
|
574
|
+
symbols.push({
|
|
575
|
+
name,
|
|
576
|
+
kind: 'variable',
|
|
577
|
+
exported: !name.startsWith('_'),
|
|
578
|
+
start_line: node.startPosition.row + 1,
|
|
579
|
+
end_line: node.endPosition.row + 1,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
case 'import_statement':
|
|
586
|
+
case 'import_from_statement': {
|
|
587
|
+
extractPythonImport(node, imports);
|
|
588
|
+
return; // import internals hold no symbols worth descending into
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
592
|
+
const child = node.namedChild(i);
|
|
593
|
+
if (child)
|
|
594
|
+
walk(child);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
walk(rootNode);
|
|
598
|
+
}
|
|
599
|
+
function extractPythonImport(node, imports) {
|
|
600
|
+
if (node.type === 'import_statement') {
|
|
601
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
602
|
+
const child = node.namedChild(i);
|
|
603
|
+
if (child && (child.type === 'dotted_name' || child.type === 'aliased_import')) {
|
|
604
|
+
const name = child.type === 'aliased_import'
|
|
605
|
+
? (child.childForFieldName('name')?.text ?? child.text)
|
|
606
|
+
: child.text;
|
|
607
|
+
imports.push({
|
|
608
|
+
source: name,
|
|
609
|
+
specifiers: [],
|
|
610
|
+
line: node.startPosition.row + 1,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
else if (node.type === 'import_from_statement') {
|
|
616
|
+
const moduleNode = node.childForFieldName('module_name');
|
|
617
|
+
const source = moduleNode?.text ?? '';
|
|
618
|
+
const specifiers = [];
|
|
619
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
620
|
+
const child = node.namedChild(i);
|
|
621
|
+
if (child && child.type === 'dotted_name' && child !== moduleNode) {
|
|
622
|
+
specifiers.push(child.text);
|
|
623
|
+
}
|
|
624
|
+
else if (child && child.type === 'aliased_import') {
|
|
625
|
+
const nameNode = child.childForFieldName('name');
|
|
626
|
+
if (nameNode)
|
|
627
|
+
specifiers.push(nameNode.text);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
imports.push({ source, specifiers, line: node.startPosition.row + 1 });
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
// ── C, C++, and C# extraction ───────────────────────────────────────────────
|
|
634
|
+
function extractCFamilySymbols(rootNode, file, symbols, interfaces, imports) {
|
|
635
|
+
const seenSymbols = new Set();
|
|
636
|
+
const seenInterfaces = new Set();
|
|
637
|
+
function addSymbol(node, kind) {
|
|
638
|
+
const name = getCFamilyName(node);
|
|
639
|
+
if (!name || seenSymbols.has(`${kind}:${name}:${node.startIndex}`))
|
|
640
|
+
return;
|
|
641
|
+
seenSymbols.add(`${kind}:${name}:${node.startIndex}`);
|
|
642
|
+
symbols.push({
|
|
643
|
+
name,
|
|
644
|
+
kind,
|
|
645
|
+
exported: isCFamilyExported(node, file.language),
|
|
646
|
+
start_line: node.startPosition.row + 1,
|
|
647
|
+
end_line: node.endPosition.row + 1,
|
|
648
|
+
signature: getCFamilySignature(node),
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
function addInterface(node, kind) {
|
|
652
|
+
const name = getCFamilyName(node);
|
|
653
|
+
if (!name || seenInterfaces.has(`${kind}:${name}:${node.startIndex}`))
|
|
654
|
+
return;
|
|
655
|
+
seenInterfaces.add(`${kind}:${name}:${node.startIndex}`);
|
|
656
|
+
interfaces.push({
|
|
657
|
+
name,
|
|
658
|
+
kind,
|
|
659
|
+
exported: isCFamilyExported(node, file.language),
|
|
660
|
+
start_line: node.startPosition.row + 1,
|
|
661
|
+
end_line: node.endPosition.row + 1,
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
function walk(node) {
|
|
665
|
+
switch (node.type) {
|
|
666
|
+
case 'function_definition':
|
|
667
|
+
case 'function_declaration':
|
|
668
|
+
case 'method_declaration':
|
|
669
|
+
case 'constructor_declaration': {
|
|
670
|
+
addSymbol(node, 'function');
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
case 'class_specifier':
|
|
674
|
+
case 'class_declaration':
|
|
675
|
+
case 'struct_specifier':
|
|
676
|
+
case 'struct_declaration':
|
|
677
|
+
addSymbol(node, 'class');
|
|
678
|
+
break;
|
|
679
|
+
case 'enum_specifier':
|
|
680
|
+
case 'enum_declaration':
|
|
681
|
+
addSymbol(node, 'enum');
|
|
682
|
+
break;
|
|
683
|
+
case 'interface_declaration':
|
|
684
|
+
addInterface(node, 'interface');
|
|
685
|
+
break;
|
|
686
|
+
case 'preproc_include':
|
|
687
|
+
addCFamilyInclude(node, imports);
|
|
688
|
+
break;
|
|
689
|
+
case 'using_directive':
|
|
690
|
+
addCSharpUsing(node, imports);
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
694
|
+
const child = node.namedChild(i);
|
|
695
|
+
if (child)
|
|
696
|
+
walk(child);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
walk(rootNode);
|
|
700
|
+
}
|
|
701
|
+
function getCFamilyName(node) {
|
|
702
|
+
const named = node.childForFieldName('name');
|
|
703
|
+
if (named?.text)
|
|
704
|
+
return named.text;
|
|
705
|
+
const declarator = node.childForFieldName('declarator');
|
|
706
|
+
if (declarator) {
|
|
707
|
+
const name = findCFamilyIdentifier(declarator);
|
|
708
|
+
if (name)
|
|
709
|
+
return name;
|
|
710
|
+
}
|
|
711
|
+
return findCFamilyIdentifier(node);
|
|
712
|
+
}
|
|
713
|
+
function findCFamilyIdentifier(node) {
|
|
714
|
+
if (node.type === 'identifier' || node.type === 'type_identifier')
|
|
715
|
+
return node.text;
|
|
716
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
717
|
+
const child = node.namedChild(i);
|
|
718
|
+
const name = child ? findCFamilyIdentifier(child) : undefined;
|
|
719
|
+
if (name)
|
|
720
|
+
return name;
|
|
721
|
+
}
|
|
722
|
+
return undefined;
|
|
723
|
+
}
|
|
724
|
+
function getCFamilySignature(node) {
|
|
725
|
+
const rawText = String(node.text ?? '').trim();
|
|
726
|
+
if (!rawText)
|
|
727
|
+
return undefined;
|
|
728
|
+
const bodyNode = node.childForFieldName('body');
|
|
729
|
+
let signature = bodyNode?.startIndex > node.startIndex
|
|
730
|
+
? rawText.slice(0, bodyNode.startIndex - node.startIndex).trim()
|
|
731
|
+
: rawText;
|
|
732
|
+
if (!bodyNode) {
|
|
733
|
+
const bodyStart = signature.indexOf('{');
|
|
734
|
+
if (bodyStart >= 0)
|
|
735
|
+
signature = signature.slice(0, bodyStart).trim();
|
|
736
|
+
}
|
|
737
|
+
signature = signature
|
|
738
|
+
.replace(/\s+/g, ' ')
|
|
739
|
+
.replace(/[;{]\s*$/, '')
|
|
740
|
+
.trim();
|
|
741
|
+
return signature.length > 300 ? `${signature.slice(0, 297)}...` : signature;
|
|
742
|
+
}
|
|
743
|
+
function isCFamilyExported(node, language) {
|
|
744
|
+
if (language !== 'csharp')
|
|
745
|
+
return true;
|
|
746
|
+
return /\bpublic\b/.test(node.text);
|
|
747
|
+
}
|
|
748
|
+
function addCFamilyInclude(node, imports) {
|
|
749
|
+
const match = node.text.match(/^\s*#\s*include\s*[<"]([^>"]+)[>"]/);
|
|
750
|
+
if (match)
|
|
751
|
+
imports.push({ source: match[1], specifiers: [], line: node.startPosition.row + 1 });
|
|
752
|
+
}
|
|
753
|
+
function addCSharpUsing(node, imports) {
|
|
754
|
+
const name = node.childForFieldName('name')?.text
|
|
755
|
+
?? node.text.replace(/^\s*using\s+/, '').replace(/;\s*$/, '').trim();
|
|
756
|
+
if (name)
|
|
757
|
+
imports.push({ source: name, specifiers: [], line: node.startPosition.row + 1 });
|
|
758
|
+
}
|
|
759
|
+
//# sourceMappingURL=nativeParser.js.map
|