@iris-code/core 0.1.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 (94) hide show
  1. package/README.md +38 -0
  2. package/dist/config.d.ts +147 -0
  3. package/dist/config.d.ts.map +1 -0
  4. package/dist/config.js +550 -0
  5. package/dist/config.js.map +1 -0
  6. package/dist/csharp/analyser.d.ts +14 -0
  7. package/dist/csharp/analyser.d.ts.map +1 -0
  8. package/dist/csharp/analyser.js +436 -0
  9. package/dist/csharp/analyser.js.map +1 -0
  10. package/dist/csharp/lexer.d.ts +29 -0
  11. package/dist/csharp/lexer.d.ts.map +1 -0
  12. package/dist/csharp/lexer.js +182 -0
  13. package/dist/csharp/lexer.js.map +1 -0
  14. package/dist/duplicates.d.ts +53 -0
  15. package/dist/duplicates.d.ts.map +1 -0
  16. package/dist/duplicates.js +491 -0
  17. package/dist/duplicates.js.map +1 -0
  18. package/dist/fileAnalyser.d.ts +4 -0
  19. package/dist/fileAnalyser.d.ts.map +1 -0
  20. package/dist/fileAnalyser.js +8 -0
  21. package/dist/fileAnalyser.js.map +1 -0
  22. package/dist/fileNaming.d.ts +68 -0
  23. package/dist/fileNaming.d.ts.map +1 -0
  24. package/dist/fileNaming.js +343 -0
  25. package/dist/fileNaming.js.map +1 -0
  26. package/dist/gate.d.ts +49 -0
  27. package/dist/gate.d.ts.map +1 -0
  28. package/dist/gate.js +242 -0
  29. package/dist/gate.js.map +1 -0
  30. package/dist/go/analyser.d.ts +5 -0
  31. package/dist/go/analyser.d.ts.map +1 -0
  32. package/dist/go/analyser.js +576 -0
  33. package/dist/go/analyser.js.map +1 -0
  34. package/dist/guards.d.ts +16 -0
  35. package/dist/guards.d.ts.map +1 -0
  36. package/dist/guards.js +355 -0
  37. package/dist/guards.js.map +1 -0
  38. package/dist/index.d.ts +22 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +110 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/languages.d.ts +46 -0
  43. package/dist/languages.d.ts.map +1 -0
  44. package/dist/languages.js +155 -0
  45. package/dist/languages.js.map +1 -0
  46. package/dist/lines.d.ts +9 -0
  47. package/dist/lines.d.ts.map +1 -0
  48. package/dist/lines.js +58 -0
  49. package/dist/lines.js.map +1 -0
  50. package/dist/python/analyser.d.ts +5 -0
  51. package/dist/python/analyser.d.ts.map +1 -0
  52. package/dist/python/analyser.js +630 -0
  53. package/dist/python/analyser.js.map +1 -0
  54. package/dist/qualitySignals.d.ts +30 -0
  55. package/dist/qualitySignals.d.ts.map +1 -0
  56. package/dist/qualitySignals.js +101 -0
  57. package/dist/qualitySignals.js.map +1 -0
  58. package/dist/ruby/analyser.d.ts +5 -0
  59. package/dist/ruby/analyser.d.ts.map +1 -0
  60. package/dist/ruby/analyser.js +450 -0
  61. package/dist/ruby/analyser.js.map +1 -0
  62. package/dist/scoring.d.ts +4 -0
  63. package/dist/scoring.d.ts.map +1 -0
  64. package/dist/scoring.js +37 -0
  65. package/dist/scoring.js.map +1 -0
  66. package/dist/secrets.d.ts +5 -0
  67. package/dist/secrets.d.ts.map +1 -0
  68. package/dist/secrets.js +245 -0
  69. package/dist/secrets.js.map +1 -0
  70. package/dist/securityContext.d.ts +2 -0
  71. package/dist/securityContext.d.ts.map +1 -0
  72. package/dist/securityContext.js +16 -0
  73. package/dist/securityContext.js.map +1 -0
  74. package/dist/sfc.d.ts +32 -0
  75. package/dist/sfc.d.ts.map +1 -0
  76. package/dist/sfc.js +228 -0
  77. package/dist/sfc.js.map +1 -0
  78. package/dist/slop.d.ts +37 -0
  79. package/dist/slop.d.ts.map +1 -0
  80. package/dist/slop.js +81 -0
  81. package/dist/slop.js.map +1 -0
  82. package/dist/suppressions.d.ts +19 -0
  83. package/dist/suppressions.d.ts.map +1 -0
  84. package/dist/suppressions.js +152 -0
  85. package/dist/suppressions.js.map +1 -0
  86. package/dist/ts/analyser.d.ts +4 -0
  87. package/dist/ts/analyser.d.ts.map +1 -0
  88. package/dist/ts/analyser.js +769 -0
  89. package/dist/ts/analyser.js.map +1 -0
  90. package/dist/types.d.ts +139 -0
  91. package/dist/types.d.ts.map +1 -0
  92. package/dist/types.js +46 -0
  93. package/dist/types.js.map +1 -0
  94. package/package.json +43 -0
