aislop 0.10.1 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp.js CHANGED
@@ -16,6 +16,10 @@ import { fileURLToPath } from "node:url";
16
16
  import os from "node:os";
17
17
  import { randomUUID } from "node:crypto";
18
18
 
19
+ //#region src/version.ts
20
+ const APP_VERSION = "0.10.2";
21
+
22
+ //#endregion
19
23
  //#region src/config/defaults.ts
20
24
  const DEFAULT_CONFIG = {
21
25
  version: 1,
@@ -68,6 +72,22 @@ const DEFAULT_CONFIG = {
68
72
  telemetry: { enabled: true },
69
73
  rules: {}
70
74
  };
75
+ const DEFAULT_GITHUB_WORKFLOW_YAML = `name: aislop
76
+
77
+ on:
78
+ push:
79
+ branches: [main]
80
+ pull_request:
81
+
82
+ jobs:
83
+ quality-gate:
84
+ runs-on: ubuntu-latest
85
+ steps:
86
+ - uses: actions/checkout@v4
87
+ - uses: scanaislop/aislop@v${APP_VERSION}
88
+ with:
89
+ version: ${APP_VERSION}
90
+ `;
71
91
 
72
92
  //#endregion
73
93
  //#region src/config/extends.ts
@@ -263,218 +283,6 @@ const loadConfig = (directory) => {
263
283
  }
264
284
  };
265
285
 
266
- //#endregion
267
- //#region src/utils/source-masker.ts
268
- const JS_EXTS$2 = new Set([
269
- ".ts",
270
- ".tsx",
271
- ".js",
272
- ".jsx",
273
- ".mjs",
274
- ".cjs"
275
- ]);
276
- const PY_EXTS = new Set([".py"]);
277
- const RB_EXTS = new Set([".rb"]);
278
- const PHP_EXTS = new Set([".php"]);
279
- const familyForExt = (ext) => {
280
- if (JS_EXTS$2.has(ext)) return "js";
281
- if (PY_EXTS.has(ext)) return "py";
282
- if (RB_EXTS.has(ext)) return "rb";
283
- if (PHP_EXTS.has(ext)) return "php";
284
- return "none";
285
- };
286
- const maskStringsAndComments = (content, ext) => {
287
- const family = familyForExt(ext);
288
- if (family === "none") return content;
289
- if (family === "js") return maskJs(content, true);
290
- return maskSimple(content, family, true);
291
- };
292
- const maskComments = (content, ext) => {
293
- const family = familyForExt(ext);
294
- if (family === "none") return content;
295
- if (family === "js") return maskJs(content, false);
296
- return maskSimple(content, family, false);
297
- };
298
- const handleQuotesAndComments = (content, i, tplStack, mask, maskStrings) => {
299
- const len = content.length;
300
- const c = content[i];
301
- const next = content[i + 1];
302
- if (c === "\"" || c === "'") {
303
- const strStart = i;
304
- const end = consumeQuotedString(content, i, c);
305
- if (maskStrings) mask(strStart + 1, end - 1);
306
- return {
307
- handled: true,
308
- nextI: end
309
- };
310
- }
311
- if (c === "`") {
312
- const scan = consumeTemplateString(content, i + 1);
313
- if (maskStrings) mask(i + 1, scan.maskEnd);
314
- if (scan.openedInterp) tplStack.push(0);
315
- return {
316
- handled: true,
317
- nextI: scan.resumeAt
318
- };
319
- }
320
- if (c === "/" && next === "/") {
321
- const strStart = i;
322
- let k = i;
323
- while (k < len && content[k] !== "\n") k++;
324
- mask(strStart, k);
325
- return {
326
- handled: true,
327
- nextI: k
328
- };
329
- }
330
- if (c === "/" && next === "*") {
331
- const strStart = i;
332
- let k = i + 2;
333
- while (k < len - 1 && !(content[k] === "*" && content[k + 1] === "/")) k++;
334
- if (k < len - 1) k += 2;
335
- mask(strStart, k);
336
- return {
337
- handled: true,
338
- nextI: k
339
- };
340
- }
341
- return {
342
- handled: false,
343
- nextI: i
344
- };
345
- };
346
- const maskJs = (content, maskStrings) => {
347
- const out = content.split("");
348
- const len = content.length;
349
- const tplStack = [];
350
- let i = 0;
351
- const mask = (start, end) => {
352
- for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
353
- };
354
- while (i < len) {
355
- const c = content[i];
356
- if (tplStack.length > 0) {
357
- if (c === "{") {
358
- tplStack[tplStack.length - 1]++;
359
- i++;
360
- continue;
361
- }
362
- if (c === "}") {
363
- if (tplStack[tplStack.length - 1] === 0) {
364
- tplStack.pop();
365
- const scan = consumeTemplateString(content, i + 1);
366
- if (maskStrings) mask(i + 1, scan.maskEnd);
367
- if (scan.openedInterp) tplStack.push(0);
368
- i = scan.resumeAt;
369
- continue;
370
- }
371
- tplStack[tplStack.length - 1]--;
372
- i++;
373
- continue;
374
- }
375
- }
376
- const handled = handleQuotesAndComments(content, i, tplStack, mask, maskStrings);
377
- if (handled.handled) {
378
- i = handled.nextI;
379
- continue;
380
- }
381
- i++;
382
- }
383
- return out.join("");
384
- };
385
- const consumeQuotedString = (content, start, quote) => {
386
- const len = content.length;
387
- let i = start + 1;
388
- while (i < len) {
389
- const c = content[i];
390
- if (c === "\\" && i + 1 < len) {
391
- i += 2;
392
- continue;
393
- }
394
- if (c === quote) return i + 1;
395
- if (c === "\n") return i;
396
- i++;
397
- }
398
- return i;
399
- };
400
- const consumeTemplateString = (content, start) => {
401
- const len = content.length;
402
- let i = start;
403
- while (i < len) {
404
- const c = content[i];
405
- if (c === "\\" && i + 1 < len) {
406
- i += 2;
407
- continue;
408
- }
409
- if (c === "`") return {
410
- maskEnd: i,
411
- resumeAt: i + 1,
412
- openedInterp: false
413
- };
414
- if (c === "$" && content[i + 1] === "{") return {
415
- maskEnd: i,
416
- resumeAt: i + 2,
417
- openedInterp: true
418
- };
419
- i++;
420
- }
421
- return {
422
- maskEnd: i,
423
- resumeAt: i,
424
- openedInterp: false
425
- };
426
- };
427
- const maskSimple = (content, family, maskStrings) => {
428
- const out = content.split("");
429
- const len = content.length;
430
- let i = 0;
431
- const mask = (start, end) => {
432
- for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
433
- };
434
- while (i < len) {
435
- const c = content[i];
436
- const next = content[i + 1];
437
- if (family === "py" && (c === "\"" || c === "'")) {
438
- if (content[i + 1] === c && content[i + 2] === c) {
439
- const triple = c + c + c;
440
- const end = content.indexOf(triple, i + 3);
441
- const stop = end === -1 ? len : end + 3;
442
- if (maskStrings) mask(i + 3, stop - 3);
443
- i = stop;
444
- continue;
445
- }
446
- }
447
- if (c === "\"" || c === "'") {
448
- const strStart = i;
449
- i = consumeQuotedString(content, i, c);
450
- if (maskStrings) mask(strStart + 1, i - 1);
451
- continue;
452
- }
453
- if ((family === "py" || family === "rb" || family === "php") && c === "#") {
454
- const strStart = i;
455
- while (i < len && content[i] !== "\n") i++;
456
- mask(strStart, i);
457
- continue;
458
- }
459
- if (family === "php" && c === "/" && next === "/") {
460
- const strStart = i;
461
- while (i < len && content[i] !== "\n") i++;
462
- mask(strStart, i);
463
- continue;
464
- }
465
- if (family === "php" && c === "/" && next === "*") {
466
- const strStart = i;
467
- i += 2;
468
- while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
469
- if (i < len - 1) i += 2;
470
- mask(strStart, i);
471
- continue;
472
- }
473
- i++;
474
- }
475
- return out.join("");
476
- };
477
-
478
286
  //#endregion
479
287
  //#region src/utils/source-files.ts
480
288
  const MAX_BUFFER = 50 * 1024 * 1024;
@@ -713,14 +521,226 @@ const isAutoGenerated = (filePath) => {
713
521
  } catch {}
714
522
  }
715
523
  };
