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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +181 -0
  3. package/bin/doorman.js +444 -0
  4. package/package.json +74 -0
  5. package/src/ai-fixer.js +559 -0
  6. package/src/ast-scanner.js +434 -0
  7. package/src/auth.js +149 -0
  8. package/src/baseline.js +48 -0
  9. package/src/compliance.js +539 -0
  10. package/src/config.js +466 -0
  11. package/src/custom-rules.js +32 -0
  12. package/src/dashboard.js +202 -0
  13. package/src/detector.js +142 -0
  14. package/src/fix-engine.js +48 -0
  15. package/src/fix-registry-extra.js +95 -0
  16. package/src/fix-registry-go-rust.js +77 -0
  17. package/src/fix-registry-java-csharp.js +77 -0
  18. package/src/fix-registry-js.js +99 -0
  19. package/src/fix-registry-mcp-ai.js +57 -0
  20. package/src/fix-registry-python.js +87 -0
  21. package/src/fixer-ruby-php.js +608 -0
  22. package/src/fixer.js +2113 -0
  23. package/src/hooks.js +115 -0
  24. package/src/ignore.js +176 -0
  25. package/src/index.js +384 -0
  26. package/src/metrics.js +126 -0
  27. package/src/monorepo.js +65 -0
  28. package/src/presets.js +54 -0
  29. package/src/reporter.js +975 -0
  30. package/src/rule-worker.js +36 -0
  31. package/src/rules/ast-rules.js +756 -0
  32. package/src/rules/bugs/accessibility.js +235 -0
  33. package/src/rules/bugs/ai-codegen-fixable.js +172 -0
  34. package/src/rules/bugs/ai-codegen.js +365 -0
  35. package/src/rules/bugs/code-smell-bugs.js +247 -0
  36. package/src/rules/bugs/crypto-bugs.js +195 -0
  37. package/src/rules/bugs/docker-bugs.js +158 -0
  38. package/src/rules/bugs/general.js +361 -0
  39. package/src/rules/bugs/go-bugs.js +279 -0
  40. package/src/rules/bugs/index.js +73 -0
  41. package/src/rules/bugs/js-api.js +257 -0
  42. package/src/rules/bugs/js-array-object.js +210 -0
  43. package/src/rules/bugs/js-async-fixable.js +223 -0
  44. package/src/rules/bugs/js-async.js +211 -0
  45. package/src/rules/bugs/js-closure-scope.js +182 -0
  46. package/src/rules/bugs/js-database.js +203 -0
  47. package/src/rules/bugs/js-error-handling.js +148 -0
  48. package/src/rules/bugs/js-logic.js +261 -0
  49. package/src/rules/bugs/js-memory.js +214 -0
  50. package/src/rules/bugs/js-node.js +361 -0
  51. package/src/rules/bugs/js-react.js +373 -0
  52. package/src/rules/bugs/js-regex.js +200 -0
  53. package/src/rules/bugs/js-state.js +272 -0
  54. package/src/rules/bugs/js-type-coercion.js +318 -0
  55. package/src/rules/bugs/nextjs-bugs.js +242 -0
  56. package/src/rules/bugs/nextjs-fixable.js +120 -0
  57. package/src/rules/bugs/node-fixable.js +178 -0
  58. package/src/rules/bugs/python-advanced.js +245 -0
  59. package/src/rules/bugs/python-fixable.js +98 -0
  60. package/src/rules/bugs/python.js +284 -0
  61. package/src/rules/bugs/react-fixable.js +207 -0
  62. package/src/rules/bugs/ruby-bugs.js +182 -0
  63. package/src/rules/bugs/shell-bugs.js +181 -0
  64. package/src/rules/bugs/silent-failures.js +261 -0
  65. package/src/rules/bugs/ts-bugs.js +235 -0
  66. package/src/rules/bugs/unused-vars.js +65 -0
  67. package/src/rules/compliance/accessibility-ext.js +468 -0
  68. package/src/rules/compliance/education.js +322 -0
  69. package/src/rules/compliance/financial.js +421 -0
  70. package/src/rules/compliance/frameworks.js +507 -0
  71. package/src/rules/compliance/healthcare.js +520 -0
  72. package/src/rules/compliance/index.js +2714 -0
  73. package/src/rules/compliance/regional-eu.js +480 -0
  74. package/src/rules/compliance/regional-international.js +903 -0
  75. package/src/rules/cost/index.js +1993 -0
  76. package/src/rules/data/index.js +2503 -0
  77. package/src/rules/dependencies/index.js +1684 -0
  78. package/src/rules/deployment/index.js +2050 -0
  79. package/src/rules/index.js +71 -0
  80. package/src/rules/infrastructure/index.js +3048 -0
  81. package/src/rules/performance/index.js +3455 -0
  82. package/src/rules/quality/index.js +3175 -0
  83. package/src/rules/reliability/index.js +3040 -0
  84. package/src/rules/scope-rules.js +815 -0
  85. package/src/rules/security/ai-api.js +1177 -0
  86. package/src/rules/security/auth.js +1328 -0
  87. package/src/rules/security/cors.js +127 -0
  88. package/src/rules/security/crypto.js +527 -0
  89. package/src/rules/security/csharp.js +862 -0
  90. package/src/rules/security/csrf.js +193 -0
  91. package/src/rules/security/dart.js +835 -0
  92. package/src/rules/security/deserialization.js +291 -0
  93. package/src/rules/security/file-upload.js +187 -0
  94. package/src/rules/security/go.js +850 -0
  95. package/src/rules/security/headers.js +235 -0
  96. package/src/rules/security/index.js +65 -0
  97. package/src/rules/security/injection.js +1639 -0
  98. package/src/rules/security/mcp-server.js +71 -0
  99. package/src/rules/security/misconfiguration.js +660 -0
  100. package/src/rules/security/oauth-jwt.js +329 -0
  101. package/src/rules/security/path-traversal.js +295 -0
  102. package/src/rules/security/php.js +1054 -0
  103. package/src/rules/security/prototype-pollution.js +283 -0
  104. package/src/rules/security/rate-limiting.js +208 -0
  105. package/src/rules/security/ruby.js +1061 -0
  106. package/src/rules/security/rust.js +693 -0
  107. package/src/rules/security/secrets.js +747 -0
  108. package/src/rules/security/shell.js +647 -0
  109. package/src/rules/security/ssrf.js +298 -0
  110. package/src/rules/security/supply-chain-advanced.js +393 -0
  111. package/src/rules/security/supply-chain.js +734 -0
  112. package/src/rules/security/swift.js +835 -0
  113. package/src/rules/security/taint.js +27 -0
  114. package/src/rules/security/xss.js +520 -0
  115. package/src/scan-cache.js +71 -0
  116. package/src/scanner.js +710 -0
  117. package/src/scope-analyzer.js +685 -0
  118. package/src/share.js +88 -0
  119. package/src/taint.js +300 -0
  120. package/src/telemetry.js +183 -0
  121. package/src/tracer.js +190 -0
  122. package/src/upload.js +35 -0
  123. 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 };