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
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract Service Provider specifics.
|
|
5
|
+
*/
|
|
6
|
+
function extract(sourceText, fileFacts) {
|
|
7
|
+
const laravelContext = { type: 'provider', registerBindings: [], bootActions: [] };
|
|
8
|
+
|
|
9
|
+
// register() method body
|
|
10
|
+
const registerFn = fileFacts.functions.find(f => f.name === 'register');
|
|
11
|
+
if (registerFn && registerFn.bodySnippet) {
|
|
12
|
+
const bindRegex = /\$this->app->(?:bind|singleton|instance|scoped)\s*\(\s*(['"])([\w\\]+)\1/g;
|
|
13
|
+
let m;
|
|
14
|
+
while ((m = bindRegex.exec(registerFn.bodySnippet)) !== null) {
|
|
15
|
+
laravelContext.registerBindings.push(m[2]);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// boot() method body
|
|
20
|
+
const bootFn = fileFacts.functions.find(f => f.name === 'boot');
|
|
21
|
+
if (bootFn && bootFn.bodySnippet) {
|
|
22
|
+
laravelContext.bootActions.push('boot() method present');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return laravelContext;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { extract };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract route definitions from Laravel route files.
|
|
5
|
+
*/
|
|
6
|
+
function extract(sourceText) {
|
|
7
|
+
const routes = [];
|
|
8
|
+
|
|
9
|
+
// Route::METHOD('uri', [Controller::class, 'method'])->middleware(...)->name(...)
|
|
10
|
+
const routeRegex = /Route::(get|post|put|patch|delete|options|any)\s*\(\s*['"]([^'"]+)['"]\s*,\s*(?:\[([^\]]+)\]|['"]([^'"]+)['"]|\$?(\w+))/gi;
|
|
11
|
+
let m;
|
|
12
|
+
while ((m = routeRegex.exec(sourceText)) !== null) {
|
|
13
|
+
const method = m[1].toUpperCase();
|
|
14
|
+
const uri = m[2];
|
|
15
|
+
let controller = null;
|
|
16
|
+
let action = null;
|
|
17
|
+
|
|
18
|
+
if (m[3]) {
|
|
19
|
+
// Array syntax: [Controller::class, 'method']
|
|
20
|
+
const parts = m[3].split(',').map(s => s.trim().replace(/['"]/g, '').replace(/::class$/, ''));
|
|
21
|
+
controller = parts[0] ? parts[0].split('\\').pop() : null;
|
|
22
|
+
action = parts[1] || null;
|
|
23
|
+
} else if (m[4]) {
|
|
24
|
+
// String 'Controller@method'
|
|
25
|
+
const parts = m[4].split('@');
|
|
26
|
+
controller = parts[0] ? parts[0].split('\\').pop() : null;
|
|
27
|
+
action = parts[1] || null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Extract middleware
|
|
31
|
+
const afterRoute = sourceText.slice(m.index, m.index + 200);
|
|
32
|
+
const middlewareMatch = afterRoute.match(/->middleware\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
33
|
+
const middleware = middlewareMatch ? middlewareMatch[1] : null;
|
|
34
|
+
|
|
35
|
+
// Extract name
|
|
36
|
+
const nameMatch = afterRoute.match(/->name\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
37
|
+
const name = nameMatch ? nameMatch[1] : null;
|
|
38
|
+
|
|
39
|
+
routes.push({ method, uri, controller, action, middleware, name });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { type: 'route_file', routes };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { extract };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { scoreComplexity } = require('../complexity-scorer');
|
|
4
|
+
const { extractBodySnippet } = require('../body-extractor');
|
|
5
|
+
|
|
6
|
+
function parse(sourceText, relativePath) {
|
|
7
|
+
const lines = sourceText.split('\n');
|
|
8
|
+
const functions = [];
|
|
9
|
+
const classes = [];
|
|
10
|
+
const imports = [];
|
|
11
|
+
const exports = [];
|
|
12
|
+
|
|
13
|
+
// Namespace detection
|
|
14
|
+
let namespace = null;
|
|
15
|
+
const nsMatch = sourceText.match(/^namespace\s+([\w\\]+)\s*;/m);
|
|
16
|
+
if (nsMatch) namespace = nsMatch[1];
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < lines.length; i++) {
|
|
19
|
+
const line = lines[i];
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
|
|
22
|
+
// Use statements
|
|
23
|
+
let m = trimmed.match(/^use\s+([\w\\]+)(?:\s+as\s+\w+)?;/);
|
|
24
|
+
if (m) { imports.push({ module: m[1], specifiers: [] }); continue; }
|
|
25
|
+
|
|
26
|
+
// Class definition
|
|
27
|
+
m = trimmed.match(/^(?:abstract\s+|final\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+([\w,\s\\]+))?/);
|
|
28
|
+
if (m) {
|
|
29
|
+
const cls = { name: m[1], methods: [], extends: m[2] || null, implements: m[3] ? m[3].split(',').map(s => s.trim()) : [] };
|
|
30
|
+
classes.push(cls);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Interface
|
|
35
|
+
m = trimmed.match(/^interface\s+(\w+)/);
|
|
36
|
+
if (m) {
|
|
37
|
+
classes.push({ name: m[1], methods: [], extends: null, isInterface: true });
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Methods
|
|
42
|
+
m = trimmed.match(/^(?:(public|protected|private)\s+)?(?:(static)\s+)?(?:(abstract|final)\s+)?(?:(async)\s+)?function\s+(\w+)\s*\(([^)]*)\)(?:\s*:\s*\??([\w\\|]+))?/);
|
|
43
|
+
if (m) {
|
|
44
|
+
const visibility = m[1] || 'public';
|
|
45
|
+
const isStatic = !!m[2];
|
|
46
|
+
const name = m[5];
|
|
47
|
+
const paramsStr = m[6] || '';
|
|
48
|
+
const returnType = m[7] || null;
|
|
49
|
+
const params = parsePhpParams(paramsStr);
|
|
50
|
+
const lineEnd = findBlockEnd(lines, i);
|
|
51
|
+
const body = lines.slice(i, lineEnd).join('\n');
|
|
52
|
+
const complexity = scoreComplexity(body);
|
|
53
|
+
const snippet = extractBodySnippet(sourceText, i + 1, lineEnd, complexity.complexityScore);
|
|
54
|
+
const fn = {
|
|
55
|
+
name,
|
|
56
|
+
params,
|
|
57
|
+
returnType,
|
|
58
|
+
isExported: visibility === 'public',
|
|
59
|
+
isStatic,
|
|
60
|
+
isAsync: !!m[4],
|
|
61
|
+
visibility,
|
|
62
|
+
lineStart: i + 1,
|
|
63
|
+
lineEnd,
|
|
64
|
+
calls: extractCalls(body),
|
|
65
|
+
...complexity,
|
|
66
|
+
bodySnippet: snippet.bodySnippet,
|
|
67
|
+
bodySnippetTruncated: snippet.bodySnippetTruncated,
|
|
68
|
+
};
|
|
69
|
+
functions.push(fn);
|
|
70
|
+
if (classes.length > 0) classes[classes.length - 1].methods.push(name);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const publicFns = functions.filter(f => f.isExported).map(f => f.name);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
relativePath,
|
|
78
|
+
language: 'php',
|
|
79
|
+
linesOfCode: lines.length,
|
|
80
|
+
namespace,
|
|
81
|
+
functions,
|
|
82
|
+
classes,
|
|
83
|
+
imports,
|
|
84
|
+
exports: publicFns,
|
|
85
|
+
callsTo: [],
|
|
86
|
+
calledBy: [],
|
|
87
|
+
hash: null,
|
|
88
|
+
parserType: 'ast',
|
|
89
|
+
laravelContext: null,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function parsePhpParams(paramsStr) {
|
|
94
|
+
if (!paramsStr.trim()) return [];
|
|
95
|
+
return paramsStr.split(',').map(p => {
|
|
96
|
+
const trimmed = p.trim();
|
|
97
|
+
// Type hint + variable: e.g. "Request $request" or "?string $name = null"
|
|
98
|
+
const m = trimmed.match(/^(?:\?)?([\w\\|]+)\s+\$(\w+)/) || trimmed.match(/^\$(\w+)/);
|
|
99
|
+
if (!m) return { name: trimmed, type: null };
|
|
100
|
+
if (m[2]) return { name: '$' + m[2], type: m[1] };
|
|
101
|
+
return { name: '$' + m[1], type: null };
|
|
102
|
+
}).filter(p => p.name);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function findBlockEnd(lines, startIdx) {
|
|
106
|
+
let depth = 0;
|
|
107
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
108
|
+
for (const ch of lines[i]) {
|
|
109
|
+
if (ch === '{') depth++;
|
|
110
|
+
else if (ch === '}') {
|
|
111
|
+
depth--;
|
|
112
|
+
if (depth === 0) return i + 1;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return lines.length;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function extractCalls(body) {
|
|
120
|
+
const calls = new Set();
|
|
121
|
+
const keywords = new Set(['if', 'foreach', 'for', 'while', 'switch', 'catch', 'function', 'return', 'throw', 'new', 'echo', 'print', 'empty', 'isset', 'unset', 'list', 'array', 'class', 'interface', 'abstract', 'static', 'public', 'private', 'protected', 'namespace', 'use', 'require', 'include', 'elseif', 'else', 'try', 'finally', 'match', 'fn']);
|
|
122
|
+
for (const match of body.matchAll(/\b(\w+)\s*\(/g)) {
|
|
123
|
+
const name = match[1];
|
|
124
|
+
if (!keywords.has(name) && name.length > 1) calls.add(name);
|
|
125
|
+
}
|
|
126
|
+
return [...calls];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = { parse };
|
package/src/parser/ast/python.js
CHANGED
|
@@ -1,227 +1,104 @@
|
|
|
1
|
-
|
|
2
|
-
* Python AST parser using web-tree-sitter.
|
|
3
|
-
* Extracts def functions, class definitions, import/from...import statements.
|
|
4
|
-
*/
|
|
1
|
+
'use strict';
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
let pyParser = null;
|
|
10
|
-
let pyInitialized = false;
|
|
11
|
-
|
|
12
|
-
async function ensureParser() {
|
|
13
|
-
if (pyInitialized) return pyParser;
|
|
14
|
-
pyInitialized = true;
|
|
15
|
-
|
|
16
|
-
const Parser = await initTreeSitter();
|
|
17
|
-
if (!Parser) return null;
|
|
18
|
-
|
|
19
|
-
const language = await loadLanguage('python');
|
|
20
|
-
if (!language) return null;
|
|
21
|
-
|
|
22
|
-
pyParser = createParser(language);
|
|
23
|
-
return pyParser;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Parse a Python file and extract structural facts.
|
|
28
|
-
* @param {string} source - Raw file content
|
|
29
|
-
* @param {string} relativePath - Relative file path
|
|
30
|
-
* @returns {Promise<import('../../parser/index.js').FileFacts>}
|
|
31
|
-
*/
|
|
32
|
-
export async function parsePython(source, relativePath) {
|
|
33
|
-
const parser = await ensureParser();
|
|
34
|
-
|
|
35
|
-
if (!parser) {
|
|
36
|
-
return parseGeneric(source, relativePath, 'python');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const tree = parser.parse(source);
|
|
40
|
-
const rootNode = tree.rootNode;
|
|
3
|
+
const { scoreComplexity } = require('../complexity-scorer');
|
|
4
|
+
const { extractBodySnippet } = require('../body-extractor');
|
|
41
5
|
|
|
6
|
+
function parse(sourceText, relativePath) {
|
|
7
|
+
const lines = sourceText.split('\n');
|
|
42
8
|
const functions = [];
|
|
43
9
|
const classes = [];
|
|
44
10
|
const imports = [];
|
|
45
11
|
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 });
|
|
50
12
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
13
|
+
for (let i = 0; i < lines.length; i++) {
|
|
14
|
+
const line = lines[i];
|
|
15
|
+
const trimmed = line.trim();
|
|
16
|
+
|
|
17
|
+
// Imports
|
|
18
|
+
let m = trimmed.match(/^import\s+(\w+)$/);
|
|
19
|
+
if (m) { imports.push({ module: m[1], specifiers: [] }); continue; }
|
|
20
|
+
m = trimmed.match(/^from\s+(\S+)\s+import\s+(.+)$/);
|
|
21
|
+
if (m) {
|
|
22
|
+
const specifiers = m[2].split(',').map(s => s.trim().split(/\s+as\s+/)[0].trim());
|
|
23
|
+
imports.push({ module: m[1], specifiers });
|
|
24
|
+
continue;
|
|
55
25
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
26
|
+
m = trimmed.match(/^import\s+(\S+)\s+as\s+\w+$/);
|
|
27
|
+
if (m) { imports.push({ module: m[1], specifiers: [] }); continue; }
|
|
28
|
+
|
|
29
|
+
// Class
|
|
30
|
+
m = trimmed.match(/^class\s+(\w+)/);
|
|
31
|
+
if (m) {
|
|
32
|
+
classes.push({ name: m[1], methods: [], extends: null });
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Function / def
|
|
37
|
+
m = trimmed.match(/^(?:async\s+)?def\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*(\S+))?:/);
|
|
38
|
+
if (m) {
|
|
39
|
+
const name = m[1];
|
|
40
|
+
const params = m[2].split(',').map(p => {
|
|
41
|
+
const pm = p.trim().match(/(\w+)(?:\s*:\s*(\S+?))?(?:\s*=.*)?$/);
|
|
42
|
+
return pm ? { name: pm[1], type: pm[2] || null } : { name: p.trim(), type: null };
|
|
43
|
+
}).filter(p => p.name && p.name !== 'self' && p.name !== 'cls');
|
|
44
|
+
const returnType = m[3] || null;
|
|
45
|
+
const lineEnd = findBlockEnd(lines, i);
|
|
46
|
+
const body = lines.slice(i, lineEnd).join('\n');
|
|
47
|
+
const complexity = scoreComplexity(body);
|
|
48
|
+
const snippet = extractBodySnippet(sourceText, i + 1, lineEnd, complexity.complexityScore);
|
|
49
|
+
functions.push({
|
|
50
|
+
name,
|
|
51
|
+
params,
|
|
52
|
+
returnType,
|
|
53
|
+
isExported: !name.startsWith('_'),
|
|
54
|
+
isAsync: /^async\s+def/.test(trimmed),
|
|
55
|
+
lineStart: i + 1,
|
|
56
|
+
lineEnd,
|
|
57
|
+
calls: extractCalls(body),
|
|
58
|
+
...complexity,
|
|
59
|
+
bodySnippet: snippet.bodySnippet,
|
|
60
|
+
bodySnippetTruncated: snippet.bodySnippetTruncated,
|
|
61
|
+
});
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
const publicFns = functions.filter(f => f.isExported).map(f => f.name);
|
|
66
|
+
|
|
63
67
|
return {
|
|
64
68
|
relativePath,
|
|
65
69
|
language: 'python',
|
|
66
|
-
linesOfCode,
|
|
67
|
-
parserType: 'tree-sitter',
|
|
70
|
+
linesOfCode: lines.length,
|
|
68
71
|
functions,
|
|
69
72
|
classes,
|
|
70
73
|
imports,
|
|
71
|
-
exports,
|
|
74
|
+
exports: publicFns,
|
|
72
75
|
callsTo: [],
|
|
73
76
|
calledBy: [],
|
|
74
|
-
hash:
|
|
77
|
+
hash: null,
|
|
78
|
+
parserType: 'ast',
|
|
75
79
|
};
|
|
76
80
|
}
|
|
77
81
|
|
|
78
|
-
function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
break;
|
|
86
|
-
case 'import_statement':
|
|
87
|
-
extractImport(node, ctx);
|
|
88
|
-
break;
|
|
89
|
-
case 'import_from_statement':
|
|
90
|
-
extractFromImport(node, ctx);
|
|
91
|
-
break;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Only walk top-level and class-level nodes
|
|
95
|
-
if (node.type === 'module' || node.type === 'class_definition') {
|
|
96
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
97
|
-
walkNode(node.child(i), ctx);
|
|
98
|
-
}
|
|
82
|
+
function findBlockEnd(lines, startIdx) {
|
|
83
|
+
const baseIndent = lines[startIdx] ? lines[startIdx].match(/^(\s*)/)[1].length : 0;
|
|
84
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
85
|
+
const line = lines[i];
|
|
86
|
+
if (line.trim() === '') continue;
|
|
87
|
+
const indent = line.match(/^(\s*)/)[1].length;
|
|
88
|
+
if (indent <= baseIndent) return i;
|
|
99
89
|
}
|
|
90
|
+
return lines.length;
|
|
100
91
|
}
|
|
101
92
|
|
|
102
|
-
function
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
ctx.functions.push({
|
|
110
|
-
name: nameNode.text,
|
|
111
|
-
params: extractParams(paramsNode),
|
|
112
|
-
returnType: returnTypeNode ? returnTypeNode.text : null,
|
|
113
|
-
isExported: !nameNode.text.startsWith('_'),
|
|
114
|
-
isAsync,
|
|
115
|
-
lineStart: node.startPosition.row + 1,
|
|
116
|
-
lineEnd: node.endPosition.row + 1,
|
|
117
|
-
calls: [],
|
|
118
|
-
});
|
|
93
|
+
function extractCalls(body) {
|
|
94
|
+
const calls = new Set();
|
|
95
|
+
const m = body.matchAll(/\b(\w+)\s*\(/g);
|
|
96
|
+
const keywords = new Set(['if', 'for', 'while', 'def', 'class', 'return', 'import', 'from', 'with', 'as', 'not', 'and', 'or', 'in', 'is', 'elif', 'else', 'try', 'except', 'finally', 'raise', 'del', 'lambda', 'yield', 'async', 'await', 'pass', 'break', 'continue', 'print']);
|
|
97
|
+
for (const match of m) {
|
|
98
|
+
const name = match[1];
|
|
99
|
+
if (!keywords.has(name) && name.length > 1) calls.add(name);
|
|
119
100
|
}
|
|
101
|
+
return [...calls];
|
|
120
102
|
}
|
|
121
103
|
|
|
122
|
-
|
|
123
|
-
const nameNode = node.childForFieldName('name');
|
|
124
|
-
const bodyNode = node.childForFieldName('body');
|
|
125
|
-
|
|
126
|
-
let superclass = null;
|
|
127
|
-
const superclassNode = node.childForFieldName('superclasses');
|
|
128
|
-
if (superclassNode && superclassNode.childCount > 0) {
|
|
129
|
-
for (let i = 0; i < superclassNode.childCount; i++) {
|
|
130
|
-
const child = superclassNode.child(i);
|
|
131
|
-
if (child.type === 'identifier') {
|
|
132
|
-
superclass = child.text;
|
|
133
|
-
break;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (nameNode) {
|
|
139
|
-
const methods = [];
|
|
140
|
-
if (bodyNode) {
|
|
141
|
-
for (let i = 0; i < bodyNode.childCount; i++) {
|
|
142
|
-
const child = bodyNode.child(i);
|
|
143
|
-
if (child.type === 'function_definition') {
|
|
144
|
-
const methodName = child.childForFieldName('name');
|
|
145
|
-
if (methodName) methods.push(methodName.text);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
ctx.classes.push({
|
|
151
|
-
name: nameNode.text,
|
|
152
|
-
methods,
|
|
153
|
-
extends: superclass,
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function extractImport(node, ctx) {
|
|
159
|
-
// import module
|
|
160
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
161
|
-
const child = node.child(i);
|
|
162
|
-
if (child.type === 'dotted_name') {
|
|
163
|
-
ctx.imports.push({
|
|
164
|
-
module: child.text,
|
|
165
|
-
specifiers: [child.text],
|
|
166
|
-
});
|
|
167
|
-
} else if (child.type === 'aliased_import') {
|
|
168
|
-
const nameNode = child.childForFieldName('name');
|
|
169
|
-
if (nameNode) {
|
|
170
|
-
ctx.imports.push({
|
|
171
|
-
module: nameNode.text,
|
|
172
|
-
specifiers: [nameNode.text],
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function extractFromImport(node, ctx) {
|
|
180
|
-
// from module import ...
|
|
181
|
-
const moduleNode = node.childForFieldName('module_name');
|
|
182
|
-
if (!moduleNode) return;
|
|
183
|
-
|
|
184
|
-
const specifiers = [];
|
|
185
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
186
|
-
const child = node.child(i);
|
|
187
|
-
if (child.type === 'dotted_name' && child !== moduleNode) {
|
|
188
|
-
specifiers.push(child.text);
|
|
189
|
-
} else if (child.type === 'aliased_import') {
|
|
190
|
-
const nameNode = child.childForFieldName('name');
|
|
191
|
-
if (nameNode) specifiers.push(nameNode.text);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
ctx.imports.push({
|
|
196
|
-
module: moduleNode.text,
|
|
197
|
-
specifiers,
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function extractParams(paramsNode) {
|
|
202
|
-
if (!paramsNode) return [];
|
|
203
|
-
const params = [];
|
|
204
|
-
for (let i = 0; i < paramsNode.childCount; i++) {
|
|
205
|
-
const child = paramsNode.child(i);
|
|
206
|
-
if (child.type === 'identifier') {
|
|
207
|
-
if (child.text !== 'self' && child.text !== 'cls') {
|
|
208
|
-
params.push({ name: child.text, type: null });
|
|
209
|
-
}
|
|
210
|
-
} else if (child.type === 'typed_parameter') {
|
|
211
|
-
const nameNode = child.childCount > 0 ? child.child(0) : null;
|
|
212
|
-
const typeNode = child.childForFieldName('type');
|
|
213
|
-
if (nameNode && nameNode.text !== 'self' && nameNode.text !== 'cls') {
|
|
214
|
-
params.push({
|
|
215
|
-
name: nameNode.text,
|
|
216
|
-
type: typeNode ? typeNode.text : null,
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
} else if (child.type === 'default_parameter') {
|
|
220
|
-
const nameNode = child.childForFieldName('name');
|
|
221
|
-
if (nameNode && nameNode.text !== 'self' && nameNode.text !== 'cls') {
|
|
222
|
-
params.push({ name: nameNode.text, type: null });
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
return params;
|
|
227
|
-
}
|
|
104
|
+
module.exports = { parse };
|