aislop 0.10.0 → 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
@@ -512,7 +532,7 @@ const getSourceFilesWithExtras = (context, extraExtensions) => {
512
532
  };
513
533
 
514
534
  //#endregion
515
- //#region src/engines/ai-slop/abstractions.ts
535
+ //#region src/utils/source-masker.ts
516
536
  const JS_EXTS$2 = new Set([
517
537
  ".ts",
518
538
  ".tsx",
@@ -521,14 +541,226 @@ const JS_EXTS$2 = new Set([
521
541
  ".mjs",
522
542
  ".cjs"
523
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
+ };
694
+ };
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("");
744
+ };
745
+
746
+ //#endregion
747
+ //#region src/engines/ai-slop/abstractions.ts
748
+ const JS_EXTS$1 = new Set([
749
+ ".ts",
750
+ ".tsx",
751
+ ".js",
752
+ ".jsx",
753
+ ".mjs",
754
+ ".cjs"
755
+ ]);
524
756
  const THIN_WRAPPER_PATTERNS = [
525
757
  {
526
758
  pattern: /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
527
- extensions: JS_EXTS$2
759
+ extensions: JS_EXTS$1
528
760
  },
529
761
  {
530
762
  pattern: /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
531
- extensions: JS_EXTS$2
763
+ extensions: JS_EXTS$1
532
764
  },
533
765
  {
534
766
  pattern: /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm,
@@ -556,16 +788,14 @@ const detectThinWrappers = (content, relativePath, ext) => {
556
788
  for (const { pattern, extensions } of THIN_WRAPPER_PATTERNS) {
557
789
  if (!extensions.has(ext)) continue;
558
790
  const regex = new RegExp(pattern.source, pattern.flags);
559
- let match;
560
- while ((match = regex.exec(content)) !== null) {
791
+ for (const match of content.matchAll(regex)) {
561
792
  const funcName = match[1];
562
793
  const matchText = match[0];
563
794
  const lineNumber = content.slice(0, match.index).split("\n").length;
564
795
  if (DUNDER_PATTERN.test(funcName)) continue;
565
796
  if (FRAMEWORK_METHOD_NAMES.test(funcName)) continue;
566
797
  if (lineNumber >= 2) {
567
- const prevLine = lines[lineNumber - 2]?.trim();
568
- if (prevLine && prevLine.startsWith("@")) continue;
798
+ if ((lines[lineNumber - 2]?.trim())?.startsWith("@")) continue;
569
799
  }
570
800
  if (!isIdentityForward(matchText)) continue;
571
801
  if (isUseContextWrapper(matchText)) continue;
@@ -621,8 +851,9 @@ const detectOverAbstraction = async (context) => {
621
851
  }
622
852
  const relativePath = path.relative(context.rootDirectory, filePath);
623
853
  const ext = path.extname(filePath);
624
- diagnostics.push(...detectThinWrappers(content, relativePath, ext));
625
- diagnostics.push(...detectAiNaming(content, relativePath));
854
+ const codeOnly = maskComments(content, ext);
855
+ diagnostics.push(...detectThinWrappers(codeOnly, relativePath, ext));
856
+ diagnostics.push(...detectAiNaming(codeOnly, relativePath));
626
857
  }
627
858
  return diagnostics;
628
859
  };
@@ -941,7 +1172,7 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
941
1172
  const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
942
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));
943
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));
944
- 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));
945
1176
  }
946
1177
  return diagnostics;
947
1178
  };
@@ -978,9 +1209,10 @@ const detectDeadPatterns = async (context) => {
978
1209
  }
979
1210
  const ext = path.extname(filePath);
980
1211
  const relativePath = path.relative(context.rootDirectory, filePath);
981
- diagnostics.push(...detectConsoleLeftovers(content, relativePath, ext));
1212
+ const codeOnly = maskComments(content, ext);
1213
+ diagnostics.push(...detectConsoleLeftovers(codeOnly, relativePath, ext));
982
1214
  diagnostics.push(...detectTodoStubs(content, relativePath));
983
- diagnostics.push(...detectDeadCodePatterns(content, relativePath, ext));
1215
+ diagnostics.push(...detectDeadCodePatterns(codeOnly, relativePath, ext));
984
1216
  diagnostics.push(...detectUnsafeTypePatterns(content, relativePath, ext));
985
1217
  }
986
1218
  return diagnostics;
