getdoorman 1.0.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/LICENSE +21 -0
- package/README.md +181 -0
- package/bin/doorman.js +444 -0
- package/package.json +74 -0
- package/src/ai-fixer.js +559 -0
- package/src/ast-scanner.js +434 -0
- package/src/auth.js +149 -0
- package/src/baseline.js +48 -0
- package/src/compliance.js +539 -0
- package/src/config.js +466 -0
- package/src/custom-rules.js +32 -0
- package/src/dashboard.js +202 -0
- package/src/detector.js +142 -0
- package/src/fix-engine.js +48 -0
- package/src/fix-registry-extra.js +95 -0
- package/src/fix-registry-go-rust.js +77 -0
- package/src/fix-registry-java-csharp.js +77 -0
- package/src/fix-registry-js.js +99 -0
- package/src/fix-registry-mcp-ai.js +57 -0
- package/src/fix-registry-python.js +87 -0
- package/src/fixer-ruby-php.js +608 -0
- package/src/fixer.js +2113 -0
- package/src/hooks.js +115 -0
- package/src/ignore.js +176 -0
- package/src/index.js +384 -0
- package/src/metrics.js +126 -0
- package/src/monorepo.js +65 -0
- package/src/presets.js +54 -0
- package/src/reporter.js +975 -0
- package/src/rule-worker.js +36 -0
- package/src/rules/ast-rules.js +756 -0
- package/src/rules/bugs/accessibility.js +235 -0
- package/src/rules/bugs/ai-codegen-fixable.js +172 -0
- package/src/rules/bugs/ai-codegen.js +365 -0
- package/src/rules/bugs/code-smell-bugs.js +247 -0
- package/src/rules/bugs/crypto-bugs.js +195 -0
- package/src/rules/bugs/docker-bugs.js +158 -0
- package/src/rules/bugs/general.js +361 -0
- package/src/rules/bugs/go-bugs.js +279 -0
- package/src/rules/bugs/index.js +73 -0
- package/src/rules/bugs/js-api.js +257 -0
- package/src/rules/bugs/js-array-object.js +210 -0
- package/src/rules/bugs/js-async-fixable.js +223 -0
- package/src/rules/bugs/js-async.js +211 -0
- package/src/rules/bugs/js-closure-scope.js +182 -0
- package/src/rules/bugs/js-database.js +203 -0
- package/src/rules/bugs/js-error-handling.js +148 -0
- package/src/rules/bugs/js-logic.js +261 -0
- package/src/rules/bugs/js-memory.js +214 -0
- package/src/rules/bugs/js-node.js +361 -0
- package/src/rules/bugs/js-react.js +373 -0
- package/src/rules/bugs/js-regex.js +200 -0
- package/src/rules/bugs/js-state.js +272 -0
- package/src/rules/bugs/js-type-coercion.js +318 -0
- package/src/rules/bugs/nextjs-bugs.js +242 -0
- package/src/rules/bugs/nextjs-fixable.js +120 -0
- package/src/rules/bugs/node-fixable.js +178 -0
- package/src/rules/bugs/python-advanced.js +245 -0
- package/src/rules/bugs/python-fixable.js +98 -0
- package/src/rules/bugs/python.js +284 -0
- package/src/rules/bugs/react-fixable.js +207 -0
- package/src/rules/bugs/ruby-bugs.js +182 -0
- package/src/rules/bugs/shell-bugs.js +181 -0
- package/src/rules/bugs/silent-failures.js +261 -0
- package/src/rules/bugs/ts-bugs.js +235 -0
- package/src/rules/bugs/unused-vars.js +65 -0
- package/src/rules/compliance/accessibility-ext.js +468 -0
- package/src/rules/compliance/education.js +322 -0
- package/src/rules/compliance/financial.js +421 -0
- package/src/rules/compliance/frameworks.js +507 -0
- package/src/rules/compliance/healthcare.js +520 -0
- package/src/rules/compliance/index.js +2714 -0
- package/src/rules/compliance/regional-eu.js +480 -0
- package/src/rules/compliance/regional-international.js +903 -0
- package/src/rules/cost/index.js +1993 -0
- package/src/rules/data/index.js +2503 -0
- package/src/rules/dependencies/index.js +1684 -0
- package/src/rules/deployment/index.js +2050 -0
- package/src/rules/index.js +71 -0
- package/src/rules/infrastructure/index.js +3048 -0
- package/src/rules/performance/index.js +3455 -0
- package/src/rules/quality/index.js +3175 -0
- package/src/rules/reliability/index.js +3040 -0
- package/src/rules/scope-rules.js +815 -0
- package/src/rules/security/ai-api.js +1177 -0
- package/src/rules/security/auth.js +1328 -0
- package/src/rules/security/cors.js +127 -0
- package/src/rules/security/crypto.js +527 -0
- package/src/rules/security/csharp.js +862 -0
- package/src/rules/security/csrf.js +193 -0
- package/src/rules/security/dart.js +835 -0
- package/src/rules/security/deserialization.js +291 -0
- package/src/rules/security/file-upload.js +187 -0
- package/src/rules/security/go.js +850 -0
- package/src/rules/security/headers.js +235 -0
- package/src/rules/security/index.js +65 -0
- package/src/rules/security/injection.js +1639 -0
- package/src/rules/security/mcp-server.js +71 -0
- package/src/rules/security/misconfiguration.js +660 -0
- package/src/rules/security/oauth-jwt.js +329 -0
- package/src/rules/security/path-traversal.js +295 -0
- package/src/rules/security/php.js +1054 -0
- package/src/rules/security/prototype-pollution.js +283 -0
- package/src/rules/security/rate-limiting.js +208 -0
- package/src/rules/security/ruby.js +1061 -0
- package/src/rules/security/rust.js +693 -0
- package/src/rules/security/secrets.js +747 -0
- package/src/rules/security/shell.js +647 -0
- package/src/rules/security/ssrf.js +298 -0
- package/src/rules/security/supply-chain-advanced.js +393 -0
- package/src/rules/security/supply-chain.js +734 -0
- package/src/rules/security/swift.js +835 -0
- package/src/rules/security/taint.js +27 -0
- package/src/rules/security/xss.js +520 -0
- package/src/scan-cache.js +71 -0
- package/src/scanner.js +710 -0
- package/src/scope-analyzer.js +685 -0
- package/src/share.js +88 -0
- package/src/taint.js +300 -0
- package/src/telemetry.js +183 -0
- package/src/tracer.js +190 -0
- package/src/upload.js +35 -0
- package/src/worker.js +31 -0
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight scope-aware analysis engine.
|
|
3
|
+
*
|
|
4
|
+
* Performs structural analysis of source code without a full AST parser,
|
|
5
|
+
* tracking block nesting, function/class boundaries, imports, and context
|
|
6
|
+
* (comments, strings, test blocks, try/catch, loops, callbacks).
|
|
7
|
+
*
|
|
8
|
+
* Export: analyzeScope(content, language) -> ScopeContext
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const JS_EXT_SET = new Set(['javascript', 'typescript', 'js', 'ts', 'jsx', 'tsx']);
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Helpers
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
function isJSLike(language) {
|
|
18
|
+
return JS_EXT_SET.has((language || '').toLowerCase());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isPython(language) {
|
|
22
|
+
return (language || '').toLowerCase() === 'python';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Detect language from file extension
|
|
26
|
+
function languageFromPath(filePath) {
|
|
27
|
+
if (!filePath) return 'javascript';
|
|
28
|
+
if (/\.(js|jsx|mjs|cjs)$/i.test(filePath)) return 'javascript';
|
|
29
|
+
if (/\.(ts|tsx)$/i.test(filePath)) return 'typescript';
|
|
30
|
+
if (/\.py$/i.test(filePath)) return 'python';
|
|
31
|
+
if (/\.rb$/i.test(filePath)) return 'ruby';
|
|
32
|
+
if (/\.go$/i.test(filePath)) return 'go';
|
|
33
|
+
return 'javascript';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Scope context class
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
class ScopeContext {
|
|
41
|
+
constructor(content, language) {
|
|
42
|
+
this.content = content;
|
|
43
|
+
this.language = language;
|
|
44
|
+
this.lines = content.split('\n');
|
|
45
|
+
this.lineCount = this.lines.length;
|
|
46
|
+
|
|
47
|
+
// Per-line metadata (indexed by 0-based line number)
|
|
48
|
+
this.lineDepth = new Array(this.lineCount).fill(0);
|
|
49
|
+
this.lineInsideComment = new Array(this.lineCount).fill(false);
|
|
50
|
+
this.lineInsideString = new Array(this.lineCount).fill(false);
|
|
51
|
+
|
|
52
|
+
// Block tracking
|
|
53
|
+
this.functions = []; // { name, startLine, endLine, params, isAsync, isExported, isArrow, isMethod }
|
|
54
|
+
this.classes = []; // { name, startLine, endLine, isExported }
|
|
55
|
+
this.tryCatches = []; // { tryStart, catchStart, catchEnd, catchBodyLines }
|
|
56
|
+
this.loops = []; // { startLine, endLine, type }
|
|
57
|
+
this.callbacks = []; // { startLine, endLine }
|
|
58
|
+
this.testBlocks = []; // { startLine, endLine }
|
|
59
|
+
|
|
60
|
+
// Imports
|
|
61
|
+
this.imports = []; // { module, alias, line }
|
|
62
|
+
this.importedModules = new Set();
|
|
63
|
+
|
|
64
|
+
// Route handlers
|
|
65
|
+
this.routeHandlers = []; // { startLine, endLine, method, path }
|
|
66
|
+
|
|
67
|
+
// Whether the file itself is a test file
|
|
68
|
+
this.isTestFile = false;
|
|
69
|
+
|
|
70
|
+
// Exported symbols
|
|
71
|
+
this.exportedNames = new Set();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Query methods -------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
isInsideFunction(lineNum) {
|
|
77
|
+
return this.functions.some(f => lineNum >= f.startLine && lineNum <= f.endLine);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getFunctionAt(lineNum) {
|
|
81
|
+
return this.functions.find(f => lineNum >= f.startLine && lineNum <= f.endLine) || null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
isInsideClass(lineNum) {
|
|
85
|
+
return this.classes.some(c => lineNum >= c.startLine && lineNum <= c.endLine);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getClassAt(lineNum) {
|
|
89
|
+
return this.classes.find(c => lineNum >= c.startLine && lineNum <= c.endLine) || null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
isInsideTryCatch(lineNum) {
|
|
93
|
+
return this.tryCatches.some(t => lineNum >= t.tryStart && lineNum <= t.catchEnd);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
isInsideCatchBlock(lineNum) {
|
|
97
|
+
return this.tryCatches.some(t => lineNum >= t.catchStart && lineNum <= t.catchEnd);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
isInsideLoop(lineNum) {
|
|
101
|
+
return this.loops.some(l => lineNum >= l.startLine && lineNum <= l.endLine);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
getLoopAt(lineNum) {
|
|
105
|
+
return this.loops.find(l => lineNum >= l.startLine && lineNum <= l.endLine) || null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
isInsideCallback(lineNum) {
|
|
109
|
+
return this.callbacks.some(cb => lineNum >= cb.startLine && lineNum <= cb.endLine);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
isInsideTest(lineNum) {
|
|
113
|
+
if (this.isTestFile) return true;
|
|
114
|
+
return this.testBlocks.some(t => lineNum >= t.startLine && lineNum <= t.endLine);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
isInsideComment(lineNum) {
|
|
118
|
+
return this.lineInsideComment[lineNum - 1] || false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
isInsideString(lineNum) {
|
|
122
|
+
return this.lineInsideString[lineNum - 1] || false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
isExported(lineNum) {
|
|
126
|
+
const fn = this.getFunctionAt(lineNum);
|
|
127
|
+
if (fn && fn.isExported) return true;
|
|
128
|
+
const cls = this.getClassAt(lineNum);
|
|
129
|
+
if (cls && cls.isExported) return true;
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
getNestingDepth(lineNum) {
|
|
134
|
+
return this.lineDepth[lineNum - 1] || 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
hasImport(moduleName) {
|
|
138
|
+
return this.importedModules.has(moduleName);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
hasAnyImport(moduleNames) {
|
|
142
|
+
return moduleNames.some(m => this.importedModules.has(m));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
getFunctionLines(fn) {
|
|
146
|
+
return fn.endLine - fn.startLine + 1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
getClassLines(cls) {
|
|
150
|
+
return cls.endLine - cls.startLine + 1;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// Main analysis
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
export function analyzeScope(content, language) {
|
|
159
|
+
const lang = language || 'javascript';
|
|
160
|
+
const ctx = new ScopeContext(content, lang);
|
|
161
|
+
|
|
162
|
+
if (isJSLike(lang)) {
|
|
163
|
+
analyzeJS(ctx);
|
|
164
|
+
} else if (isPython(lang)) {
|
|
165
|
+
analyzePython(ctx);
|
|
166
|
+
} else {
|
|
167
|
+
// Fallback: treat as JS-like
|
|
168
|
+
analyzeJS(ctx);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return ctx;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// JS / TS analysis
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
function analyzeJS(ctx) {
|
|
179
|
+
const lines = ctx.lines;
|
|
180
|
+
let inBlockComment = false;
|
|
181
|
+
let inTemplateLiteral = false;
|
|
182
|
+
let depth = 0;
|
|
183
|
+
|
|
184
|
+
// Track brace-based block stack for assigning end lines
|
|
185
|
+
// Stack entries: { type, startLine, ref } ref = the object in ctx.functions / ctx.classes etc.
|
|
186
|
+
const blockStack = [];
|
|
187
|
+
|
|
188
|
+
for (let i = 0; i < lines.length; i++) {
|
|
189
|
+
const lineNum = i + 1;
|
|
190
|
+
const line = lines[i];
|
|
191
|
+
const trimmed = line.trim();
|
|
192
|
+
|
|
193
|
+
// --- Multi-line comment tracking ---
|
|
194
|
+
if (inBlockComment) {
|
|
195
|
+
ctx.lineInsideComment[i] = true;
|
|
196
|
+
if (trimmed.includes('*/')) {
|
|
197
|
+
inBlockComment = false;
|
|
198
|
+
}
|
|
199
|
+
ctx.lineDepth[i] = depth;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (trimmed.startsWith('/*') && !trimmed.includes('*/')) {
|
|
204
|
+
inBlockComment = true;
|
|
205
|
+
ctx.lineInsideComment[i] = true;
|
|
206
|
+
ctx.lineDepth[i] = depth;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Single-line block comment /* ... */ or single-line comment //
|
|
211
|
+
if (trimmed.startsWith('//') || (trimmed.startsWith('/*') && trimmed.includes('*/'))) {
|
|
212
|
+
ctx.lineInsideComment[i] = true;
|
|
213
|
+
ctx.lineDepth[i] = depth;
|
|
214
|
+
// Still parse for depth changes on comment lines that contain braces? No, skip.
|
|
215
|
+
parseLineForContext(ctx, line, lineNum, blockStack);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// --- Template literal tracking (simplified) ---
|
|
220
|
+
if (inTemplateLiteral) {
|
|
221
|
+
ctx.lineInsideString[i] = true;
|
|
222
|
+
if (line.includes('`')) {
|
|
223
|
+
inTemplateLiteral = false;
|
|
224
|
+
}
|
|
225
|
+
ctx.lineDepth[i] = depth;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Check for template literal start without close on same line
|
|
230
|
+
const backtickCount = (line.match(/`/g) || []).length;
|
|
231
|
+
if (backtickCount % 2 !== 0) {
|
|
232
|
+
inTemplateLiteral = true;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// --- Imports ---
|
|
236
|
+
parseJSImport(ctx, trimmed, lineNum);
|
|
237
|
+
|
|
238
|
+
// --- Exports ---
|
|
239
|
+
if (/^export\s/.test(trimmed)) {
|
|
240
|
+
const exportMatch = trimmed.match(/export\s+(?:default\s+)?(?:async\s+)?(?:function|class|const|let|var)\s+(\w+)/);
|
|
241
|
+
if (exportMatch) {
|
|
242
|
+
ctx.exportedNames.add(exportMatch[1]);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// --- Brace depth ---
|
|
247
|
+
const openBraces = (stripStringsAndComments(line).match(/\{/g) || []).length;
|
|
248
|
+
const closeBraces = (stripStringsAndComments(line).match(/\}/g) || []).length;
|
|
249
|
+
|
|
250
|
+
ctx.lineDepth[i] = depth;
|
|
251
|
+
|
|
252
|
+
// Parse line for function/class/control structures before updating depth
|
|
253
|
+
parseLineForContext(ctx, line, lineNum, blockStack);
|
|
254
|
+
|
|
255
|
+
depth += openBraces - closeBraces;
|
|
256
|
+
if (depth < 0) depth = 0;
|
|
257
|
+
|
|
258
|
+
// Close blocks when depth decreases
|
|
259
|
+
let remaining = closeBraces;
|
|
260
|
+
while (remaining > 0 && blockStack.length > 0) {
|
|
261
|
+
const top = blockStack[blockStack.length - 1];
|
|
262
|
+
if (depth <= top.depth) {
|
|
263
|
+
blockStack.pop();
|
|
264
|
+
remaining--;
|
|
265
|
+
if (top.ref) {
|
|
266
|
+
top.ref.endLine = lineNum;
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Close any remaining open blocks
|
|
275
|
+
for (const block of blockStack) {
|
|
276
|
+
if (block.ref && !block.ref.endLine) {
|
|
277
|
+
block.ref.endLine = lines.length;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Post-process try/catch: compute catchEnd by brace-matching from catchStart
|
|
282
|
+
for (const tc of ctx.tryCatches) {
|
|
283
|
+
if (tc.catchStart > 0 && tc.catchEnd === 0) {
|
|
284
|
+
tc.catchEnd = findBraceEnd(lines, tc.catchStart - 1);
|
|
285
|
+
}
|
|
286
|
+
// If catchEnd still 0, set it equal to catchStart
|
|
287
|
+
if (tc.catchEnd === 0 && tc.catchStart > 0) {
|
|
288
|
+
tc.catchEnd = tc.catchStart;
|
|
289
|
+
}
|
|
290
|
+
// Compute catchBodyLines (lines between catch { and })
|
|
291
|
+
if (tc.catchStart > 0 && tc.catchEnd > 0) {
|
|
292
|
+
tc.catchBodyLines = Math.max(0, tc.catchEnd - tc.catchStart - 1);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Detect test file
|
|
297
|
+
ctx.isTestFile = detectTestFile(ctx);
|
|
298
|
+
|
|
299
|
+
// Build test blocks
|
|
300
|
+
detectTestBlocks(ctx);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Find the line number where the brace block starting at startIdx closes.
|
|
305
|
+
* startIdx is 0-based line index. Returns 1-based line number.
|
|
306
|
+
*/
|
|
307
|
+
function findBraceEnd(lines, startIdx) {
|
|
308
|
+
let depth = 0;
|
|
309
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
310
|
+
const cleaned = stripStringsAndComments(lines[i]);
|
|
311
|
+
const opens = (cleaned.match(/\{/g) || []).length;
|
|
312
|
+
const closes = (cleaned.match(/\}/g) || []).length;
|
|
313
|
+
depth += opens - closes;
|
|
314
|
+
if (depth <= 0 && opens + closes > 0 && i > startIdx) {
|
|
315
|
+
return i + 1; // 1-based
|
|
316
|
+
}
|
|
317
|
+
// Handle single-line: try { ... } catch(e) { ... }
|
|
318
|
+
if (i === startIdx && opens > 0 && depth <= 0) {
|
|
319
|
+
return i + 1;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return lines.length;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function stripStringsAndComments(line) {
|
|
326
|
+
// Remove string literals and comments to count braces accurately
|
|
327
|
+
let result = '';
|
|
328
|
+
let inSingle = false;
|
|
329
|
+
let inDouble = false;
|
|
330
|
+
let inTemplate = false;
|
|
331
|
+
let escape = false;
|
|
332
|
+
|
|
333
|
+
for (let i = 0; i < line.length; i++) {
|
|
334
|
+
const ch = line[i];
|
|
335
|
+
if (escape) { escape = false; continue; }
|
|
336
|
+
if (ch === '\\') { escape = true; continue; }
|
|
337
|
+
|
|
338
|
+
if (!inSingle && !inDouble && !inTemplate) {
|
|
339
|
+
if (ch === '/' && line[i + 1] === '/') break; // rest is comment
|
|
340
|
+
if (ch === '/' && line[i + 1] === '*') {
|
|
341
|
+
// skip to */
|
|
342
|
+
const end = line.indexOf('*/', i + 2);
|
|
343
|
+
if (end >= 0) { i = end + 1; continue; }
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
if (ch === "'") { inSingle = true; continue; }
|
|
347
|
+
if (ch === '"') { inDouble = true; continue; }
|
|
348
|
+
if (ch === '`') { inTemplate = true; continue; }
|
|
349
|
+
result += ch;
|
|
350
|
+
} else {
|
|
351
|
+
if (inSingle && ch === "'") inSingle = false;
|
|
352
|
+
if (inDouble && ch === '"') inDouble = false;
|
|
353
|
+
if (inTemplate && ch === '`') inTemplate = false;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function parseJSImport(ctx, trimmed, lineNum) {
|
|
360
|
+
// import X from 'mod'
|
|
361
|
+
let m = trimmed.match(/^import\s+.*\s+from\s+['"]([^'"]+)['"]/);
|
|
362
|
+
if (m) {
|
|
363
|
+
ctx.imports.push({ module: m[1], line: lineNum });
|
|
364
|
+
ctx.importedModules.add(m[1]);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
// import 'mod'
|
|
368
|
+
m = trimmed.match(/^import\s+['"]([^'"]+)['"]/);
|
|
369
|
+
if (m) {
|
|
370
|
+
ctx.imports.push({ module: m[1], line: lineNum });
|
|
371
|
+
ctx.importedModules.add(m[1]);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
// const X = require('mod')
|
|
375
|
+
m = trimmed.match(/(?:const|let|var)\s+\w+\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
376
|
+
if (m) {
|
|
377
|
+
ctx.imports.push({ module: m[1], line: lineNum });
|
|
378
|
+
ctx.importedModules.add(m[1]);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
// require('mod') standalone
|
|
382
|
+
m = trimmed.match(/^require\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
383
|
+
if (m) {
|
|
384
|
+
ctx.imports.push({ module: m[1], line: lineNum });
|
|
385
|
+
ctx.importedModules.add(m[1]);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function parseLineForContext(ctx, line, lineNum, blockStack) {
|
|
390
|
+
const trimmed = line.trim();
|
|
391
|
+
const depth = ctx.lineDepth[lineNum - 1];
|
|
392
|
+
|
|
393
|
+
// --- Function declarations ---
|
|
394
|
+
// async function name(...) / function name(...)
|
|
395
|
+
let m = trimmed.match(/^(export\s+(?:default\s+)?)?(async\s+)?function\s*(\*?\s*\w+)?\s*\(([^)]*)\)/);
|
|
396
|
+
if (m) {
|
|
397
|
+
const isExported = !!m[1] || ctx.exportedNames.has((m[3] || '').trim());
|
|
398
|
+
const fn = {
|
|
399
|
+
name: (m[3] || '<anonymous>').trim(),
|
|
400
|
+
startLine: lineNum,
|
|
401
|
+
endLine: lineNum, // will be updated when block closes
|
|
402
|
+
params: parseParams(m[4]),
|
|
403
|
+
paramCount: parseParams(m[4]).length,
|
|
404
|
+
isAsync: !!m[2],
|
|
405
|
+
isExported,
|
|
406
|
+
isArrow: false,
|
|
407
|
+
isMethod: false,
|
|
408
|
+
};
|
|
409
|
+
ctx.functions.push(fn);
|
|
410
|
+
if (trimmed.includes('{')) {
|
|
411
|
+
blockStack.push({ type: 'function', depth, ref: fn });
|
|
412
|
+
}
|
|
413
|
+
detectRouteOrMiddleware(ctx, fn, trimmed, lineNum);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// const name = (async) (...) => { ... }
|
|
418
|
+
m = trimmed.match(/^(export\s+(?:default\s+)?)?(?:const|let|var)\s+(\w+)\s*=\s*(async\s+)?(?:\([^)]*\)|[a-zA-Z_]\w*)\s*=>/);
|
|
419
|
+
if (m) {
|
|
420
|
+
const paramMatch = trimmed.match(/=\s*(?:async\s+)?\(([^)]*)\)\s*=>/);
|
|
421
|
+
const params = paramMatch ? parseParams(paramMatch[1]) : [];
|
|
422
|
+
const isExported = !!m[1] || ctx.exportedNames.has(m[2]);
|
|
423
|
+
const fn = {
|
|
424
|
+
name: m[2],
|
|
425
|
+
startLine: lineNum,
|
|
426
|
+
endLine: lineNum,
|
|
427
|
+
params,
|
|
428
|
+
paramCount: params.length,
|
|
429
|
+
isAsync: !!m[3],
|
|
430
|
+
isExported,
|
|
431
|
+
isArrow: true,
|
|
432
|
+
isMethod: false,
|
|
433
|
+
};
|
|
434
|
+
ctx.functions.push(fn);
|
|
435
|
+
if (trimmed.includes('{')) {
|
|
436
|
+
blockStack.push({ type: 'function', depth, ref: fn });
|
|
437
|
+
}
|
|
438
|
+
detectRouteOrMiddleware(ctx, fn, trimmed, lineNum);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Method: name(...) { or async name(...) {
|
|
443
|
+
m = trimmed.match(/^(async\s+)?(\w+)\s*\(([^)]*)\)\s*\{/);
|
|
444
|
+
if (m && !['if', 'for', 'while', 'switch', 'catch', 'with'].includes(m[2])) {
|
|
445
|
+
const fn = {
|
|
446
|
+
name: m[2],
|
|
447
|
+
startLine: lineNum,
|
|
448
|
+
endLine: lineNum,
|
|
449
|
+
params: parseParams(m[3]),
|
|
450
|
+
paramCount: parseParams(m[3]).length,
|
|
451
|
+
isAsync: !!m[1],
|
|
452
|
+
isExported: false,
|
|
453
|
+
isArrow: false,
|
|
454
|
+
isMethod: true,
|
|
455
|
+
};
|
|
456
|
+
ctx.functions.push(fn);
|
|
457
|
+
blockStack.push({ type: 'function', depth, ref: fn });
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// --- Class ---
|
|
462
|
+
m = trimmed.match(/^(export\s+(?:default\s+)?)?class\s+(\w+)/);
|
|
463
|
+
if (m) {
|
|
464
|
+
const cls = {
|
|
465
|
+
name: m[2],
|
|
466
|
+
startLine: lineNum,
|
|
467
|
+
endLine: lineNum,
|
|
468
|
+
isExported: !!m[1] || ctx.exportedNames.has(m[2]),
|
|
469
|
+
};
|
|
470
|
+
ctx.classes.push(cls);
|
|
471
|
+
if (trimmed.includes('{')) {
|
|
472
|
+
blockStack.push({ type: 'class', depth, ref: cls });
|
|
473
|
+
}
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// --- Try/catch ---
|
|
478
|
+
if (/^\s*try\s*\{/.test(line) || trimmed === 'try {' || trimmed === 'try{') {
|
|
479
|
+
const tc = { tryStart: lineNum, catchStart: 0, catchEnd: 0, catchBodyLines: 0 };
|
|
480
|
+
ctx.tryCatches.push(tc);
|
|
481
|
+
blockStack.push({ type: 'try', depth, ref: tc });
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (/^\s*\}\s*catch\s*/.test(line) || /catch\s*\(/.test(trimmed)) {
|
|
486
|
+
// Find the most recent try block
|
|
487
|
+
const tc = ctx.tryCatches[ctx.tryCatches.length - 1];
|
|
488
|
+
if (tc && tc.catchStart === 0) {
|
|
489
|
+
tc.catchStart = lineNum;
|
|
490
|
+
// We need to count catch body lines. The catch block ref will update endLine.
|
|
491
|
+
const catchRef = { endLine: lineNum };
|
|
492
|
+
blockStack.push({ type: 'catch', depth, ref: catchRef });
|
|
493
|
+
// Store catch ref so we can compute body lines later
|
|
494
|
+
tc._catchRef = catchRef;
|
|
495
|
+
}
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// --- Loops ---
|
|
500
|
+
if (/^\s*(for|while|do)\s*[\({]/.test(line) || /^\s*for\s+/.test(line)) {
|
|
501
|
+
const loopType = trimmed.startsWith('for') ? 'for' : trimmed.startsWith('while') ? 'while' : 'do';
|
|
502
|
+
const loop = { startLine: lineNum, endLine: lineNum, type: loopType };
|
|
503
|
+
ctx.loops.push(loop);
|
|
504
|
+
if (trimmed.includes('{')) {
|
|
505
|
+
blockStack.push({ type: 'loop', depth, ref: loop });
|
|
506
|
+
}
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// --- Callbacks (function passed as argument) ---
|
|
511
|
+
if (/\.\w+\s*\(\s*(async\s+)?(?:function|\([^)]*\)\s*=>|\w+\s*=>)/.test(trimmed)) {
|
|
512
|
+
const cb = { startLine: lineNum, endLine: lineNum };
|
|
513
|
+
ctx.callbacks.push(cb);
|
|
514
|
+
if (trimmed.includes('{')) {
|
|
515
|
+
blockStack.push({ type: 'callback', depth, ref: cb });
|
|
516
|
+
}
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// --- Route handlers: app.get('/path', ...) / router.post(...) ---
|
|
521
|
+
m = trimmed.match(/(?:app|router)\.(get|post|put|patch|delete|use|all)\s*\(\s*['"]([^'"]*)['"]/);
|
|
522
|
+
if (m) {
|
|
523
|
+
const handler = { startLine: lineNum, endLine: lineNum, method: m[1], path: m[2] };
|
|
524
|
+
ctx.routeHandlers.push(handler);
|
|
525
|
+
if (trimmed.includes('{')) {
|
|
526
|
+
blockStack.push({ type: 'route', depth, ref: handler });
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function detectRouteOrMiddleware(ctx, fn, trimmed, lineNum) {
|
|
532
|
+
// Detect middleware pattern: (req, res, next) or (req, res)
|
|
533
|
+
const hasReqRes = fn.params.some(p => p === 'req' || p === 'request') &&
|
|
534
|
+
fn.params.some(p => p === 'res' || p === 'response');
|
|
535
|
+
if (hasReqRes) {
|
|
536
|
+
fn.isRouteHandler = true;
|
|
537
|
+
fn.isMiddleware = fn.params.some(p => p === 'next');
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function parseParams(paramStr) {
|
|
542
|
+
if (!paramStr || !paramStr.trim()) return [];
|
|
543
|
+
return paramStr.split(',')
|
|
544
|
+
.map(p => p.trim().replace(/\s*=.*$/, '').replace(/:\s*\w+.*$/, '').replace(/\.\.\./, ''))
|
|
545
|
+
.filter(Boolean);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function detectTestFile(ctx) {
|
|
549
|
+
// Check imports for test frameworks
|
|
550
|
+
const testModules = ['jest', 'mocha', 'vitest', 'node:test', 'ava', 'tap', '@jest/globals'];
|
|
551
|
+
for (const imp of ctx.imports) {
|
|
552
|
+
if (testModules.includes(imp.module)) return true;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Check for describe/it/test at top level
|
|
556
|
+
for (let i = 0; i < Math.min(ctx.lines.length, 30); i++) {
|
|
557
|
+
const line = ctx.lines[i].trim();
|
|
558
|
+
if (/^(describe|it|test)\s*\(/.test(line)) return true;
|
|
559
|
+
if (/^def\s+test_/.test(line)) return true;
|
|
560
|
+
if (/@Test/.test(line)) return true;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function detectTestBlocks(ctx) {
|
|
567
|
+
// Look for describe/it/test blocks
|
|
568
|
+
for (let i = 0; i < ctx.lines.length; i++) {
|
|
569
|
+
const line = ctx.lines[i].trim();
|
|
570
|
+
if (/^(?:describe|it|test|beforeEach|afterEach|beforeAll|afterAll)\s*\(/.test(line)) {
|
|
571
|
+
// Find matching function in callbacks or just estimate end
|
|
572
|
+
const cb = ctx.callbacks.find(c => c.startLine === i + 1);
|
|
573
|
+
if (cb) {
|
|
574
|
+
ctx.testBlocks.push({ startLine: i + 1, endLine: cb.endLine });
|
|
575
|
+
} else {
|
|
576
|
+
// Estimate: look for the block end
|
|
577
|
+
ctx.testBlocks.push({ startLine: i + 1, endLine: i + 1 });
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// ---------------------------------------------------------------------------
|
|
584
|
+
// Python analysis (simplified)
|
|
585
|
+
// ---------------------------------------------------------------------------
|
|
586
|
+
|
|
587
|
+
function analyzePython(ctx) {
|
|
588
|
+
const lines = ctx.lines;
|
|
589
|
+
let inDocstring = false;
|
|
590
|
+
let inComment = false;
|
|
591
|
+
const indentStack = [0];
|
|
592
|
+
|
|
593
|
+
for (let i = 0; i < lines.length; i++) {
|
|
594
|
+
const lineNum = i + 1;
|
|
595
|
+
const line = lines[i];
|
|
596
|
+
const trimmed = line.trim();
|
|
597
|
+
|
|
598
|
+
// Track indentation depth
|
|
599
|
+
const indent = line.search(/\S/);
|
|
600
|
+
const depth = indent >= 0 ? Math.floor(indent / 4) : 0;
|
|
601
|
+
ctx.lineDepth[i] = depth;
|
|
602
|
+
|
|
603
|
+
// Docstrings
|
|
604
|
+
if (inDocstring) {
|
|
605
|
+
ctx.lineInsideString[i] = true;
|
|
606
|
+
if (trimmed.includes('"""') || trimmed.includes("'''")) {
|
|
607
|
+
inDocstring = false;
|
|
608
|
+
}
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if ((trimmed.startsWith('"""') || trimmed.startsWith("'''")) &&
|
|
613
|
+
!trimmed.endsWith('"""', trimmed.length) && !trimmed.endsWith("'''", trimmed.length)) {
|
|
614
|
+
const quote = trimmed.slice(0, 3);
|
|
615
|
+
if ((trimmed.match(new RegExp(quote.replace(/'/g, "\\'"), 'g')) || []).length === 1) {
|
|
616
|
+
inDocstring = true;
|
|
617
|
+
ctx.lineInsideString[i] = true;
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Comments
|
|
623
|
+
if (trimmed.startsWith('#')) {
|
|
624
|
+
ctx.lineInsideComment[i] = true;
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Imports
|
|
629
|
+
let m = trimmed.match(/^(?:from\s+(\S+)\s+)?import\s+(\S+)/);
|
|
630
|
+
if (m) {
|
|
631
|
+
const mod = m[1] || m[2];
|
|
632
|
+
ctx.imports.push({ module: mod, line: lineNum });
|
|
633
|
+
ctx.importedModules.add(mod);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Functions
|
|
637
|
+
m = trimmed.match(/^(async\s+)?def\s+(\w+)\s*\(([^)]*)\)/);
|
|
638
|
+
if (m) {
|
|
639
|
+
const fn = {
|
|
640
|
+
name: m[2],
|
|
641
|
+
startLine: lineNum,
|
|
642
|
+
endLine: lineNum, // will be approximated
|
|
643
|
+
params: parseParams(m[3]),
|
|
644
|
+
paramCount: parseParams(m[3]).length,
|
|
645
|
+
isAsync: !!m[1],
|
|
646
|
+
isExported: !m[2].startsWith('_'),
|
|
647
|
+
isArrow: false,
|
|
648
|
+
isMethod: false,
|
|
649
|
+
};
|
|
650
|
+
// Find end line by indentation
|
|
651
|
+
fn.endLine = findPythonBlockEnd(lines, i);
|
|
652
|
+
ctx.functions.push(fn);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Classes
|
|
656
|
+
m = trimmed.match(/^class\s+(\w+)/);
|
|
657
|
+
if (m) {
|
|
658
|
+
const cls = { name: m[1], startLine: lineNum, endLine: lineNum, isExported: !m[1].startsWith('_') };
|
|
659
|
+
cls.endLine = findPythonBlockEnd(lines, i);
|
|
660
|
+
ctx.classes.push(cls);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Test functions
|
|
664
|
+
if (/^def\s+test_/.test(trimmed)) {
|
|
665
|
+
ctx.isTestFile = true;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function findPythonBlockEnd(lines, startIdx) {
|
|
671
|
+
const startIndent = lines[startIdx].search(/\S/);
|
|
672
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
673
|
+
const line = lines[i];
|
|
674
|
+
if (line.trim() === '') continue;
|
|
675
|
+
const indent = line.search(/\S/);
|
|
676
|
+
if (indent <= startIndent) return i; // line i is no longer in the block
|
|
677
|
+
}
|
|
678
|
+
return lines.length;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// ---------------------------------------------------------------------------
|
|
682
|
+
// Re-exports for convenience
|
|
683
|
+
// ---------------------------------------------------------------------------
|
|
684
|
+
|
|
685
|
+
export { ScopeContext, languageFromPath };
|