legacyver 2.1.0 → 2.1.2
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/.agent/skills/openspec-apply-change/SKILL.md +156 -0
- package/.agent/skills/openspec-archive-change/SKILL.md +114 -0
- package/.agent/skills/openspec-bulk-archive-change/SKILL.md +246 -0
- package/.agent/skills/openspec-continue-change/SKILL.md +118 -0
- package/.agent/skills/openspec-explore/SKILL.md +290 -0
- package/.agent/skills/openspec-ff-change/SKILL.md +101 -0
- package/.agent/skills/openspec-new-change/SKILL.md +74 -0
- package/.agent/skills/openspec-onboard/SKILL.md +529 -0
- package/.agent/skills/openspec-sync-specs/SKILL.md +138 -0
- package/.agent/skills/openspec-verify-change/SKILL.md +168 -0
- package/.agent/workflows/opsx-apply.md +149 -0
- package/.agent/workflows/opsx-archive.md +154 -0
- package/.agent/workflows/opsx-bulk-archive.md +239 -0
- package/.agent/workflows/opsx-continue.md +111 -0
- package/.agent/workflows/opsx-explore.md +171 -0
- package/.agent/workflows/opsx-ff.md +91 -0
- package/.agent/workflows/opsx-new.md +66 -0
- package/.agent/workflows/opsx-onboard.md +522 -0
- package/.agent/workflows/opsx-sync.md +131 -0
- package/.agent/workflows/opsx-verify.md +161 -0
- package/.github/prompts/opsx-apply.prompt.md +149 -0
- package/.github/prompts/opsx-archive.prompt.md +154 -0
- package/.github/prompts/opsx-bulk-archive.prompt.md +239 -0
- package/.github/prompts/opsx-continue.prompt.md +111 -0
- package/.github/prompts/opsx-explore.prompt.md +171 -0
- package/.github/prompts/opsx-ff.prompt.md +91 -0
- package/.github/prompts/opsx-new.prompt.md +66 -0
- package/.github/prompts/opsx-onboard.prompt.md +522 -0
- package/.github/prompts/opsx-sync.prompt.md +131 -0
- package/.github/prompts/opsx-verify.prompt.md +161 -0
- package/.github/skills/openspec-apply-change/SKILL.md +156 -0
- package/.github/skills/openspec-archive-change/SKILL.md +114 -0
- package/.github/skills/openspec-bulk-archive-change/SKILL.md +246 -0
- package/.github/skills/openspec-continue-change/SKILL.md +118 -0
- package/.github/skills/openspec-explore/SKILL.md +290 -0
- package/.github/skills/openspec-ff-change/SKILL.md +101 -0
- package/.github/skills/openspec-new-change/SKILL.md +74 -0
- package/.github/skills/openspec-onboard/SKILL.md +529 -0
- package/.github/skills/openspec-sync-specs/SKILL.md +138 -0
- package/.github/skills/openspec-verify-change/SKILL.md +168 -0
- package/.legacyverignore.example +43 -0
- package/.legacyverrc +7 -0
- package/.opencode/command/opsx-apply.md +149 -0
- package/.opencode/command/opsx-archive.md +154 -0
- package/.opencode/command/opsx-bulk-archive.md +239 -0
- package/.opencode/command/opsx-continue.md +111 -0
- package/.opencode/command/opsx-explore.md +171 -0
- package/.opencode/command/opsx-ff.md +91 -0
- package/.opencode/command/opsx-new.md +66 -0
- package/.opencode/command/opsx-onboard.md +522 -0
- package/.opencode/command/opsx-sync.md +131 -0
- package/.opencode/command/opsx-verify.md +161 -0
- package/.opencode/skills/openspec-apply-change/SKILL.md +156 -0
- package/.opencode/skills/openspec-archive-change/SKILL.md +114 -0
- package/.opencode/skills/openspec-bulk-archive-change/SKILL.md +246 -0
- package/.opencode/skills/openspec-continue-change/SKILL.md +118 -0
- package/.opencode/skills/openspec-explore/SKILL.md +290 -0
- package/.opencode/skills/openspec-ff-change/SKILL.md +101 -0
- package/.opencode/skills/openspec-new-change/SKILL.md +74 -0
- package/.opencode/skills/openspec-onboard/SKILL.md +529 -0
- package/.opencode/skills/openspec-sync-specs/SKILL.md +138 -0
- package/.opencode/skills/openspec-verify-change/SKILL.md +168 -0
- package/LICENSE +1 -1
- package/README.md +128 -83
- package/bin/legacyver.js +48 -25
- package/legacyver-docs/SUMMARY.md +3 -0
- package/legacyver-docs/components.md +57 -0
- package/legacyver-docs/index.md +15 -0
- package/nul +2 -0
- package/package.json +23 -25
- package/src/cache/hash.js +9 -10
- package/src/cache/index.js +43 -65
- package/src/cli/commands/analyze.js +212 -190
- package/src/cli/commands/cache.js +15 -35
- package/src/cli/commands/init.js +63 -107
- package/src/cli/commands/providers.js +56 -81
- package/src/cli/commands/version.js +7 -10
- package/src/cli/ui.js +58 -77
- package/src/crawler/filters.js +41 -40
- package/src/crawler/index.js +52 -36
- package/src/crawler/manifest.js +31 -43
- package/src/crawler/walk.js +32 -38
- package/src/llm/chunker.js +34 -56
- package/src/llm/cost-estimator.js +68 -51
- package/src/llm/free-model.js +67 -0
- package/src/llm/index.js +22 -43
- package/src/llm/prompts.js +45 -33
- package/src/llm/providers/gemini.js +94 -0
- package/src/llm/providers/groq.js +55 -40
- package/src/llm/providers/ollama.js +38 -65
- package/src/llm/providers/openrouter.js +67 -0
- package/src/llm/queue.js +59 -88
- package/src/llm/re-prompter.js +41 -0
- package/src/llm/validator.js +72 -0
- package/src/parser/ast/generic.js +45 -222
- package/src/parser/ast/go.js +86 -205
- package/src/parser/ast/java.js +76 -146
- package/src/parser/ast/javascript.js +173 -241
- package/src/parser/ast/laravel/blade.js +56 -0
- package/src/parser/ast/laravel/classifier.js +30 -0
- package/src/parser/ast/laravel/controller.js +35 -0
- package/src/parser/ast/laravel/index.js +54 -0
- package/src/parser/ast/laravel/model.js +41 -0
- package/src/parser/ast/laravel/provider.js +28 -0
- package/src/parser/ast/laravel/routes.js +45 -0
- package/src/parser/ast/php.js +129 -0
- package/src/parser/ast/python.js +76 -199
- package/src/parser/ast/typescript.js +10 -244
- package/src/parser/body-extractor.js +40 -0
- package/src/parser/call-graph.js +50 -67
- package/src/parser/complexity-scorer.js +59 -0
- package/src/parser/index.js +61 -86
- package/src/parser/pattern-detector.js +71 -0
- package/src/parser/pkg-builder.js +36 -83
- package/src/renderer/html.js +63 -135
- package/src/renderer/index.js +23 -35
- package/src/renderer/json.js +17 -35
- package/src/renderer/markdown.js +83 -117
- package/src/utils/config.js +52 -53
- package/src/utils/errors.js +26 -41
- package/src/utils/logger.js +32 -53
- package/src/cli/flags.js +0 -87
- package/src/llm/providers/anthropic.js +0 -57
- package/src/llm/providers/google.js +0 -65
- package/src/llm/providers/openai.js +0 -52
- package/src/parser/ast/tree-sitter-init.js +0 -80
|
@@ -1,250 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
* TypeScript AST parser using web-tree-sitter.
|
|
3
|
-
* Extends JS parser with TypeScript grammar and type annotations.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { initTreeSitter, loadLanguage, createParser } from './tree-sitter-init.js';
|
|
7
|
-
import { parseGeneric } from './generic.js';
|
|
8
|
-
|
|
9
|
-
let tsParser = null;
|
|
10
|
-
let tsInitialized = false;
|
|
11
|
-
|
|
12
|
-
async function ensureParser() {
|
|
13
|
-
if (tsInitialized) return tsParser;
|
|
14
|
-
tsInitialized = true;
|
|
15
|
-
|
|
16
|
-
const Parser = await initTreeSitter();
|
|
17
|
-
if (!Parser) return null;
|
|
18
|
-
|
|
19
|
-
const language = await loadLanguage('typescript');
|
|
20
|
-
if (!language) return null;
|
|
21
|
-
|
|
22
|
-
tsParser = createParser(language);
|
|
23
|
-
return tsParser;
|
|
24
|
-
}
|
|
1
|
+
'use strict';
|
|
25
2
|
|
|
26
3
|
/**
|
|
27
|
-
*
|
|
28
|
-
* @param {string} source - Raw file content
|
|
29
|
-
* @param {string} relativePath - Relative file path
|
|
30
|
-
* @returns {Promise<import('../../parser/index.js').FileFacts>}
|
|
4
|
+
* TypeScript parser — extends JS parser, handles type annotations.
|
|
31
5
|
*/
|
|
32
|
-
|
|
33
|
-
const parser = await ensureParser();
|
|
34
|
-
|
|
35
|
-
if (!parser) {
|
|
36
|
-
return parseGeneric(source, relativePath, 'typescript');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const tree = parser.parse(source);
|
|
40
|
-
const rootNode = tree.rootNode;
|
|
41
|
-
|
|
42
|
-
const functions = [];
|
|
43
|
-
const classes = [];
|
|
44
|
-
const imports = [];
|
|
45
|
-
const exports = [];
|
|
46
|
-
const lines = source.split('\n');
|
|
47
|
-
const linesOfCode = lines.filter((l) => l.trim().length > 0).length;
|
|
48
|
-
|
|
49
|
-
walkNode(rootNode, { functions, classes, imports, exports, source, lines });
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
relativePath,
|
|
53
|
-
language: 'typescript',
|
|
54
|
-
linesOfCode,
|
|
55
|
-
parserType: 'tree-sitter',
|
|
56
|
-
functions,
|
|
57
|
-
classes,
|
|
58
|
-
imports,
|
|
59
|
-
exports: exports.map((e) => e.name || e),
|
|
60
|
-
callsTo: [],
|
|
61
|
-
calledBy: [],
|
|
62
|
-
hash: '',
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function walkNode(node, ctx) {
|
|
67
|
-
switch (node.type) {
|
|
68
|
-
case 'function_declaration':
|
|
69
|
-
extractFunction(node, ctx, false);
|
|
70
|
-
break;
|
|
71
|
-
case 'export_statement':
|
|
72
|
-
handleExport(node, ctx);
|
|
73
|
-
break;
|
|
74
|
-
case 'import_statement':
|
|
75
|
-
extractImport(node, ctx);
|
|
76
|
-
break;
|
|
77
|
-
case 'class_declaration':
|
|
78
|
-
extractClass(node, ctx);
|
|
79
|
-
break;
|
|
80
|
-
case 'lexical_declaration':
|
|
81
|
-
case 'variable_declaration':
|
|
82
|
-
extractVariableDeclaration(node, ctx);
|
|
83
|
-
break;
|
|
84
|
-
case 'interface_declaration':
|
|
85
|
-
case 'type_alias_declaration':
|
|
86
|
-
// Track type exports
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
91
|
-
walkNode(node.child(i), ctx);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function extractFunction(node, ctx, isExported) {
|
|
96
|
-
const nameNode = node.childForFieldName('name');
|
|
97
|
-
const paramsNode = node.childForFieldName('parameters');
|
|
98
|
-
const returnTypeNode = node.childForFieldName('return_type');
|
|
99
|
-
const isAsync = node.text.startsWith('async');
|
|
6
|
+
const { parse: parseJS } = require('./javascript');
|
|
100
7
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
isAsync,
|
|
108
|
-
lineStart: node.startPosition.row + 1,
|
|
109
|
-
lineEnd: node.endPosition.row + 1,
|
|
110
|
-
calls: [],
|
|
111
|
-
});
|
|
112
|
-
}
|
|
8
|
+
function parse(sourceText, relativePath) {
|
|
9
|
+
const result = parseJS(sourceText, relativePath);
|
|
10
|
+
result.language = 'typescript';
|
|
11
|
+
// TypeScript annotations are partially handled in the JS parser already
|
|
12
|
+
// (the regex catches `: Type` patterns in params and return types)
|
|
13
|
+
return result;
|
|
113
14
|
}
|
|
114
15
|
|
|
115
|
-
|
|
116
|
-
const nameNode = node.childForFieldName('name');
|
|
117
|
-
const bodyNode = node.childForFieldName('body');
|
|
118
|
-
|
|
119
|
-
let superclass = null;
|
|
120
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
121
|
-
const child = node.child(i);
|
|
122
|
-
if (child.type === 'class_heritage') {
|
|
123
|
-
const extendsClause = child.text;
|
|
124
|
-
const match = extendsClause.match(/extends\s+(\w+)/);
|
|
125
|
-
if (match) superclass = match[1];
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (nameNode) {
|
|
130
|
-
const methods = [];
|
|
131
|
-
if (bodyNode) {
|
|
132
|
-
for (let i = 0; i < bodyNode.childCount; i++) {
|
|
133
|
-
const child = bodyNode.child(i);
|
|
134
|
-
if (child.type === 'method_definition' || child.type === 'public_field_definition') {
|
|
135
|
-
const methodName = child.childForFieldName('name');
|
|
136
|
-
if (methodName) methods.push(methodName.text);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
ctx.classes.push({
|
|
142
|
-
name: nameNode.text,
|
|
143
|
-
methods,
|
|
144
|
-
extends: superclass,
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function extractImport(node, ctx) {
|
|
150
|
-
const sourceNode = node.childForFieldName('source');
|
|
151
|
-
if (!sourceNode) return;
|
|
152
|
-
|
|
153
|
-
const module = sourceNode.text.replace(/['"]/g, '');
|
|
154
|
-
const specifiers = [];
|
|
155
|
-
|
|
156
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
157
|
-
const child = node.child(i);
|
|
158
|
-
if (child.type === 'import_clause') {
|
|
159
|
-
for (let j = 0; j < child.childCount; j++) {
|
|
160
|
-
const clauseChild = child.child(j);
|
|
161
|
-
if (clauseChild.type === 'identifier') {
|
|
162
|
-
specifiers.push(clauseChild.text);
|
|
163
|
-
} else if (clauseChild.type === 'named_imports') {
|
|
164
|
-
for (let k = 0; k < clauseChild.childCount; k++) {
|
|
165
|
-
const importSpec = clauseChild.child(k);
|
|
166
|
-
if (importSpec.type === 'import_specifier') {
|
|
167
|
-
const name = importSpec.childForFieldName('name');
|
|
168
|
-
if (name) specifiers.push(name.text);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
ctx.imports.push({ module, specifiers });
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function handleExport(node, ctx) {
|
|
180
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
181
|
-
const child = node.child(i);
|
|
182
|
-
if (child.type === 'function_declaration') {
|
|
183
|
-
extractFunction(child, ctx, true);
|
|
184
|
-
const nameNode = child.childForFieldName('name');
|
|
185
|
-
if (nameNode) ctx.exports.push({ name: nameNode.text });
|
|
186
|
-
} else if (child.type === 'class_declaration') {
|
|
187
|
-
extractClass(child, ctx);
|
|
188
|
-
const nameNode = child.childForFieldName('name');
|
|
189
|
-
if (nameNode) ctx.exports.push({ name: nameNode.text });
|
|
190
|
-
} else if (child.type === 'lexical_declaration') {
|
|
191
|
-
extractVariableDeclaration(child, ctx);
|
|
192
|
-
for (let j = 0; j < child.childCount; j++) {
|
|
193
|
-
const decl = child.child(j);
|
|
194
|
-
if (decl.type === 'variable_declarator') {
|
|
195
|
-
const nameNode = decl.childForFieldName('name');
|
|
196
|
-
if (nameNode) ctx.exports.push({ name: nameNode.text });
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
} else if (child.type === 'interface_declaration' || child.type === 'type_alias_declaration') {
|
|
200
|
-
const nameNode = child.childForFieldName('name');
|
|
201
|
-
if (nameNode) ctx.exports.push({ name: nameNode.text });
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function extractVariableDeclaration(node, ctx) {
|
|
207
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
208
|
-
const child = node.child(i);
|
|
209
|
-
if (child.type === 'variable_declarator') {
|
|
210
|
-
const nameNode = child.childForFieldName('name');
|
|
211
|
-
const valueNode = child.childForFieldName('value');
|
|
212
|
-
if (nameNode && valueNode) {
|
|
213
|
-
if (valueNode.type === 'arrow_function' || valueNode.type === 'function') {
|
|
214
|
-
const paramsNode = valueNode.childForFieldName('parameters');
|
|
215
|
-
const returnTypeNode = valueNode.childForFieldName('return_type');
|
|
216
|
-
const isAsync = valueNode.text.startsWith('async');
|
|
217
|
-
ctx.functions.push({
|
|
218
|
-
name: nameNode.text,
|
|
219
|
-
params: extractTypedParams(paramsNode),
|
|
220
|
-
returnType: returnTypeNode ? returnTypeNode.text.replace(/^:\s*/, '') : null,
|
|
221
|
-
isExported: false,
|
|
222
|
-
isAsync,
|
|
223
|
-
lineStart: node.startPosition.row + 1,
|
|
224
|
-
lineEnd: node.endPosition.row + 1,
|
|
225
|
-
calls: [],
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function extractTypedParams(paramsNode) {
|
|
234
|
-
if (!paramsNode) return [];
|
|
235
|
-
const params = [];
|
|
236
|
-
for (let i = 0; i < paramsNode.childCount; i++) {
|
|
237
|
-
const child = paramsNode.child(i);
|
|
238
|
-
if (child.type === 'required_parameter' || child.type === 'optional_parameter') {
|
|
239
|
-
const nameNode = child.childForFieldName('pattern') || child.childForFieldName('name');
|
|
240
|
-
const typeNode = child.childForFieldName('type');
|
|
241
|
-
params.push({
|
|
242
|
-
name: nameNode ? nameNode.text : child.text,
|
|
243
|
-
type: typeNode ? typeNode.text.replace(/^:\s*/, '') : null,
|
|
244
|
-
});
|
|
245
|
-
} else if (child.type === 'identifier') {
|
|
246
|
-
params.push({ name: child.text, type: null });
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
return params;
|
|
250
|
-
}
|
|
16
|
+
module.exports = { parse };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const MAX_BODY_LINES = 60;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Extract body snippet from source text given line start/end positions.
|
|
7
|
+
* Returns null for simple functions (complexityScore <= 3).
|
|
8
|
+
* @param {string} sourceText
|
|
9
|
+
* @param {number} lineStart 1-indexed
|
|
10
|
+
* @param {number} lineEnd 1-indexed
|
|
11
|
+
* @param {number} complexityScore
|
|
12
|
+
* @returns {{ bodySnippet: string|null, bodySnippetTruncated: boolean }}
|
|
13
|
+
*/
|
|
14
|
+
function extractBodySnippet(sourceText, lineStart, lineEnd, complexityScore) {
|
|
15
|
+
if (complexityScore <= 3) {
|
|
16
|
+
return { bodySnippet: null, bodySnippetTruncated: false };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!sourceText || lineStart == null || lineEnd == null) {
|
|
20
|
+
return { bodySnippet: null, bodySnippetTruncated: false };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const lines = sourceText.split('\n');
|
|
24
|
+
const startIdx = Math.max(0, (lineStart || 1) - 1);
|
|
25
|
+
const endIdx = Math.min(lines.length, lineEnd || lines.length);
|
|
26
|
+
const bodyLines = lines.slice(startIdx, endIdx);
|
|
27
|
+
|
|
28
|
+
let truncated = false;
|
|
29
|
+
let snippet;
|
|
30
|
+
if (bodyLines.length > MAX_BODY_LINES) {
|
|
31
|
+
snippet = bodyLines.slice(0, MAX_BODY_LINES).join('\n');
|
|
32
|
+
truncated = true;
|
|
33
|
+
} else {
|
|
34
|
+
snippet = bodyLines.join('\n');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { bodySnippet: snippet, bodySnippetTruncated: truncated };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { extractBodySnippet };
|
package/src/parser/call-graph.js
CHANGED
|
@@ -1,86 +1,69 @@
|
|
|
1
|
-
|
|
2
|
-
* Call graph resolver.
|
|
3
|
-
* Post-processes all FileFacts:
|
|
4
|
-
* - Resolves relative import paths to file paths
|
|
5
|
-
* - Populates calledBy arrays on referenced files
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { dirname, join, normalize } from 'node:path';
|
|
1
|
+
'use strict';
|
|
9
2
|
|
|
10
3
|
/**
|
|
11
|
-
*
|
|
12
|
-
* @param {import('./index.js').FileFacts[]} allFacts
|
|
13
|
-
* @returns {import('./index.js').FileFacts[]} Facts with callsTo/calledBy populated
|
|
4
|
+
* Build cross-file call graph by resolving import paths.
|
|
14
5
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const pathMap = new Map();
|
|
6
|
+
function buildCallGraph(allFacts) {
|
|
7
|
+
const fileMap = new Map();
|
|
18
8
|
for (const facts of allFacts) {
|
|
19
|
-
|
|
20
|
-
// Also index without extension for resolution
|
|
21
|
-
const withoutExt = facts.relativePath.replace(/\.\w+$/, '');
|
|
22
|
-
pathMap.set(normalize(withoutExt), facts);
|
|
9
|
+
fileMap.set(facts.relativePath, facts);
|
|
23
10
|
}
|
|
24
11
|
|
|
25
|
-
// Resolve imports to file paths
|
|
26
12
|
for (const facts of allFacts) {
|
|
27
|
-
|
|
13
|
+
facts.callsTo = [];
|
|
14
|
+
facts.calledBy = [];
|
|
15
|
+
}
|
|
28
16
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
17
|
+
for (const facts of allFacts) {
|
|
18
|
+
for (const imp of (facts.imports || [])) {
|
|
19
|
+
const resolved = resolveImport(imp.module, facts.relativePath, fileMap);
|
|
20
|
+
if (resolved && !facts.callsTo.includes(resolved)) {
|
|
21
|
+
facts.callsTo.push(resolved);
|
|
22
|
+
const target = fileMap.get(resolved);
|
|
23
|
+
if (target && !target.calledBy.includes(facts.relativePath)) {
|
|
24
|
+
target.calledBy.push(facts.relativePath);
|
|
25
|
+
}
|
|
33
26
|
}
|
|
27
|
+
}
|
|
34
28
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
resolved + '.go',
|
|
50
|
-
join(resolved, 'index.js'),
|
|
51
|
-
join(resolved, 'index.ts'),
|
|
52
|
-
];
|
|
53
|
-
|
|
54
|
-
for (const candidate of candidates) {
|
|
55
|
-
const normalizedCandidate = normalize(candidate);
|
|
56
|
-
if (pathMap.has(normalizedCandidate)) {
|
|
57
|
-
callsTo.add(pathMap.get(normalizedCandidate).relativePath);
|
|
58
|
-
break;
|
|
29
|
+
// Laravel: resolve controller references from route laravelContext
|
|
30
|
+
if (facts.laravelContext && facts.laravelContext.type === 'route_file') {
|
|
31
|
+
for (const route of (facts.laravelContext.routes || [])) {
|
|
32
|
+
if (route.controller) {
|
|
33
|
+
// Try to find a matching controller file
|
|
34
|
+
for (const [relPath] of fileMap) {
|
|
35
|
+
if (relPath.includes(route.controller)) {
|
|
36
|
+
if (!facts.callsTo.includes(relPath)) facts.callsTo.push(relPath);
|
|
37
|
+
const target = fileMap.get(relPath);
|
|
38
|
+
if (target && !target.calledBy.includes(facts.relativePath)) {
|
|
39
|
+
target.calledBy.push(facts.relativePath);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
59
43
|
}
|
|
60
44
|
}
|
|
61
45
|
}
|
|
62
|
-
|
|
63
|
-
facts.callsTo = [...callsTo];
|
|
64
46
|
}
|
|
65
47
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
for (const facts of allFacts) {
|
|
69
|
-
calledByMap.set(facts.relativePath, new Set());
|
|
70
|
-
}
|
|
48
|
+
return allFacts;
|
|
49
|
+
}
|
|
71
50
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (calledByMap.has(target)) {
|
|
75
|
-
calledByMap.get(target).add(facts.relativePath);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
51
|
+
function resolveImport(modulePath, fromPath, fileMap) {
|
|
52
|
+
if (!modulePath.startsWith('.')) return null;
|
|
79
53
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
54
|
+
const fromDir = fromPath.includes('/') ? fromPath.split('/').slice(0, -1).join('/') : '';
|
|
55
|
+
const candidates = [
|
|
56
|
+
`${fromDir}/${modulePath}`,
|
|
57
|
+
`${fromDir}/${modulePath}.js`,
|
|
58
|
+
`${fromDir}/${modulePath}.ts`,
|
|
59
|
+
`${fromDir}/${modulePath}/index.js`,
|
|
60
|
+
`${fromDir}/${modulePath}/index.ts`,
|
|
61
|
+
].map(p => p.replace(/^\//, '').replace(/\/\//g, '/'));
|
|
84
62
|
|
|
85
|
-
|
|
63
|
+
for (const candidate of candidates) {
|
|
64
|
+
if (fileMap.has(candidate)) return candidate;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
86
67
|
}
|
|
68
|
+
|
|
69
|
+
module.exports = { buildCallGraph };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Complexity scorer — given function/method facts + body text,
|
|
5
|
+
* computes a complexityScore and classifies it.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const DOMAIN_PATTERNS = require('./pattern-detector');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Score a function based on its body text using heuristics.
|
|
12
|
+
* @param {string} bodyText raw source of the function body
|
|
13
|
+
* @returns {{ complexityScore: number, complexityClass: string, detectedPatterns: string[] }}
|
|
14
|
+
*/
|
|
15
|
+
function scoreComplexity(bodyText) {
|
|
16
|
+
if (!bodyText) {
|
|
17
|
+
return { complexityScore: 0, complexityClass: 'simple', detectedPatterns: [] };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let score = 0;
|
|
21
|
+
|
|
22
|
+
// Binary/ternary operators
|
|
23
|
+
const arithmeticOps = (bodyText.match(/[\+\-\*\/\%\*\*\?\?]/g) || []).length;
|
|
24
|
+
score += Math.min(arithmeticOps, 5); // cap to avoid noise
|
|
25
|
+
|
|
26
|
+
// Conditional branches
|
|
27
|
+
const ifCount = (bodyText.match(/\bif\s*\(/g) || []).length;
|
|
28
|
+
const elseIfCount = (bodyText.match(/\belse\s+if\s*\(/g) || []).length;
|
|
29
|
+
const switchCase = (bodyText.match(/\bcase\b/g) || []).length;
|
|
30
|
+
const ternary = (bodyText.match(/\?[^:]/g) || []).length;
|
|
31
|
+
score += ifCount + elseIfCount + switchCase + ternary;
|
|
32
|
+
|
|
33
|
+
// Loop constructs
|
|
34
|
+
const forCount = (bodyText.match(/\bfor\s*\(/g) || []).length;
|
|
35
|
+
const foreachCount = (bodyText.match(/\bforeach\s*\(/g) || []).length;
|
|
36
|
+
const whileCount = (bodyText.match(/\bwhile\s*\(/g) || []).length;
|
|
37
|
+
const doCount = (bodyText.match(/\bdo\s*\{/g) || []).length;
|
|
38
|
+
score += forCount + foreachCount + whileCount + doCount;
|
|
39
|
+
|
|
40
|
+
// Nesting depth beyond level 1
|
|
41
|
+
let depth = 0;
|
|
42
|
+
let maxDepth = 0;
|
|
43
|
+
for (const ch of bodyText) {
|
|
44
|
+
if (ch === '{') { depth++; if (depth > maxDepth) maxDepth = depth; }
|
|
45
|
+
else if (ch === '}') { depth--; }
|
|
46
|
+
}
|
|
47
|
+
if (maxDepth > 1) score += maxDepth - 1;
|
|
48
|
+
|
|
49
|
+
// Domain patterns
|
|
50
|
+
const detectedPatterns = DOMAIN_PATTERNS.detectPatterns(bodyText);
|
|
51
|
+
score += detectedPatterns.length * 2;
|
|
52
|
+
|
|
53
|
+
const complexityClass =
|
|
54
|
+
score <= 3 ? 'simple' : score <= 8 ? 'moderate' : 'complex';
|
|
55
|
+
|
|
56
|
+
return { complexityScore: score, complexityClass, detectedPatterns };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { scoreComplexity };
|
package/src/parser/index.js
CHANGED
|
@@ -1,106 +1,81 @@
|
|
|
1
|
-
|
|
2
|
-
* Language-aware parser dispatcher.
|
|
3
|
-
* Maps language string to correct AST parser.
|
|
4
|
-
* Falls back to generic.js on unsupported language or Tree-sitter parse failure.
|
|
5
|
-
*/
|
|
1
|
+
'use strict';
|
|
6
2
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import { parsePython } from './ast/python.js';
|
|
11
|
-
import { parseJava } from './ast/java.js';
|
|
12
|
-
import { parseGo } from './ast/go.js';
|
|
13
|
-
import { parseGeneric } from './ast/generic.js';
|
|
14
|
-
import { logger } from '../utils/logger.js';
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { readFileSync } = require('fs');
|
|
5
|
+
const logger = require('../utils/logger');
|
|
15
6
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
* @property {Array} classes
|
|
24
|
-
* @property {Array} imports
|
|
25
|
-
* @property {string[]} exports
|
|
26
|
-
* @property {string[]} callsTo
|
|
27
|
-
* @property {string[]} calledBy
|
|
28
|
-
* @property {string} hash
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
const LANGUAGE_PARSERS = {
|
|
32
|
-
javascript: parseJavaScript,
|
|
33
|
-
typescript: parseTypeScript,
|
|
34
|
-
python: parsePython,
|
|
35
|
-
java: parseJava,
|
|
36
|
-
go: parseGo,
|
|
7
|
+
const PARSERS = {
|
|
8
|
+
javascript: () => require('./ast/javascript'),
|
|
9
|
+
typescript: () => require('./ast/typescript'),
|
|
10
|
+
python: () => require('./ast/python'),
|
|
11
|
+
java: () => require('./ast/java'),
|
|
12
|
+
go: () => require('./ast/go'),
|
|
13
|
+
php: () => require('./ast/php'),
|
|
37
14
|
};
|
|
38
15
|
|
|
39
16
|
/**
|
|
40
|
-
*
|
|
41
|
-
* @param {import('../crawler/
|
|
42
|
-
* @param {
|
|
43
|
-
* @returns {
|
|
17
|
+
* Dispatch parsing to the appropriate language parser.
|
|
18
|
+
* @param {import('../crawler/index').FileManifest} fileManifest
|
|
19
|
+
* @param {Object} meta project metadata (includes framework)
|
|
20
|
+
* @returns {Object} FileFacts
|
|
44
21
|
*/
|
|
45
|
-
|
|
46
|
-
const {
|
|
22
|
+
function parseFile(fileManifest, meta) {
|
|
23
|
+
const { absolutePath, relativePath, language } = fileManifest;
|
|
47
24
|
|
|
48
|
-
let
|
|
25
|
+
let sourceText;
|
|
49
26
|
try {
|
|
50
|
-
|
|
51
|
-
} catch (
|
|
52
|
-
logger.warn(`
|
|
53
|
-
return
|
|
27
|
+
sourceText = readFileSync(absolutePath, 'utf8');
|
|
28
|
+
} catch (e) {
|
|
29
|
+
logger.warn(`Could not read file: ${relativePath}`);
|
|
30
|
+
return null;
|
|
54
31
|
}
|
|
55
32
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
33
|
+
let facts;
|
|
34
|
+
if (language && PARSERS[language]) {
|
|
35
|
+
try {
|
|
36
|
+
const parser = PARSERS[language]();
|
|
37
|
+
facts = parser.parse(sourceText, relativePath);
|
|
38
|
+
} catch (e) {
|
|
39
|
+
logger.warn(`AST parse failed for ${relativePath}: ${e.message}. Falling back to generic.`);
|
|
40
|
+
facts = require('./ast/generic').parse(sourceText, relativePath, language);
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
facts = require('./ast/generic').parse(sourceText, relativePath, language);
|
|
63
44
|
}
|
|
64
45
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
facts
|
|
73
|
-
return facts;
|
|
46
|
+
if (!facts) return null;
|
|
47
|
+
|
|
48
|
+
facts.hash = fileManifest.hash;
|
|
49
|
+
|
|
50
|
+
// Laravel enrichment
|
|
51
|
+
if (language === 'php' && meta && meta.framework === 'laravel') {
|
|
52
|
+
const { enrich } = require('./ast/laravel/index');
|
|
53
|
+
facts = enrich(sourceText, facts);
|
|
74
54
|
}
|
|
55
|
+
|
|
56
|
+
return facts;
|
|
75
57
|
}
|
|
76
58
|
|
|
77
59
|
/**
|
|
78
|
-
* Parse all files
|
|
79
|
-
* @param {import('../crawler/
|
|
80
|
-
* @param {
|
|
81
|
-
* @
|
|
60
|
+
* Parse all files and assemble the PKG.
|
|
61
|
+
* @param {import('../crawler/index').FileManifest[]} files
|
|
62
|
+
* @param {Object} meta
|
|
63
|
+
* @param {Object} config
|
|
64
|
+
* @returns {Object} PKG
|
|
82
65
|
*/
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
66
|
+
async function parseFiles(files, meta, config) {
|
|
67
|
+
const allFacts = [];
|
|
68
|
+
|
|
69
|
+
for (const file of files) {
|
|
70
|
+
const facts = parseFile(file, meta);
|
|
71
|
+
if (facts) allFacts.push(facts);
|
|
88
72
|
}
|
|
89
|
-
return results;
|
|
90
|
-
}
|
|
91
73
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
parserType: 'none',
|
|
98
|
-
functions: [],
|
|
99
|
-
classes: [],
|
|
100
|
-
imports: [],
|
|
101
|
-
exports: [],
|
|
102
|
-
callsTo: [],
|
|
103
|
-
calledBy: [],
|
|
104
|
-
hash,
|
|
105
|
-
};
|
|
74
|
+
const { buildCallGraph } = require('./call-graph');
|
|
75
|
+
const factsWithGraph = buildCallGraph(allFacts);
|
|
76
|
+
|
|
77
|
+
const { buildPKG } = require('./pkg-builder');
|
|
78
|
+
return buildPKG(factsWithGraph, meta);
|
|
106
79
|
}
|
|
80
|
+
|
|
81
|
+
module.exports = { parseFile, parseFiles };
|