@@ -1170,6 +1402,7 @@ const JS_EXTENSIONS$2 = new Set([
1170
1402
  const IMPORT_FROM_RE = /^\s*import\s+([^;]*?)\s+from\s+["']([^"']+)["']/;
1171
1403
  const TYPE_ONLY_RE = /^\s*type\b/;
1172
1404
  const VALUE_BINDING_RE = /\{([^}]*)\}/;
1405
+ const NAMESPACE_RE = /\*\s+as\s+/;
1173
1406
  const isTypeOnly = (clause) => {
1174
1407
  if (TYPE_ONLY_RE.test(clause)) return true;
1175
1408
  const braces = VALUE_BINDING_RE.exec(clause);
@@ -1187,7 +1420,8 @@ const extractImportLines = (content) => {
1187
1420
  results.push({
1188
1421
  spec: match[2],
1189
1422
  line: i + 1,
1190
- typeOnly: isTypeOnly(match[1])
1423
+ typeOnly: isTypeOnly(match[1]),
1424
+ namespace: NAMESPACE_RE.test(match[1])
1191
1425
  });
1192
1426
  }
1193
1427
  return results;
@@ -1204,11 +1438,11 @@ const detectDuplicateImports = async (context) => {
1204
1438
  } catch {
1205
1439
  continue;
1206
1440
  }
1207
- const imports = extractImportLines(content);
1441
+ const imports = extractImportLines(maskComments(content, path.extname(filePath)));
1208
1442
  if (imports.length < 2) continue;
1209
1443
  const byBucket = /* @__PURE__ */ new Map();
1210
1444
  for (const imp of imports) {
1211
- const key = `${imp.typeOnly ? "type" : "value"}\0${imp.spec}`;
1445
+ const key = `${imp.namespace ? "ns" : imp.typeOnly ? "type" : "value"}\0${imp.spec}`;
1212
1446
  const list = byBucket.get(key) ?? [];
1213
1447
  list.push(imp);
1214
1448
  byBucket.set(key, list);
@@ -1324,9 +1558,8 @@ const detectSwallowedExceptions = async (context) => {
1324
1558
  const relativePath = path.relative(context.rootDirectory, filePath);
1325
1559
  for (const { pattern, languages, message } of SWALLOWED_EXCEPTION_PATTERNS) {
1326
1560
  if (!languages.includes(ext)) continue;
1327
- let match;
1328
1561
  const regex = new RegExp(pattern.source, pattern.flags + (pattern.flags.includes("g") ? "" : "g"));
1329
- while ((match = regex.exec(content)) !== null) {
1562
+ for (const match of content.matchAll(regex)) {
1330
1563
  if (isIntentionalIgnore(match[0], ext)) continue;
1331
1564
  const line = content.slice(0, match.index).split("\n").length;
1332
1565
  diagnostics.push({
@@ -1421,170 +1654,10 @@ const detectGoPatterns = async (context) => {
1421
1654
  };
1422
1655
 
1423
1656
  //#endregion
1424
- //#region src/engines/ai-slop/hardcoded-config.ts
1425
- const SOURCE_EXTENSIONS = new Set([
1426
- ".ts",
1427
- ".tsx",
1428
- ".js",
1429
- ".jsx",
1430
- ".mjs",
1431
- ".cjs",
1432
- ".py",
1433
- ".go",
1434
- ".rs",
1435
- ".rb",
1436
- ".java",
1437
- ".php"
1438
- ]);
1439
- const URL_LITERAL_RE = /(["'`])(https?:\/\/[^"'`\s<>]+)\1/g;
1440
- const ID_LITERAL_RE = /(["'])([A-Za-z][A-Za-z0-9_-]{15,})\1/g;
1441
- const ENV_REFERENCE_RE = /\b(?:process\.env|import\.meta\.env|Deno\.env|os\.environ|getenv|env\()\b/i;
1442
- const DOC_URL_CONTEXT_RE = /\b(?:docs?|documentation|homepage|repository|bugs|license|readme|source|svgUrl|pageUrl|href|link|install)\b/i;
1443
- const URL_CONFIG_CONTEXT_RE = /\b(?:api|base[_-]?url|baseUrl|endpoint|host|origin|webhook|callback|redirect|server|service|domain|url)\b/i;
1444
- 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;
1445
- 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;
1446
- const MIGRATION_PATH_RE$1 = /(?:^|[\\/])(?:migrations?|db[\\/]migrate)[\\/]/i;
1447
- const PLACEHOLDER_HOSTS = new Set([
1448
- "example.com",
1449
- "example.org",
1450
- "example.net"
1451
- ]);
1452
- const LOOPBACK_HOSTS = new Set([
1453
- "localhost",
1454
- "127.0.0.1",
1455
- "0.0.0.0",
1456
- "::1"
1457
- ]);
1458
- const VENDOR_API_DOMAINS = [
1459
- "github.com",
1460
- "githubusercontent.com",
1461
- "googleapis.com",
1462
- "accounts.google.com",
1463
- "stripe.com",
1464
- "openai.com",
1465
- "anthropic.com",
1466
- "slack.com",
1467
- "twilio.com",
1468
- "sendgrid.com",
1469
- "mailgun.net",
1470
- "cloudflare.com",
1471
- "discord.com",
1472
- "telegram.org",
1473
- "login.microsoftonline.com",
1474
- "graph.microsoft.com",
1475
- "twitter.com",
1476
- "x.com",
1477
- "twimg.com",
1478
- "t.co",
1479
- "api.telegram.org"
1480
- ];
1481
- const isVendorApiHost = (host) => VENDOR_API_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
1482
- const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
1483
- const HARDCODED_URL_FINDING = {
1484
- rule: "ai-slop/hardcoded-url",
1485
- message: "Hardcoded environment URL in production code",
1486
- help: "Move deployment-specific URLs to environment variables or a typed config module. Keep only stable documentation/public links inline."
1487
- };
1488
- const HARDCODED_ID_FINDING = {
1489
- rule: "ai-slop/hardcoded-id",
1490
- message: "Hardcoded provider/project ID in production code",
1491
- 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."
1492
- };
1493
- const makeFinding = (filePath, line, spec) => ({
1494
- filePath,
1495
- engine: "ai-slop",
1496
- rule: spec.rule,
1497
- severity: "warning",
1498
- message: spec.message,
1499
- help: spec.help,
1500
- line,
1501
- column: 0,
1502
- category: "AI Slop",
1503
- fixable: false
1504
- });
1505
- const isCommentOnlyLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*");
1506
- const commentStartsBefore = (line, index, ext) => {
1507
- const prefix = line.slice(0, index);
1508
- if (ext === ".py" || ext === ".rb") return prefix.includes("#");
1509
- if (ext === ".php") return prefix.includes("//") || prefix.includes("#");
1510
- return prefix.includes("//") || prefix.includes("/*");
1511
- };
1512
- const safeUrlHost = (urlText) => {
1513
- try {
1514
- return new URL(urlText).hostname.toLowerCase();
1515
- } catch {
1516
- return null;
1517
- }
1518
- };
1519
- const isEnvBackedLine = (line) => ENV_REFERENCE_RE.test(line);
1520
- const shouldFlagUrlLiteral = (line, urlText) => {
1521
- if (isEnvBackedLine(line)) return false;
1522
- const host = safeUrlHost(urlText);
1523
- if (!host) return false;
1524
- if (PLACEHOLDER_HOSTS.has(host)) return false;
1525
- if (LOOPBACK_HOSTS.has(host)) return false;
1526
- if (isVendorApiHost(host)) return false;
1527
- if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
1528
- return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
1529
- };
1530
- const ENV_VAR_NAME_RE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/;
1531
- const hasUsefulIdShape = (value) => {
1532
- if (PLACEHOLDER_ID_RE.test(value)) return false;
1533
- if (ENV_VAR_NAME_RE.test(value)) return false;
1534
- if (/^https?:\/\//i.test(value)) return false;
1535
- if (/^[A-Za-z]+$/.test(value)) return false;
1536
- return /[0-9]/.test(value);
1537
- };
1538
- const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
1539
- const diagnostics = [];
1540
- if (isCommentOnlyLine(line.trim())) return diagnostics;
1541
- URL_LITERAL_RE.lastIndex = 0;
1542
- let urlMatch;
1543
- while ((urlMatch = URL_LITERAL_RE.exec(line)) !== null) {
1544
- const urlText = urlMatch[2];
1545
- if (commentStartsBefore(line, urlMatch.index, ext)) continue;
1546
- if (!shouldFlagUrlLiteral(line, urlText)) continue;
1547
- diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_URL_FINDING));
1548
- }
1549
- if (!ID_CONTEXT_RE.test(line) || isEnvBackedLine(line) || DOC_URL_CONTEXT_RE.test(line)) return diagnostics;
1550
- ID_LITERAL_RE.lastIndex = 0;
1551
- let idMatch;
1552
- while ((idMatch = ID_LITERAL_RE.exec(line)) !== null) {
1553
- const value = idMatch[2];
1554
- if (commentStartsBefore(line, idMatch.index, ext)) continue;
1555
- if (!hasUsefulIdShape(value)) continue;
1556
- diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_ID_FINDING));
1557
- }
1558
- return diagnostics;
1559
- };
1560
- const scanFileForConfigLiterals = (content, relativePath, ext) => {
1561
- if (!SOURCE_EXTENSIONS.has(ext)) return [];
1562
- if (isNonProductionPath(relativePath)) return [];
1563
- if (MIGRATION_PATH_RE$1.test(relativePath)) return [];
1564
- return content.split("\n").flatMap((line, index) => scanLineForConfigLiterals(line, relativePath, ext, index + 1));
1565
- };
1566
- const detectHardcodedConfigLiterals = async (context) => {
1567
- const diagnostics = [];
1568
- for (const filePath of getSourceFiles(context)) {
1569
- if (isAutoGenerated(filePath)) continue;
1570
- let content;
1571
- try {
1572
- content = fs.readFileSync(filePath, "utf-8");
1573
- } catch {
1574
- continue;
1575
- }
1576
- const relativePath = path.relative(context.rootDirectory, filePath);
1577
- const ext = path.extname(filePath);
1578
- diagnostics.push(...scanFileForConfigLiterals(content, relativePath, ext));
1579
- }
1580
- return diagnostics;
1581
- };
1582
-
1583
- //#endregion
1584
- //#region src/engines/ai-slop/js-import-aliases.ts
1585
- const TS_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
1586
- const JS_RESOLUTION_EXTENSIONS = [
1587
- "",
1657
+ //#region src/engines/ai-slop/js-import-aliases.ts
1658
+ const TS_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
1659
+ const JS_RESOLUTION_EXTENSIONS = [
1660
+ "",
1588
1661
  ".ts",
1589
1662
  ".tsx",
1590
1663
  ".js",
@@ -1677,15 +1750,18 @@ const readWorkspaceGlobs = (rootDir, rootPkg) => {
1677
1750
  }
1678
1751
  return globs;
1679
1752
  };
1753
+ const readWorkspaceEntries = (dir) => {
1754
+ try {
1755
+ return fs.readdirSync(dir, { withFileTypes: true });
1756
+ } catch {
1757
+ return [];
1758
+ }
1759
+ };
1680
1760
  const expandWorkspaceDirs = (rootDir, globs) => {
1681
1761
  const dirs = [];
1682
1762
  for (const glob of globs) if (glob.endsWith("/*")) {
1683
1763
  const parent = path.join(rootDir, glob.slice(0, -2));
1684
- try {
1685
- for (const entry of fs.readdirSync(parent, { withFileTypes: true })) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
1686
- } catch {
1687
- continue;
1688
- }
1764
+ for (const entry of readWorkspaceEntries(parent)) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
1689
1765
  } else if (!glob.includes("*")) dirs.push(path.join(rootDir, glob));
1690
1766
  return dirs;
1691
1767
  };
@@ -2318,6 +2394,167 @@ const detectHallucinatedImports = async (context) => {
2318
2394
  return diagnostics;
2319
2395
  };
2320
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
+
2321
2558
  //#endregion
2322
2559
  //#region src/engines/ai-slop/comment-blocks.ts
2323
2560
  const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
@@ -2341,6 +2578,7 @@ const getCommentSyntax = (ext) => {
2341
2578
  };
2342
2579
  const getMatchedLinePrefix = (line, syntax) => {
2343
2580
  const trimmed = line.trimStart();
2581
+ if (isAislopDirectiveLine(trimmed)) return null;
2344
2582
  for (const prefix of syntax.linePrefixes) {
2345
2583
  if (!trimmed.startsWith(prefix)) continue;
2346
2584
  if (prefix === "#" && trimmed.startsWith("#!")) return null;
@@ -3081,7 +3319,7 @@ const detectRustPatterns = async (context) => {
3081
3319
 
3082
3320
  //#endregion
3083
3321
  //#region src/engines/ai-slop/silent-recovery.ts
3084
- const JS_EXTS$1 = new Set([
3322
+ const JS_EXTS = new Set([
3085
3323
  ".ts",
3086
3324
  ".tsx",
3087
3325
  ".js",
@@ -3140,9 +3378,7 @@ const isLogOnlyBody = (body) => {
3140
3378
  };
3141
3379
  const detectJsSilentRecovery = (content, relPath) => {
3142
3380
  const out = [];
3143
- CATCH_HEAD_RE.lastIndex = 0;
3144
- let match;
3145
- while ((match = CATCH_HEAD_RE.exec(content)) !== null) {
3381
+ for (const match of content.matchAll(CATCH_HEAD_RE)) {
3146
3382
  const body = extractCatchBody(content, match.index + match[0].length - 1);
3147
3383
  if (body === null) continue;
3148
3384
  if (!isLogOnlyBody(body)) continue;
@@ -3210,7 +3446,7 @@ const detectSilentRecovery = async (context) => {
3210
3446
  for (const filePath of files) {
3211
3447
  if (isAutoGenerated(filePath)) continue;
3212
3448
  const ext = path.extname(filePath);
3213
- const isJs = JS_EXTS$1.has(ext);
3449
+ const isJs = JS_EXTS.has(ext);
3214
3450
  if (!isJs && !(ext === ".py")) continue;
3215
3451
  const relPath = path.relative(context.rootDirectory, filePath);
3216
3452
  if (isNonProductionPath(relPath)) continue;
@@ -3318,18 +3554,22 @@ const extractPyImportedSymbols = (lines) => {
3318
3554
  }
3319
3555
  continue;
3320
3556
  }
3321
- const importMatch = trimmed.match(/^import\s+([\w.]+)(?:\s+as\s+(\w+))?/);
3557
+ const importMatch = trimmed.match(/^import\s+(.+)/);
3322
3558
  if (importMatch) {
3323
3559
  importLines.add(i);
3324
- const alias = importMatch[2];
3325
- if (alias && alias === importMatch[1]) continue;
3326
- const simpleName = (alias ?? importMatch[1]).split(".")[0];
3327
- if (simpleName && /^\w+$/.test(simpleName)) symbols.push({
3328
- name: simpleName,
3329
- line: i + 1,
3330
- isDefault: false,
3331
- isNamespace: true
3332
- });
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
+ }
3333
3573
  }
3334
3574
  }
3335
3575
  return {
@@ -3339,8 +3579,7 @@ const extractPyImportedSymbols = (lines) => {
3339
3579
  };
3340
3580
  const isSymbolUsed = (name, content, importLines, lines) => {
3341
3581
  const pattern = new RegExp(`\\b${name}\\b`, "g");
3342
- let match;
3343
- while ((match = pattern.exec(content)) !== null) {
3582
+ for (const match of content.matchAll(pattern)) {
3344
3583
  const lineIndex = content.slice(0, match.index).split("\n").length - 1;
3345
3584
  if (!importLines.has(lineIndex)) return true;
3346
3585
  }
@@ -3441,6 +3680,18 @@ const aiSlopEngine = {
3441
3680
 
3442
3681
  //#endregion
3443
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
+ ]);
3444
3695
  const minimatch = (filePath, pattern) => {
3445
3696
  let regex = "";
3446
3697
  let i = 0;
@@ -3465,7 +3716,7 @@ const minimatch = (filePath, pattern) => {
3465
3716
  regex += pattern.slice(i, closeIndex + 1);
3466
3717
  i = closeIndex + 1;
3467
3718
  }
3468
- } else if (".+^${}()|\\".includes(ch)) {
3719
+ } else if (REGEX_SPECIAL_CHARS.has(ch)) {
3469
3720
  regex += `\\${ch}`;
3470
3721
  i++;
3471
3722
  } else {
@@ -3485,27 +3736,15 @@ const extractImports = (content, ext) => {
3485
3736
  ".mjs",
3486
3737
  ".cjs"
3487
3738
  ].includes(ext)) {
3488
- const esPattern = /(?:import|from)\s+["']([^"']+)["']/g;
3489
- let match;
3490
- while ((match = esPattern.exec(content)) !== null) imports.push(match[1]);
3491
- const reqPattern = /require\s*\(\s*["']([^"']+)["']\s*\)/g;
3492
- while ((match = reqPattern.exec(content)) !== null) imports.push(match[1]);
3493
- }
3494
- if (ext === ".py") {
3495
- const pyPattern = /(?:from|import)\s+([\w.]+)/g;
3496
- let match;
3497
- 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]);
3498
3741
  }
3742
+ if (ext === ".py") for (const match of content.matchAll(/(?:from|import)\s+([\w.]+)/g)) imports.push(match[1]);
3499
3743
  if (ext === ".go") {
3500
- const goSingleImport = /^\s*import\s+"([^"]+)"/gm;
3501
- let match;
3502
- while ((match = goSingleImport.exec(content)) !== null) imports.push(match[1]);
3503
- const goMultiImport = /import\s*\(([^)]*)\)/gs;
3504
- 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)) {
3505
3746
  const block = match[1];
3506
- const pkgPattern = /"([^"]+)"/g;
3507
- let pkgMatch;
3508
- while ((pkgMatch = pkgPattern.exec(block)) !== null) imports.push(pkgMatch[1]);
3747
+ for (const pkgMatch of block.matchAll(/"([^"]+)"/g)) imports.push(pkgMatch[1]);
3509
3748
  }
3510
3749
  }
3511
3750
  return imports;
@@ -3632,10 +3871,10 @@ const architectureEngine = {
3632
3871
  //#endregion
3633
3872
  //#region src/engines/code-quality/function-boundaries.ts
3634
3873
  const PYTHON_CONTROL_FLOW_RE = /^\s*(?:if|for|while|with|try|except|else|elif|finally|def|class)\b/;
3635
- const ARROW_BLOCK_RE = /* @__PURE__ */ new RegExp("=>\\s*\\{");
3636
- const ARROW_END_RE = /* @__PURE__ */ new RegExp("=>\\s*$");
3637
- const BRACE_START_RE = /* @__PURE__ */ new RegExp("^\\s*\\{");
3638
- 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/;
3639
3878
  const isControlFlowBrace = (lineText, braceIndex) => {
3640
3879
  const before = lineText.substring(0, braceIndex).trimEnd();
3641
3880
  if (before.endsWith(")")) return true;
@@ -3689,12 +3928,92 @@ const findBraceFunctionEnd = (lines, startIndex) => {
3689
3928
  maxNesting
3690
3929
  };
3691
3930
  };
3692
- const findPythonFunctionEnd = (lines, startIndex) => {
3693
- const baseIndent = lines[startIndex].match(/^(\s*)/)?.[1].length ?? 0;
3694
- let endLine = startIndex;
3931
+ const extractPythonSignature = (lines, startIndex) => {
3932
+ let depth = 0;
3933
+ let started = false;
3934
+ let params = "";
3935
+ for (let j = startIndex; j < lines.length; j++) {
3936
+ const l = lines[j];
3937
+ for (let ci = 0; ci < l.length; ci++) {
3938
+ const ch = l[ci];
3939
+ if (ch === "(") {
3940
+ depth++;
3941
+ if (depth === 1 && !started) {
3942
+ started = true;
3943
+ continue;
3944
+ }
3945
+ } else if (ch === ")") {
3946
+ depth--;
3947
+ if (depth === 0) return {
3948
+ params,
3949
+ sigEndIndex: j
3950
+ };
3951
+ }
3952
+ if (started) params += ch;
3953
+ }
3954
+ if (started) params += " ";
3955
+ }
3956
+ return {
3957
+ params,
3958
+ sigEndIndex: startIndex
3959
+ };
3960
+ };
3961
+ const countPythonParams = (signature) => {
3962
+ let depth = 0;
3963
+ const parts = [];
3964
+ let current = "";
3965
+ for (const ch of signature) {
3966
+ if (ch === "(" || ch === "[" || ch === "{") depth++;
3967
+ else if (ch === ")" || ch === "]" || ch === "}") depth--;
3968
+ if (ch === "," && depth === 0) {
3969
+ parts.push(current);
3970
+ current = "";
3971
+ continue;
3972
+ }
3973
+ current += ch;
3974
+ }
3975
+ parts.push(current);
3976
+ let count = 0;
3977
+ for (const raw of parts) {
3978
+ const p = raw.trim();
3979
+ if (p.length === 0 || p === "*" || p === "/") continue;
3980
+ if (p.startsWith("*")) continue;
3981
+ if (p.includes("=")) continue;
3982
+ const name = p.split(":")[0].trim();
3983
+ if (name === "self" || name === "cls") continue;
3984
+ count++;
3985
+ }
3986
+ return count;
3987
+ };
3988
+ const countPythonBodyCodeLines = (lines, sigEndIndex, endLine) => {
3989
+ let count = 0;
3990
+ let inDoc = false;
3991
+ let delim = "";
3992
+ for (let j = sigEndIndex + 1; j <= endLine && j < lines.length; j++) {
3993
+ const t = lines[j].trim();
3994
+ if (inDoc) {
3995
+ if (t.includes(delim)) inDoc = false;
3996
+ continue;
3997
+ }
3998
+ if (t === "" || t.startsWith("#")) continue;
3999
+ const opener = t.startsWith("\"\"\"") ? "\"\"\"" : t.startsWith("'''") ? "'''" : "";
4000
+ if (opener) {
4001
+ if (!t.slice(3).includes(opener)) {
4002
+ inDoc = true;
4003
+ delim = opener;
4004
+ }
4005
+ continue;
4006
+ }
4007
+ count++;
4008
+ }
4009
+ return count;
4010
+ };
4011
+ const findPythonFunctionEnd = (lines, defIndex, bodyStartIndex) => {
4012
+ const baseIndent = lines[defIndex].match(/^(\s*)/)?.[1].length ?? 0;
4013
+ let endLine = bodyStartIndex;
3695
4014
  let maxNesting = 0;
3696
4015
  const controlIndentStack = [];
3697
- for (let j = startIndex + 1; j < lines.length; j++) {
4016
+ for (let j = bodyStartIndex + 1; j < lines.length; j++) {
3698
4017
  const l = lines[j];
3699
4018
  if (l.trim() === "") {
3700
4019
  endLine = j;
@@ -3716,7 +4035,10 @@ const findPythonFunctionEnd = (lines, startIndex) => {
3716
4035
  };
3717
4036
  };
3718
4037
  const findFunctionEnd = (lines, startIndex, isPython) => {
3719
- if (isPython) return findPythonFunctionEnd(lines, startIndex);
4038
+ if (isPython) {
4039
+ const { sigEndIndex } = extractPythonSignature(lines, startIndex);
4040
+ return findPythonFunctionEnd(lines, startIndex, sigEndIndex);
4041
+ }
3720
4042
  return findBraceFunctionEnd(lines, startIndex);
3721
4043
  };
3722
4044
  const isBlockArrow = (lines, startIndex) => {
@@ -3738,14 +4060,14 @@ const countTemplateLines = (bodyLines) => {
3738
4060
  let templateLineCount = 0;
3739
4061
  for (const line of bodyLines) {
3740
4062
  const startedInside = insideTemplate;
3741
- let escape = false;
4063
+ let escaped = false;
3742
4064
  for (const ch of line) {
3743
- if (escape) {
3744
- escape = false;
4065
+ if (escaped) {
4066
+ escaped = false;
3745
4067
  continue;
3746
4068
  }
3747
4069
  if (ch === "\\") {
3748
- escape = true;
4070
+ escaped = true;
3749
4071
  continue;
3750
4072
  }
3751
4073
  if (ch === "`") insideTemplate = !insideTemplate;
@@ -3781,7 +4103,7 @@ const FUNCTION_PATTERNS = [
3781
4103
  ]
3782
4104
  },
3783
4105
  {
3784
- regex: /^\s*def\s+(\w+)\s*\(([^)]*)\)/,
4106
+ regex: /^\s*(?:async\s+)?def\s+(\w+)\s*\(/,
3785
4107
  langFilter: [".py"]
3786
4108
  },
3787
4109
  {
@@ -3838,14 +4160,23 @@ const analyzeFunctions = (content, ext) => {
3838
4160
  const isPython = fnMatch.patternIndex === 2;
3839
4161
  if (fnMatch.patternIndex === 1 && !isBlockArrow(lines, i)) continue;
3840
4162
  const { endLine, maxNesting } = findFunctionEnd(lines, i, isPython);
3841
- const bodyLines = lines.slice(i + 1, endLine);
3842
- const templateLines = isPython ? 0 : countTemplateLines(bodyLines);
4163
+ let templateLines;
4164
+ let paramCount;
4165
+ if (isPython) {
4166
+ const sig = extractPythonSignature(lines, i);
4167
+ const codeLines = countPythonBodyCodeLines(lines, sig.sigEndIndex, endLine);
4168
+ templateLines = endLine - i + 1 - codeLines;
4169
+ paramCount = countPythonParams(sig.params);
4170
+ } else {
4171
+ templateLines = countTemplateLines(lines.slice(i + 1, endLine));
4172
+ paramCount = countParams(fnMatch.params);
4173
+ }
3843
4174
  functions.push({
3844
4175
  name: fnMatch.name,
3845
4176
  startLine: i + 1,
3846
4177
  lineCount: endLine - i + 1,
3847
4178
  maxNesting,
3848
- paramCount: countParams(fnMatch.params),
4179
+ paramCount,
3849
4180
  templateLines
3850
4181
  });
3851
4182
  }
@@ -4656,9 +4987,7 @@ const runRuffFormat = async (context) => {
4656
4987
  };
4657
4988
  const parseRuffFormatOutput = (output, rootDir) => {
4658
4989
  const diagnostics = [];
4659
- const filePattern = /^--- (.+)$/gm;
4660
- let match;
4661
- while ((match = filePattern.exec(output)) !== null) {
4990
+ for (const match of output.matchAll(/^--- (.+)$/gm)) {
4662
4991
  const filePath = getRuffDiagnosticPath(rootDir, match[1]);
4663
4992
  diagnostics.push({
4664
4993
  filePath,
@@ -4685,10 +5014,10 @@ const formatEngine = {
4685
5014
  const { languages, installedTools } = context;
4686
5015
  const promises = [];
4687
5016
  if (languages.includes("typescript") || languages.includes("javascript")) promises.push(runBiomeFormat(context));
4688
- if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffFormat(context));
4689
- if (languages.includes("go") && installedTools["gofmt"]) promises.push(runGofmt(context));
4690
- if (languages.includes("rust") && installedTools["rustfmt"]) promises.push(runGenericFormatter(context, "rust"));
4691
- 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"));
4692
5021
  if (languages.includes("php") && installedTools["php-cs-fixer"]) promises.push(runGenericFormatter(context, "php"));
4693
5022
  const results = await Promise.allSettled(promises);
4694
5023
  for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
@@ -4892,6 +5221,8 @@ const createOxlintConfig = (options) => {
4892
5221
  if (options.mode === "fix") {
4893
5222
  rules["no-unused-vars"] = "off";
4894
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";
4895
5226
  }
4896
5227
  const plugins = [
4897
5228
  "import",
@@ -5056,9 +5387,7 @@ const collectAmbientGlobals = (rootDir) => {
5056
5387
  if (!relativePath.endsWith(".d.ts")) continue;
5057
5388
  const content = readTextFile$1(path.join(rootDir, relativePath));
5058
5389
  if (!content) continue;
5059
- AMBIENT_GLOBAL_RE.lastIndex = 0;
5060
- let match;
5061
- 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]);
5062
5391
  }
5063
5392
  const deps = collectPackageNames(rootDir);
5064
5393
  if (deps.has("@types/bun") || deps.has("bun-types")) globals.add("Bun");
@@ -5274,10 +5603,10 @@ const lintEngine = {
5274
5603
  if (context.config.lint.typecheck) promises.push(import("./typecheck-By967nny.js").then((mod) => mod.runTypecheck(context)));
5275
5604
  }
5276
5605
  if (context.frameworks.includes("expo")) promises.push(import("./expo-doctor-T4DswmX5.js").then((mod) => mod.runExpoDoctor(context)));
5277
- if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffLint(context));
5606
+ if (languages.includes("python") && installedTools.ruff) promises.push(runRuffLint(context));
5278
5607
  if (languages.includes("go") && installedTools["golangci-lint"]) promises.push(runGolangciLint(context));
5279
- if (languages.includes("rust") && installedTools["cargo"]) promises.push(runGenericLinter(context, "rust"));
5280
- 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"));
5281
5610
  const results = await Promise.allSettled(promises);
5282
5611
  for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
5283
5612
  return {
@@ -5307,7 +5636,7 @@ const runDependencyAudit = async (context) => {
5307
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));
5308
5637
  }
5309
5638
  if (context.languages.includes("python") && context.installedTools["pip-audit"]) promises.push(runPipAudit(context.rootDirectory, timeout));
5310
- 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));
5311
5640
  if (context.languages.includes("rust")) promises.push(runCargoAudit(context.rootDirectory, timeout));
5312
5641
  const results = await Promise.allSettled(promises);
5313
5642
  for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
@@ -5412,9 +5741,12 @@ const parseLegacyAdvisories = (advisories, source) => {
5412
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 ?? "");
5413
5742
  return [...bucket.values()].map((agg) => aggregateToDiagnostic(agg, source));
5414
5743
  };
5744
+ const carriesAdvisory = (vulnerability) => Array.isArray(vulnerability.via) && vulnerability.via.some((entry) => entry !== null && typeof entry === "object");
5415
5745
  const parseModernVulnerabilities = (vulnerabilities, source) => {
5416
5746
  const bucket = /* @__PURE__ */ new Map();
5747
+ const hasRootCauses = Object.values(vulnerabilities).some(carriesAdvisory);
5417
5748
  for (const [packageName, vulnerability] of Object.entries(vulnerabilities)) {
5749
+ if (hasRootCauses && !carriesAdvisory(vulnerability)) continue;
5418
5750
  const severity = (vulnerability.severity ?? "moderate").toLowerCase();
5419
5751
  const fixAvailable = vulnerability.fixAvailable;
5420
5752
  const isDirect = vulnerability.isDirect === true;
@@ -5557,212 +5889,6 @@ const runCargoAudit = async (rootDir, timeout) => {
5557
5889
  }
5558
5890
  };
5559
5891
 
5560
- //#endregion
5561
- //#region src/utils/source-masker.ts
5562
- const JS_EXTS = new Set([
5563
- ".ts",
5564
- ".tsx",
5565
- ".js",
5566
- ".jsx",
5567
- ".mjs",
5568
- ".cjs"
5569
- ]);
5570
- const PY_EXTS = new Set([".py"]);
5571
- const RB_EXTS = new Set([".rb"]);
5572
- const PHP_EXTS = new Set([".php"]);
5573
- const familyForExt = (ext) => {
5574
- if (JS_EXTS.has(ext)) return "js";
5575
- if (PY_EXTS.has(ext)) return "py";
5576
- if (RB_EXTS.has(ext)) return "rb";
5577
- if (PHP_EXTS.has(ext)) return "php";
5578
- return "none";
5579
- };
5580
- const maskStringsAndComments = (content, ext) => {
5581
- const family = familyForExt(ext);
5582
- if (family === "none") return content;
5583
- if (family === "js") return maskJs(content);
5584
- return maskSimple(content, family);
5585
- };
5586
- const handleQuotesAndComments = (content, i, tplStack, mask) => {
5587
- const len = content.length;
5588
- const c = content[i];
5589
- const next = content[i + 1];
5590
- if (c === "\"" || c === "'") {
5591
- const strStart = i;
5592
- const end = consumeQuotedString(content, i, c);
5593
- mask(strStart + 1, end - 1);
5594
- return {
5595
- handled: true,
5596
- nextI: end
5597
- };
5598
- }
5599
- if (c === "`") {
5600
- const scan = consumeTemplateString(content, i + 1);
5601
- mask(i + 1, scan.maskEnd);
5602
- if (scan.openedInterp) tplStack.push(0);
5603
- return {
5604
- handled: true,
5605
- nextI: scan.resumeAt
5606
- };
5607
- }
5608
- if (c === "/" && next === "/") {
5609
- const strStart = i;
5610
- let k = i;
5611
- while (k < len && content[k] !== "\n") k++;
5612
- mask(strStart, k);
5613
- return {
5614
- handled: true,
5615
- nextI: k
5616
- };
5617
- }
5618
- if (c === "/" && next === "*") {
5619
- const strStart = i;
5620
- let k = i + 2;
5621
- while (k < len - 1 && !(content[k] === "*" && content[k + 1] === "/")) k++;
5622
- if (k < len - 1) k += 2;
5623
- mask(strStart, k);
5624
- return {
5625
- handled: true,
5626
- nextI: k
5627
- };
5628
- }
5629
- return {
5630
- handled: false,
5631
- nextI: i
5632
- };
5633
- };
5634
- const maskJs = (content) => {
5635
- const out = content.split("");
5636
- const len = content.length;
5637
- const tplStack = [];
5638
- let i = 0;
5639
- const mask = (start, end) => {
5640
- for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
5641
- };
5642
- while (i < len) {
5643
- const c = content[i];
5644
- if (tplStack.length > 0) {
5645
- if (c === "{") {
5646
- tplStack[tplStack.length - 1]++;
5647
- i++;
5648
- continue;
5649
- }
5650
- if (c === "}") {
5651
- if (tplStack[tplStack.length - 1] === 0) {
5652
- tplStack.pop();
5653
- const scan = consumeTemplateString(content, i + 1);
5654
- mask(i + 1, scan.maskEnd);
5655
- if (scan.openedInterp) tplStack.push(0);
5656
- i = scan.resumeAt;
5657
- continue;
5658
- }
5659
- tplStack[tplStack.length - 1]--;
5660
- i++;
5661
- continue;
5662
- }
5663
- }
5664
- const handled = handleQuotesAndComments(content, i, tplStack, mask);
5665
- if (handled.handled) {
5666
- i = handled.nextI;
5667
- continue;
5668
- }
5669
- i++;
5670
- }
5671
- return out.join("");
5672
- };
5673
- const consumeQuotedString = (content, start, quote) => {
5674
- const len = content.length;
5675
- let i = start + 1;
5676
- while (i < len) {
5677
- const c = content[i];
5678
- if (c === "\\" && i + 1 < len) {
5679
- i += 2;
5680
- continue;
5681
- }
5682
- if (c === quote) return i + 1;
5683
- if (c === "\n") return i;
5684
- i++;
5685
- }
5686
- return i;
5687
- };
5688
- const consumeTemplateString = (content, start) => {
5689
- const len = content.length;
5690
- let i = start;
5691
- while (i < len) {
5692
- const c = content[i];
5693
- if (c === "\\" && i + 1 < len) {
5694
- i += 2;
5695
- continue;
5696
- }
5697
- if (c === "`") return {
5698
- maskEnd: i,
5699
- resumeAt: i + 1,
5700
- openedInterp: false
5701
- };
5702
- if (c === "$" && content[i + 1] === "{") return {
5703
- maskEnd: i,
5704
- resumeAt: i + 2,
5705
- openedInterp: true
5706
- };
5707
- i++;
5708
- }
5709
- return {
5710
- maskEnd: i,
5711
- resumeAt: i,
5712
- openedInterp: false
5713
- };
5714
- };
5715
- const maskSimple = (content, family) => {
5716
- const out = content.split("");
5717
- const len = content.length;
5718
- let i = 0;
5719
- const mask = (start, end) => {
5720
- for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
5721
- };
5722
- while (i < len) {
5723
- const c = content[i];
5724
- const next = content[i + 1];
5725
- if (family === "py" && (c === "\"" || c === "'")) {
5726
- if (content[i + 1] === c && content[i + 2] === c) {
5727
- const triple = c + c + c;
5728
- const end = content.indexOf(triple, i + 3);
5729
- const stop = end === -1 ? len : end + 3;
5730
- mask(i + 3, stop - 3);
5731
- i = stop;
5732
- continue;
5733
- }
5734
- }
5735
- if (c === "\"" || c === "'") {
5736
- const strStart = i;
5737
- i = consumeQuotedString(content, i, c);
5738
- mask(strStart + 1, i - 1);
5739
- continue;
5740
- }
5741
- if ((family === "py" || family === "rb" || family === "php") && c === "#") {
5742
- const strStart = i;
5743
- while (i < len && content[i] !== "\n") i++;
5744
- mask(strStart, i);
5745
- continue;
5746
- }
5747
- if (family === "php" && c === "/" && next === "/") {
5748
- const strStart = i;
5749
- while (i < len && content[i] !== "\n") i++;
5750
- mask(strStart, i);
5751
- continue;
5752
- }
5753
- if (family === "php" && c === "/" && next === "*") {
5754
- const strStart = i;
5755
- i += 2;
5756
- while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
5757
- if (i < len - 1) i += 2;
5758
- mask(strStart, i);
5759
- continue;
5760
- }
5761
- i++;
5762
- }
5763
- return out.join("");
5764
- };
5765
-
5766
5892
  //#endregion
5767
5893
  //#region src/engines/security/risky.ts
5768
5894
  const ev = "eval";
@@ -5908,8 +6034,7 @@ const detectRiskyConstructs = async (context) => {
5908
6034
  if (!extensions.includes(ext)) continue;
5909
6035
  if (isMigrationOrSeeder && name === "sql-injection") continue;
5910
6036
  const regex = new RegExp(pattern.source, pattern.flags);
5911
- let match;
5912
- while ((match = regex.exec(masked)) !== null) {
6037
+ for (const match of masked.matchAll(regex)) {
5913
6038
  const line = content.slice(0, match.index).split("\n").length;
5914
6039
  if (name === "innerhtml") {
5915
6040
  const beforeMatch = content.slice(Math.max(0, match.index - 200), match.index);
@@ -6037,11 +6162,11 @@ const scanSecrets = async (context) => {
6037
6162
  } catch {
6038
6163
  continue;
6039
6164
  }
6165
+ content = maskComments(content, path.extname(filePath));
6040
6166
  const relativePath = path.relative(context.rootDirectory, filePath);
6041
6167
  for (const { pattern, name, keywordPrefixed } of SECRET_PATTERNS) {
6042
6168
  const regex = new RegExp(pattern.source, pattern.flags);
6043
- let match;
6044
- while ((match = regex.exec(content)) !== null) {
6169
+ for (const match of content.matchAll(regex)) {
6045
6170
  if (isPlaceholderValue(match[1] ?? match[0])) continue;
6046
6171
  if (keywordPrefixed && isInsideStringLiteral(content, match.index)) continue;
6047
6172
  const line = content.slice(0, match.index).split("\n").length;
@@ -6162,6 +6287,64 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
6162
6287
 
6163
6288
  //#endregion
6164
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
+ };
6165
6348
  const LANGUAGE_SIGNALS = {
6166
6349
  "tsconfig.json": "typescript",
6167
6350
  "go.mod": "go",
@@ -6281,11 +6464,12 @@ const checkInstalledTools = async () => {
6281
6464
  }));
6282
6465
  return results;
6283
6466
  };
6284
- const discoverProject = async (directory) => {
6467
+ const discoverProject = async (directory, excludePatterns = []) => {
6285
6468
  const resolvedDir = path.resolve(directory);
6286
6469
  const languages = detectLanguages(resolvedDir);
6287
6470
  const frameworks = detectFrameworks(resolvedDir);
6288
6471
  const sourceFileCount = countSourceFiles(resolvedDir);
6472
+ const coverage = analyzeCoverage(resolvedDir, excludePatterns);
6289
6473
  const installedTools = await checkInstalledTools();
6290
6474
  return {
6291
6475
  rootDirectory: resolvedDir,
@@ -6293,6 +6477,7 @@ const discoverProject = async (directory) => {
6293
6477
  languages,
6294
6478
  frameworks,
6295
6479
  sourceFileCount,
6480
+ coverage,
6296
6481
  installedTools
6297
6482
  };
6298
6483
  };
@@ -6512,10 +6697,6 @@ const handleAislopBaseline = (input) => {
6512
6697
  };
6513
6698
  };
6514
6699
 
6515
- //#endregion
6516
- //#region src/version.ts
6517
- const APP_VERSION = "0.10.0";
6518
-
6519
6700
  //#endregion
6520
6701
  //#region src/telemetry/env.ts
6521
6702
  const detectPackageManager = (env = process.env) => {
@@ -6710,9 +6891,14 @@ const track = (input) => {
6710
6891
  pendingRequests.add(request);
6711
6892
  return { installCreated };
6712
6893
  };
6713
- const flushTelemetry = async () => {
6894
+ const flushTelemetry = async (timeoutMs) => {
6714
6895
  if (pendingRequests.size === 0) return;
6715
- 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))]);
6716
6902
  };
6717
6903
 
6718
6904
  //#endregion