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/README.md +56 -8
- package/dist/cli.js +942 -612
- package/dist/{expo-doctor-BcIkOte5.js → expo-doctor-c-jE6pR2.js} +1 -1
- package/dist/{generic-D_T4cUaC.js → generic-BsQa13CS.js} +1 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +2673 -2346
- package/dist/{json-Bqkcl1DF.js → json-B01i-GOz.js} +7 -5
- package/dist/{json-OIzja7OM.js → json-CXV4D0Ib.js} +5 -3
- package/dist/mcp.js +584 -501
- package/dist/{sarif-C-vh4wcC.js → sarif-cy5SiDDq.js} +1 -1
- package/dist/{typecheck-DQSzG8fX.js → typecheck-BdQ7uFyK.js} +1 -1
- package/dist/version-BfJVwhN2.js +5 -0
- package/package.json +8 -11
- package/dist/version-rlhQD8Qh.js +0 -5
- /package/dist/{engine-info-DCvIfZ0f.js → engine-info-Cpt36DqZ.js} +0 -0
- /package/dist/{subprocess-CQUJDGgn.js → subprocess-0uXz8HdE.js} +0 -0
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
|
|
722
|
-
|
|
723
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1747
|
-
const
|
|
1748
|
-
if (
|
|
1749
|
-
|
|
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
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
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
|
|
1610
|
+
return false;
|
|
1775
1611
|
};
|
|
1776
|
-
const
|
|
1777
|
-
if (
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
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
|
|
1635
|
+
const detectGoPatterns = async (context) => {
|
|
1783
1636
|
const diagnostics = [];
|
|
1784
|
-
|
|
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
|
|
1793
|
-
const
|
|
1794
|
-
|
|
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
|
-
|
|
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
|
|
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+(
|
|
3557
|
+
const importMatch = trimmed.match(/^import\s+(.+)/);
|
|
3538
3558
|
if (importMatch) {
|
|
3539
3559
|
importLines.add(i);
|
|
3540
|
-
const
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
3705
|
-
|
|
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
|
|
3717
|
-
|
|
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
|
|
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 =
|
|
3852
|
-
const ARROW_END_RE =
|
|
3853
|
-
const BRACE_START_RE =
|
|
3854
|
-
const NEW_STATEMENT_RE =
|
|
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
|
|
4063
|
+
let escaped = false;
|
|
4041
4064
|
for (const ch of line) {
|
|
4042
|
-
if (
|
|
4043
|
-
|
|
4065
|
+
if (escaped) {
|
|
4066
|
+
escaped = false;
|
|
4044
4067
|
continue;
|
|
4045
4068
|
}
|
|
4046
4069
|
if (ch === "\\") {
|
|
4047
|
-
|
|
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
|
|
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
|
|
4997
|
-
if (languages.includes("go") && installedTools
|
|
4998
|
-
if (languages.includes("rust") && installedTools
|
|
4999
|
-
if (languages.includes("ruby") && installedTools
|
|
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.
|
|
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
|
|
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
|
|
5588
|
-
if (languages.includes("ruby") && installedTools
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|