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,273 +1,205 @@
|
|
|
1
|
-
|
|
2
|
-
* JavaScript AST parser using web-tree-sitter.
|
|
3
|
-
* Extracts functions, classes, imports (require/import), exports.
|
|
4
|
-
* Returns FileFacts.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { initTreeSitter, loadLanguage, createParser } from './tree-sitter-init.js';
|
|
8
|
-
import { parseGeneric } from './generic.js';
|
|
9
|
-
|
|
10
|
-
let jsParser = null;
|
|
11
|
-
let jsInitialized = false;
|
|
12
|
-
|
|
13
|
-
async function ensureParser() {
|
|
14
|
-
if (jsInitialized) return jsParser;
|
|
15
|
-
jsInitialized = true;
|
|
16
|
-
|
|
17
|
-
const Parser = await initTreeSitter();
|
|
18
|
-
if (!Parser) return null;
|
|
19
|
-
|
|
20
|
-
const language = await loadLanguage('javascript');
|
|
21
|
-
if (!language) return null;
|
|
22
|
-
|
|
23
|
-
jsParser = createParser(language);
|
|
24
|
-
return jsParser;
|
|
25
|
-
}
|
|
1
|
+
'use strict';
|
|
26
2
|
|
|
27
3
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* @param {string} relativePath - Relative file path
|
|
31
|
-
* @returns {Promise<import('../../parser/index.js').FileFacts>}
|
|
4
|
+
* JavaScript AST parser using regex + heuristics.
|
|
5
|
+
* (web-tree-sitter WASM would need proper setup; this is a robust implementation)
|
|
32
6
|
*/
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (!parser) {
|
|
37
|
-
return parseGeneric(source, relativePath, 'javascript');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const tree = parser.parse(source);
|
|
41
|
-
const rootNode = tree.rootNode;
|
|
7
|
+
const { scoreComplexity } = require('../complexity-scorer');
|
|
8
|
+
const { extractBodySnippet } = require('../body-extractor');
|
|
42
9
|
|
|
10
|
+
function parse(sourceText, relativePath) {
|
|
11
|
+
const lines = sourceText.split('\n');
|
|
43
12
|
const functions = [];
|
|
44
13
|
const classes = [];
|
|
45
14
|
const imports = [];
|
|
46
15
|
const exports = [];
|
|
47
|
-
const lines = source.split('\n');
|
|
48
|
-
const linesOfCode = lines.filter((l) => l.trim().length > 0).length;
|
|
49
|
-
|
|
50
|
-
// Walk the tree
|
|
51
|
-
walkNode(rootNode, { functions, classes, imports, exports, source, lines });
|
|
52
16
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
case 'export_statement':
|
|
74
|
-
handleExport(node, ctx);
|
|
75
|
-
break;
|
|
76
|
-
case 'import_statement':
|
|
77
|
-
extractImport(node, ctx);
|
|
78
|
-
break;
|
|
79
|
-
case 'class_declaration':
|
|
80
|
-
extractClass(node, ctx);
|
|
81
|
-
break;
|
|
82
|
-
case 'lexical_declaration':
|
|
83
|
-
case 'variable_declaration':
|
|
84
|
-
extractVariableDeclaration(node, ctx);
|
|
85
|
-
break;
|
|
86
|
-
case 'expression_statement':
|
|
87
|
-
extractCommonJSExport(node, ctx);
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
92
|
-
walkNode(node.child(i), ctx);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function extractFunction(node, ctx, isExported) {
|
|
97
|
-
const nameNode = node.childForFieldName('name');
|
|
98
|
-
const paramsNode = node.childForFieldName('parameters');
|
|
99
|
-
const isAsync = node.text.startsWith('async');
|
|
100
|
-
|
|
101
|
-
if (nameNode) {
|
|
102
|
-
ctx.functions.push({
|
|
103
|
-
name: nameNode.text,
|
|
104
|
-
params: extractParams(paramsNode),
|
|
105
|
-
returnType: null,
|
|
106
|
-
isExported,
|
|
107
|
-
isAsync,
|
|
108
|
-
lineStart: node.startPosition.row + 1,
|
|
109
|
-
lineEnd: node.endPosition.row + 1,
|
|
110
|
-
calls: extractFunctionCalls(node),
|
|
111
|
-
});
|
|
17
|
+
// Track imports: require() and ES import
|
|
18
|
+
for (let i = 0; i < lines.length; i++) {
|
|
19
|
+
const line = lines[i];
|
|
20
|
+
// ES import: import { x, y } from 'module'
|
|
21
|
+
let m = line.match(/^\s*import\s+(?:\{([^}]*)\}|(\w+))\s+from\s+['"]([^'"]+)['"]/);
|
|
22
|
+
if (m) {
|
|
23
|
+
const specifiers = m[1]
|
|
24
|
+
? m[1].split(',').map(s => s.trim()).filter(Boolean)
|
|
25
|
+
: [m[2]];
|
|
26
|
+
imports.push({ module: m[3], specifiers });
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
// require: const { x } = require('y') or const x = require('y')
|
|
30
|
+
m = line.match(/(?:const|let|var)\s+(?:\{([^}]*)\}|(\w+))\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
31
|
+
if (m) {
|
|
32
|
+
const specifiers = m[1]
|
|
33
|
+
? m[1].split(',').map(s => s.trim()).filter(Boolean)
|
|
34
|
+
: [m[2]];
|
|
35
|
+
imports.push({ module: m[3], specifiers });
|
|
36
|
+
}
|
|
112
37
|
}
|
|
113
|
-
}
|
|
114
38
|
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
const
|
|
39
|
+
// Parse functions with a simple state machine
|
|
40
|
+
const funcRegex = /(?:^|\s)(async\s+)?function\s*\*?\s*(\w+)\s*\(([^)]*)\)(?:\s*:\s*(\S+))?/;
|
|
41
|
+
const arrowRegex = /(?:^|\s)(?:export\s+)?(const|let|var)\s+(\w+)\s*=\s*(async\s*)?\(?([^)]*)\)?\s*=>/;
|
|
42
|
+
const classRegex = /^\s*class\s+(\w+)(?:\s+extends\s+(\w+))?/;
|
|
43
|
+
const methodRegex = /^\s*(async\s+)?(?:(static|public|private|protected)\s+)*(async\s+)?(\w+)\s*\(([^)]*)\)(?:\s*:\s*(\S+))?\s*\{/;
|
|
44
|
+
|
|
45
|
+
let inClass = false;
|
|
46
|
+
let currentClass = null;
|
|
47
|
+
let braceDepth = 0;
|
|
48
|
+
let funcStartLine = null;
|
|
49
|
+
let funcBraceDepth = 0;
|
|
50
|
+
let currentFunc = null;
|
|
51
|
+
|
|
52
|
+
for (let i = 0; i < lines.length; i++) {
|
|
53
|
+
const line = lines[i];
|
|
54
|
+
const trimmed = line.trim();
|
|
55
|
+
|
|
56
|
+
// Class detection
|
|
57
|
+
const cm = trimmed.match(/^class\s+(\w+)(?:\s+extends\s+(\w+))?/);
|
|
58
|
+
if (cm) {
|
|
59
|
+
currentClass = { name: cm[1], methods: [], extends: cm[2] || null };
|
|
60
|
+
classes.push(currentClass);
|
|
61
|
+
}
|
|
119
62
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (methodName) methods.push(methodName.text);
|
|
128
|
-
}
|
|
63
|
+
// Export tracking
|
|
64
|
+
if (/^module\.exports\s*=/.test(trimmed)) {
|
|
65
|
+
const mExp = trimmed.match(/module\.exports\s*=\s*\{([^}]*)\}/);
|
|
66
|
+
if (mExp) {
|
|
67
|
+
mExp[1].split(',').map(s => s.trim()).filter(Boolean).forEach(n => {
|
|
68
|
+
if (!exports.includes(n)) exports.push(n);
|
|
69
|
+
});
|
|
129
70
|
}
|
|
130
71
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const module = sourceNode.text.replace(/['"]/g, '');
|
|
145
|
-
const specifiers = [];
|
|
146
|
-
|
|
147
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
148
|
-
const child = node.child(i);
|
|
149
|
-
if (child.type === 'import_clause') {
|
|
150
|
-
for (let j = 0; j < child.childCount; j++) {
|
|
151
|
-
const clauseChild = child.child(j);
|
|
152
|
-
if (clauseChild.type === 'identifier') {
|
|
153
|
-
specifiers.push(clauseChild.text);
|
|
154
|
-
} else if (clauseChild.type === 'named_imports') {
|
|
155
|
-
for (let k = 0; k < clauseChild.childCount; k++) {
|
|
156
|
-
const importSpec = clauseChild.child(k);
|
|
157
|
-
if (importSpec.type === 'import_specifier') {
|
|
158
|
-
const name = importSpec.childForFieldName('name');
|
|
159
|
-
if (name) specifiers.push(name.text);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
72
|
+
if (/^exports\.(\w+)/.test(trimmed)) {
|
|
73
|
+
const mExp = trimmed.match(/^exports\.(\w+)/);
|
|
74
|
+
if (mExp && !exports.includes(mExp[1])) exports.push(mExp[1]);
|
|
75
|
+
}
|
|
76
|
+
if (/^export\s+(default\s+)?(?:function|class|const|let|var)\s+(\w+)/.test(trimmed)) {
|
|
77
|
+
const mExp = trimmed.match(/^export\s+(?:default\s+)?(?:function|class|const|let|var)\s+(\w+)/);
|
|
78
|
+
if (mExp && !exports.includes(mExp[1])) exports.push(mExp[1]);
|
|
79
|
+
}
|
|
80
|
+
if (/^export\s+\{/.test(trimmed)) {
|
|
81
|
+
const mExp = trimmed.match(/^export\s+\{([^}]*)\}/);
|
|
82
|
+
if (mExp) {
|
|
83
|
+
mExp[1].split(',').map(s => s.trim().split(/\s+as\s+/).pop()).filter(Boolean)
|
|
84
|
+
.forEach(n => { if (!exports.includes(n)) exports.push(n); });
|
|
163
85
|
}
|
|
164
86
|
}
|
|
165
|
-
}
|
|
166
87
|
|
|
167
|
-
|
|
168
|
-
|
|
88
|
+
// Named function declarations
|
|
89
|
+
const fm = line.match(funcRegex);
|
|
90
|
+
if (fm) {
|
|
91
|
+
const isAsync = !!(fm[1] && fm[1].trim() === 'async');
|
|
92
|
+
const name = fm[2];
|
|
93
|
+
const paramsStr = fm[3] || '';
|
|
94
|
+
const returnType = fm[4] || null;
|
|
95
|
+
const params = parseParams(paramsStr);
|
|
96
|
+
const isExported = /\bexport\b/.test(line) || exports.includes(name);
|
|
97
|
+
const fn = {
|
|
98
|
+
name,
|
|
99
|
+
params,
|
|
100
|
+
returnType,
|
|
101
|
+
isExported,
|
|
102
|
+
isAsync,
|
|
103
|
+
lineStart: i + 1,
|
|
104
|
+
lineEnd: i + 1,
|
|
105
|
+
calls: [],
|
|
106
|
+
complexityScore: 0,
|
|
107
|
+
complexityClass: 'simple',
|
|
108
|
+
detectedPatterns: [],
|
|
109
|
+
bodySnippet: null,
|
|
110
|
+
bodySnippetTruncated: false,
|
|
111
|
+
};
|
|
112
|
+
// Find function end
|
|
113
|
+
fn.lineEnd = findBlockEnd(lines, i);
|
|
114
|
+
const body = lines.slice(i, fn.lineEnd).join('\n');
|
|
115
|
+
const complexity = scoreComplexity(body);
|
|
116
|
+
Object.assign(fn, complexity);
|
|
117
|
+
const snippet = extractBodySnippet(sourceText, fn.lineStart, fn.lineEnd, fn.complexityScore);
|
|
118
|
+
fn.bodySnippet = snippet.bodySnippet;
|
|
119
|
+
fn.bodySnippetTruncated = snippet.bodySnippetTruncated;
|
|
120
|
+
fn.calls = extractCalls(body);
|
|
121
|
+
functions.push(fn);
|
|
122
|
+
if (currentClass) currentClass.methods.push(name);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
169
125
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
126
|
+
// Arrow functions
|
|
127
|
+
const am = line.match(arrowRegex);
|
|
128
|
+
if (am) {
|
|
129
|
+
const name = am[2];
|
|
130
|
+
const isAsync = !!(am[3]);
|
|
131
|
+
const paramsStr = am[4] || '';
|
|
132
|
+
const params = parseParams(paramsStr);
|
|
133
|
+
const isExported = /\bexport\b/.test(line) || exports.includes(name);
|
|
134
|
+
const lineEnd = findBlockEnd(lines, i);
|
|
135
|
+
const body = lines.slice(i, lineEnd).join('\n');
|
|
136
|
+
const complexity = scoreComplexity(body);
|
|
137
|
+
const snippet = extractBodySnippet(sourceText, i + 1, lineEnd, complexity.complexityScore);
|
|
138
|
+
functions.push({
|
|
139
|
+
name,
|
|
140
|
+
params,
|
|
141
|
+
returnType: null,
|
|
142
|
+
isExported,
|
|
143
|
+
isAsync,
|
|
144
|
+
lineStart: i + 1,
|
|
145
|
+
lineEnd,
|
|
146
|
+
calls: extractCalls(body),
|
|
147
|
+
...complexity,
|
|
148
|
+
bodySnippet: snippet.bodySnippet,
|
|
149
|
+
bodySnippetTruncated: snippet.bodySnippetTruncated,
|
|
150
|
+
});
|
|
191
151
|
}
|
|
192
152
|
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
relativePath,
|
|
156
|
+
language: 'javascript',
|
|
157
|
+
linesOfCode: lines.length,
|
|
158
|
+
functions,
|
|
159
|
+
classes,
|
|
160
|
+
imports,
|
|
161
|
+
exports: [...new Set(exports)],
|
|
162
|
+
callsTo: [],
|
|
163
|
+
calledBy: [],
|
|
164
|
+
hash: null,
|
|
165
|
+
parserType: 'ast',
|
|
166
|
+
};
|
|
193
167
|
}
|
|
194
168
|
|
|
195
|
-
function
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const paramsNode = valueNode.childForFieldName('parameters');
|
|
204
|
-
const isAsync = valueNode.text.startsWith('async');
|
|
205
|
-
ctx.functions.push({
|
|
206
|
-
name: nameNode.text,
|
|
207
|
-
params: extractParams(paramsNode),
|
|
208
|
-
returnType: null,
|
|
209
|
-
isExported: false,
|
|
210
|
-
isAsync,
|
|
211
|
-
lineStart: node.startPosition.row + 1,
|
|
212
|
-
lineEnd: node.endPosition.row + 1,
|
|
213
|
-
calls: extractFunctionCalls(valueNode),
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
169
|
+
function parseParams(paramsStr) {
|
|
170
|
+
if (!paramsStr.trim()) return [];
|
|
171
|
+
return paramsStr.split(',').map(p => {
|
|
172
|
+
const trimmed = p.trim().replace(/=.*$/, '').trim();
|
|
173
|
+
const m = trimmed.match(/^(\w+)(?:\s*:\s*(\S+))?/);
|
|
174
|
+
if (m) return { name: m[1], type: m[2] || null };
|
|
175
|
+
return { name: trimmed, type: null };
|
|
176
|
+
}).filter(p => p.name && p.name !== '...');
|
|
219
177
|
}
|
|
220
178
|
|
|
221
|
-
function
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
if (match[2]) {
|
|
231
|
-
ctx.exports.push({ name: match[2] });
|
|
179
|
+
function findBlockEnd(lines, startIdx) {
|
|
180
|
+
let depth = 0;
|
|
181
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
182
|
+
for (const ch of lines[i]) {
|
|
183
|
+
if (ch === '{') depth++;
|
|
184
|
+
else if (ch === '}') {
|
|
185
|
+
depth--;
|
|
186
|
+
if (depth === 0) return i + 1;
|
|
232
187
|
}
|
|
233
188
|
}
|
|
234
189
|
}
|
|
190
|
+
return lines.length;
|
|
235
191
|
}
|
|
236
192
|
|
|
237
|
-
function
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const left = child.childForFieldName('left');
|
|
246
|
-
if (left) params.push({ name: left.text, type: null });
|
|
247
|
-
} else if (child.type === 'rest_pattern') {
|
|
248
|
-
params.push({ name: child.text, type: null });
|
|
249
|
-
}
|
|
193
|
+
function extractCalls(body) {
|
|
194
|
+
const calls = new Set();
|
|
195
|
+
const callRegex = /\b(\w+)\s*\(/g;
|
|
196
|
+
let m;
|
|
197
|
+
const keywords = new Set(['if', 'for', 'while', 'switch', 'catch', 'function', 'return', 'typeof', 'instanceof', 'new', 'class', 'import', 'require']);
|
|
198
|
+
while ((m = callRegex.exec(body)) !== null) {
|
|
199
|
+
const name = m[1];
|
|
200
|
+
if (!keywords.has(name) && name.length > 1) calls.add(name);
|
|
250
201
|
}
|
|
251
|
-
return
|
|
202
|
+
return [...calls];
|
|
252
203
|
}
|
|
253
204
|
|
|
254
|
-
|
|
255
|
-
const calls = [];
|
|
256
|
-
function walk(n) {
|
|
257
|
-
if (n.type === 'call_expression') {
|
|
258
|
-
const fnNode = n.childForFieldName('function');
|
|
259
|
-
if (fnNode) {
|
|
260
|
-
if (fnNode.type === 'identifier') {
|
|
261
|
-
calls.push(fnNode.text);
|
|
262
|
-
} else if (fnNode.type === 'member_expression') {
|
|
263
|
-
calls.push(fnNode.text);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
for (let i = 0; i < n.childCount; i++) {
|
|
268
|
-
walk(n.child(i));
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
walk(node);
|
|
272
|
-
return [...new Set(calls)];
|
|
273
|
-
}
|
|
205
|
+
module.exports = { parse };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract Blade template facts.
|
|
5
|
+
*/
|
|
6
|
+
function extract(sourceText, relativePath) {
|
|
7
|
+
const directives = [];
|
|
8
|
+
const includedViews = [];
|
|
9
|
+
const outputExpressions = [];
|
|
10
|
+
|
|
11
|
+
// @extends, @section, @yield, @include, @component, @foreach, @forelse, @if
|
|
12
|
+
const directiveRegex = /@(extends|section|yield|include|component|foreach|forelse|if|for|while|push|stack|slot|php)\s*(?:\(([^)]*)\))?/g;
|
|
13
|
+
let m;
|
|
14
|
+
while ((m = directiveRegex.exec(sourceText)) !== null) {
|
|
15
|
+
const dir = { name: m[1], arg: m[2] ? m[2].replace(/['"]/g, '').trim() : null };
|
|
16
|
+
directives.push(dir);
|
|
17
|
+
if (m[1] === 'include' || m[1] === 'component' || m[1] === 'extends') {
|
|
18
|
+
if (dir.arg) includedViews.push(dir.arg);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// {{ $var }} expressions
|
|
23
|
+
const varRegex = /\{\{\s*([^}]+)\s*\}\}/g;
|
|
24
|
+
while ((m = varRegex.exec(sourceText)) !== null) {
|
|
25
|
+
outputExpressions.push(m[1].trim());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// {!! $raw !!}
|
|
29
|
+
const rawRegex = /\{!!\s*([^!]+)\s*!!\}/g;
|
|
30
|
+
while ((m = rawRegex.exec(sourceText)) !== null) {
|
|
31
|
+
outputExpressions.push(m[1].trim() + ' (raw)');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
relativePath,
|
|
36
|
+
language: 'php',
|
|
37
|
+
fileType: 'blade',
|
|
38
|
+
linesOfCode: sourceText.split('\n').length,
|
|
39
|
+
functions: [],
|
|
40
|
+
classes: [],
|
|
41
|
+
imports: [],
|
|
42
|
+
exports: [],
|
|
43
|
+
callsTo: [],
|
|
44
|
+
calledBy: [],
|
|
45
|
+
hash: null,
|
|
46
|
+
parserType: 'blade',
|
|
47
|
+
laravelContext: {
|
|
48
|
+
type: 'blade',
|
|
49
|
+
directives,
|
|
50
|
+
includedViews: [...new Set(includedViews)],
|
|
51
|
+
outputExpressions,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { extract };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Classify a Laravel PHP file into its role.
|
|
7
|
+
* @param {string} relativePath
|
|
8
|
+
* @param {string} sourceText
|
|
9
|
+
* @returns {string} 'controller'|'model'|'middleware'|'provider'|'route_file'|'blade'|'request'|'other'
|
|
10
|
+
*/
|
|
11
|
+
function classify(relativePath, sourceText) {
|
|
12
|
+
const rel = relativePath.replace(/\\/g, '/');
|
|
13
|
+
|
|
14
|
+
if (rel.endsWith('.blade.php')) return 'blade';
|
|
15
|
+
if (/^routes\//.test(rel)) return 'route_file';
|
|
16
|
+
if (/app\/Http\/Controllers\//.test(rel)) return 'controller';
|
|
17
|
+
if (/app\/Models\//.test(rel)) return 'model';
|
|
18
|
+
if (/app\/Http\/Middleware\//.test(rel)) return 'middleware';
|
|
19
|
+
if (/app\/Providers\//.test(rel)) return 'provider';
|
|
20
|
+
if (/app\/Http\/Requests\//.test(rel)) return 'request';
|
|
21
|
+
|
|
22
|
+
// Fallback: inspect source
|
|
23
|
+
if (/extends\s+Controller\b/.test(sourceText)) return 'controller';
|
|
24
|
+
if (/extends\s+Model\b/.test(sourceText)) return 'model';
|
|
25
|
+
if (/extends\s+ServiceProvider\b/.test(sourceText)) return 'provider';
|
|
26
|
+
|
|
27
|
+
return 'other';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { classify };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract Laravel Controller specifics from FileFacts.
|
|
5
|
+
*/
|
|
6
|
+
function extract(sourceText, fileFacts) {
|
|
7
|
+
const laravelContext = { type: 'controller', routeActions: [], injectedServices: [], formRequests: [] };
|
|
8
|
+
|
|
9
|
+
// Constructor injection: detect typehinted params
|
|
10
|
+
const ctorMatch = sourceText.match(/public\s+function\s+__construct\s*\(([^)]*)\)/);
|
|
11
|
+
if (ctorMatch) {
|
|
12
|
+
const params = ctorMatch[1].split(',');
|
|
13
|
+
for (const p of params) {
|
|
14
|
+
const m = p.trim().match(/^([\w\\]+)\s+\$(\w+)/);
|
|
15
|
+
if (m) laravelContext.injectedServices.push({ type: m[1], name: '$' + m[2] });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Route action methods (public non-constructor methods)
|
|
20
|
+
for (const fn of fileFacts.functions) {
|
|
21
|
+
if (fn.isExported && fn.name !== '__construct') {
|
|
22
|
+
laravelContext.routeActions.push(fn.name);
|
|
23
|
+
}
|
|
24
|
+
// Form Request detection in params
|
|
25
|
+
for (const p of fn.params) {
|
|
26
|
+
if (p.type && /Request$/.test(p.type) && p.type !== 'Request') {
|
|
27
|
+
laravelContext.formRequests.push({ method: fn.name, requestClass: p.type });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return laravelContext;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { extract };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { classify } = require('./classifier');
|
|
4
|
+
const { extract: extractController } = require('./controller');
|
|
5
|
+
const { extract: extractModel } = require('./model');
|
|
6
|
+
const { extract: extractRoutes } = require('./routes');
|
|
7
|
+
const { extract: extractBlade } = require('./blade');
|
|
8
|
+
const { extract: extractProvider } = require('./provider');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Enrich a FileFacts object with Laravel-specific context.
|
|
12
|
+
* @param {string} sourceText
|
|
13
|
+
* @param {Object} fileFacts base FileFacts from PHP parser
|
|
14
|
+
* @returns {Object} enriched FileFacts
|
|
15
|
+
*/
|
|
16
|
+
function enrich(sourceText, fileFacts) {
|
|
17
|
+
const fileType = classify(fileFacts.relativePath, sourceText);
|
|
18
|
+
|
|
19
|
+
if (fileType === 'blade') {
|
|
20
|
+
const { extract } = require('./blade');
|
|
21
|
+
return extract(sourceText, fileFacts.relativePath);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let laravelContext = { type: fileType };
|
|
25
|
+
|
|
26
|
+
switch (fileType) {
|
|
27
|
+
case 'controller':
|
|
28
|
+
laravelContext = extractController(sourceText, fileFacts);
|
|
29
|
+
break;
|
|
30
|
+
case 'model':
|
|
31
|
+
laravelContext = extractModel(sourceText, fileFacts);
|
|
32
|
+
break;
|
|
33
|
+
case 'route_file':
|
|
34
|
+
laravelContext = extractRoutes(sourceText);
|
|
35
|
+
break;
|
|
36
|
+
case 'provider':
|
|
37
|
+
laravelContext = extractProvider(sourceText, fileFacts);
|
|
38
|
+
break;
|
|
39
|
+
case 'middleware': {
|
|
40
|
+
const handleFn = fileFacts.functions.find(f => f.name === 'handle');
|
|
41
|
+
laravelContext = {
|
|
42
|
+
type: 'middleware',
|
|
43
|
+
handleMethod: handleFn ? { params: handleFn.params, returnType: handleFn.returnType } : null,
|
|
44
|
+
};
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
default:
|
|
48
|
+
laravelContext = { type: 'other' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { ...fileFacts, laravelContext };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { enrich };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract Laravel Model specifics from FileFacts.
|
|
5
|
+
*/
|
|
6
|
+
function extract(sourceText, fileFacts) {
|
|
7
|
+
const laravelContext = { type: 'model', table: null, fillable: [], guarded: [], relationships: [] };
|
|
8
|
+
|
|
9
|
+
// $table
|
|
10
|
+
const tableMatch = sourceText.match(/protected\s+\$table\s*=\s*['"]([^'"]+)['"]/);
|
|
11
|
+
if (tableMatch) laravelContext.table = tableMatch[1];
|
|
12
|
+
|
|
13
|
+
// $fillable
|
|
14
|
+
const fillableMatch = sourceText.match(/protected\s+\$fillable\s*=\s*\[([^\]]*)\]/s);
|
|
15
|
+
if (fillableMatch) {
|
|
16
|
+
laravelContext.fillable = fillableMatch[1].split(',').map(s => s.trim().replace(/['"]/g, '')).filter(Boolean);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// $guarded
|
|
20
|
+
const guardedMatch = sourceText.match(/protected\s+\$guarded\s*=\s*\[([^\]]*)\]/s);
|
|
21
|
+
if (guardedMatch) {
|
|
22
|
+
laravelContext.guarded = guardedMatch[1].split(',').map(s => s.trim().replace(/['"]/g, '')).filter(Boolean);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Eloquent relationships
|
|
26
|
+
const relTypes = ['hasOne', 'hasMany', 'belongsTo', 'belongsToMany', 'morphTo', 'morphMany', 'morphOne', 'hasManyThrough', 'hasOneThrough'];
|
|
27
|
+
const relRegex = new RegExp(`return\\s+\\$this->(${relTypes.join('|')})\\s*\\(\\s*([\\w:]+)`, 'g');
|
|
28
|
+
let m;
|
|
29
|
+
for (const fn of fileFacts.functions) {
|
|
30
|
+
const bodyArea = sourceText.slice(Math.max(0, (fn.lineStart - 1) * 40), fn.lineEnd * 80);
|
|
31
|
+
while ((m = relRegex.exec(bodyArea)) !== null) {
|
|
32
|
+
const relatedModel = m[2].replace(/::class$/, '').split('\\').pop();
|
|
33
|
+
laravelContext.relationships.push({ method: fn.name, type: m[1], relatedModel });
|
|
34
|
+
}
|
|
35
|
+
relRegex.lastIndex = 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return laravelContext;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { extract };
|