@@ -0,0 +1,769 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyseTs = analyseTs;
4
+ const lines_1 = require("../lines");
5
+ const secrets_1 = require("../secrets");
6
+ const securityContext_1 = require("../securityContext");
7
+ const guards_1 = require("../guards");
8
+ const scoring_1 = require("../scoring");
9
+ const sfc_1 = require("../sfc");
10
+ // --- regex constants ---
11
+ const FUNCTION_DECL_RE = /(?:^|\s)(?:async\s+)?function\s+(\w+)\s*\(/gm;
12
+ // Two deliberate hardenings here, both from the 2026-08-04 security pass, which
13
+ // measured a single 200k-line file taking 127 SECONDS to analyse - past the 2s
14
+ // hard limit in TESTING.md, and a denial of service reachable by merely opening
15
+ // a file from a cloned repo (CPU profile: 3.73s of 3.78s inside METHOD_RE).
16
+ //
17
+ // 1. LEADING INDENT IS `[ \t]*`, NOT `\s*`. This was the quadratic term. `\s`
18
+ // matches newlines, so under /m the engine anchored at a line start, let
19
+ // `\s*` swallow every blank line to EOF, failed `(\w+)`, then backtracked one
20
+ // character at a time - O(filesize) per line, O(n^2) overall. `^` already
21
+ // means line start under /m, so indentation is spaces and tabs by definition
22
+ // and this is both faster and more correct.
23
+ // 2. THE PARAMETER SPAN IS BOUNDED. `[\s\S]*?` is unbounded lazy
24
+ // match-anything, so a `(` with no matching `)` scans the rest of the file on
25
+ // every attempt. The cap limits that to a fixed window.
26
+ //
27
+ // Neither change can create a false positive: both only ever narrow what
28
+ // matches, and no real signature has a 2000-character parameter list.
29
+ const PARAM_SPAN = '[\\s\\S]{0,2000}?';
30
+ const ARROW_FN_RE = new RegExp(`(?:^|\\s)(?:export\\s+)?(?:const|let)\\s+(\\w+)\\s*=\\s*(?:async\\s*)?\\(${PARAM_SPAN}\\)[ \\t]*(?::[ \\t]*\\S+[ \\t]*)?[ \\t]*=>`, 'gm');
31
+ const METHOD_RE = new RegExp(`^[ \\t]*(?:async[ \\t]+)?(\\w+)[ \\t]*\\(${PARAM_SPAN}\\)[ \\t]*(?::[ \\t]*\\S+[ \\t]*)?\\{`, 'gm');
32
+ const IMPORT_RE = /^import\s+(?:.+\s+from\s+)?['"]([^'"]+)['"]/gm;
33
+ const EXPORT_FROM_RE = /^export\s+(?:.+\s+from\s+)?['"]([^'"]+)['"]/gm;
34
+ const REQUIRE_RE = /\brequire\(\s*['"]([^'"]+)['"]\s*\)/g;
35
+ const DYNAMIC_IMPORT_RE = /\bimport\(\s*['"]([^'"]+)['"]\s*\)/g;
36
+ const BUILTIN_PREFIXES = ['.', '/', 'node:'];
37
+ const NODE_BUILTINS = new Set([
38
+ 'fs', 'path', 'os', 'crypto', 'http', 'https', 'net', 'stream',
39
+ 'events', 'util', 'url', 'querystring', 'buffer', 'child_process',
40
+ 'cluster', 'dns', 'domain', 'readline', 'repl', 'string_decoder',
41
+ 'timers', 'tls', 'tty', 'v8', 'vm', 'zlib', 'assert', 'module',
42
+ 'process', 'perf_hooks', 'worker_threads', 'async_hooks', 'inspector',
43
+ ]);
44
+ // --- helpers ---
45
+ // Replace comment text with spaces (preserving newlines so line numbers stay accurate).
46
+ // Handles //-line comments and /* block comments */ while skipping string literals.
47
+ function stripCStyleComments(source) {
48
+ let result = '';
49
+ let i = 0;
50
+ const len = source.length;
51
+ while (i < len) {
52
+ const ch = source[i];
53
+ if (ch === '"' || ch === "'" || ch === '`') {
54
+ const q = ch;
55
+ result += ch;
56
+ i++;
57
+ while (i < len) {
58
+ const c = source[i];
59
+ if (c === '\\') {
60
+ result += c + (source[i + 1] ?? '');
61
+ i += 2;
62
+ }
63
+ else {
64
+ result += c;
65
+ i++;
66
+ if (c === q)
67
+ break;
68
+ }
69
+ }
70
+ }
71
+ else if (ch === '/' && source[i + 1] === '/') {
72
+ while (i < len && source[i] !== '\n') {
73
+ result += ' ';
74
+ i++;
75
+ }
76
+ }
77
+ else if (ch === '/' && source[i + 1] === '*') {
78
+ result += ' ';
79
+ i++;
80
+ result += ' ';
81
+ i++;
82
+ while (i < len) {
83
+ if (source[i] === '*' && source[i + 1] === '/') {
84
+ result += ' ';
85
+ i += 2;
86
+ break;
87
+ }
88
+ result += source[i] === '\n' ? '\n' : ' ';
89
+ i++;
90
+ }
91
+ }
92
+ else {
93
+ result += ch;
94
+ i++;
95
+ }
96
+ }
97
+ return result;
98
+ }
99
+ function stripQuotedTextPreserveNewlines(source, quotes) {
100
+ const quoteSet = new Set(quotes);
101
+ let result = '';
102
+ let i = 0;
103
+ while (i < source.length) {
104
+ const ch = source[i];
105
+ if (!quoteSet.has(ch)) {
106
+ result += ch;
107
+ i++;
108
+ continue;
109
+ }
110
+ result += ' ';
111
+ i++;
112
+ while (i < source.length) {
113
+ const current = source[i];
114
+ if (current === '\n') {
115
+ result += '\n';
116
+ i++;
117
+ continue;
118
+ }
119
+ if (current === '\\') {
120
+ result += ' ';
121
+ if (i + 1 < source.length) {
122
+ result += source[i + 1] === '\n' ? '\n' : ' ';
123
+ }
124
+ i += 2;
125
+ continue;
126
+ }
127
+ result += ' ';
128
+ i++;
129
+ if (current === ch)
130
+ break;
131
+ }
132
+ }
133
+ return result;
134
+ }
135
+ function findFunctionEnd(lines, startLine) {
136
+ let depth = 0;
137
+ let parenDepth = 0;
138
+ let foundOpen = false;
139
+ for (let i = startLine - 1; i < lines.length; i++) {
140
+ const line = lines[i];
141
+ let j = 0;
142
+ while (j < line.length) {
143
+ const ch = line[j];
144
+ if (ch === '"' || ch === "'" || ch === '`') {
145
+ const q = ch;
146
+ j++;
147
+ while (j < line.length && line[j] !== q) {
148
+ if (line[j] === '\\')
149
+ j++;
150
+ j++;
151
+ }
152
+ }
153
+ else if (ch === '/' && line[j + 1] === '/') {
154
+ break;
155
+ }
156
+ else if (ch === '(') {
157
+ parenDepth++;
158
+ }
159
+ else if (ch === ')') {
160
+ parenDepth = Math.max(0, parenDepth - 1);
161
+ }
162
+ else if (ch === '{' && parenDepth === 0) {
163
+ // Braces inside parentheses are destructured parameters, object
164
+ // arguments, or JSX expression containers nested in call arguments -
165
+ // never the function body. Counting them ended components like
166
+ // `function F({ props }: Props)` on their own first line, so
167
+ // function-too-long could never fire for React components (bug found
168
+ // via the 5.10 testbed, fixed 2026-07-19). Known remaining edge: an
169
+ // object-literal RETURN TYPE (`): { x: number } {`) still closes at
170
+ // depth 0 early - rare enough to accept.
171
+ depth++;
172
+ foundOpen = true;
173
+ }
174
+ else if (ch === '}' && parenDepth === 0 && foundOpen) {
175
+ depth--;
176
+ if (depth === 0)
177
+ return i + 1;
178
+ }
179
+ j++;
180
+ }
181
+ }
182
+ return startLine;
183
+ }
184
+ // Returns a copy of `source` in which only regex SOURCE survives - the body of
185
+ // a `/.../ ` regex literal and the string argument of a `RegExp(...)` /
186
+ // `new RegExp(...)` call - with every other character blanked to a space
187
+ // (newlines preserved so offsets and line numbers stay exact). The ReDoS
188
+ // heuristic runs over this so arithmetic like `(a+b)*c` can never be mistaken
189
+ // for a nested-quantifier regex (5.11 benchmark FP unsafe-regex/negative/
190
+ // math_helpers.ts). A `/` is read as a regex start only when the previous
191
+ // significant character cannot end an expression - the standard divide-vs-regex
192
+ // disambiguation; when uncertain it favours NOT treating `/` as regex, so the
193
+ // only failure mode is a missed regex (recall, guarded by the benchmark
194
+ // positives), never a new false positive.
195
+ function regexOnlySource(source) {
196
+ const out = [];
197
+ const n = source.length;
198
+ let i = 0;
199
+ let prevSig = '';
200
+ const EXPR_START = '([{,;:!&|=?+-*%^~<>';
201
+ const blank = (ch) => (ch === '\n' ? '\n' : ' ');
202
+ while (i < n) {
203
+ const ch = source[i];
204
+ if (ch === '/' && source[i + 1] === '/') {
205
+ while (i < n && source[i] !== '\n') {
206
+ out.push(' ');
207
+ i++;
208
+ }
209
+ continue;
210
+ }
211
+ if (ch === '/' && source[i + 1] === '*') {
212
+ out.push(' ', ' ');
213
+ i += 2;
214
+ while (i < n && !(source[i] === '*' && source[i + 1] === '/')) {
215
+ out.push(blank(source[i]));
216
+ i++;
217
+ }
218
+ if (i < n) {
219
+ out.push(' ', ' ');
220
+ i += 2;
221
+ }
222
+ continue;
223
+ }
224
+ if (ch === '"' || ch === "'" || ch === '`') {
225
+ const q = ch;
226
+ const before = source.slice(Math.max(0, i - 48), i);
227
+ const isRegexArg = /RegExp\s*\(\s*$/.test(before);
228
+ out.push(' ');
229
+ i++; // opening quote
230
+ while (i < n) {
231
+ const c = source[i];
232
+ if (c === '\\') {
233
+ out.push(isRegexArg ? c : ' ');
234
+ if (i + 1 < n)
235
+ out.push(isRegexArg ? source[i + 1] : blank(source[i + 1]));
236
+ i += 2;
237
+ continue;
238
+ }
239
+ if (c === q) {
240
+ out.push(' ');
241
+ i++;
242
+ break;
243
+ }
244
+ out.push(isRegexArg ? (c === '\n' ? '\n' : c) : blank(c));
245
+ i++;
246
+ }
247
+ prevSig = q;
248
+ continue;
249
+ }
250
+ if (ch === '/') {
251
+ const startsRegex = prevSig === '' || EXPR_START.includes(prevSig);
252
+ if (startsRegex) {
253
+ out.push('/');
254
+ i++;
255
+ let inClass = false;
256
+ while (i < n) {
257
+ const c = source[i];
258
+ if (c === '\n')
259
+ break;
260
+ if (c === '\\') {
261
+ out.push(c);
262
+ if (i + 1 < n)
263
+ out.push(source[i + 1]);
264
+ i += 2;
265
+ continue;
266
+ }
267
+ if (c === '[')
268
+ inClass = true;
269
+ else if (c === ']')
270
+ inClass = false;
271
+ out.push(c);
272
+ i++;
273
+ if (c === '/' && !inClass)
274
+ break;
275
+ }
276
+ while (i < n && /[a-z]/i.test(source[i])) {
277
+ out.push(' ');
278
+ i++;
279
+ }
280
+ prevSig = '/';
281
+ continue;
282
+ }
283
+ out.push(' ');
284
+ prevSig = '/';
285
+ i++;
286
+ continue;
287
+ }
288
+ out.push(blank(ch));
289
+ if (!/\s/.test(ch))
290
+ prevSig = ch;
291
+ i++;
292
+ }
293
+ return out.join('');
294
+ }
295
+ function isThirdParty(specifier) {
296
+ if (BUILTIN_PREFIXES.some(p => specifier.startsWith(p)))
297
+ return false;
298
+ const root = specifier.startsWith('@')
299
+ ? specifier.split('/').slice(0, 2).join('/')
300
+ : specifier.split('/')[0];
301
+ return !NODE_BUILTINS.has(root);
302
+ }
303
+ // --- unused import detection ---
304
+ function extractImportBindings(importText, startLine) {
305
+ const bindings = [];
306
+ if (/^import\s*['"]/.test(importText.trim()))
307
+ return bindings;
308
+ const normalized = importText.replace(/\bimport\s+type\s+/, 'import ');
309
+ const fromMatch = /\bfrom\s+['"]/.exec(normalized);
310
+ if (!fromMatch)
311
+ return bindings;
312
+ const importKw = normalized.indexOf('import ');
313
+ const clause = normalized.slice(importKw + 'import '.length, fromMatch.index).trim();
314
+ const nsMatch = /\*\s+as\s+(\w+)/.exec(clause);
315
+ if (nsMatch)
316
+ bindings.push({ name: nsMatch[1], line: startLine });
317
+ const namedMatch = /\{([^}]*)\}/.exec(clause);
318
+ if (namedMatch) {
319
+ for (const part of namedMatch[1].split(',')) {
320
+ const t = part.trim().replace(/^type\s+/, '');
321
+ if (!t)
322
+ continue;
323
+ const asMatch = /^(\w+)\s+as\s+(\w+)$/.exec(t);
324
+ const localName = asMatch ? asMatch[2] : t.split(/\s+/)[0];
325
+ if (localName && /^\w+$/.test(localName))
326
+ bindings.push({ name: localName, line: startLine });
327
+ }
328
+ }
329
+ const defaultPart = clause
330
+ .replace(/\{[^}]*\}/, '')
331
+ .replace(/\*\s+as\s+\w+/, '')
332
+ .replace(/,/g, ' ')
333
+ .trim();
334
+ if (defaultPart && /^\w+$/.test(defaultPart))
335
+ bindings.push({ name: defaultPart, line: startLine });
336
+ return bindings;
337
+ }
338
+ function detectUnusedImports(source) {
339
+ const lines = source.split('\n');
340
+ const allBindings = [];
341
+ const importLineSet = new Set();
342
+ let i = 0;
343
+ while (i < lines.length) {
344
+ const trimmed = lines[i].trimStart();
345
+ if (/^import(\s+type)?\s/.test(trimmed)) {
346
+ const startLine = i + 1;
347
+ importLineSet.add(i);
348
+ let importText = lines[i];
349
+ while (!/\bfrom\s+['"]/.test(importText) && i + 1 < lines.length) {
350
+ i++;
351
+ importLineSet.add(i);
352
+ importText += ' ' + lines[i];
353
+ }
354
+ allBindings.push(...extractImportBindings(importText, startLine));
355
+ }
356
+ i++;
357
+ }
358
+ const body = lines.map((l, idx) => importLineSet.has(idx) ? '' : l).join('\n');
359
+ return allBindings.filter(b => {
360
+ const re = new RegExp(`\\b${b.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`);
361
+ return !re.test(body);
362
+ });
363
+ }
364
+ // --- core detectors ---
365
+ function detectFunctions(source) {
366
+ const stripped = stripCStyleComments(source);
367
+ const results = [];
368
+ const NOISE = new Set(['if', 'for', 'while', 'switch', 'catch', 'return']);
369
+ for (const re of [FUNCTION_DECL_RE, ARROW_FN_RE, METHOD_RE]) {
370
+ re.lastIndex = 0;
371
+ let match;
372
+ while ((match = re.exec(stripped)) !== null) {
373
+ const name = match[1];
374
+ if (NOISE.has(name))
375
+ continue;
376
+ results.push({ name, line: (0, lines_1.lineOf)(source, match.index) });
377
+ }
378
+ }
379
+ const seen = new Set();
380
+ return results.filter(f => {
381
+ const key = `${f.name}:${f.line}`;
382
+ if (seen.has(key))
383
+ return false;
384
+ seen.add(key);
385
+ return true;
386
+ }).sort((a, b) => a.line - b.line);
387
+ }
388
+ function extractThirdPartyImports(source) {
389
+ const imports = new Set();
390
+ const patterns = [IMPORT_RE, EXPORT_FROM_RE, REQUIRE_RE, DYNAMIC_IMPORT_RE];
391
+ for (const pattern of patterns) {
392
+ pattern.lastIndex = 0;
393
+ let match;
394
+ while ((match = pattern.exec(source)) !== null) {
395
+ const specifier = match[1];
396
+ if (isThirdParty(specifier)) {
397
+ const pkg = specifier.startsWith('@')
398
+ ? specifier.split('/').slice(0, 2).join('/')
399
+ : specifier.split('/')[0];
400
+ imports.add(pkg);
401
+ }
402
+ }
403
+ }
404
+ return [...imports].sort();
405
+ }
406
+ function scoreComplexity(source, codeLines, functions, thirdPartyImports) {
407
+ let score = 1;
408
+ const fnDensity = codeLines > 0 ? functions.length / (codeLines / 50) : 0;
409
+ score += Math.min(3, Math.floor(fnDensity));
410
+ const maxIndent = source.split('\n').reduce((max, line) => {
411
+ const trimmed = line.trimStart();
412
+ if (!trimmed)
413
+ return max;
414
+ return Math.max(max, line.length - trimmed.length);
415
+ }, 0);
416
+ score += Math.min(2, Math.floor(Math.floor(maxIndent / 2) / 3));
417
+ const cfCount = (source.match(/\b(if|else|for|while|switch|catch|ternary|\?[^:])\b/g) ?? []).length;
418
+ score += Math.min(2, Math.floor(cfCount / 10));
419
+ score += Math.min(2, Math.floor(thirdPartyImports.length / 3));
420
+ return Math.max(1, Math.min(10, score));
421
+ }
422
+ function detectTypeScriptMetrics(source, config) {
423
+ const anyUsages = [];
424
+ // Scan comment- and string-stripped source so `: any` inside a comment or a
425
+ // string literal is not counted as a type annotation (5.11 benchmark FP:
426
+ // any-usage/negative/comment_mention.ts). The strippers preserve offsets and
427
+ // newlines, so lineOf against the original source stays accurate.
428
+ const anySource = stripQuotedTextPreserveNewlines(stripCStyleComments(source), [`'`, `"`, '`']);
429
+ const explicitRe = /:\s*any\b|\bas\s+any\b/g;
430
+ explicitRe.lastIndex = 0;
431
+ let em;
432
+ while ((em = explicitRe.exec(anySource)) !== null) {
433
+ anyUsages.push({ line: (0, lines_1.lineOf)(source, em.index) });
434
+ }
435
+ const tsIgnoreCount = (source.match(/\/\/\s*@ts-ignore/g) ?? []).length;
436
+ const tsExpectErrorCount = (source.match(/\/\/\s*@ts-expect-error/g) ?? []).length;
437
+ const nonNullAssertions = (source.match(/[\w)\]]!(?!=)/g) ?? []).length;
438
+ const typeAssertions = (source.match(/\bas\s+(?!any\b)\w/g) ?? []).length;
439
+ let missingReturnTypes = 0;
440
+ if (config.enableMissingReturnTypeWarnings) {
441
+ const exportedTotal = (source.match(/export\s+(?:async\s+)?function\s+\w+\s*\(/gm) ?? []).length;
442
+ const exportedWithReturn = (source.match(/export\s+(?:async\s+)?function\s+\w+[^{(]*\([^{]*\)\s*:\s*\w[^{]*\{/gm) ?? []).length;
443
+ missingReturnTypes = Math.max(0, exportedTotal - exportedWithReturn);
444
+ }
445
+ return { anyUsages, tsIgnoreCount, tsExpectErrorCount, nonNullAssertions, typeAssertions, missingReturnTypes };
446
+ }
447
+ // Collects the text between the first ( and its matching ) starting at startLineIdx,
448
+ // spanning multiple lines if needed. Returns null if no opening ( is found.
449
+ function extractParamInner(lines, startLineIdx) {
450
+ let collected = '';
451
+ let depth = 0;
452
+ let started = false;
453
+ for (let i = startLineIdx; i < lines.length; i++) {
454
+ const line = lines[i];
455
+ for (const ch of line) {
456
+ if (ch === '(') {
457
+ if (!started) {
458
+ started = true;
459
+ depth = 1;
460
+ }
461
+ else
462
+ depth++;
463
+ }
464
+ else if (ch === ')' && started) {
465
+ depth--;
466
+ if (depth === 0)
467
+ return collected;
468
+ else
469
+ collected += ch;
470
+ }
471
+ else if (started) {
472
+ collected += ch;
473
+ }
474
+ }
475
+ if (started)
476
+ collected += ' ';
477
+ }
478
+ return null;
479
+ }
480
+ function detectCodeSmells(source, functions, lineCount, config, isTest = false) {
481
+ const lines = source.split('\n');
482
+ const consoleLogs = [];
483
+ if (config.enableConsoleLogWarnings) {
484
+ // Scan comment-stripped source so "console.log(...)" inside a comment is
485
+ // not flagged; the stripper preserves offsets, so lineOf stays accurate.
486
+ const clogSource = stripCStyleComments(source);
487
+ const clogRe = /\bconsole\.(log|warn|error)\s*\(/g;
488
+ clogRe.lastIndex = 0;
489
+ let cm;
490
+ while ((cm = clogRe.exec(clogSource)) !== null) {
491
+ consoleLogs.push({ line: (0, lines_1.lineOf)(source, cm.index) });
492
+ }
493
+ }
494
+ const todos = [];
495
+ if (config.enableTodoDetection) {
496
+ const todoRe = /\/\/.*\b(TODO|FIXME|HACK)\b[:\s]*(.*)/;
497
+ lines.forEach((line, i) => {
498
+ const m = todoRe.exec(line);
499
+ if (m)
500
+ todos.push({ text: `${m[1]}: ${m[2].trim()}`.trim(), line: i + 1 });
501
+ });
502
+ }
503
+ let deepNesting = 0;
504
+ for (let i = 0; i < functions.length; i++) {
505
+ const startIdx = functions[i].line;
506
+ const endIdx = (functions[i + 1]?.line ?? lineCount + 1) - 1;
507
+ const body = lines.slice(startIdx, endIdx);
508
+ const maxIndent = body.reduce((max, l) => {
509
+ const t = l.trimStart();
510
+ return t ? Math.max(max, l.length - t.length) : max;
511
+ }, 0);
512
+ if (maxIndent >= 8)
513
+ deepNesting++;
514
+ }
515
+ const longParameterLists = [];
516
+ if (config.enableLongParamDetection) {
517
+ for (const fn of functions) {
518
+ const inner = extractParamInner(lines, fn.line - 1);
519
+ if (inner) {
520
+ const paramCount = inner.split(',').filter(p => p.trim()).length;
521
+ if (paramCount >= config.maxParameterCount)
522
+ longParameterLists.push({ name: fn.name, paramCount, line: fn.line });
523
+ }
524
+ }
525
+ }
526
+ const magicNumbers = [];
527
+ if (config.enableMagicNumberDetection) {
528
+ // Matches named assignments that should not be flagged:
529
+ // export? const/let/var name[: type] = number
530
+ // class fields with at least one modifier: private/public/protected/static/readonly/override/abstract/declare
531
+ const namedAssignRe = /^\s*(?:export\s+)?(?:const|let|var)\s+\w+(?:\s*:\s*[\w<>[\]|&?]+)?\s*=\s*-?[\d.]+\s*[;,]?\s*$|^\s*(?:export\s+)?(?:(?:private|public|protected|static|readonly|override|abstract|declare)\s+)+\w+(?:\s*:\s*[\w<>[\]|&?]+)?\s*=\s*-?[\d.]+\s*[;,]?\s*$/;
532
+ const seen = new Set();
533
+ const sanitizedLines = stripQuotedTextPreserveNewlines(stripCStyleComments(source), [`'`, `"`, '`']).split('\n');
534
+ sanitizedLines.forEach((stripped, i) => {
535
+ if (namedAssignRe.test(stripped))
536
+ return;
537
+ const numRe = /\b(\d+(?:\.\d+)?)\b/g;
538
+ let nm;
539
+ while ((nm = numRe.exec(stripped)) !== null) {
540
+ const n = nm[1];
541
+ const key = `${i + 1}:${n}`;
542
+ if (n !== '0' && n !== '1' && !seen.has(key)) {
543
+ magicNumbers.push({ value: n, line: i + 1 });
544
+ seen.add(key);
545
+ }
546
+ }
547
+ });
548
+ }
549
+ // --- unused vars and functions ---
550
+ const { unusedVars, unusedFunctions } = config.enableUnusedDetection
551
+ ? detectUnusedDeclarations(source, functions)
552
+ : { unusedVars: [], unusedFunctions: [] };
553
+ // --- security smells ---
554
+ const evalUsage = [];
555
+ const sqlConcatenation = [];
556
+ const insecureRandom = [];
557
+ const unsafeRegex = [];
558
+ const hardcodedLocalhost = [];
559
+ const disabledTlsVerification = [];
560
+ const debugFlagsEnabled = [];
561
+ const weakHashing = [];
562
+ const openRedirect = [];
563
+ if (config.enableSecuritySmells) {
564
+ const stripped = stripCStyleComments(source);
565
+ // eval/new Function scan runs over comment- AND string-stripped source so
566
+ // the token `eval(` inside a string literal (e.g. a lint-message constant
567
+ // 'Calls like eval(userInput)...') is not flagged - the 5.11 benchmark FP
568
+ // eval-usage/negative/lint_messages.js. A real `eval(expr)` keeps its
569
+ // `eval(` outside the string, so detection is unaffected. Offsets preserved.
570
+ const evalSource = stripQuotedTextPreserveNewlines(stripped, [`'`, `"`, '`']);
571
+ const evalRe = /\beval\s*\(|new\s+Function\s*\(/g;
572
+ evalRe.lastIndex = 0;
573
+ let ev;
574
+ while ((ev = evalRe.exec(evalSource)) !== null) {
575
+ evalUsage.push({ line: (0, lines_1.lineOf)(source, ev.index) });
576
+ }
577
+ const sqlRe = /['"`][^'"`\n]*\b(?:SELECT|INSERT|UPDATE|DELETE|DROP)\b[^'"`\n]*['"`]\s*\+/g;
578
+ sqlRe.lastIndex = 0;
579
+ let sq;
580
+ while ((sq = sqlRe.exec(source)) !== null) {
581
+ sqlConcatenation.push({ line: (0, lines_1.lineOf)(source, sq.index) });
582
+ }
583
+ // Template literal SQL injection: `SELECT ... ${var}`
584
+ const sqlTplRe = /`[^`\n]*\b(?:SELECT|INSERT|UPDATE|DELETE|DROP)\b[^`\n]*\$\{[^}]+\}/g;
585
+ sqlTplRe.lastIndex = 0;
586
+ let sqt;
587
+ while ((sqt = sqlTplRe.exec(source)) !== null) {
588
+ sqlConcatenation.push({ line: (0, lines_1.lineOf)(source, sqt.index) });
589
+ }
590
+ const rngRe = /\bMath\.random\s*\(/g;
591
+ rngRe.lastIndex = 0;
592
+ let rn;
593
+ while ((rn = rngRe.exec(stripped)) !== null) {
594
+ if ((0, securityContext_1.hasSecurityContext)(stripped, rn.index)) {
595
+ insecureRandom.push({ line: (0, lines_1.lineOf)(source, rn.index) });
596
+ }
597
+ }
598
+ // Confine the nested-quantifier heuristic to actual regex source (literals
599
+ // and RegExp(...) args) so arithmetic like `(a+b)*c` is never flagged.
600
+ const regexSource = regexOnlySource(source);
601
+ const reRe = /\([^)\n]*[+*][^)\n]*\)[+*]/g;
602
+ reRe.lastIndex = 0;
603
+ let rx;
604
+ while ((rx = reRe.exec(regexSource)) !== null) {
605
+ unsafeRegex.push({ line: (0, lines_1.lineOf)(source, rx.index) });
606
+ }
607
+ if (!isTest) {
608
+ const localRe = /\blocalhost\b|127\.0\.0\.1/g;
609
+ localRe.lastIndex = 0;
610
+ let lh;
611
+ while ((lh = localRe.exec(stripped)) !== null) {
612
+ hardcodedLocalhost.push({ line: (0, lines_1.lineOf)(source, lh.index) });
613
+ }
614
+ }
615
+ const tlsRe = /rejectUnauthorized\s*:\s*false/g;
616
+ tlsRe.lastIndex = 0;
617
+ let tl;
618
+ while ((tl = tlsRe.exec(stripped)) !== null) {
619
+ disabledTlsVerification.push({ line: (0, lines_1.lineOf)(source, tl.index) });
620
+ }
621
+ if (!isTest) {
622
+ const dbgRe = /\bdebug\s*:\s*true\b/gi;
623
+ dbgRe.lastIndex = 0;
624
+ let db;
625
+ while ((db = dbgRe.exec(stripped)) !== null) {
626
+ debugFlagsEnabled.push({ line: (0, lines_1.lineOf)(source, db.index) });
627
+ }
628
+ }
629
+ const hashRe = /\bcreateHash\s*\(\s*['"`](?:md5|sha1)['"`]\s*\)/gi;
630
+ hashRe.lastIndex = 0;
631
+ let wh;
632
+ while ((wh = hashRe.exec(stripped)) !== null) {
633
+ weakHashing.push({ line: (0, lines_1.lineOf)(source, wh.index) });
634
+ }
635
+ const redirectRe = /res(?:ponse)?\.redirect\s*\([^)]*(?:req(?:uest)?\.(?:query|params|body)|\bparams\b\[|\bquery\b\[)/gi;
636
+ redirectRe.lastIndex = 0;
637
+ let or;
638
+ while ((or = redirectRe.exec(stripped)) !== null) {
639
+ openRedirect.push({ line: (0, lines_1.lineOf)(source, or.index) });
640
+ }
641
+ }
642
+ return { consoleLogs, todos, deepNesting, longParameterLists, magicNumbers, unusedVars, unusedFunctions, hardcodedSecrets: [], bareSuppressions: [], duplicateBlocks: [], fileNamingViolations: [], evalUsage, sqlConcatenation, insecureRandom, unsafeRegex, hardcodedLocalhost, disabledTlsVerification, debugFlagsEnabled, weakHashing, openRedirect };
643
+ }
644
+ function detectUnusedDeclarations(source, functions) {
645
+ const lines = source.split('\n');
646
+ const unusedVars = [];
647
+ const unusedFunctions = [];
648
+ // --- unused variables ---
649
+ // Match const/let/var declarations. Destructuring ({, [) is naturally excluded because
650
+ // the name capture \w+ won't match { or [.
651
+ const varDeclRe = /^[ \t]*(?:export\s+)?(?:const|let|var)\s+(\w+)/gm;
652
+ varDeclRe.lastIndex = 0;
653
+ let vm;
654
+ while ((vm = varDeclRe.exec(source)) !== null) {
655
+ const name = vm[1];
656
+ const line = (0, lines_1.lineOf)(source, vm.index);
657
+ const declLine = lines[line - 1] ?? '';
658
+ // Skip exported declarations
659
+ if (/\bexport\b/.test(declLine))
660
+ continue;
661
+ // Skip function assignments (arrow fns / function expressions) — handled by unusedFunctions
662
+ if (/=\s*(?:async\s+)?(?:\(|function\b)/.test(declLine))
663
+ continue;
664
+ // Skip _ (conventional throwaway)
665
+ if (name === '_')
666
+ continue;
667
+ // Check usage in the rest of the file (don't strip strings — JSX / template literals count)
668
+ const bodyWithoutDecl = lines.map((l, i) => (i === line - 1 ? '' : l)).join('\n');
669
+ if (!new RegExp(`\\b${name}\\b`).test(bodyWithoutDecl)) {
670
+ unusedVars.push({ name, line });
671
+ }
672
+ }
673
+ // --- unused functions ---
674
+ for (const fn of functions) {
675
+ const declLine = lines[fn.line - 1] ?? '';
676
+ // Skip exported functions
677
+ if (/\bexport\b/.test(declLine))
678
+ continue;
679
+ const bodyWithoutDecl = lines.map((l, i) => (i === fn.line - 1 ? '' : l)).join('\n');
680
+ if (!new RegExp(`\\b${fn.name}\\b`).test(bodyWithoutDecl)) {
681
+ unusedFunctions.push({ name: fn.name, line: fn.line });
682
+ }
683
+ }
684
+ return { unusedVars, unusedFunctions };
685
+ }
686
+ function detectWarnings(lineCount, functions, thirdPartyImports, source, config, filePath = '') {
687
+ const warnings = [];
688
+ const sev = (type, fallback) => config.severityOverrides[type] ?? fallback;
689
+ const warnLine = Math.round(config.fileLengthThreshold * 2 / 3);
690
+ if (lineCount > config.fileLengthThreshold) {
691
+ warnings.push({ type: 'file-too-long', message: `File is ${lineCount} lines (>${config.fileLengthThreshold})`, severity: sev('file-too-long', 'error') });
692
+ }
693
+ else if (lineCount > warnLine) {
694
+ warnings.push({ type: 'file-too-long', message: `File is ${lineCount} lines (>${warnLine})`, severity: sev('file-too-long', 'warning') });
695
+ }
696
+ const srcLines = source.split('\n');
697
+ const errorThreshold = config.functionLengthThreshold * 2;
698
+ for (const fn of functions) {
699
+ const endLine = findFunctionEnd(srcLines, fn.line);
700
+ const span = endLine - fn.line + 1;
701
+ if (span > errorThreshold) {
702
+ warnings.push({ type: 'function-too-long', message: `${fn.name} is ~${span} lines (>${errorThreshold})`, severity: sev('function-too-long', 'error'), line: fn.line });
703
+ }
704
+ else if (span > config.functionLengthThreshold) {
705
+ warnings.push({ type: 'function-too-long', message: `${fn.name} is ~${span} lines (>${config.functionLengthThreshold})`, severity: sev('function-too-long', 'warning'), line: fn.line });
706
+ }
707
+ }
708
+ if (functions.length > config.maxFunctionsPerFile) {
709
+ warnings.push({ type: 'too-many-functions', message: `${functions.length} functions in one file (>${config.maxFunctionsPerFile})`, severity: sev('too-many-functions', 'warning') });
710
+ }
711
+ if (thirdPartyImports.length > config.maxImportsPerFile) {
712
+ warnings.push({ type: 'too-many-imports', message: `${thirdPartyImports.length} third-party imports (>${config.maxImportsPerFile})`, severity: sev('too-many-imports', 'warning') });
713
+ }
714
+ // A CommonJS module (`module.exports = ...` / `exports.foo = ...`) has no
715
+ // `export` keyword but is not export-less - \bexport\b never matches "exports"
716
+ // (the trailing "s" defeats the boundary). Recognise both so .js/.cjs modules
717
+ // are not falsely flagged (5.11 benchmark FP no-exports/negative/exported_utils.js).
718
+ //
719
+ // A Vue or Svelte single-file component is NEVER export-less: the component
720
+ // itself is the module's export, produced by the compiler. `<script setup>`
721
+ // exists precisely so the author writes no `export` statement, a Svelte
722
+ // component body never has one, and a template-only SFC has no script at all.
723
+ // Flagging those made no-exports fire on essentially every component in a Vue
724
+ // or Svelte codebase.
725
+ const hasEsExport = /\bexport\b/.test(source);
726
+ const hasCommonJsExport = /\bmodule\.exports\b|\bexports\s*(?:\.|\[)/.test(source);
727
+ if (!hasEsExport && !hasCommonJsExport && !(0, sfc_1.isSingleFileComponent)(filePath)) {
728
+ warnings.push({ type: 'no-exports', message: 'No exports detected', severity: sev('no-exports', 'info') });
729
+ }
730
+ return warnings;
731
+ }
732
+ function analyseTs(source, config, filePath = '') {
733
+ const rawLines = source.split('\n');
734
+ const lineCount = rawLines.length;
735
+ const blankLines = rawLines.filter(l => l.trim() === '').length;
736
+ const codeLines = lineCount - blankLines;
737
+ const allFunctions = detectFunctions(source);
738
+ const functions = config.ignoreFunctions.length > 0
739
+ ? allFunctions.filter(f => !config.ignoreFunctions.includes(f.name))
740
+ : allFunctions;
741
+ const thirdPartyImports = extractThirdPartyImports(source);
742
+ const unusedImports = detectUnusedImports(source);
743
+ const complexityScore = scoreComplexity(source, codeLines, functions, thirdPartyImports);
744
+ const warnings = detectWarnings(lineCount, functions, thirdPartyImports, source, config, filePath);
745
+ const typeScriptMetrics = detectTypeScriptMetrics(source, config);
746
+ const baseSmells = detectCodeSmells(source, functions, lineCount, config, (0, guards_1.isTestFile)(filePath));
747
+ const hardcodedSecrets = (0, secrets_1.detectHardcodedSecrets)(source, config, 'typescript', filePath);
748
+ const secretLines = new Set(hardcodedSecrets.map(s => s.line));
749
+ const codeSmells = {
750
+ ...baseSmells,
751
+ unusedVars: baseSmells.unusedVars.filter(v => !secretLines.has(v.line)),
752
+ hardcodedSecrets,
753
+ };
754
+ const healthScore = (0, scoring_1.computeHealthScore)(warnings, typeScriptMetrics, codeSmells, config);
755
+ return {
756
+ lineCount,
757
+ blankLines,
758
+ codeLines,
759
+ functions,
760
+ thirdPartyImports,
761
+ unusedImports,
762
+ complexityScore,
763
+ warnings,
764
+ typeScriptMetrics,
765
+ codeSmells,
766
+ healthScore,
767
+ };
768
+ }
769
+ //# sourceMappingURL=analyser.js.map