716
- const getSourceFilesForRoot = (rootDirectory) => filterProjectFiles(rootDirectory, listProjectFiles(rootDirectory));
717
- const getSourceFiles = (context) => {
718
- if (context.files) return filterExplicitFiles(context.rootDirectory, context.files);
719
- return getSourceFilesForRoot(context.rootDirectory);
524
+ const getSourceFilesForRoot = (rootDirectory) => filterProjectFiles(rootDirectory, listProjectFiles(rootDirectory));
525
+ const getSourceFiles = (context) => {
526
+ if (context.files) return filterExplicitFiles(context.rootDirectory, context.files);
527
+ return getSourceFilesForRoot(context.rootDirectory);
528
+ };
529
+ const getSourceFilesWithExtras = (context, extraExtensions) => {
530
+ if (context.files) return filterExplicitFiles(context.rootDirectory, context.files, extraExtensions);
531
+ return filterProjectFiles(context.rootDirectory, listProjectFiles(context.rootDirectory), extraExtensions);
532
+ };
533
+
534
+ //#endregion
535
+ //#region src/utils/source-masker.ts
536
+ const JS_EXTS$2 = new Set([
537
+ ".ts",
538
+ ".tsx",
539
+ ".js",
540
+ ".jsx",
541
+ ".mjs",
542
+ ".cjs"
543
+ ]);
544
+ const PY_EXTS = new Set([".py"]);
545
+ const RB_EXTS = new Set([".rb"]);
546
+ const PHP_EXTS = new Set([".php"]);
547
+ const familyForExt = (ext) => {
548
+ if (JS_EXTS$2.has(ext)) return "js";
549
+ if (PY_EXTS.has(ext)) return "py";
550
+ if (RB_EXTS.has(ext)) return "rb";
551
+ if (PHP_EXTS.has(ext)) return "php";
552
+ return "none";
553
+ };
554
+ const maskStringsAndComments = (content, ext) => {
555
+ const family = familyForExt(ext);
556
+ if (family === "none") return content;
557
+ if (family === "js") return maskJs(content, true);
558
+ return maskSimple(content, family, true);
559
+ };
560
+ const maskComments = (content, ext) => {
561
+ const family = familyForExt(ext);
562
+ if (family === "none") return content;
563
+ if (family === "js") return maskJs(content, false);
564
+ return maskSimple(content, family, false);
565
+ };
566
+ const handleQuotesAndComments = (content, i, tplStack, mask, maskStrings) => {
567
+ const len = content.length;
568
+ const c = content[i];
569
+ const next = content[i + 1];
570
+ if (c === "\"" || c === "'") {
571
+ const strStart = i;
572
+ const end = consumeQuotedString(content, i, c);
573
+ if (maskStrings) mask(strStart + 1, end - 1);
574
+ return {
575
+ handled: true,
576
+ nextI: end
577
+ };
578
+ }
579
+ if (c === "`") {
580
+ const scan = consumeTemplateString(content, i + 1);
581
+ if (maskStrings) mask(i + 1, scan.maskEnd);
582
+ if (scan.openedInterp) tplStack.push(0);
583
+ return {
584
+ handled: true,
585
+ nextI: scan.resumeAt
586
+ };
587
+ }
588
+ if (c === "/" && next === "/") {
589
+ const strStart = i;
590
+ let k = i;
591
+ while (k < len && content[k] !== "\n") k++;
592
+ mask(strStart, k);
593
+ return {
594
+ handled: true,
595
+ nextI: k
596
+ };
597
+ }
598
+ if (c === "/" && next === "*") {
599
+ const strStart = i;
600
+ let k = i + 2;
601
+ while (k < len - 1 && !(content[k] === "*" && content[k + 1] === "/")) k++;
602
+ if (k < len - 1) k += 2;
603
+ mask(strStart, k);
604
+ return {
605
+ handled: true,
606
+ nextI: k
607
+ };
608
+ }
609
+ return {
610
+ handled: false,
611
+ nextI: i
612
+ };
613
+ };
614
+ const maskJs = (content, maskStrings) => {
615
+ const out = content.split("");
616
+ const len = content.length;
617
+ const tplStack = [];
618
+ let i = 0;
619
+ const mask = (start, end) => {
620
+ for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
621
+ };
622
+ while (i < len) {
623
+ const c = content[i];
624
+ if (tplStack.length > 0) {
625
+ if (c === "{") {
626
+ tplStack[tplStack.length - 1]++;
627
+ i++;
628
+ continue;
629
+ }
630
+ if (c === "}") {
631
+ if (tplStack[tplStack.length - 1] === 0) {
632
+ tplStack.pop();
633
+ const scan = consumeTemplateString(content, i + 1);
634
+ if (maskStrings) mask(i + 1, scan.maskEnd);
635
+ if (scan.openedInterp) tplStack.push(0);
636
+ i = scan.resumeAt;
637
+ continue;
638
+ }
639
+ tplStack[tplStack.length - 1]--;
640
+ i++;
641
+ continue;
642
+ }
643
+ }
644
+ const handled = handleQuotesAndComments(content, i, tplStack, mask, maskStrings);
645
+ if (handled.handled) {
646
+ i = handled.nextI;
647
+ continue;
648
+ }
649
+ i++;
650
+ }
651
+ return out.join("");
652
+ };
653
+ const consumeQuotedString = (content, start, quote) => {
654
+ const len = content.length;
655
+ let i = start + 1;
656
+ while (i < len) {
657
+ const c = content[i];
658
+ if (c === "\\" && i + 1 < len) {
659
+ i += 2;
660
+ continue;
661
+ }
662
+ if (c === quote) return i + 1;
663
+ if (c === "\n") return i;
664
+ i++;
665
+ }
666
+ return i;
667
+ };
668
+ const consumeTemplateString = (content, start) => {
669
+ const len = content.length;
670
+ let i = start;
671
+ while (i < len) {
672
+ const c = content[i];
673
+ if (c === "\\" && i + 1 < len) {
674
+ i += 2;
675
+ continue;
676
+ }
677
+ if (c === "`") return {
678
+ maskEnd: i,
679
+ resumeAt: i + 1,
680
+ openedInterp: false
681
+ };
682
+ if (c === "$" && content[i + 1] === "{") return {
683
+ maskEnd: i,
684
+ resumeAt: i + 2,
685
+ openedInterp: true
686
+ };
687
+ i++;
688
+ }
689
+ return {
690
+ maskEnd: i,
691
+ resumeAt: i,
692
+ openedInterp: false
693
+ };
720
694
  };
721
- const getSourceFilesWithExtras = (context, extraExtensions) => {
722
- if (context.files) return filterExplicitFiles(context.rootDirectory, context.files, extraExtensions);
723
- return filterProjectFiles(context.rootDirectory, listProjectFiles(context.rootDirectory), extraExtensions);
695
+ const maskSimple = (content, family, maskStrings) => {
696
+ const out = content.split("");
697
+ const len = content.length;
698
+ let i = 0;
699
+ const mask = (start, end) => {
700
+ for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
701
+ };
702
+ while (i < len) {
703
+ const c = content[i];
704
+ const next = content[i + 1];
705
+ if (family === "py" && (c === "\"" || c === "'")) {
706
+ if (content[i + 1] === c && content[i + 2] === c) {
707
+ const triple = c + c + c;
708
+ const end = content.indexOf(triple, i + 3);
709
+ const stop = end === -1 ? len : end + 3;
710
+ if (maskStrings) mask(i + 3, stop - 3);
711
+ i = stop;
712
+ continue;
713
+ }
714
+ }
715
+ if (c === "\"" || c === "'") {
716
+ const strStart = i;
717
+ i = consumeQuotedString(content, i, c);
718
+ if (maskStrings) mask(strStart + 1, i - 1);
719
+ continue;
720
+ }
721
+ if ((family === "py" || family === "rb" || family === "php") && c === "#") {
722
+ const strStart = i;
723
+ while (i < len && content[i] !== "\n") i++;
724
+ mask(strStart, i);
725
+ continue;
726
+ }
727
+ if (family === "php" && c === "/" && next === "/") {
728
+ const strStart = i;
729
+ while (i < len && content[i] !== "\n") i++;
730
+ mask(strStart, i);
731
+ continue;
732
+ }
733
+ if (family === "php" && c === "/" && next === "*") {
734
+ const strStart = i;
735
+ i += 2;
736
+ while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
737
+ if (i < len - 1) i += 2;
738
+ mask(strStart, i);
739
+ continue;
740
+ }
741
+ i++;
742
+ }
743
+ return out.join("");
724
744
  };
725
745
 
726
746
  //#endregion
@@ -768,16 +788,14 @@ const detectThinWrappers = (content, relativePath, ext) => {
768
788
  for (const { pattern, extensions } of THIN_WRAPPER_PATTERNS) {
769
789
  if (!extensions.has(ext)) continue;
770
790
  const regex = new RegExp(pattern.source, pattern.flags);
771
- let match;
772
- while ((match = regex.exec(content)) !== null) {
791
+ for (const match of content.matchAll(regex)) {
773
792
  const funcName = match[1];
774
793
  const matchText = match[0];
775
794
  const lineNumber = content.slice(0, match.index).split("\n").length;
776
795
  if (DUNDER_PATTERN.test(funcName)) continue;
777
796
  if (FRAMEWORK_METHOD_NAMES.test(funcName)) continue;
778
797
  if (lineNumber >= 2) {
779
- const prevLine = lines[lineNumber - 2]?.trim();
780
- if (prevLine && prevLine.startsWith("@")) continue;
798
+ if ((lines[lineNumber - 2]?.trim())?.startsWith("@")) continue;
781
799
  }
782
800
  if (!isIdentityForward(matchText)) continue;
783
801
  if (isUseContextWrapper(matchText)) continue;
@@ -1154,7 +1172,7 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
1154
1172
  const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
1155
1173
  if (JS_EXTENSIONS$3.has(ext) && /^(?:return|throw)\b/.test(trimmed) && trimmed.endsWith(";") && nextLine && nextLine.length > 0 && !isGuardedSingleLineExit(lines, i) && !isBlockCloserAfterReturn(nextLine) && !nextLine.startsWith("//") && !nextLine.startsWith("/*") && !nextLine.startsWith("case ") && !nextLine.startsWith("default:") && !nextLine.startsWith("if ") && !nextLine.startsWith("if(") && !nextLine.startsWith("else")) diagnostics.push(slop(relativePath, i + 2, "ai-slop/unreachable-code", "warning", "Code after return/throw statement is unreachable", "Remove the unreachable code or restructure the control flow", false));
1156
1174
  if (/\bif\s*\(\s*(?:false|true|0|1)\s*\)/.test(trimmed) && !trimmed.startsWith("//") && !trimmed.startsWith("*") && !/["'`].*\bif\s*\(/.test(trimmed) && !/\/.*\bif\s*\(/.test(trimmed.replace(/\/\/.*$/, ""))) diagnostics.push(slop(relativePath, i + 1, "ai-slop/constant-condition", "warning", "Conditional with a constant value — likely debugging leftover", "Remove the constant condition or replace with proper logic", false));
1157
- if (JS_EXTENSIONS$3.has(ext) && /(?:function\s+\w+|=>\s*)\s*\{\s*\}\s*;?\s*$/.test(trimmed) && !trimmed.startsWith("interface") && !trimmed.startsWith("type ")) diagnostics.push(slop(relativePath, i + 1, "ai-slop/empty-function", "info", "Empty function body — possible stub or unfinished implementation", "Implement the function body or add a comment explaining why it's empty", false));
1175
+ if (JS_EXTENSIONS$3.has(ext) && /(?:function\s+\w+\s*\([^)]*\)|=>\s*)\s*\{\s*\}\s*;?\s*$/.test(trimmed) && !trimmed.startsWith("interface") && !trimmed.startsWith("type ")) diagnostics.push(slop(relativePath, i + 1, "ai-slop/empty-function", "info", "Empty function body — possible stub or unfinished implementation", "Implement the function body or add a comment explaining why it's empty", false));
1158
1176
  }
1159
1177
  return diagnostics;
1160
1178
  };
@@ -1540,9 +1558,8 @@ const detectSwallowedExceptions = async (context) => {
1540
1558
  const relativePath = path.relative(context.rootDirectory, filePath);
1541
1559
  for (const { pattern, languages, message } of SWALLOWED_EXCEPTION_PATTERNS) {
1542
1560
  if (!languages.includes(ext)) continue;
1543
- let match;
1544
1561
  const regex = new RegExp(pattern.source, pattern.flags + (pattern.flags.includes("g") ? "" : "g"));
1545
- while ((match = regex.exec(content)) !== null) {
1562
+ for (const match of content.matchAll(regex)) {
1546
1563
  if (isIntentionalIgnore(match[0], ext)) continue;
1547
1564
  const line = content.slice(0, match.index).split("\n").length;
1548
1565
  diagnostics.push({
@@ -1576,222 +1593,62 @@ const detectPackageName = (lines) => {
1576
1593
  const m = PACKAGE_DECL_RE.exec(line);
1577
1594
  if (m) return m[1];
1578
1595
  }
1579
- return null;
1580
- };
1581
- const PANIC_INTENT_LOOKBACK = 3;
1582
- const hasIntentComment$1 = (lines, panicLineIdx) => {
1583
- for (let j = panicLineIdx - 1; j >= Math.max(0, panicLineIdx - PANIC_INTENT_LOOKBACK); j--) if (COMMENT_LINE_RE$1.test(lines[j])) return true;
1584
- return false;
1585
- };
1586
- const isNilGuardPanic = (lines, panicLineIdx, line) => {
1587
- if (!SHORT_STRING_PANIC_RE.test(line)) return false;
1588
- for (let j = panicLineIdx - 1; j >= Math.max(0, panicLineIdx - 2); j--) {
1589
- const prev = lines[j];
1590
- if (prev.trim() === "") continue;
1591
- return NIL_GUARD_RE.test(prev);
1592
- }
1593
- return false;
1594
- };
1595
- const flagLibraryPanic = (lines, relPath, pkg, out) => {
1596
- if (pkg === "main") return;
1597
- for (let i = 0; i < lines.length; i++) {
1598
- const line = lines[i];
1599
- if (COMMENT_LINE_RE$1.test(line)) continue;
1600
- PANIC_CALL_RE.lastIndex = 0;
1601
- if (!PANIC_CALL_RE.test(line)) continue;
1602
- if (hasIntentComment$1(lines, i)) continue;
1603
- if (isNilGuardPanic(lines, i, line)) continue;
1604
- out.push({
1605
- filePath: relPath,
1606
- engine: "ai-slop",
1607
- rule: "ai-slop/go-library-panic",
1608
- severity: "warning",
1609
- message: `\`panic()\` in package \`${pkg}\` (non-main, non-test). Library code should return errors, not unwind the goroutine.`,
1610
- help: "Convert to `return fmt.Errorf(...)` (or a wrapped error) and let the caller decide. Reserve `panic` for genuinely-impossible states (corrupt internal invariants), and mark those with a comment so future readers know it's intentional.",
1611
- line: i + 1,
1612
- column: 1,
1613
- category: "AI Slop",
1614
- fixable: false
1615
- });
1616
- }
1617
- };
1618
- const detectGoPatterns = async (context) => {
1619
- const diagnostics = [];
1620
- const files = getSourceFiles(context);
1621
- for (const filePath of files) {
1622
- if (!GO_EXTENSIONS.has(path.extname(filePath))) continue;
1623
- if (isAutoGenerated(filePath)) continue;
1624
- if (filePath.endsWith("_test.go")) continue;
1625
- let content;
1626
- try {
1627
- content = fs.readFileSync(filePath, "utf-8");
1628
- } catch {
1629
- continue;
1630
- }
1631
- const lines = content.split("\n");
1632
- const pkg = detectPackageName(lines);
1633
- if (!pkg) continue;
1634
- flagLibraryPanic(lines, path.relative(context.rootDirectory, filePath), pkg, diagnostics);
1635
- }
1636
- return diagnostics;
1637
- };
1638
-
1639
- //#endregion
1640
- //#region src/engines/ai-slop/hardcoded-config.ts
1641
- const SOURCE_EXTENSIONS = new Set([
1642
- ".ts",
1643
- ".tsx",
1644
- ".js",
1645
- ".jsx",
1646
- ".mjs",
1647
- ".cjs",
1648
- ".py",
1649
- ".go",
1650
- ".rs",
1651
- ".rb",
1652
- ".java",
1653
- ".php"
1654
- ]);
1655
- const URL_LITERAL_RE = /(["'`])(https?:\/\/[^"'`\s<>]+)\1/g;
1656
- const ID_LITERAL_RE = /(["'])([A-Za-z][A-Za-z0-9_-]{15,})\1/g;
1657
- const ENV_REFERENCE_RE = /\b(?:process\.env|import\.meta\.env|Deno\.env|os\.environ|getenv|env\()\b/i;
1658
- const DOC_URL_CONTEXT_RE = /\b(?:docs?|documentation|homepage|repository|bugs|license|readme|source|svgUrl|pageUrl|href|link|install)\b/i;
1659
- const URL_CONFIG_CONTEXT_RE = /\b(?:api|base[_-]?url|baseUrl|endpoint|host|origin|webhook|callback|redirect|server|service|domain|url)\b/i;
1660
- const ENVIRONMENT_HOST_RE = /(?:^|[.-])(?:api|app|admin|auth|staging|stage|prod|dev|sandbox|webhook|internal)(?:[.-]|$)|^(?:localhost|127\.0\.0\.1|0\.0\.0\.0)$/i;
1661
- const ID_CONTEXT_RE = /(?:^|[^A-Za-z0-9])(?:api[_-]?key|client[_-]?id|project[_-]?id|org(?:anization)?[_-]?id|workspace[_-]?id|tenant[_-]?id|price[_-]?id|product[_-]?id|customer[_-]?id|subscription[_-]?id|account[_-]?id|app[_-]?id|key|token|secret)(?:$|[^A-Za-z0-9])/i;
1662
- const MIGRATION_PATH_RE$1 = /(?:^|[\\/])(?:migrations?|db[\\/]migrate)[\\/]/i;
1663
- const PLACEHOLDER_HOSTS = new Set([
1664
- "example.com",
1665
- "example.org",
1666
- "example.net"
1667
- ]);
1668
- const LOOPBACK_HOSTS = new Set([
1669
- "localhost",
1670
- "127.0.0.1",
1671
- "0.0.0.0",
1672
- "::1"
1673
- ]);
1674
- const VENDOR_API_DOMAINS = [
1675
- "github.com",
1676
- "githubusercontent.com",
1677
- "googleapis.com",
1678
- "accounts.google.com",
1679
- "stripe.com",
1680
- "openai.com",
1681
- "anthropic.com",
1682
- "slack.com",
1683
- "twilio.com",
1684
- "sendgrid.com",
1685
- "mailgun.net",
1686
- "cloudflare.com",
1687
- "discord.com",
1688
- "telegram.org",
1689
- "login.microsoftonline.com",
1690
- "graph.microsoft.com",
1691
- "twitter.com",
1692
- "x.com",
1693
- "twimg.com",
1694
- "t.co",
1695
- "api.telegram.org"
1696
- ];
1697
- const isVendorApiHost = (host) => VENDOR_API_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
1698
- const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
1699
- const HARDCODED_URL_FINDING = {
1700
- rule: "ai-slop/hardcoded-url",
1701
- message: "Hardcoded environment URL in production code",
1702
- help: "Move deployment-specific URLs to environment variables or a typed config module. Keep only stable documentation/public links inline."
1703
- };
1704
- const HARDCODED_ID_FINDING = {
1705
- rule: "ai-slop/hardcoded-id",
1706
- message: "Hardcoded provider/project ID in production code",
1707
- help: "Move provider IDs, tenant IDs, price IDs, and similar deployment-specific identifiers to env/config so agents do not bake one environment into source."
1708
- };
1709
- const makeFinding = (filePath, line, spec) => ({
1710
- filePath,
1711
- engine: "ai-slop",
1712
- rule: spec.rule,
1713
- severity: "warning",
1714
- message: spec.message,
1715
- help: spec.help,
1716
- line,
1717
- column: 0,
1718
- category: "AI Slop",
1719
- fixable: false
1720
- });
1721
- const isCommentOnlyLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*");
1722
- const commentStartsBefore = (line, index, ext) => {
1723
- const prefix = line.slice(0, index);
1724
- if (ext === ".py" || ext === ".rb") return prefix.includes("#");
1725
- if (ext === ".php") return prefix.includes("//") || prefix.includes("#");
1726
- return prefix.includes("//") || prefix.includes("/*");
1727
- };
1728
- const safeUrlHost = (urlText) => {
1729
- try {
1730
- return new URL(urlText).hostname.toLowerCase();
1731
- } catch {
1732
- return null;
1733
- }
1734
- };
1735
- const isEnvBackedLine = (line) => ENV_REFERENCE_RE.test(line);
1736
- const shouldFlagUrlLiteral = (line, urlText) => {
1737
- if (isEnvBackedLine(line)) return false;
1738
- const host = safeUrlHost(urlText);
1739
- if (!host) return false;
1740
- if (PLACEHOLDER_HOSTS.has(host)) return false;
1741
- if (LOOPBACK_HOSTS.has(host)) return false;
1742
- if (isVendorApiHost(host)) return false;
1743
- if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
1744
- return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
1596
+ return null;
1745
1597
  };
1746
- const ENV_VAR_NAME_RE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/;
1747
- const hasUsefulIdShape = (value) => {
1748
- if (PLACEHOLDER_ID_RE.test(value)) return false;
1749
- if (ENV_VAR_NAME_RE.test(value)) return false;
1750
- if (/^https?:\/\//i.test(value)) return false;
1751
- if (/^[A-Za-z]+$/.test(value)) return false;
1752
- return /[0-9]/.test(value);
1598
+ const PANIC_INTENT_LOOKBACK = 3;
1599
+ const hasIntentComment$1 = (lines, panicLineIdx) => {
1600
+ for (let j = panicLineIdx - 1; j >= Math.max(0, panicLineIdx - PANIC_INTENT_LOOKBACK); j--) if (COMMENT_LINE_RE$1.test(lines[j])) return true;
1601
+ return false;
1753
1602
  };
1754
- const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
1755
- const diagnostics = [];
1756
- if (isCommentOnlyLine(line.trim())) return diagnostics;
1757
- URL_LITERAL_RE.lastIndex = 0;
1758
- let urlMatch;
1759
- while ((urlMatch = URL_LITERAL_RE.exec(line)) !== null) {
1760
- const urlText = urlMatch[2];
1761
- if (commentStartsBefore(line, urlMatch.index, ext)) continue;
1762
- if (!shouldFlagUrlLiteral(line, urlText)) continue;
1763
- diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_URL_FINDING));
1764
- }
1765
- if (!ID_CONTEXT_RE.test(line) || isEnvBackedLine(line) || DOC_URL_CONTEXT_RE.test(line)) return diagnostics;
1766
- ID_LITERAL_RE.lastIndex = 0;
1767
- let idMatch;
1768
- while ((idMatch = ID_LITERAL_RE.exec(line)) !== null) {
1769
- const value = idMatch[2];
1770
- if (commentStartsBefore(line, idMatch.index, ext)) continue;
1771
- if (!hasUsefulIdShape(value)) continue;
1772
- diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_ID_FINDING));
1603
+ const isNilGuardPanic = (lines, panicLineIdx, line) => {
1604
+ if (!SHORT_STRING_PANIC_RE.test(line)) return false;
1605
+ for (let j = panicLineIdx - 1; j >= Math.max(0, panicLineIdx - 2); j--) {
1606
+ const prev = lines[j];
1607
+ if (prev.trim() === "") continue;
1608
+ return NIL_GUARD_RE.test(prev);
1773
1609
  }
1774
- return diagnostics;
1610
+ return false;
1775
1611
  };
1776
- const scanFileForConfigLiterals = (content, relativePath, ext) => {
1777
- if (!SOURCE_EXTENSIONS.has(ext)) return [];
1778
- if (isNonProductionPath(relativePath)) return [];
1779
- if (MIGRATION_PATH_RE$1.test(relativePath)) return [];
1780
- return content.split("\n").flatMap((line, index) => scanLineForConfigLiterals(line, relativePath, ext, index + 1));
1612
+ const flagLibraryPanic = (lines, relPath, pkg, out) => {
1613
+ if (pkg === "main") return;
1614
+ for (let i = 0; i < lines.length; i++) {
1615
+ const line = lines[i];
1616
+ if (COMMENT_LINE_RE$1.test(line)) continue;
1617
+ PANIC_CALL_RE.lastIndex = 0;
1618
+ if (!PANIC_CALL_RE.test(line)) continue;
1619
+ if (hasIntentComment$1(lines, i)) continue;
1620
+ if (isNilGuardPanic(lines, i, line)) continue;
1621
+ out.push({
1622
+ filePath: relPath,
1623
+ engine: "ai-slop",
1624
+ rule: "ai-slop/go-library-panic",
1625
+ severity: "warning",
1626
+ message: `\`panic()\` in package \`${pkg}\` (non-main, non-test). Library code should return errors, not unwind the goroutine.`,
1627
+ help: "Convert to `return fmt.Errorf(...)` (or a wrapped error) and let the caller decide. Reserve `panic` for genuinely-impossible states (corrupt internal invariants), and mark those with a comment so future readers know it's intentional.",
1628
+ line: i + 1,
1629
+ column: 1,
1630
+ category: "AI Slop",
1631
+ fixable: false
1632
+ });
1633
+ }
1781
1634
  };
1782
- const detectHardcodedConfigLiterals = async (context) => {
1635
+ const detectGoPatterns = async (context) => {
1783
1636
  const diagnostics = [];
1784
- for (const filePath of getSourceFiles(context)) {
1637
+ const files = getSourceFiles(context);
1638
+ for (const filePath of files) {
1639
+ if (!GO_EXTENSIONS.has(path.extname(filePath))) continue;
1785
1640
  if (isAutoGenerated(filePath)) continue;
1641
+ if (filePath.endsWith("_test.go")) continue;
1786
1642
  let content;
1787
1643
  try {
1788
1644
  content = fs.readFileSync(filePath, "utf-8");
1789
1645
  } catch {
1790
1646
  continue;
1791
1647
  }
1792
- const relativePath = path.relative(context.rootDirectory, filePath);
1793
- const ext = path.extname(filePath);
1794
- diagnostics.push(...scanFileForConfigLiterals(maskComments(content, ext), relativePath, ext));
1648
+ const lines = content.split("\n");
1649
+ const pkg = detectPackageName(lines);
1650
+ if (!pkg) continue;
1651
+ flagLibraryPanic(lines, path.relative(context.rootDirectory, filePath), pkg, diagnostics);
1795
1652
  }
1796
1653
  return diagnostics;
1797
1654
  };
@@ -1893,15 +1750,18 @@ const readWorkspaceGlobs = (rootDir, rootPkg) => {
1893
1750
  }
1894
1751
  return globs;
1895
1752
  };
1753
+ const readWorkspaceEntries = (dir) => {
1754
+ try {
1755
+ return fs.readdirSync(dir, { withFileTypes: true });
1756
+ } catch {
1757
+ return [];
1758
+ }
1759
+ };
1896
1760
  const expandWorkspaceDirs = (rootDir, globs) => {
1897
1761
  const dirs = [];
1898
1762
  for (const glob of globs) if (glob.endsWith("/*")) {
1899
1763
  const parent = path.join(rootDir, glob.slice(0, -2));
1900
- try {
1901
- for (const entry of fs.readdirSync(parent, { withFileTypes: true })) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
1902
- } catch {
1903
- continue;
1904
- }
1764
+ for (const entry of readWorkspaceEntries(parent)) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
1905
1765
  } else if (!glob.includes("*")) dirs.push(path.join(rootDir, glob));
1906
1766
  return dirs;
1907
1767
  };
@@ -2534,6 +2394,167 @@ const detectHallucinatedImports = async (context) => {
2534
2394
  return diagnostics;
2535
2395
  };
2536
2396
 
2397
+ //#endregion
2398
+ //#region src/engines/ai-slop/hardcoded-config.ts
2399
+ const SOURCE_EXTENSIONS = new Set([
2400
+ ".ts",
2401
+ ".tsx",
2402
+ ".js",
2403
+ ".jsx",
2404
+ ".mjs",
2405
+ ".cjs",
2406
+ ".py",
2407
+ ".go",
2408
+ ".rs",
2409
+ ".rb",
2410
+ ".java",
2411
+ ".php"
2412
+ ]);
2413
+ const URL_LITERAL_RE = /(["'`])(https?:\/\/[^"'`\s<>]+)\1/g;
2414
+ const ID_LITERAL_RE = /(["'])([A-Za-z][A-Za-z0-9_-]{15,})\1/g;
2415
+ const ENV_REFERENCE_RE = /\b(?:process\.env|import\.meta\.env|Deno\.env|os\.environ|getenv|env\()\b/i;
2416
+ const DOC_URL_CONTEXT_RE = /\b(?:docs?|documentation|homepage|repository|bugs|license|readme|source|svgUrl|pageUrl|href|link|install)\b/i;
2417
+ const URL_CONFIG_CONTEXT_RE = /\b(?:api|base[_-]?url|baseUrl|endpoint|host|origin|webhook|callback|redirect|server|service|domain|url)\b/i;
2418
+ const ENVIRONMENT_HOST_RE = /(?:^|[.-])(?:api|app|admin|auth|staging|stage|prod|dev|sandbox|webhook|internal)(?:[.-]|$)|^(?:localhost|127\.0\.0\.1|0\.0\.0\.0)$/i;
2419
+ const ID_CONTEXT_RE = /(?:^|[^A-Za-z0-9])(?:api[_-]?key|client[_-]?id|project[_-]?id|org(?:anization)?[_-]?id|workspace[_-]?id|tenant[_-]?id|price[_-]?id|product[_-]?id|customer[_-]?id|subscription[_-]?id|account[_-]?id|app[_-]?id|key|token|secret)(?:$|[^A-Za-z0-9])/i;
2420
+ const MIGRATION_PATH_RE$1 = /(?:^|[\\/])(?:migrations?|db[\\/]migrate)[\\/]/i;
2421
+ const PLACEHOLDER_HOSTS = new Set([
2422
+ "example.com",
2423
+ "example.org",
2424
+ "example.net"
2425
+ ]);
2426
+ const LOOPBACK_HOSTS = new Set([
2427
+ "localhost",
2428
+ "127.0.0.1",
2429
+ "0.0.0.0",
2430
+ "::1"
2431
+ ]);
2432
+ const VENDOR_API_DOMAINS = [
2433
+ "github.com",
2434
+ "githubusercontent.com",
2435
+ "googleapis.com",
2436
+ "accounts.google.com",
2437
+ "stripe.com",
2438
+ "openai.com",
2439
+ "anthropic.com",
2440
+ "slack.com",
2441
+ "twilio.com",
2442
+ "sendgrid.com",
2443
+ "mailgun.net",
2444
+ "cloudflare.com",
2445
+ "discord.com",
2446
+ "telegram.org",
2447
+ "login.microsoftonline.com",
2448
+ "graph.microsoft.com",
2449
+ "twitter.com",
2450
+ "x.com",
2451
+ "twimg.com",
2452
+ "t.co",
2453
+ "api.telegram.org"
2454
+ ];
2455
+ const isVendorApiHost = (host) => VENDOR_API_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
2456
+ const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
2457
+ const HARDCODED_URL_FINDING = {
2458
+ rule: "ai-slop/hardcoded-url",
2459
+ message: "Hardcoded environment URL in production code",
2460
+ help: "Move deployment-specific URLs to environment variables or a typed config module. Keep only stable documentation/public links inline."
2461
+ };
2462
+ const HARDCODED_ID_FINDING = {
2463
+ rule: "ai-slop/hardcoded-id",
2464
+ message: "Hardcoded provider/project ID in production code",
2465
+ help: "Move provider IDs, tenant IDs, price IDs, and similar deployment-specific identifiers to env/config so agents do not bake one environment into source."
2466
+ };
2467
+ const makeFinding = (filePath, line, spec) => ({
2468
+ filePath,
2469
+ engine: "ai-slop",
2470
+ rule: spec.rule,
2471
+ severity: "warning",
2472
+ message: spec.message,
2473
+ help: spec.help,
2474
+ line,
2475
+ column: 0,
2476
+ category: "AI Slop",
2477
+ fixable: false
2478
+ });
2479
+ const isCommentOnlyLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*");
2480
+ const commentStartsBefore = (line, index, ext) => {
2481
+ const prefix = line.slice(0, index);
2482
+ if (ext === ".py" || ext === ".rb") return prefix.includes("#");
2483
+ if (ext === ".php") return prefix.includes("//") || prefix.includes("#");
2484
+ return prefix.includes("//") || prefix.includes("/*");
2485
+ };
2486
+ const safeUrlHost = (urlText) => {
2487
+ try {
2488
+ return new URL(urlText).hostname.toLowerCase();
2489
+ } catch {
2490
+ return null;
2491
+ }
2492
+ };
2493
+ const isEnvBackedLine = (line) => ENV_REFERENCE_RE.test(line);
2494
+ const shouldFlagUrlLiteral = (line, urlText) => {
2495
+ if (isEnvBackedLine(line)) return false;
2496
+ const host = safeUrlHost(urlText);
2497
+ if (!host) return false;
2498
+ if (PLACEHOLDER_HOSTS.has(host)) return false;
2499
+ if (LOOPBACK_HOSTS.has(host)) return false;
2500
+ if (isVendorApiHost(host)) return false;
2501
+ if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
2502
+ return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
2503
+ };
2504
+ const ENV_VAR_NAME_RE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/;
2505
+ const hasUsefulIdShape = (value) => {
2506
+ if (PLACEHOLDER_ID_RE.test(value)) return false;
2507
+ if (ENV_VAR_NAME_RE.test(value)) return false;
2508
+ if (/^https?:\/\//i.test(value)) return false;
2509
+ if (/^[A-Za-z]+$/.test(value)) return false;
2510
+ return /[0-9]/.test(value);
2511
+ };
2512
+ const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
2513
+ const diagnostics = [];
2514
+ if (isCommentOnlyLine(line.trim())) return diagnostics;
2515
+ for (const urlMatch of line.matchAll(URL_LITERAL_RE)) {
2516
+ const urlText = urlMatch[2];
2517
+ if (commentStartsBefore(line, urlMatch.index, ext)) continue;
2518
+ if (!shouldFlagUrlLiteral(line, urlText)) continue;
2519
+ diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_URL_FINDING));
2520
+ }
2521
+ if (!ID_CONTEXT_RE.test(line) || isEnvBackedLine(line) || DOC_URL_CONTEXT_RE.test(line)) return diagnostics;
2522
+ for (const idMatch of line.matchAll(ID_LITERAL_RE)) {
2523
+ const value = idMatch[2];
2524
+ if (commentStartsBefore(line, idMatch.index, ext)) continue;
2525
+ if (!hasUsefulIdShape(value)) continue;
2526
+ diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_ID_FINDING));
2527
+ }
2528
+ return diagnostics;
2529
+ };
2530
+ const scanFileForConfigLiterals = (content, relativePath, ext) => {
2531
+ if (!SOURCE_EXTENSIONS.has(ext)) return [];
2532
+ if (isNonProductionPath(relativePath)) return [];
2533
+ if (MIGRATION_PATH_RE$1.test(relativePath)) return [];
2534
+ return content.split("\n").flatMap((line, index) => scanLineForConfigLiterals(line, relativePath, ext, index + 1));
2535
+ };
2536
+ const detectHardcodedConfigLiterals = async (context) => {
2537
+ const diagnostics = [];
2538
+ for (const filePath of getSourceFiles(context)) {
2539
+ if (isAutoGenerated(filePath)) continue;
2540
+ let content;
2541
+ try {
2542
+ content = fs.readFileSync(filePath, "utf-8");
2543
+ } catch {
2544
+ continue;
2545
+ }
2546
+ const relativePath = path.relative(context.rootDirectory, filePath);
2547
+ const ext = path.extname(filePath);
2548
+ diagnostics.push(...scanFileForConfigLiterals(maskComments(content, ext), relativePath, ext));
2549
+ }
2550
+ return diagnostics;
2551
+ };
2552
+
2553
+ //#endregion
2554
+ //#region src/utils/suppress.ts
2555
+ const DIRECTIVE_RE = /(?:\/\/|\/\*|#|<!--|\*)\s*aislop-ignore-(next-line|line|file)\b([^\n]*)/;
2556
+ const isAislopDirectiveLine = (line) => DIRECTIVE_RE.test(line);
2557
+
2537
2558
  //#endregion
2538
2559
  //#region src/engines/ai-slop/comment-blocks.ts
2539
2560
  const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
@@ -2557,6 +2578,7 @@ const getCommentSyntax = (ext) => {
2557
2578
  };
2558
2579
  const getMatchedLinePrefix = (line, syntax) => {
2559
2580
  const trimmed = line.trimStart();
2581
+ if (isAislopDirectiveLine(trimmed)) return null;
2560
2582
  for (const prefix of syntax.linePrefixes) {
2561
2583
  if (!trimmed.startsWith(prefix)) continue;
2562
2584
  if (prefix === "#" && trimmed.startsWith("#!")) return null;
@@ -3356,9 +3378,7 @@ const isLogOnlyBody = (body) => {
3356
3378
  };
3357
3379
  const detectJsSilentRecovery = (content, relPath) => {
3358
3380
  const out = [];
3359
- CATCH_HEAD_RE.lastIndex = 0;
3360
- let match;
3361
- while ((match = CATCH_HEAD_RE.exec(content)) !== null) {
3381
+ for (const match of content.matchAll(CATCH_HEAD_RE)) {
3362
3382
  const body = extractCatchBody(content, match.index + match[0].length - 1);
3363
3383
  if (body === null) continue;
3364
3384
  if (!isLogOnlyBody(body)) continue;
@@ -3534,18 +3554,22 @@ const extractPyImportedSymbols = (lines) => {
3534
3554
  }
3535
3555
  continue;
3536
3556
  }
3537
- const importMatch = trimmed.match(/^import\s+([\w.]+)(?:\s+as\s+(\w+))?/);
3557
+ const importMatch = trimmed.match(/^import\s+(.+)/);
3538
3558
  if (importMatch) {
3539
3559
  importLines.add(i);
3540
- const alias = importMatch[2];
3541
- if (alias && alias === importMatch[1]) continue;
3542
- const simpleName = (alias ?? importMatch[1]).split(".")[0];
3543
- if (simpleName && /^\w+$/.test(simpleName)) symbols.push({
3544
- name: simpleName,
3545
- line: i + 1,
3546
- isDefault: false,
3547
- isNamespace: true
3548
- });
3560
+ for (const clause of importMatch[1].replace(/#.*$/, "").split(",")) {
3561
+ const clauseMatch = clause.trim().match(/^([\w.]+)(?:\s+as\s+(\w+))?/);
3562
+ if (!clauseMatch) continue;
3563
+ const alias = clauseMatch[2];
3564
+ if (alias && alias === clauseMatch[1]) continue;
3565
+ const simpleName = (alias ?? clauseMatch[1]).split(".")[0];
3566
+ if (simpleName && /^\w+$/.test(simpleName)) symbols.push({
3567
+ name: simpleName,
3568
+ line: i + 1,
3569
+ isDefault: false,
3570
+ isNamespace: true
3571
+ });
3572
+ }
3549
3573
  }
3550
3574
  }
3551
3575
  return {
@@ -3555,8 +3579,7 @@ const extractPyImportedSymbols = (lines) => {
3555
3579
  };
3556
3580
  const isSymbolUsed = (name, content, importLines, lines) => {
3557
3581
  const pattern = new RegExp(`\\b${name}\\b`, "g");
3558
- let match;
3559
- while ((match = pattern.exec(content)) !== null) {
3582
+ for (const match of content.matchAll(pattern)) {
3560
3583
  const lineIndex = content.slice(0, match.index).split("\n").length - 1;
3561
3584
  if (!importLines.has(lineIndex)) return true;
3562
3585
  }
@@ -3657,6 +3680,18 @@ const aiSlopEngine = {
3657
3680
 
3658
3681
  //#endregion
3659
3682
  //#region src/engines/architecture/matchers.ts
3683
+ const REGEX_SPECIAL_CHARS = new Set([
3684
+ ".",
3685
+ "+",
3686
+ "^",
3687
+ "$",
3688
+ "{",
3689
+ "}",
3690
+ "(",
3691
+ ")",
3692
+ "|",
3693
+ "\\"
3694
+ ]);
3660
3695
  const minimatch = (filePath, pattern) => {
3661
3696
  let regex = "";
3662
3697
  let i = 0;
@@ -3681,7 +3716,7 @@ const minimatch = (filePath, pattern) => {
3681
3716
  regex += pattern.slice(i, closeIndex + 1);
3682
3717
  i = closeIndex + 1;
3683
3718
  }
3684
- } else if (".+^${}()|\\".includes(ch)) {
3719
+ } else if (REGEX_SPECIAL_CHARS.has(ch)) {
3685
3720
  regex += `\\${ch}`;
3686
3721
  i++;
3687
3722
  } else {
@@ -3701,27 +3736,15 @@ const extractImports = (content, ext) => {
3701
3736
  ".mjs",
3702
3737
  ".cjs"
3703
3738
  ].includes(ext)) {
3704
- const esPattern = /(?:import|from)\s+["']([^"']+)["']/g;
3705
- let match;
3706
- while ((match = esPattern.exec(content)) !== null) imports.push(match[1]);
3707
- const reqPattern = /require\s*\(\s*["']([^"']+)["']\s*\)/g;
3708
- while ((match = reqPattern.exec(content)) !== null) imports.push(match[1]);
3709
- }
3710
- if (ext === ".py") {
3711
- const pyPattern = /(?:from|import)\s+([\w.]+)/g;
3712
- let match;
3713
- while ((match = pyPattern.exec(content)) !== null) imports.push(match[1]);
3739
+ for (const match of content.matchAll(/(?:import|from)\s+["']([^"']+)["']/g)) imports.push(match[1]);
3740
+ for (const match of content.matchAll(/require\s*\(\s*["']([^"']+)["']\s*\)/g)) imports.push(match[1]);
3714
3741
  }
3742
+ if (ext === ".py") for (const match of content.matchAll(/(?:from|import)\s+([\w.]+)/g)) imports.push(match[1]);
3715
3743
  if (ext === ".go") {
3716
- const goSingleImport = /^\s*import\s+"([^"]+)"/gm;
3717
- let match;
3718
- while ((match = goSingleImport.exec(content)) !== null) imports.push(match[1]);
3719
- const goMultiImport = /import\s*\(([^)]*)\)/gs;
3720
- while ((match = goMultiImport.exec(content)) !== null) {
3744
+ for (const match of content.matchAll(/^\s*import\s+"([^"]+)"/gm)) imports.push(match[1]);
3745
+ for (const match of content.matchAll(/import\s*\(([^)]*)\)/gs)) {
3721
3746
  const block = match[1];
3722
- const pkgPattern = /"([^"]+)"/g;
3723
- let pkgMatch;
3724
- while ((pkgMatch = pkgPattern.exec(block)) !== null) imports.push(pkgMatch[1]);
3747
+ for (const pkgMatch of block.matchAll(/"([^"]+)"/g)) imports.push(pkgMatch[1]);
3725
3748
  }
3726
3749
  }
3727
3750
  return imports;
@@ -3848,10 +3871,10 @@ const architectureEngine = {
3848
3871
  //#endregion
3849
3872
  //#region src/engines/code-quality/function-boundaries.ts
3850
3873
  const PYTHON_CONTROL_FLOW_RE = /^\s*(?:if|for|while|with|try|except|else|elif|finally|def|class)\b/;
3851
- const ARROW_BLOCK_RE = /* @__PURE__ */ new RegExp("=>\\s*\\{");
3852
- const ARROW_END_RE = /* @__PURE__ */ new RegExp("=>\\s*$");
3853
- const BRACE_START_RE = /* @__PURE__ */ new RegExp("^\\s*\\{");
3854
- const NEW_STATEMENT_RE = /* @__PURE__ */ new RegExp("^(?:export\\s+)?(?:const|let|var|function|class)\\s");
3874
+ const ARROW_BLOCK_RE = /=>\s*\{/;
3875
+ const ARROW_END_RE = /=>\s*$/;
3876
+ const BRACE_START_RE = /^\s*\{/;
3877
+ const NEW_STATEMENT_RE = /^(?:export\s+)?(?:const|let|var|function|class)\s/;
3855
3878
  const isControlFlowBrace = (lineText, braceIndex) => {
3856
3879
  const before = lineText.substring(0, braceIndex).trimEnd();
3857
3880
  if (before.endsWith(")")) return true;
@@ -4037,14 +4060,14 @@ const countTemplateLines = (bodyLines) => {
4037
4060
  let templateLineCount = 0;
4038
4061
  for (const line of bodyLines) {
4039
4062
  const startedInside = insideTemplate;
4040
- let escape = false;
4063
+ let escaped = false;
4041
4064
  for (const ch of line) {
4042
- if (escape) {
4043
- escape = false;
4065
+ if (escaped) {
4066
+ escaped = false;
4044
4067
  continue;
4045
4068
  }
4046
4069
  if (ch === "\\") {
4047
- escape = true;
4070
+ escaped = true;
4048
4071
  continue;
4049
4072
  }
4050
4073
  if (ch === "`") insideTemplate = !insideTemplate;
@@ -4964,9 +4987,7 @@ const runRuffFormat = async (context) => {
4964
4987
  };
4965
4988
  const parseRuffFormatOutput = (output, rootDir) => {
4966
4989
  const diagnostics = [];
4967
- const filePattern = /^--- (.+)$/gm;
4968
- let match;
4969
- while ((match = filePattern.exec(output)) !== null) {
4990
+ for (const match of output.matchAll(/^--- (.+)$/gm)) {
4970
4991
  const filePath = getRuffDiagnosticPath(rootDir, match[1]);
4971
4992
  diagnostics.push({
4972
4993
  filePath,
@@ -4993,10 +5014,10 @@ const formatEngine = {
4993
5014
  const { languages, installedTools } = context;
4994
5015
  const promises = [];
4995
5016
  if (languages.includes("typescript") || languages.includes("javascript")) promises.push(runBiomeFormat(context));
4996
- if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffFormat(context));
4997
- if (languages.includes("go") && installedTools["gofmt"]) promises.push(runGofmt(context));
4998
- if (languages.includes("rust") && installedTools["rustfmt"]) promises.push(runGenericFormatter(context, "rust"));
4999
- if (languages.includes("ruby") && installedTools["rubocop"]) promises.push(runGenericFormatter(context, "ruby"));
5017
+ if (languages.includes("python") && installedTools.ruff) promises.push(runRuffFormat(context));
5018
+ if (languages.includes("go") && installedTools.gofmt) promises.push(runGofmt(context));
5019
+ if (languages.includes("rust") && installedTools.rustfmt) promises.push(runGenericFormatter(context, "rust"));
5020
+ if (languages.includes("ruby") && installedTools.rubocop) promises.push(runGenericFormatter(context, "ruby"));
5000
5021
  if (languages.includes("php") && installedTools["php-cs-fixer"]) promises.push(runGenericFormatter(context, "php"));
5001
5022
  const results = await Promise.allSettled(promises);
5002
5023
  for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
@@ -5200,6 +5221,8 @@ const createOxlintConfig = (options) => {
5200
5221
  if (options.mode === "fix") {
5201
5222
  rules["no-unused-vars"] = "off";
5202
5223
  rules["react-hooks/exhaustive-deps"] = "off";
5224
+ rules["jsx-a11y/no-aria-hidden-on-focusable"] = "off";
5225
+ rules["unicorn/no-useless-fallback-in-spread"] = "off";
5203
5226
  }
5204
5227
  const plugins = [
5205
5228
  "import",
@@ -5364,9 +5387,7 @@ const collectAmbientGlobals = (rootDir) => {
5364
5387
  if (!relativePath.endsWith(".d.ts")) continue;
5365
5388
  const content = readTextFile$1(path.join(rootDir, relativePath));
5366
5389
  if (!content) continue;
5367
- AMBIENT_GLOBAL_RE.lastIndex = 0;
5368
- let match;
5369
- while ((match = AMBIENT_GLOBAL_RE.exec(content)) !== null) globals.add(match[1]);
5390
+ for (const match of content.matchAll(AMBIENT_GLOBAL_RE)) globals.add(match[1]);
5370
5391
  }
5371
5392
  const deps = collectPackageNames(rootDir);
5372
5393
  if (deps.has("@types/bun") || deps.has("bun-types")) globals.add("Bun");
@@ -5582,10 +5603,10 @@ const lintEngine = {
5582
5603
  if (context.config.lint.typecheck) promises.push(import("./typecheck-By967nny.js").then((mod) => mod.runTypecheck(context)));
5583
5604
  }
5584
5605
  if (context.frameworks.includes("expo")) promises.push(import("./expo-doctor-T4DswmX5.js").then((mod) => mod.runExpoDoctor(context)));
5585
- if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffLint(context));
5606
+ if (languages.includes("python") && installedTools.ruff) promises.push(runRuffLint(context));
5586
5607
  if (languages.includes("go") && installedTools["golangci-lint"]) promises.push(runGolangciLint(context));
5587
- if (languages.includes("rust") && installedTools["cargo"]) promises.push(runGenericLinter(context, "rust"));
5588
- if (languages.includes("ruby") && installedTools["rubocop"]) promises.push(runGenericLinter(context, "ruby"));
5608
+ if (languages.includes("rust") && installedTools.cargo) promises.push(runGenericLinter(context, "rust"));
5609
+ if (languages.includes("ruby") && installedTools.rubocop) promises.push(runGenericLinter(context, "ruby"));
5589
5610
  const results = await Promise.allSettled(promises);
5590
5611
  for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
5591
5612
  return {
@@ -5615,7 +5636,7 @@ const runDependencyAudit = async (context) => {
5615
5636
  else if (fs.existsSync(path.join(context.rootDirectory, "package-lock.json")) || fs.existsSync(path.join(context.rootDirectory, "package.json"))) promises.push(runNpmAudit(context.rootDirectory, timeout));
5616
5637
  }
5617
5638
  if (context.languages.includes("python") && context.installedTools["pip-audit"]) promises.push(runPipAudit(context.rootDirectory, timeout));
5618
- if (context.languages.includes("go") && context.installedTools["govulncheck"]) promises.push(runGovulncheck(context.rootDirectory, timeout));
5639
+ if (context.languages.includes("go") && context.installedTools.govulncheck) promises.push(runGovulncheck(context.rootDirectory, timeout));
5619
5640
  if (context.languages.includes("rust")) promises.push(runCargoAudit(context.rootDirectory, timeout));
5620
5641
  const results = await Promise.allSettled(promises);
5621
5642
  for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
@@ -5720,9 +5741,12 @@ const parseLegacyAdvisories = (advisories, source) => {
5720
5741
  for (const [key, advisory] of Object.entries(advisories)) upsertVuln(bucket, advisory.module_name ?? advisory.name ?? advisory.package ?? key, (advisory.severity ?? "moderate").toLowerCase(), advisory.recommendation ?? advisory.title ?? "");
5721
5742
  return [...bucket.values()].map((agg) => aggregateToDiagnostic(agg, source));
5722
5743
  };
5744
+ const carriesAdvisory = (vulnerability) => Array.isArray(vulnerability.via) && vulnerability.via.some((entry) => entry !== null && typeof entry === "object");
5723
5745
  const parseModernVulnerabilities = (vulnerabilities, source) => {
5724
5746
  const bucket = /* @__PURE__ */ new Map();
5747
+ const hasRootCauses = Object.values(vulnerabilities).some(carriesAdvisory);
5725
5748
  for (const [packageName, vulnerability] of Object.entries(vulnerabilities)) {
5749
+ if (hasRootCauses && !carriesAdvisory(vulnerability)) continue;
5726
5750
  const severity = (vulnerability.severity ?? "moderate").toLowerCase();
5727
5751
  const fixAvailable = vulnerability.fixAvailable;
5728
5752
  const isDirect = vulnerability.isDirect === true;
@@ -6010,8 +6034,7 @@ const detectRiskyConstructs = async (context) => {
6010
6034
  if (!extensions.includes(ext)) continue;
6011
6035
  if (isMigrationOrSeeder && name === "sql-injection") continue;
6012
6036
  const regex = new RegExp(pattern.source, pattern.flags);
6013
- let match;
6014
- while ((match = regex.exec(masked)) !== null) {
6037
+ for (const match of masked.matchAll(regex)) {
6015
6038
  const line = content.slice(0, match.index).split("\n").length;
6016
6039
  if (name === "innerhtml") {
6017
6040
  const beforeMatch = content.slice(Math.max(0, match.index - 200), match.index);
@@ -6143,8 +6166,7 @@ const scanSecrets = async (context) => {
6143
6166
  const relativePath = path.relative(context.rootDirectory, filePath);
6144
6167
  for (const { pattern, name, keywordPrefixed } of SECRET_PATTERNS) {
6145
6168
  const regex = new RegExp(pattern.source, pattern.flags);
6146
- let match;
6147
- while ((match = regex.exec(content)) !== null) {
6169
+ for (const match of content.matchAll(regex)) {
6148
6170
  if (isPlaceholderValue(match[1] ?? match[0])) continue;
6149
6171
  if (keywordPrefixed && isInsideStringLiteral(content, match.index)) continue;
6150
6172
  const line = content.slice(0, match.index).split("\n").length;
@@ -6265,6 +6287,64 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
6265
6287
 
6266
6288
  //#endregion
6267
6289
  //#region src/utils/discover.ts
6290
+ const UNSUPPORTED_CODE_EXTENSIONS = {
6291
+ ".c": "C/C++",
6292
+ ".h": "C/C++",
6293
+ ".cc": "C/C++",
6294
+ ".cpp": "C/C++",
6295
+ ".cxx": "C/C++",
6296
+ ".hpp": "C/C++",
6297
+ ".hh": "C/C++",
6298
+ ".hxx": "C/C++",
6299
+ ".cs": "C#",
6300
+ ".swift": "Swift",
6301
+ ".kt": "Kotlin",
6302
+ ".kts": "Kotlin",
6303
+ ".m": "Objective-C",
6304
+ ".mm": "Objective-C",
6305
+ ".scala": "Scala",
6306
+ ".dart": "Dart",
6307
+ ".ex": "Elixir",
6308
+ ".exs": "Elixir",
6309
+ ".erl": "Erlang",
6310
+ ".hs": "Haskell",
6311
+ ".clj": "Clojure",
6312
+ ".cljs": "Clojure",
6313
+ ".lua": "Lua",
6314
+ ".jl": "Julia",
6315
+ ".zig": "Zig",
6316
+ ".nim": "Nim",
6317
+ ".ml": "OCaml",
6318
+ ".fs": "F#",
6319
+ ".sol": "Solidity",
6320
+ ".groovy": "Groovy"
6321
+ };
6322
+ const analyzeCoverage = (rootDirectory, excludePatterns = []) => {
6323
+ const allFiles = listProjectFiles(rootDirectory);
6324
+ const supportedFiles = filterProjectFiles(rootDirectory, allFiles, [], excludePatterns).length;
6325
+ const counts = /* @__PURE__ */ new Map();
6326
+ let unsupportedFiles = 0;
6327
+ const candidates = filterProjectFiles(rootDirectory, allFiles, Object.keys(UNSUPPORTED_CODE_EXTENSIONS), excludePatterns);
6328
+ for (const file of candidates) {
6329
+ const lang = UNSUPPORTED_CODE_EXTENSIONS[path.extname(file).toLowerCase()];
6330
+ if (!lang) continue;
6331
+ unsupportedFiles += 1;
6332
+ counts.set(lang, (counts.get(lang) ?? 0) + 1);
6333
+ }
6334
+ let dominantUnsupported = null;
6335
+ let max = 0;
6336
+ for (const [lang, count] of counts) if (count > max) {
6337
+ max = count;
6338
+ dominantUnsupported = lang;
6339
+ }
6340
+ const negligible = supportedFiles === 0 || unsupportedFiles >= 10 && unsupportedFiles > supportedFiles * 3;
6341
+ return {
6342
+ supportedFiles,
6343
+ unsupportedFiles,
6344
+ dominantUnsupported,
6345
+ scoreable: !negligible
6346
+ };
6347
+ };
6268
6348
  const LANGUAGE_SIGNALS = {
6269
6349
  "tsconfig.json": "typescript",
6270
6350
  "go.mod": "go",
@@ -6384,11 +6464,12 @@ const checkInstalledTools = async () => {
6384
6464
  }));
6385
6465
  return results;
6386
6466
  };
6387
- const discoverProject = async (directory) => {
6467
+ const discoverProject = async (directory, excludePatterns = []) => {
6388
6468
  const resolvedDir = path.resolve(directory);
6389
6469
  const languages = detectLanguages(resolvedDir);
6390
6470
  const frameworks = detectFrameworks(resolvedDir);
6391
6471
  const sourceFileCount = countSourceFiles(resolvedDir);
6472
+ const coverage = analyzeCoverage(resolvedDir, excludePatterns);
6392
6473
  const installedTools = await checkInstalledTools();
6393
6474
  return {
6394
6475
  rootDirectory: resolvedDir,
@@ -6396,6 +6477,7 @@ const discoverProject = async (directory) => {
6396
6477
  languages,
6397
6478
  frameworks,
6398
6479
  sourceFileCount,
6480
+ coverage,
6399
6481
  installedTools
6400
6482
  };
6401
6483
  };
@@ -6615,10 +6697,6 @@ const handleAislopBaseline = (input) => {
6615
6697
  };
6616
6698
  };
6617
6699
 
6618
- //#endregion
6619
- //#region src/version.ts
6620
- const APP_VERSION = "0.10.1";
6621
-
6622
6700
  //#endregion
6623
6701
  //#region src/telemetry/env.ts
6624
6702
  const detectPackageManager = (env = process.env) => {
@@ -6813,9 +6891,14 @@ const track = (input) => {
6813
6891
  pendingRequests.add(request);
6814
6892
  return { installCreated };
6815
6893
  };
6816
- const flushTelemetry = async () => {
6894
+ const flushTelemetry = async (timeoutMs) => {
6817
6895
  if (pendingRequests.size === 0) return;
6818
- await Promise.all(pendingRequests);
6896
+ const all = Promise.all(pendingRequests);
6897
+ if (timeoutMs == null) {
6898
+ await all;
6899
+ return;
6900
+ }
6901
+ await Promise.race([all, new Promise((resolve) => setTimeout(resolve, timeoutMs))]);
6819
6902
  };
6820
6903
 
6821
6904
  //#endregion