aislop 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +187 -57
- package/dist/cli.js +2748 -989
- package/dist/{expo-doctor-T4DswmX5.js → expo-doctor-BM2JR6f6.js} +1 -1
- package/dist/{expo-doctor-BcIkOte5.js → expo-doctor-BwLKXF__.js} +1 -1
- package/dist/finding-assessment-PCl1fnok.js +149 -0
- package/dist/index.d.ts +20 -3
- package/dist/index.js +3537 -2410
- package/dist/{json-Bqkcl1DF.js → json-0lJPTrwO.js} +10 -6
- package/dist/{json-OIzja7OM.js → json-pHsqtKkz.js} +9 -5
- package/dist/mcp.js +1172 -646
- package/dist/{sarif-C-vh4wcC.js → sarif-BXUicqQU.js} +1 -1
- package/dist/{sarif-CZVuavf_.js → sarif-CjxSBcqx.js} +1 -1
- package/dist/{typecheck-wVSohmOX.js → typecheck-yOGXIIGU.js} +1 -1
- package/dist/version-BJA3AcRM.js +7 -0
- package/package.json +8 -11
- package/dist/engine-info-DCvIfZ0f.js +0 -31
- package/dist/version-rlhQD8Qh.js +0 -5
package/dist/mcp.js
CHANGED
|
@@ -59,7 +59,8 @@ const DEFAULT_CONFIG = {
|
|
|
59
59
|
good: 75,
|
|
60
60
|
ok: 50
|
|
61
61
|
},
|
|
62
|
-
smoothing: 20
|
|
62
|
+
smoothing: 20,
|
|
63
|
+
maxPerRule: 40
|
|
63
64
|
},
|
|
64
65
|
ci: {
|
|
65
66
|
failBelow: 70,
|
|
@@ -150,7 +151,8 @@ const ScoringSchema = z$1.object({
|
|
|
150
151
|
good: 75,
|
|
151
152
|
ok: 50
|
|
152
153
|
})),
|
|
153
|
-
smoothing: z$1.number().nonnegative().default(20)
|
|
154
|
+
smoothing: z$1.number().nonnegative().default(20),
|
|
155
|
+
maxPerRule: z$1.number().positive().default(40)
|
|
154
156
|
});
|
|
155
157
|
const CiSchema = z$1.object({
|
|
156
158
|
failBelow: z$1.number().default(70),
|
|
@@ -190,7 +192,8 @@ const AislopConfigSchema = z$1.object({
|
|
|
190
192
|
good: 75,
|
|
191
193
|
ok: 50
|
|
192
194
|
},
|
|
193
|
-
smoothing: 20
|
|
195
|
+
smoothing: 20,
|
|
196
|
+
maxPerRule: 40
|
|
194
197
|
})),
|
|
195
198
|
ci: CiSchema.default(() => ({
|
|
196
199
|
failBelow: 70,
|
|
@@ -263,218 +266,6 @@ const loadConfig = (directory) => {
|
|
|
263
266
|
}
|
|
264
267
|
};
|
|
265
268
|
|
|
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
269
|
//#endregion
|
|
479
270
|
//#region src/utils/source-files.ts
|
|
480
271
|
const MAX_BUFFER = 50 * 1024 * 1024;
|
|
@@ -713,14 +504,226 @@ const isAutoGenerated = (filePath) => {
|
|
|
713
504
|
} catch {}
|
|
714
505
|
}
|
|
715
506
|
};
|
|
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);
|
|
720
|
-
};
|
|
721
|
-
const getSourceFilesWithExtras = (context, extraExtensions) => {
|
|
722
|
-
if (context.files) return filterExplicitFiles(context.rootDirectory, context.files, extraExtensions);
|
|
723
|
-
return filterProjectFiles(context.rootDirectory, listProjectFiles(context.rootDirectory), extraExtensions);
|
|
507
|
+
const getSourceFilesForRoot = (rootDirectory) => filterProjectFiles(rootDirectory, listProjectFiles(rootDirectory));
|
|
508
|
+
const getSourceFiles = (context) => {
|
|
509
|
+
if (context.files) return filterExplicitFiles(context.rootDirectory, context.files);
|
|
510
|
+
return getSourceFilesForRoot(context.rootDirectory);
|
|
511
|
+
};
|
|
512
|
+
const getSourceFilesWithExtras = (context, extraExtensions) => {
|
|
513
|
+
if (context.files) return filterExplicitFiles(context.rootDirectory, context.files, extraExtensions);
|
|
514
|
+
return filterProjectFiles(context.rootDirectory, listProjectFiles(context.rootDirectory), extraExtensions);
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
//#endregion
|
|
518
|
+
//#region src/utils/source-masker.ts
|
|
519
|
+
const JS_EXTS$2 = new Set([
|
|
520
|
+
".ts",
|
|
521
|
+
".tsx",
|
|
522
|
+
".js",
|
|
523
|
+
".jsx",
|
|
524
|
+
".mjs",
|
|
525
|
+
".cjs"
|
|
526
|
+
]);
|
|
527
|
+
const PY_EXTS = new Set([".py"]);
|
|
528
|
+
const RB_EXTS = new Set([".rb"]);
|
|
529
|
+
const PHP_EXTS = new Set([".php"]);
|
|
530
|
+
const familyForExt = (ext) => {
|
|
531
|
+
if (JS_EXTS$2.has(ext)) return "js";
|
|
532
|
+
if (PY_EXTS.has(ext)) return "py";
|
|
533
|
+
if (RB_EXTS.has(ext)) return "rb";
|
|
534
|
+
if (PHP_EXTS.has(ext)) return "php";
|
|
535
|
+
return "none";
|
|
536
|
+
};
|
|
537
|
+
const maskStringsAndComments = (content, ext) => {
|
|
538
|
+
const family = familyForExt(ext);
|
|
539
|
+
if (family === "none") return content;
|
|
540
|
+
if (family === "js") return maskJs(content, true);
|
|
541
|
+
return maskSimple(content, family, true);
|
|
542
|
+
};
|
|
543
|
+
const maskComments = (content, ext) => {
|
|
544
|
+
const family = familyForExt(ext);
|
|
545
|
+
if (family === "none") return content;
|
|
546
|
+
if (family === "js") return maskJs(content, false);
|
|
547
|
+
return maskSimple(content, family, false);
|
|
548
|
+
};
|
|
549
|
+
const handleQuotesAndComments = (content, i, tplStack, mask, maskStrings) => {
|
|
550
|
+
const len = content.length;
|
|
551
|
+
const c = content[i];
|
|
552
|
+
const next = content[i + 1];
|
|
553
|
+
if (c === "\"" || c === "'") {
|
|
554
|
+
const strStart = i;
|
|
555
|
+
const end = consumeQuotedString(content, i, c);
|
|
556
|
+
if (maskStrings) mask(strStart + 1, end - 1);
|
|
557
|
+
return {
|
|
558
|
+
handled: true,
|
|
559
|
+
nextI: end
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
if (c === "`") {
|
|
563
|
+
const scan = consumeTemplateString(content, i + 1);
|
|
564
|
+
if (maskStrings) mask(i + 1, scan.maskEnd);
|
|
565
|
+
if (scan.openedInterp) tplStack.push(0);
|
|
566
|
+
return {
|
|
567
|
+
handled: true,
|
|
568
|
+
nextI: scan.resumeAt
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
if (c === "/" && next === "/") {
|
|
572
|
+
const strStart = i;
|
|
573
|
+
let k = i;
|
|
574
|
+
while (k < len && content[k] !== "\n") k++;
|
|
575
|
+
mask(strStart, k);
|
|
576
|
+
return {
|
|
577
|
+
handled: true,
|
|
578
|
+
nextI: k
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
if (c === "/" && next === "*") {
|
|
582
|
+
const strStart = i;
|
|
583
|
+
let k = i + 2;
|
|
584
|
+
while (k < len - 1 && !(content[k] === "*" && content[k + 1] === "/")) k++;
|
|
585
|
+
if (k < len - 1) k += 2;
|
|
586
|
+
mask(strStart, k);
|
|
587
|
+
return {
|
|
588
|
+
handled: true,
|
|
589
|
+
nextI: k
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
return {
|
|
593
|
+
handled: false,
|
|
594
|
+
nextI: i
|
|
595
|
+
};
|
|
596
|
+
};
|
|
597
|
+
const maskJs = (content, maskStrings) => {
|
|
598
|
+
const out = content.split("");
|
|
599
|
+
const len = content.length;
|
|
600
|
+
const tplStack = [];
|
|
601
|
+
let i = 0;
|
|
602
|
+
const mask = (start, end) => {
|
|
603
|
+
for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
|
|
604
|
+
};
|
|
605
|
+
while (i < len) {
|
|
606
|
+
const c = content[i];
|
|
607
|
+
if (tplStack.length > 0) {
|
|
608
|
+
if (c === "{") {
|
|
609
|
+
tplStack[tplStack.length - 1]++;
|
|
610
|
+
i++;
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
if (c === "}") {
|
|
614
|
+
if (tplStack[tplStack.length - 1] === 0) {
|
|
615
|
+
tplStack.pop();
|
|
616
|
+
const scan = consumeTemplateString(content, i + 1);
|
|
617
|
+
if (maskStrings) mask(i + 1, scan.maskEnd);
|
|
618
|
+
if (scan.openedInterp) tplStack.push(0);
|
|
619
|
+
i = scan.resumeAt;
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
tplStack[tplStack.length - 1]--;
|
|
623
|
+
i++;
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
const handled = handleQuotesAndComments(content, i, tplStack, mask, maskStrings);
|
|
628
|
+
if (handled.handled) {
|
|
629
|
+
i = handled.nextI;
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
i++;
|
|
633
|
+
}
|
|
634
|
+
return out.join("");
|
|
635
|
+
};
|
|
636
|
+
const consumeQuotedString = (content, start, quote) => {
|
|
637
|
+
const len = content.length;
|
|
638
|
+
let i = start + 1;
|
|
639
|
+
while (i < len) {
|
|
640
|
+
const c = content[i];
|
|
641
|
+
if (c === "\\" && i + 1 < len) {
|
|
642
|
+
i += 2;
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
if (c === quote) return i + 1;
|
|
646
|
+
if (c === "\n") return i;
|
|
647
|
+
i++;
|
|
648
|
+
}
|
|
649
|
+
return i;
|
|
650
|
+
};
|
|
651
|
+
const consumeTemplateString = (content, start) => {
|
|
652
|
+
const len = content.length;
|
|
653
|
+
let i = start;
|
|
654
|
+
while (i < len) {
|
|
655
|
+
const c = content[i];
|
|
656
|
+
if (c === "\\" && i + 1 < len) {
|
|
657
|
+
i += 2;
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
if (c === "`") return {
|
|
661
|
+
maskEnd: i,
|
|
662
|
+
resumeAt: i + 1,
|
|
663
|
+
openedInterp: false
|
|
664
|
+
};
|
|
665
|
+
if (c === "$" && content[i + 1] === "{") return {
|
|
666
|
+
maskEnd: i,
|
|
667
|
+
resumeAt: i + 2,
|
|
668
|
+
openedInterp: true
|
|
669
|
+
};
|
|
670
|
+
i++;
|
|
671
|
+
}
|
|
672
|
+
return {
|
|
673
|
+
maskEnd: i,
|
|
674
|
+
resumeAt: i,
|
|
675
|
+
openedInterp: false
|
|
676
|
+
};
|
|
677
|
+
};
|
|
678
|
+
const maskSimple = (content, family, maskStrings) => {
|
|
679
|
+
const out = content.split("");
|
|
680
|
+
const len = content.length;
|
|
681
|
+
let i = 0;
|
|
682
|
+
const mask = (start, end) => {
|
|
683
|
+
for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
|
|
684
|
+
};
|
|
685
|
+
while (i < len) {
|
|
686
|
+
const c = content[i];
|
|
687
|
+
const next = content[i + 1];
|
|
688
|
+
if (family === "py" && (c === "\"" || c === "'")) {
|
|
689
|
+
if (content[i + 1] === c && content[i + 2] === c) {
|
|
690
|
+
const triple = c + c + c;
|
|
691
|
+
const end = content.indexOf(triple, i + 3);
|
|
692
|
+
const stop = end === -1 ? len : end + 3;
|
|
693
|
+
if (maskStrings) mask(i + 3, stop - 3);
|
|
694
|
+
i = stop;
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (c === "\"" || c === "'") {
|
|
699
|
+
const strStart = i;
|
|
700
|
+
i = consumeQuotedString(content, i, c);
|
|
701
|
+
if (maskStrings) mask(strStart + 1, i - 1);
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
if ((family === "py" || family === "rb" || family === "php") && c === "#") {
|
|
705
|
+
const strStart = i;
|
|
706
|
+
while (i < len && content[i] !== "\n") i++;
|
|
707
|
+
mask(strStart, i);
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
if (family === "php" && c === "/" && next === "/") {
|
|
711
|
+
const strStart = i;
|
|
712
|
+
while (i < len && content[i] !== "\n") i++;
|
|
713
|
+
mask(strStart, i);
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
if (family === "php" && c === "/" && next === "*") {
|
|
717
|
+
const strStart = i;
|
|
718
|
+
i += 2;
|
|
719
|
+
while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
|
|
720
|
+
if (i < len - 1) i += 2;
|
|
721
|
+
mask(strStart, i);
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
i++;
|
|
725
|
+
}
|
|
726
|
+
return out.join("");
|
|
724
727
|
};
|
|
725
728
|
|
|
726
729
|
//#endregion
|
|
@@ -768,16 +771,14 @@ const detectThinWrappers = (content, relativePath, ext) => {
|
|
|
768
771
|
for (const { pattern, extensions } of THIN_WRAPPER_PATTERNS) {
|
|
769
772
|
if (!extensions.has(ext)) continue;
|
|
770
773
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
771
|
-
|
|
772
|
-
while ((match = regex.exec(content)) !== null) {
|
|
774
|
+
for (const match of content.matchAll(regex)) {
|
|
773
775
|
const funcName = match[1];
|
|
774
776
|
const matchText = match[0];
|
|
775
777
|
const lineNumber = content.slice(0, match.index).split("\n").length;
|
|
776
778
|
if (DUNDER_PATTERN.test(funcName)) continue;
|
|
777
779
|
if (FRAMEWORK_METHOD_NAMES.test(funcName)) continue;
|
|
778
780
|
if (lineNumber >= 2) {
|
|
779
|
-
|
|
780
|
-
if (prevLine && prevLine.startsWith("@")) continue;
|
|
781
|
+
if ((lines[lineNumber - 2]?.trim())?.startsWith("@")) continue;
|
|
781
782
|
}
|
|
782
783
|
if (!isIdentityForward(matchText)) continue;
|
|
783
784
|
if (isUseContextWrapper(matchText)) continue;
|
|
@@ -961,8 +962,8 @@ const PHP_DECL_START = /^\s*(?:(?:public|private|protected|static|final|abstract
|
|
|
961
962
|
|
|
962
963
|
//#endregion
|
|
963
964
|
//#region src/engines/ai-slop/non-production-paths.ts
|
|
964
|
-
const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|docs?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
|
|
965
|
-
const BASENAME_PATTERN = /(?:^|\/)(?:benchmark|bench|demo|example|script|seed|migrate|profile|smoke|stress|load|debug|repro)[-_.][^/]
|
|
965
|
+
const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|docs?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|prototypes?|experiments?|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
|
|
966
|
+
const BASENAME_PATTERN = /(?:^|\/)(?:(?:prototype|experiment)(?:[-_.][^/]*)?|(?:benchmark|bench|demo|example|script|seed|migrate|profile|smoke|stress|load|debug|repro)[-_.][^/]*)\.[mc]?[jt]sx?$|(?:^|\/)[^/]+[-_](?:benchmark|bench|demo|example|prototype|experiment)\.[mc]?[jt]sx?$/i;
|
|
966
967
|
const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) || BASENAME_PATTERN.test(relativePath);
|
|
967
968
|
|
|
968
969
|
//#endregion
|
|
@@ -1078,7 +1079,7 @@ const JS_EXTENSIONS$3 = new Set([
|
|
|
1078
1079
|
".mjs",
|
|
1079
1080
|
".cjs"
|
|
1080
1081
|
]);
|
|
1081
|
-
const
|
|
1082
|
+
const CONSOLE_CALL_PATTERN = /\bconsole\.(log|debug|info|trace|dir|table)\s*\(/;
|
|
1082
1083
|
const slop = (filePath, line, rule, severity, message, help, fixable) => ({
|
|
1083
1084
|
filePath,
|
|
1084
1085
|
engine: "ai-slop",
|
|
@@ -1092,20 +1093,35 @@ const slop = (filePath, line, rule, severity, message, help, fixable) => ({
|
|
|
1092
1093
|
fixable
|
|
1093
1094
|
});
|
|
1094
1095
|
const LOGGER_FILE_PATTERN = /(?:^|\/)(?:logger|logging|log)\.[^/]+$/i;
|
|
1096
|
+
const CLI_ENTRYPOINT_PATTERN = /(?:^|\/)(?:cli|cli[-_.][^/]*|[^/]+[-_]cli)\.[mc]?[jt]sx?$/i;
|
|
1097
|
+
const ENTRYPOINT_GUARD_PATTERN = /\b(?:import\.meta\.main|require\.main\s*===\s*module)\b/;
|
|
1098
|
+
const OPERATIONAL_LOG_PATTERN = /\bconsole\.(?:log|info)\s*\(\s*(?:`|["'])\s*\[[^\]\n]{1,48}\]/;
|
|
1099
|
+
const DEBUG_SIGNAL_PATTERN = /\b(?:debug|dbg|trace|dump|inspect|todo|tmp|temp|remove\s+me|leftover|here|checkpoint)\b/i;
|
|
1100
|
+
const shouldFlagConsoleCall = (trimmed) => {
|
|
1101
|
+
const match = CONSOLE_CALL_PATTERN.exec(trimmed);
|
|
1102
|
+
if (!match) return false;
|
|
1103
|
+
const method = match[1];
|
|
1104
|
+
if (method === "trace" || method === "dir" || method === "table") return true;
|
|
1105
|
+
if (method === "debug") return DEBUG_SIGNAL_PATTERN.test(trimmed) || !OPERATIONAL_LOG_PATTERN.test(trimmed);
|
|
1106
|
+
if (method === "info" || method === "log") {
|
|
1107
|
+
if (/console\.log\(\s*JSON\.stringify\b/.test(trimmed)) return false;
|
|
1108
|
+
if (OPERATIONAL_LOG_PATTERN.test(trimmed)) return false;
|
|
1109
|
+
return true;
|
|
1110
|
+
}
|
|
1111
|
+
return false;
|
|
1112
|
+
};
|
|
1095
1113
|
const detectConsoleLeftovers = (content, relativePath, ext) => {
|
|
1096
1114
|
if (!JS_EXTENSIONS$3.has(ext)) return [];
|
|
1097
1115
|
if (LOGGER_FILE_PATTERN.test(relativePath)) return [];
|
|
1098
|
-
if (isNonProductionPath(relativePath)) return [];
|
|
1116
|
+
if (isNonProductionPath(relativePath) || CLI_ENTRYPOINT_PATTERN.test(relativePath)) return [];
|
|
1117
|
+
if (content.startsWith("#!")) return [];
|
|
1118
|
+
if (ENTRYPOINT_GUARD_PATTERN.test(content)) return [];
|
|
1099
1119
|
const diagnostics = [];
|
|
1100
1120
|
const lines = content.split("\n");
|
|
1101
1121
|
for (let i = 0; i < lines.length; i++) {
|
|
1102
1122
|
const trimmed = lines[i].trim();
|
|
1103
1123
|
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
1104
|
-
if (
|
|
1105
|
-
if (/console\.(?:error|warn)\s*\(/.test(trimmed)) continue;
|
|
1106
|
-
if (/console\.log\(\s*JSON\.stringify\b/.test(trimmed)) continue;
|
|
1107
|
-
diagnostics.push(slop(relativePath, i + 1, "ai-slop/console-leftover", "warning", "console.log/debug/info statement left in production code", "Remove debugging console statements or replace with a proper logger", true));
|
|
1108
|
-
}
|
|
1124
|
+
if (shouldFlagConsoleCall(trimmed)) diagnostics.push(slop(relativePath, i + 1, "ai-slop/console-leftover", "warning", "console.log/debug/info statement left in production code", "Remove debugging console statements or replace with a proper logger", true));
|
|
1109
1125
|
}
|
|
1110
1126
|
return diagnostics;
|
|
1111
1127
|
};
|
|
@@ -1133,6 +1149,7 @@ const isGuardedSingleLineExit = (lines, lineIndex) => {
|
|
|
1133
1149
|
const control = contextLines.join(" ");
|
|
1134
1150
|
return /(?:^|[}\s])(?:if|else\s+if|for|while)\s*\(/.test(control) && !/{\s*$/.test(control);
|
|
1135
1151
|
};
|
|
1152
|
+
const isPropertyNoopAssignment = (trimmed) => /^(?:[\w$]+\.)+[\w$]+\s*=\s*(?:function\s*)?\([^)]*\)\s*(?:=>)?\s*\{\s*\}\s*;?$/.test(trimmed);
|
|
1136
1153
|
const detectTodoStubs = (content, relativePath) => {
|
|
1137
1154
|
const diagnostics = [];
|
|
1138
1155
|
const lines = content.split("\n");
|
|
@@ -1154,7 +1171,7 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
|
|
|
1154
1171
|
const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
|
|
1155
1172
|
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
1173
|
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
|
|
1174
|
+
if (JS_EXTENSIONS$3.has(ext) && /(?:function\s+\w+\s*\([^)]*\)|=>\s*)\s*\{\s*\}\s*;?\s*$/.test(trimmed) && !trimmed.startsWith("interface") && !trimmed.startsWith("type ") && !isPropertyNoopAssignment(trimmed)) 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
1175
|
}
|
|
1159
1176
|
return diagnostics;
|
|
1160
1177
|
};
|
|
@@ -1540,9 +1557,8 @@ const detectSwallowedExceptions = async (context) => {
|
|
|
1540
1557
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
1541
1558
|
for (const { pattern, languages, message } of SWALLOWED_EXCEPTION_PATTERNS) {
|
|
1542
1559
|
if (!languages.includes(ext)) continue;
|
|
1543
|
-
let match;
|
|
1544
1560
|
const regex = new RegExp(pattern.source, pattern.flags + (pattern.flags.includes("g") ? "" : "g"));
|
|
1545
|
-
|
|
1561
|
+
for (const match of content.matchAll(regex)) {
|
|
1546
1562
|
if (isIntentionalIgnore(match[0], ext)) continue;
|
|
1547
1563
|
const line = content.slice(0, match.index).split("\n").length;
|
|
1548
1564
|
diagnostics.push({
|
|
@@ -1558,240 +1574,80 @@ const detectSwallowedExceptions = async (context) => {
|
|
|
1558
1574
|
fixable: false
|
|
1559
1575
|
});
|
|
1560
1576
|
}
|
|
1561
|
-
}
|
|
1562
|
-
}
|
|
1563
|
-
return diagnostics;
|
|
1564
|
-
};
|
|
1565
|
-
|
|
1566
|
-
//#endregion
|
|
1567
|
-
//#region src/engines/ai-slop/go-patterns.ts
|
|
1568
|
-
const GO_EXTENSIONS = new Set([".go"]);
|
|
1569
|
-
const PACKAGE_DECL_RE = /^\s*package\s+(\w+)/;
|
|
1570
|
-
const PANIC_CALL_RE = /\bpanic\s*\(/;
|
|
1571
|
-
const COMMENT_LINE_RE$1 = /^\s*\/\//;
|
|
1572
|
-
const NIL_GUARD_RE = /^\s*if\s+[\w.]+(?:\(\))?\s*==\s*nil\s*\{?\s*$/;
|
|
1573
|
-
const SHORT_STRING_PANIC_RE = /\bpanic\s*\(\s*"[^"]{1,40}"\s*\)/;
|
|
1574
|
-
const detectPackageName = (lines) => {
|
|
1575
|
-
for (const line of lines) {
|
|
1576
|
-
const m = PACKAGE_DECL_RE.exec(line);
|
|
1577
|
-
if (m) return m[1];
|
|
1578
|
-
}
|
|
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;
|
|
1577
|
+
}
|
|
1733
1578
|
}
|
|
1579
|
+
return diagnostics;
|
|
1734
1580
|
};
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1581
|
+
|
|
1582
|
+
//#endregion
|
|
1583
|
+
//#region src/engines/ai-slop/go-patterns.ts
|
|
1584
|
+
const GO_EXTENSIONS = new Set([".go"]);
|
|
1585
|
+
const PACKAGE_DECL_RE = /^\s*package\s+(\w+)/;
|
|
1586
|
+
const PANIC_CALL_RE = /\bpanic\s*\(/;
|
|
1587
|
+
const COMMENT_LINE_RE$1 = /^\s*\/\//;
|
|
1588
|
+
const NIL_GUARD_RE = /^\s*if\s+[\w.]+(?:\(\))?\s*==\s*nil\s*\{?\s*$/;
|
|
1589
|
+
const SHORT_STRING_PANIC_RE = /\bpanic\s*\(\s*"[^"]{1,40}"\s*\)/;
|
|
1590
|
+
const detectPackageName = (lines) => {
|
|
1591
|
+
for (const line of lines) {
|
|
1592
|
+
const m = PACKAGE_DECL_RE.exec(line);
|
|
1593
|
+
if (m) return m[1];
|
|
1594
|
+
}
|
|
1595
|
+
return null;
|
|
1745
1596
|
};
|
|
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);
|
|
1597
|
+
const PANIC_INTENT_LOOKBACK = 3;
|
|
1598
|
+
const hasIntentComment$1 = (lines, panicLineIdx) => {
|
|
1599
|
+
for (let j = panicLineIdx - 1; j >= Math.max(0, panicLineIdx - PANIC_INTENT_LOOKBACK); j--) if (COMMENT_LINE_RE$1.test(lines[j])) return true;
|
|
1600
|
+
return false;
|
|
1753
1601
|
};
|
|
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));
|
|
1602
|
+
const isNilGuardPanic = (lines, panicLineIdx, line) => {
|
|
1603
|
+
if (!SHORT_STRING_PANIC_RE.test(line)) return false;
|
|
1604
|
+
for (let j = panicLineIdx - 1; j >= Math.max(0, panicLineIdx - 2); j--) {
|
|
1605
|
+
const prev = lines[j];
|
|
1606
|
+
if (prev.trim() === "") continue;
|
|
1607
|
+
return NIL_GUARD_RE.test(prev);
|
|
1773
1608
|
}
|
|
1774
|
-
return
|
|
1609
|
+
return false;
|
|
1775
1610
|
};
|
|
1776
|
-
const
|
|
1777
|
-
if (
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1611
|
+
const flagLibraryPanic = (lines, relPath, pkg, out) => {
|
|
1612
|
+
if (pkg === "main") return;
|
|
1613
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1614
|
+
const line = lines[i];
|
|
1615
|
+
if (COMMENT_LINE_RE$1.test(line)) continue;
|
|
1616
|
+
PANIC_CALL_RE.lastIndex = 0;
|
|
1617
|
+
if (!PANIC_CALL_RE.test(line)) continue;
|
|
1618
|
+
if (hasIntentComment$1(lines, i)) continue;
|
|
1619
|
+
if (isNilGuardPanic(lines, i, line)) continue;
|
|
1620
|
+
out.push({
|
|
1621
|
+
filePath: relPath,
|
|
1622
|
+
engine: "ai-slop",
|
|
1623
|
+
rule: "ai-slop/go-library-panic",
|
|
1624
|
+
severity: "warning",
|
|
1625
|
+
message: `\`panic()\` in package \`${pkg}\` (non-main, non-test). Library code should return errors, not unwind the goroutine.`,
|
|
1626
|
+
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.",
|
|
1627
|
+
line: i + 1,
|
|
1628
|
+
column: 1,
|
|
1629
|
+
category: "AI Slop",
|
|
1630
|
+
fixable: false
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1781
1633
|
};
|
|
1782
|
-
const
|
|
1634
|
+
const detectGoPatterns = async (context) => {
|
|
1783
1635
|
const diagnostics = [];
|
|
1784
|
-
|
|
1636
|
+
const files = getSourceFiles(context);
|
|
1637
|
+
for (const filePath of files) {
|
|
1638
|
+
if (!GO_EXTENSIONS.has(path.extname(filePath))) continue;
|
|
1785
1639
|
if (isAutoGenerated(filePath)) continue;
|
|
1640
|
+
if (filePath.endsWith("_test.go")) continue;
|
|
1786
1641
|
let content;
|
|
1787
1642
|
try {
|
|
1788
1643
|
content = fs.readFileSync(filePath, "utf-8");
|
|
1789
1644
|
} catch {
|
|
1790
1645
|
continue;
|
|
1791
1646
|
}
|
|
1792
|
-
const
|
|
1793
|
-
const
|
|
1794
|
-
|
|
1647
|
+
const lines = content.split("\n");
|
|
1648
|
+
const pkg = detectPackageName(lines);
|
|
1649
|
+
if (!pkg) continue;
|
|
1650
|
+
flagLibraryPanic(lines, path.relative(context.rootDirectory, filePath), pkg, diagnostics);
|
|
1795
1651
|
}
|
|
1796
1652
|
return diagnostics;
|
|
1797
1653
|
};
|
|
@@ -1813,7 +1669,7 @@ const JS_RESOLUTION_EXTENSIONS = [
|
|
|
1813
1669
|
"/index.js",
|
|
1814
1670
|
"/index.jsx"
|
|
1815
1671
|
];
|
|
1816
|
-
const readJson$
|
|
1672
|
+
const readJson$3 = (filePath) => {
|
|
1817
1673
|
try {
|
|
1818
1674
|
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
1819
1675
|
} catch {
|
|
@@ -1828,7 +1684,7 @@ const buildAliasMatcher = (key) => {
|
|
|
1828
1684
|
return (spec) => spec.length >= before.length + after.length && spec.startsWith(before) && spec.endsWith(after);
|
|
1829
1685
|
};
|
|
1830
1686
|
const collectAliasMatchersFromConfig = (configPath, matchers) => {
|
|
1831
|
-
const opts = readJson$
|
|
1687
|
+
const opts = readJson$3(configPath)?.compilerOptions;
|
|
1832
1688
|
if (!opts || typeof opts !== "object") return;
|
|
1833
1689
|
const configDir = path.dirname(configPath);
|
|
1834
1690
|
const paths = opts.paths;
|
|
@@ -1851,7 +1707,7 @@ const collectTsPathAliases = (rootDir, workspaceDirs) => {
|
|
|
1851
1707
|
|
|
1852
1708
|
//#endregion
|
|
1853
1709
|
//#region src/engines/ai-slop/js-workspaces.ts
|
|
1854
|
-
const readJson$
|
|
1710
|
+
const readJson$2 = (filePath) => {
|
|
1855
1711
|
try {
|
|
1856
1712
|
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
1857
1713
|
} catch {
|
|
@@ -1871,7 +1727,7 @@ const readWorkspaceGlobs = (rootDir, rootPkg) => {
|
|
|
1871
1727
|
}
|
|
1872
1728
|
}
|
|
1873
1729
|
}
|
|
1874
|
-
const lerna = readJson$
|
|
1730
|
+
const lerna = readJson$2(path.join(rootDir, "lerna.json"));
|
|
1875
1731
|
if (lerna && Array.isArray(lerna.packages)) {
|
|
1876
1732
|
for (const g of lerna.packages) if (typeof g === "string") globs.push(g);
|
|
1877
1733
|
}
|
|
@@ -1893,15 +1749,18 @@ const readWorkspaceGlobs = (rootDir, rootPkg) => {
|
|
|
1893
1749
|
}
|
|
1894
1750
|
return globs;
|
|
1895
1751
|
};
|
|
1752
|
+
const readWorkspaceEntries = (dir) => {
|
|
1753
|
+
try {
|
|
1754
|
+
return fs.readdirSync(dir, { withFileTypes: true });
|
|
1755
|
+
} catch {
|
|
1756
|
+
return [];
|
|
1757
|
+
}
|
|
1758
|
+
};
|
|
1896
1759
|
const expandWorkspaceDirs = (rootDir, globs) => {
|
|
1897
1760
|
const dirs = [];
|
|
1898
1761
|
for (const glob of globs) if (glob.endsWith("/*")) {
|
|
1899
1762
|
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
|
-
}
|
|
1763
|
+
for (const entry of readWorkspaceEntries(parent)) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
|
|
1905
1764
|
} else if (!glob.includes("*")) dirs.push(path.join(rootDir, glob));
|
|
1906
1765
|
return dirs;
|
|
1907
1766
|
};
|
|
@@ -2269,7 +2128,7 @@ const JS_EXTENSIONS$1 = new Set([
|
|
|
2269
2128
|
".cjs"
|
|
2270
2129
|
]);
|
|
2271
2130
|
const PY_EXTENSIONS$2 = new Set([".py"]);
|
|
2272
|
-
const readJson = (filePath) => {
|
|
2131
|
+
const readJson$1 = (filePath) => {
|
|
2273
2132
|
try {
|
|
2274
2133
|
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
2275
2134
|
} catch {
|
|
@@ -2313,7 +2172,7 @@ const collectNestedManifests = (rootDir, jsDeps) => {
|
|
|
2313
2172
|
const full = path.join(dir, entry.name);
|
|
2314
2173
|
if (entry.isDirectory()) walk(full, depth + 1);
|
|
2315
2174
|
else if (entry.name === "package.json" && depth > 0) {
|
|
2316
|
-
const wsPkg = readJson(full);
|
|
2175
|
+
const wsPkg = readJson$1(full);
|
|
2317
2176
|
if (!wsPkg) continue;
|
|
2318
2177
|
if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
|
|
2319
2178
|
addDepsFromPkg(wsPkg, jsDeps);
|
|
@@ -2325,13 +2184,13 @@ const collectNestedManifests = (rootDir, jsDeps) => {
|
|
|
2325
2184
|
const collectJsDeps = (rootDir, jsDeps) => {
|
|
2326
2185
|
const pkgPath = path.join(rootDir, "package.json");
|
|
2327
2186
|
if (!fs.existsSync(pkgPath)) return false;
|
|
2328
|
-
const pkg = readJson(pkgPath);
|
|
2187
|
+
const pkg = readJson$1(pkgPath);
|
|
2329
2188
|
if (!pkg || typeof pkg !== "object") return false;
|
|
2330
2189
|
addDepsFromPkg(pkg, jsDeps);
|
|
2331
2190
|
if (typeof pkg.name === "string") jsDeps.add(pkg.name);
|
|
2332
2191
|
const workspaceDirs = collectWorkspaceDirs(rootDir, pkg);
|
|
2333
2192
|
for (const wsDir of workspaceDirs) {
|
|
2334
|
-
const wsPkg = readJson(path.join(wsDir, "package.json"));
|
|
2193
|
+
const wsPkg = readJson$1(path.join(wsDir, "package.json"));
|
|
2335
2194
|
if (!wsPkg) continue;
|
|
2336
2195
|
if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
|
|
2337
2196
|
addDepsFromPkg(wsPkg, jsDeps);
|
|
@@ -2360,7 +2219,11 @@ const VIRTUAL_MODULE_PREFIXES = [
|
|
|
2360
2219
|
"astro:",
|
|
2361
2220
|
"virtual:",
|
|
2362
2221
|
"bun:",
|
|
2363
|
-
"file:"
|
|
2222
|
+
"file:",
|
|
2223
|
+
"http:",
|
|
2224
|
+
"https:",
|
|
2225
|
+
"jsr:",
|
|
2226
|
+
"npm:"
|
|
2364
2227
|
];
|
|
2365
2228
|
const isJsVirtualModule = (spec, manifest) => {
|
|
2366
2229
|
if (VIRTUAL_MODULE_PREFIXES.some((p) => spec.startsWith(p))) return true;
|
|
@@ -2461,48 +2324,230 @@ const extractPyImports = (content) => {
|
|
|
2461
2324
|
});
|
|
2462
2325
|
}
|
|
2463
2326
|
}
|
|
2464
|
-
return results;
|
|
2327
|
+
return results;
|
|
2328
|
+
};
|
|
2329
|
+
const checkJsImport = (rawSpec, manifest, tsAliasMatchers) => {
|
|
2330
|
+
const spec = stripImportQuery(rawSpec);
|
|
2331
|
+
if (spec.length === 0) return null;
|
|
2332
|
+
if (isJsRelativeOrAbsolute(spec)) return null;
|
|
2333
|
+
if (isJsBuiltin(spec)) return null;
|
|
2334
|
+
if (isJsVirtualModule(spec, manifest)) return null;
|
|
2335
|
+
if (tsAliasMatchers.some((m) => m(spec))) return null;
|
|
2336
|
+
const pkg = packageNameFromImport(spec);
|
|
2337
|
+
if (manifest.jsDeps.has(pkg)) return null;
|
|
2338
|
+
if (pkg.startsWith("@types/")) {
|
|
2339
|
+
const realPkg = pkg.slice(7);
|
|
2340
|
+
if (manifest.jsDeps.has(realPkg)) return null;
|
|
2341
|
+
}
|
|
2342
|
+
if (manifest.jsDeps.has(typesPackageName(pkg))) return null;
|
|
2343
|
+
return pkg;
|
|
2344
|
+
};
|
|
2345
|
+
const normalizePyName = (name) => name.toLowerCase().replace(/_/g, "-");
|
|
2346
|
+
const checkPyImport = (spec, manifest) => {
|
|
2347
|
+
const root = spec.split(".")[0];
|
|
2348
|
+
if (PYTHON_STDLIB.has(root)) return null;
|
|
2349
|
+
const normalized = normalizePyName(root);
|
|
2350
|
+
if (manifest.pyDeps.has(normalized)) return null;
|
|
2351
|
+
if ((PYTHON_IMPORT_TO_PIP[root] ?? PYTHON_IMPORT_TO_PIP[normalized])?.some((dist) => manifest.pyDeps.has(normalizePyName(dist)))) return null;
|
|
2352
|
+
return root;
|
|
2353
|
+
};
|
|
2354
|
+
const detectHallucinatedImports = async (context) => {
|
|
2355
|
+
const rootPkg = readJson$1(path.join(context.rootDirectory, "package.json"));
|
|
2356
|
+
const workspaceDirs = collectWorkspaceDirs(context.rootDirectory, rootPkg);
|
|
2357
|
+
const manifest = loadManifest(context.rootDirectory);
|
|
2358
|
+
if (!manifest.hasJsManifest && !manifest.hasPyManifest) return [];
|
|
2359
|
+
const tsAliasMatchers = manifest.hasJsManifest ? collectTsPathAliases(context.rootDirectory, workspaceDirs) : [];
|
|
2360
|
+
const diagnostics = [];
|
|
2361
|
+
const files = getSourceFiles(context);
|
|
2362
|
+
for (const filePath of files) {
|
|
2363
|
+
const ext = path.extname(filePath);
|
|
2364
|
+
const isJs = JS_EXTENSIONS$1.has(ext);
|
|
2365
|
+
const isPy = PY_EXTENSIONS$2.has(ext);
|
|
2366
|
+
if (!isJs && !isPy) continue;
|
|
2367
|
+
if (isJs && !manifest.hasJsManifest) continue;
|
|
2368
|
+
if (isPy && !manifest.hasPyManifest) continue;
|
|
2369
|
+
if (isAutoGenerated(filePath)) continue;
|
|
2370
|
+
let content;
|
|
2371
|
+
try {
|
|
2372
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
2373
|
+
} catch {
|
|
2374
|
+
continue;
|
|
2375
|
+
}
|
|
2376
|
+
const relPath = path.relative(context.rootDirectory, filePath);
|
|
2377
|
+
if (isNonProductionPath(relPath)) continue;
|
|
2378
|
+
const imports = isJs ? extractJsImports(content) : extractPyImports(content);
|
|
2379
|
+
for (const { spec, line } of imports) {
|
|
2380
|
+
const hallucinated = isJs ? checkJsImport(spec, manifest, tsAliasMatchers) : checkPyImport(spec, manifest);
|
|
2381
|
+
if (!hallucinated) continue;
|
|
2382
|
+
const manifestLabel = isJs ? "package.json" : "requirements.txt / pyproject.toml / Pipfile";
|
|
2383
|
+
diagnostics.push({
|
|
2384
|
+
filePath: relPath,
|
|
2385
|
+
engine: "ai-slop",
|
|
2386
|
+
rule: "ai-slop/hallucinated-import",
|
|
2387
|
+
severity: "error",
|
|
2388
|
+
message: `Imports "${hallucinated}" but it's not declared in ${manifestLabel}${isPy ? " and isn't Python stdlib" : ""}`,
|
|
2389
|
+
help: "Most often this is an LLM hallucinating a plausible-sounding package name. Either add the package to your manifest, or correct the import.",
|
|
2390
|
+
line,
|
|
2391
|
+
column: 1,
|
|
2392
|
+
category: "AI Slop",
|
|
2393
|
+
fixable: false
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
return diagnostics;
|
|
2398
|
+
};
|
|
2399
|
+
|
|
2400
|
+
//#endregion
|
|
2401
|
+
//#region src/engines/ai-slop/hardcoded-config.ts
|
|
2402
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
2403
|
+
".ts",
|
|
2404
|
+
".tsx",
|
|
2405
|
+
".js",
|
|
2406
|
+
".jsx",
|
|
2407
|
+
".mjs",
|
|
2408
|
+
".cjs",
|
|
2409
|
+
".py",
|
|
2410
|
+
".go",
|
|
2411
|
+
".rs",
|
|
2412
|
+
".rb",
|
|
2413
|
+
".java",
|
|
2414
|
+
".php"
|
|
2415
|
+
]);
|
|
2416
|
+
const URL_LITERAL_RE = /(["'`])(https?:\/\/[^"'`\s<>]+)\1/g;
|
|
2417
|
+
const ID_LITERAL_RE = /(["'])([A-Za-z][A-Za-z0-9_-]{15,})\1/g;
|
|
2418
|
+
const ENV_REFERENCE_RE = /\b(?:process\.env|import\.meta\.env|Deno\.env|os\.environ|getenv|env\()\b/i;
|
|
2419
|
+
const DOC_URL_CONTEXT_RE = /\b(?:docs?|documentation|homepage|repository|bugs|license|readme|source|svgUrl|pageUrl|href|link|install)\b/i;
|
|
2420
|
+
const URL_CONFIG_CONTEXT_RE = /\b(?:api|base[_-]?url|baseUrl|endpoint|host|origin|webhook|callback|redirect|server|service|domain|url)\b/i;
|
|
2421
|
+
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;
|
|
2422
|
+
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;
|
|
2423
|
+
const MIGRATION_PATH_RE$1 = /(?:^|[\\/])(?:migrations?|db[\\/]migrate)[\\/]/i;
|
|
2424
|
+
const PLACEHOLDER_HOSTS = new Set([
|
|
2425
|
+
"example.com",
|
|
2426
|
+
"example.org",
|
|
2427
|
+
"example.net"
|
|
2428
|
+
]);
|
|
2429
|
+
const LOOPBACK_HOSTS = new Set([
|
|
2430
|
+
"localhost",
|
|
2431
|
+
"127.0.0.1",
|
|
2432
|
+
"0.0.0.0",
|
|
2433
|
+
"::1"
|
|
2434
|
+
]);
|
|
2435
|
+
const VENDOR_API_DOMAINS = [
|
|
2436
|
+
"github.com",
|
|
2437
|
+
"githubusercontent.com",
|
|
2438
|
+
"googleapis.com",
|
|
2439
|
+
"accounts.google.com",
|
|
2440
|
+
"stripe.com",
|
|
2441
|
+
"openai.com",
|
|
2442
|
+
"anthropic.com",
|
|
2443
|
+
"slack.com",
|
|
2444
|
+
"twilio.com",
|
|
2445
|
+
"sendgrid.com",
|
|
2446
|
+
"mailgun.net",
|
|
2447
|
+
"cloudflare.com",
|
|
2448
|
+
"discord.com",
|
|
2449
|
+
"telegram.org",
|
|
2450
|
+
"login.microsoftonline.com",
|
|
2451
|
+
"graph.microsoft.com",
|
|
2452
|
+
"twitter.com",
|
|
2453
|
+
"x.com",
|
|
2454
|
+
"twimg.com",
|
|
2455
|
+
"t.co",
|
|
2456
|
+
"api.telegram.org"
|
|
2457
|
+
];
|
|
2458
|
+
const isVendorApiHost = (host) => VENDOR_API_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
|
|
2459
|
+
const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
|
|
2460
|
+
const PROVIDER_ID_RE = /^(?:price|prod|cus|sub|acct|org|app|tenant|workspace|project|client|key|tok|token|sk|pk)_[A-Za-z0-9][A-Za-z0-9_-]{7,}$/i;
|
|
2461
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
2462
|
+
const READABLE_KEY_RE = /^[a-z][a-z0-9]*(?:[_-][a-z0-9]+){2,}$/;
|
|
2463
|
+
const HARDCODED_URL_FINDING = {
|
|
2464
|
+
rule: "ai-slop/hardcoded-url",
|
|
2465
|
+
message: "Hardcoded environment URL in production code",
|
|
2466
|
+
help: "Move deployment-specific URLs to environment variables or a typed config module. Keep only stable documentation/public links inline."
|
|
2467
|
+
};
|
|
2468
|
+
const HARDCODED_ID_FINDING = {
|
|
2469
|
+
rule: "ai-slop/hardcoded-id",
|
|
2470
|
+
message: "Hardcoded provider/project ID in production code",
|
|
2471
|
+
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."
|
|
2472
|
+
};
|
|
2473
|
+
const makeFinding = (filePath, line, spec) => ({
|
|
2474
|
+
filePath,
|
|
2475
|
+
engine: "ai-slop",
|
|
2476
|
+
rule: spec.rule,
|
|
2477
|
+
severity: "warning",
|
|
2478
|
+
message: spec.message,
|
|
2479
|
+
help: spec.help,
|
|
2480
|
+
line,
|
|
2481
|
+
column: 0,
|
|
2482
|
+
category: "AI Slop",
|
|
2483
|
+
fixable: false
|
|
2484
|
+
});
|
|
2485
|
+
const isCommentOnlyLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*");
|
|
2486
|
+
const commentStartsBefore = (line, index, ext) => {
|
|
2487
|
+
const prefix = line.slice(0, index);
|
|
2488
|
+
if (ext === ".py" || ext === ".rb") return prefix.includes("#");
|
|
2489
|
+
if (ext === ".php") return prefix.includes("//") || prefix.includes("#");
|
|
2490
|
+
return prefix.includes("//") || prefix.includes("/*");
|
|
2491
|
+
};
|
|
2492
|
+
const safeUrlHost = (urlText) => {
|
|
2493
|
+
try {
|
|
2494
|
+
return new URL(urlText).hostname.toLowerCase();
|
|
2495
|
+
} catch {
|
|
2496
|
+
return null;
|
|
2497
|
+
}
|
|
2465
2498
|
};
|
|
2466
|
-
const
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
if (
|
|
2470
|
-
if (
|
|
2471
|
-
|
|
2472
|
-
if (
|
|
2473
|
-
|
|
2474
|
-
if (
|
|
2475
|
-
if (
|
|
2476
|
-
|
|
2477
|
-
|
|
2499
|
+
const isEnvBackedLine = (line) => ENV_REFERENCE_RE.test(line);
|
|
2500
|
+
const TEMPLATE_INTERPOLATION_START = "${";
|
|
2501
|
+
const shouldFlagUrlLiteral = (line, urlText) => {
|
|
2502
|
+
if (isEnvBackedLine(line)) return false;
|
|
2503
|
+
if (urlText.includes(TEMPLATE_INTERPOLATION_START) && /\bnew\s+URL\s*\(/.test(line)) return false;
|
|
2504
|
+
const host = safeUrlHost(urlText);
|
|
2505
|
+
if (!host) return false;
|
|
2506
|
+
if (PLACEHOLDER_HOSTS.has(host)) return false;
|
|
2507
|
+
if (LOOPBACK_HOSTS.has(host)) return false;
|
|
2508
|
+
if (isVendorApiHost(host)) return false;
|
|
2509
|
+
if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
|
|
2510
|
+
return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
|
|
2511
|
+
};
|
|
2512
|
+
const ENV_VAR_NAME_RE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/;
|
|
2513
|
+
const hasUsefulIdShape = (value) => {
|
|
2514
|
+
if (PLACEHOLDER_ID_RE.test(value)) return false;
|
|
2515
|
+
if (ENV_VAR_NAME_RE.test(value)) return false;
|
|
2516
|
+
if (/^https?:\/\//i.test(value)) return false;
|
|
2517
|
+
if (/^[A-Za-z]+$/.test(value)) return false;
|
|
2518
|
+
if (READABLE_KEY_RE.test(value) && !PROVIDER_ID_RE.test(value)) return false;
|
|
2519
|
+
if (PROVIDER_ID_RE.test(value)) return true;
|
|
2520
|
+
if (UUID_RE.test(value)) return true;
|
|
2521
|
+
if (!/[0-9]/.test(value)) return false;
|
|
2522
|
+
return value.length >= 24 && !/[_-]/.test(value) && /[a-z]/.test(value) && /[A-Z]/.test(value);
|
|
2523
|
+
};
|
|
2524
|
+
const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
|
|
2525
|
+
const diagnostics = [];
|
|
2526
|
+
if (isCommentOnlyLine(line.trim())) return diagnostics;
|
|
2527
|
+
for (const urlMatch of line.matchAll(URL_LITERAL_RE)) {
|
|
2528
|
+
const urlText = urlMatch[2];
|
|
2529
|
+
if (commentStartsBefore(line, urlMatch.index, ext)) continue;
|
|
2530
|
+
if (!shouldFlagUrlLiteral(line, urlText)) continue;
|
|
2531
|
+
diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_URL_FINDING));
|
|
2478
2532
|
}
|
|
2479
|
-
if (
|
|
2480
|
-
|
|
2533
|
+
if (!ID_CONTEXT_RE.test(line) || isEnvBackedLine(line) || DOC_URL_CONTEXT_RE.test(line)) return diagnostics;
|
|
2534
|
+
for (const idMatch of line.matchAll(ID_LITERAL_RE)) {
|
|
2535
|
+
const value = idMatch[2];
|
|
2536
|
+
if (commentStartsBefore(line, idMatch.index, ext)) continue;
|
|
2537
|
+
if (!hasUsefulIdShape(value)) continue;
|
|
2538
|
+
diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_ID_FINDING));
|
|
2539
|
+
}
|
|
2540
|
+
return diagnostics;
|
|
2481
2541
|
};
|
|
2482
|
-
const
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
if (
|
|
2486
|
-
|
|
2487
|
-
if (manifest.pyDeps.has(normalized)) return null;
|
|
2488
|
-
if ((PYTHON_IMPORT_TO_PIP[root] ?? PYTHON_IMPORT_TO_PIP[normalized])?.some((dist) => manifest.pyDeps.has(normalizePyName(dist)))) return null;
|
|
2489
|
-
return root;
|
|
2542
|
+
const scanFileForConfigLiterals = (content, relativePath, ext) => {
|
|
2543
|
+
if (!SOURCE_EXTENSIONS.has(ext)) return [];
|
|
2544
|
+
if (isNonProductionPath(relativePath)) return [];
|
|
2545
|
+
if (MIGRATION_PATH_RE$1.test(relativePath)) return [];
|
|
2546
|
+
return content.split("\n").flatMap((line, index) => scanLineForConfigLiterals(line, relativePath, ext, index + 1));
|
|
2490
2547
|
};
|
|
2491
|
-
const
|
|
2492
|
-
const rootPkg = readJson(path.join(context.rootDirectory, "package.json"));
|
|
2493
|
-
const workspaceDirs = collectWorkspaceDirs(context.rootDirectory, rootPkg);
|
|
2494
|
-
const manifest = loadManifest(context.rootDirectory);
|
|
2495
|
-
if (!manifest.hasJsManifest && !manifest.hasPyManifest) return [];
|
|
2496
|
-
const tsAliasMatchers = manifest.hasJsManifest ? collectTsPathAliases(context.rootDirectory, workspaceDirs) : [];
|
|
2548
|
+
const detectHardcodedConfigLiterals = async (context) => {
|
|
2497
2549
|
const diagnostics = [];
|
|
2498
|
-
const
|
|
2499
|
-
for (const filePath of files) {
|
|
2500
|
-
const ext = path.extname(filePath);
|
|
2501
|
-
const isJs = JS_EXTENSIONS$1.has(ext);
|
|
2502
|
-
const isPy = PY_EXTENSIONS$2.has(ext);
|
|
2503
|
-
if (!isJs && !isPy) continue;
|
|
2504
|
-
if (isJs && !manifest.hasJsManifest) continue;
|
|
2505
|
-
if (isPy && !manifest.hasPyManifest) continue;
|
|
2550
|
+
for (const filePath of getSourceFiles(context)) {
|
|
2506
2551
|
if (isAutoGenerated(filePath)) continue;
|
|
2507
2552
|
let content;
|
|
2508
2553
|
try {
|
|
@@ -2510,30 +2555,18 @@ const detectHallucinatedImports = async (context) => {
|
|
|
2510
2555
|
} catch {
|
|
2511
2556
|
continue;
|
|
2512
2557
|
}
|
|
2513
|
-
const
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
for (const { spec, line } of imports) {
|
|
2517
|
-
const hallucinated = isJs ? checkJsImport(spec, manifest, tsAliasMatchers) : checkPyImport(spec, manifest);
|
|
2518
|
-
if (!hallucinated) continue;
|
|
2519
|
-
const manifestLabel = isJs ? "package.json" : "requirements.txt / pyproject.toml / Pipfile";
|
|
2520
|
-
diagnostics.push({
|
|
2521
|
-
filePath: relPath,
|
|
2522
|
-
engine: "ai-slop",
|
|
2523
|
-
rule: "ai-slop/hallucinated-import",
|
|
2524
|
-
severity: "error",
|
|
2525
|
-
message: `Imports "${hallucinated}" but it's not declared in ${manifestLabel}${isPy ? " and isn't Python stdlib" : ""}`,
|
|
2526
|
-
help: "Most often this is an LLM hallucinating a plausible-sounding package name. Either add the package to your manifest, or correct the import.",
|
|
2527
|
-
line,
|
|
2528
|
-
column: 1,
|
|
2529
|
-
category: "AI Slop",
|
|
2530
|
-
fixable: false
|
|
2531
|
-
});
|
|
2532
|
-
}
|
|
2558
|
+
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
2559
|
+
const ext = path.extname(filePath);
|
|
2560
|
+
diagnostics.push(...scanFileForConfigLiterals(maskComments(content, ext), relativePath, ext));
|
|
2533
2561
|
}
|
|
2534
2562
|
return diagnostics;
|
|
2535
2563
|
};
|
|
2536
2564
|
|
|
2565
|
+
//#endregion
|
|
2566
|
+
//#region src/utils/suppress.ts
|
|
2567
|
+
const DIRECTIVE_RE = /(?:\/\/|\/\*|#|<!--|\*)\s*aislop-ignore-(next-line|line|file)\b([^\n]*)/;
|
|
2568
|
+
const isAislopDirectiveLine = (line) => DIRECTIVE_RE.test(line);
|
|
2569
|
+
|
|
2537
2570
|
//#endregion
|
|
2538
2571
|
//#region src/engines/ai-slop/comment-blocks.ts
|
|
2539
2572
|
const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
|
|
@@ -2557,6 +2590,7 @@ const getCommentSyntax = (ext) => {
|
|
|
2557
2590
|
};
|
|
2558
2591
|
const getMatchedLinePrefix = (line, syntax) => {
|
|
2559
2592
|
const trimmed = line.trimStart();
|
|
2593
|
+
if (isAislopDirectiveLine(trimmed)) return null;
|
|
2560
2594
|
for (const prefix of syntax.linePrefixes) {
|
|
2561
2595
|
if (!trimmed.startsWith(prefix)) continue;
|
|
2562
2596
|
if (prefix === "#" && trimmed.startsWith("#!")) return null;
|
|
@@ -3356,9 +3390,7 @@ const isLogOnlyBody = (body) => {
|
|
|
3356
3390
|
};
|
|
3357
3391
|
const detectJsSilentRecovery = (content, relPath) => {
|
|
3358
3392
|
const out = [];
|
|
3359
|
-
CATCH_HEAD_RE
|
|
3360
|
-
let match;
|
|
3361
|
-
while ((match = CATCH_HEAD_RE.exec(content)) !== null) {
|
|
3393
|
+
for (const match of content.matchAll(CATCH_HEAD_RE)) {
|
|
3362
3394
|
const body = extractCatchBody(content, match.index + match[0].length - 1);
|
|
3363
3395
|
if (body === null) continue;
|
|
3364
3396
|
if (!isLogOnlyBody(body)) continue;
|
|
@@ -3534,18 +3566,22 @@ const extractPyImportedSymbols = (lines) => {
|
|
|
3534
3566
|
}
|
|
3535
3567
|
continue;
|
|
3536
3568
|
}
|
|
3537
|
-
const importMatch = trimmed.match(/^import\s+(
|
|
3569
|
+
const importMatch = trimmed.match(/^import\s+(.+)/);
|
|
3538
3570
|
if (importMatch) {
|
|
3539
3571
|
importLines.add(i);
|
|
3540
|
-
const
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3572
|
+
for (const clause of importMatch[1].replace(/#.*$/, "").split(",")) {
|
|
3573
|
+
const clauseMatch = clause.trim().match(/^([\w.]+)(?:\s+as\s+(\w+))?/);
|
|
3574
|
+
if (!clauseMatch) continue;
|
|
3575
|
+
const alias = clauseMatch[2];
|
|
3576
|
+
if (alias && alias === clauseMatch[1]) continue;
|
|
3577
|
+
const simpleName = (alias ?? clauseMatch[1]).split(".")[0];
|
|
3578
|
+
if (simpleName && /^\w+$/.test(simpleName)) symbols.push({
|
|
3579
|
+
name: simpleName,
|
|
3580
|
+
line: i + 1,
|
|
3581
|
+
isDefault: false,
|
|
3582
|
+
isNamespace: true
|
|
3583
|
+
});
|
|
3584
|
+
}
|
|
3549
3585
|
}
|
|
3550
3586
|
}
|
|
3551
3587
|
return {
|
|
@@ -3555,8 +3591,7 @@ const extractPyImportedSymbols = (lines) => {
|
|
|
3555
3591
|
};
|
|
3556
3592
|
const isSymbolUsed = (name, content, importLines, lines) => {
|
|
3557
3593
|
const pattern = new RegExp(`\\b${name}\\b`, "g");
|
|
3558
|
-
|
|
3559
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
3594
|
+
for (const match of content.matchAll(pattern)) {
|
|
3560
3595
|
const lineIndex = content.slice(0, match.index).split("\n").length - 1;
|
|
3561
3596
|
if (!importLines.has(lineIndex)) return true;
|
|
3562
3597
|
}
|
|
@@ -3657,6 +3692,18 @@ const aiSlopEngine = {
|
|
|
3657
3692
|
|
|
3658
3693
|
//#endregion
|
|
3659
3694
|
//#region src/engines/architecture/matchers.ts
|
|
3695
|
+
const REGEX_SPECIAL_CHARS = new Set([
|
|
3696
|
+
".",
|
|
3697
|
+
"+",
|
|
3698
|
+
"^",
|
|
3699
|
+
"$",
|
|
3700
|
+
"{",
|
|
3701
|
+
"}",
|
|
3702
|
+
"(",
|
|
3703
|
+
")",
|
|
3704
|
+
"|",
|
|
3705
|
+
"\\"
|
|
3706
|
+
]);
|
|
3660
3707
|
const minimatch = (filePath, pattern) => {
|
|
3661
3708
|
let regex = "";
|
|
3662
3709
|
let i = 0;
|
|
@@ -3681,7 +3728,7 @@ const minimatch = (filePath, pattern) => {
|
|
|
3681
3728
|
regex += pattern.slice(i, closeIndex + 1);
|
|
3682
3729
|
i = closeIndex + 1;
|
|
3683
3730
|
}
|
|
3684
|
-
} else if (
|
|
3731
|
+
} else if (REGEX_SPECIAL_CHARS.has(ch)) {
|
|
3685
3732
|
regex += `\\${ch}`;
|
|
3686
3733
|
i++;
|
|
3687
3734
|
} else {
|
|
@@ -3701,27 +3748,15 @@ const extractImports = (content, ext) => {
|
|
|
3701
3748
|
".mjs",
|
|
3702
3749
|
".cjs"
|
|
3703
3750
|
].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]);
|
|
3751
|
+
for (const match of content.matchAll(/(?:import|from)\s+["']([^"']+)["']/g)) imports.push(match[1]);
|
|
3752
|
+
for (const match of content.matchAll(/require\s*\(\s*["']([^"']+)["']\s*\)/g)) imports.push(match[1]);
|
|
3714
3753
|
}
|
|
3754
|
+
if (ext === ".py") for (const match of content.matchAll(/(?:from|import)\s+([\w.]+)/g)) imports.push(match[1]);
|
|
3715
3755
|
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) {
|
|
3756
|
+
for (const match of content.matchAll(/^\s*import\s+"([^"]+)"/gm)) imports.push(match[1]);
|
|
3757
|
+
for (const match of content.matchAll(/import\s*\(([^)]*)\)/gs)) {
|
|
3721
3758
|
const block = match[1];
|
|
3722
|
-
const
|
|
3723
|
-
let pkgMatch;
|
|
3724
|
-
while ((pkgMatch = pkgPattern.exec(block)) !== null) imports.push(pkgMatch[1]);
|
|
3759
|
+
for (const pkgMatch of block.matchAll(/"([^"]+)"/g)) imports.push(pkgMatch[1]);
|
|
3725
3760
|
}
|
|
3726
3761
|
}
|
|
3727
3762
|
return imports;
|
|
@@ -3848,10 +3883,10 @@ const architectureEngine = {
|
|
|
3848
3883
|
//#endregion
|
|
3849
3884
|
//#region src/engines/code-quality/function-boundaries.ts
|
|
3850
3885
|
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 =
|
|
3886
|
+
const ARROW_BLOCK_RE = /=>\s*\{/;
|
|
3887
|
+
const ARROW_END_RE = /=>\s*$/;
|
|
3888
|
+
const BRACE_START_RE = /^\s*\{/;
|
|
3889
|
+
const NEW_STATEMENT_RE = /^(?:export\s+)?(?:const|let|var|function|class)\s/;
|
|
3855
3890
|
const isControlFlowBrace = (lineText, braceIndex) => {
|
|
3856
3891
|
const before = lineText.substring(0, braceIndex).trimEnd();
|
|
3857
3892
|
if (before.endsWith(")")) return true;
|
|
@@ -4037,14 +4072,14 @@ const countTemplateLines = (bodyLines) => {
|
|
|
4037
4072
|
let templateLineCount = 0;
|
|
4038
4073
|
for (const line of bodyLines) {
|
|
4039
4074
|
const startedInside = insideTemplate;
|
|
4040
|
-
let
|
|
4075
|
+
let escaped = false;
|
|
4041
4076
|
for (const ch of line) {
|
|
4042
|
-
if (
|
|
4043
|
-
|
|
4077
|
+
if (escaped) {
|
|
4078
|
+
escaped = false;
|
|
4044
4079
|
continue;
|
|
4045
4080
|
}
|
|
4046
4081
|
if (ch === "\\") {
|
|
4047
|
-
|
|
4082
|
+
escaped = true;
|
|
4048
4083
|
continue;
|
|
4049
4084
|
}
|
|
4050
4085
|
if (ch === "`") insideTemplate = !insideTemplate;
|
|
@@ -4435,8 +4470,8 @@ const shouldIncludeIssue = (issueType, filePath) => {
|
|
|
4435
4470
|
return !filePath.replace(/\\/g, "/").includes(".github/workflows/");
|
|
4436
4471
|
};
|
|
4437
4472
|
const DEPENDENCY_HELP = {
|
|
4438
|
-
dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `
|
|
4439
|
-
devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `
|
|
4473
|
+
dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
|
|
4474
|
+
devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
|
|
4440
4475
|
unlisted: "This package is imported in code but not declared in package.json. Run `npm install` to add it.",
|
|
4441
4476
|
unresolved: "This import cannot be resolved. Check for typos or missing packages.",
|
|
4442
4477
|
binaries: "This binary is used but its package is not in package.json."
|
|
@@ -4743,7 +4778,7 @@ const parseBiomeJsonOutput = (output, rootDir) => {
|
|
|
4743
4778
|
rule: "formatting",
|
|
4744
4779
|
severity,
|
|
4745
4780
|
message,
|
|
4746
|
-
help: "Run `
|
|
4781
|
+
help: "Run `aislop fix` to auto-format",
|
|
4747
4782
|
line: entry.location?.start?.line ?? 0,
|
|
4748
4783
|
column: entry.location?.start?.column ?? 0,
|
|
4749
4784
|
category: "Format",
|
|
@@ -4772,7 +4807,7 @@ const FORMATTERS = {
|
|
|
4772
4807
|
rule: "rust-formatting",
|
|
4773
4808
|
severity: "warning",
|
|
4774
4809
|
message: "Rust file is not formatted correctly",
|
|
4775
|
-
help: "Run `
|
|
4810
|
+
help: "Run `aislop fix` to auto-format with rustfmt",
|
|
4776
4811
|
line: parseInt(match[2], 10),
|
|
4777
4812
|
column: 0,
|
|
4778
4813
|
category: "Format",
|
|
@@ -4805,7 +4840,7 @@ const FORMATTERS = {
|
|
|
4805
4840
|
rule: offense.cop_name ?? "ruby-formatting",
|
|
4806
4841
|
severity: "warning",
|
|
4807
4842
|
message: offense.message ?? "Ruby formatting issue",
|
|
4808
|
-
help: "Run `
|
|
4843
|
+
help: "Run `aislop fix` to auto-format",
|
|
4809
4844
|
line: offense.location?.start_line ?? 0,
|
|
4810
4845
|
column: offense.location?.start_column ?? 0,
|
|
4811
4846
|
category: "Format",
|
|
@@ -4836,7 +4871,7 @@ const FORMATTERS = {
|
|
|
4836
4871
|
rule: "php-formatting",
|
|
4837
4872
|
severity: "warning",
|
|
4838
4873
|
message: "PHP file is not formatted correctly",
|
|
4839
|
-
help: "Run `
|
|
4874
|
+
help: "Run `aislop fix` to auto-format",
|
|
4840
4875
|
line: 0,
|
|
4841
4876
|
column: 0,
|
|
4842
4877
|
category: "Format",
|
|
@@ -4880,7 +4915,7 @@ const runGofmt = async (context) => {
|
|
|
4880
4915
|
rule: "go-formatting",
|
|
4881
4916
|
severity: "warning",
|
|
4882
4917
|
message: "Go file is not formatted correctly",
|
|
4883
|
-
help: "Run `
|
|
4918
|
+
help: "Run `aislop fix` to auto-format with gofmt",
|
|
4884
4919
|
line: 0,
|
|
4885
4920
|
column: 0,
|
|
4886
4921
|
category: "Format",
|
|
@@ -4964,9 +4999,7 @@ const runRuffFormat = async (context) => {
|
|
|
4964
4999
|
};
|
|
4965
5000
|
const parseRuffFormatOutput = (output, rootDir) => {
|
|
4966
5001
|
const diagnostics = [];
|
|
4967
|
-
const
|
|
4968
|
-
let match;
|
|
4969
|
-
while ((match = filePattern.exec(output)) !== null) {
|
|
5002
|
+
for (const match of output.matchAll(/^--- (.+)$/gm)) {
|
|
4970
5003
|
const filePath = getRuffDiagnosticPath(rootDir, match[1]);
|
|
4971
5004
|
diagnostics.push({
|
|
4972
5005
|
filePath,
|
|
@@ -4974,7 +5007,7 @@ const parseRuffFormatOutput = (output, rootDir) => {
|
|
|
4974
5007
|
rule: "python-formatting",
|
|
4975
5008
|
severity: "warning",
|
|
4976
5009
|
message: "Python file is not formatted correctly",
|
|
4977
|
-
help: "Run `
|
|
5010
|
+
help: "Run `aislop fix` to auto-format with ruff",
|
|
4978
5011
|
line: 0,
|
|
4979
5012
|
column: 0,
|
|
4980
5013
|
category: "Format",
|
|
@@ -4993,10 +5026,10 @@ const formatEngine = {
|
|
|
4993
5026
|
const { languages, installedTools } = context;
|
|
4994
5027
|
const promises = [];
|
|
4995
5028
|
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
|
|
5029
|
+
if (languages.includes("python") && installedTools.ruff) promises.push(runRuffFormat(context));
|
|
5030
|
+
if (languages.includes("go") && installedTools.gofmt) promises.push(runGofmt(context));
|
|
5031
|
+
if (languages.includes("rust") && installedTools.rustfmt) promises.push(runGenericFormatter(context, "rust"));
|
|
5032
|
+
if (languages.includes("ruby") && installedTools.rubocop) promises.push(runGenericFormatter(context, "ruby"));
|
|
5000
5033
|
if (languages.includes("php") && installedTools["php-cs-fixer"]) promises.push(runGenericFormatter(context, "php"));
|
|
5001
5034
|
const results = await Promise.allSettled(promises);
|
|
5002
5035
|
for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
|
|
@@ -5200,6 +5233,8 @@ const createOxlintConfig = (options) => {
|
|
|
5200
5233
|
if (options.mode === "fix") {
|
|
5201
5234
|
rules["no-unused-vars"] = "off";
|
|
5202
5235
|
rules["react-hooks/exhaustive-deps"] = "off";
|
|
5236
|
+
rules["jsx-a11y/no-aria-hidden-on-focusable"] = "off";
|
|
5237
|
+
rules["unicorn/no-useless-fallback-in-spread"] = "off";
|
|
5203
5238
|
}
|
|
5204
5239
|
const plugins = [
|
|
5205
5240
|
"import",
|
|
@@ -5250,6 +5285,7 @@ const AMBIENT_GLOBAL_DEPS = [
|
|
|
5250
5285
|
const SST_PLATFORM_REF_RE = /\/\/\/\s*<reference\s+path=["'][^"']*sst[\\/]+platform[\\/]+config\.d\.ts["']/;
|
|
5251
5286
|
const ICON_AUTOIMPORT_RE = /^Icon[A-Z]/;
|
|
5252
5287
|
const NO_UNDEF_IDENT_RE = /^['‘"`]([^'’"`]+)['’"`]/;
|
|
5288
|
+
const SUPABASE_FUNCTION_PATH_RE = /(?:^|\/)supabase\/functions\/[^/]+\/.+\.[cm]?[jt]sx?$/;
|
|
5253
5289
|
const detectAmbientSources = (rootDir) => {
|
|
5254
5290
|
const found = /* @__PURE__ */ new Set();
|
|
5255
5291
|
const skipDirs = new Set([
|
|
@@ -5294,6 +5330,36 @@ const detectAmbientSources = (rootDir) => {
|
|
|
5294
5330
|
const extractNoUndefIdentifier = (message) => {
|
|
5295
5331
|
return NO_UNDEF_IDENT_RE.exec(message)?.[1] ?? null;
|
|
5296
5332
|
};
|
|
5333
|
+
const looksLikeChromeExtensionManifest = (filePath) => {
|
|
5334
|
+
try {
|
|
5335
|
+
const manifest = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
5336
|
+
return typeof manifest.manifest_version === "number" && ("background" in manifest || "content_scripts" in manifest || "permissions" in manifest);
|
|
5337
|
+
} catch {
|
|
5338
|
+
return false;
|
|
5339
|
+
}
|
|
5340
|
+
};
|
|
5341
|
+
const chromeExtensionFileCache = /* @__PURE__ */ new Map();
|
|
5342
|
+
const isChromeExtensionFile = (rootDir, relativeFilePath) => {
|
|
5343
|
+
const cacheKey = `${rootDir}:${relativeFilePath.split(path.sep).join("/")}`;
|
|
5344
|
+
const cached = chromeExtensionFileCache.get(cacheKey);
|
|
5345
|
+
if (cached !== void 0) return cached;
|
|
5346
|
+
const absolute = path.isAbsolute(relativeFilePath) ? relativeFilePath : path.join(rootDir, relativeFilePath);
|
|
5347
|
+
const root = path.resolve(rootDir);
|
|
5348
|
+
let dir = path.dirname(path.resolve(absolute));
|
|
5349
|
+
let matched = false;
|
|
5350
|
+
while (true) {
|
|
5351
|
+
const relativeToRoot = path.relative(root, dir);
|
|
5352
|
+
if (relativeToRoot.startsWith("..") || path.isAbsolute(relativeToRoot)) break;
|
|
5353
|
+
if (looksLikeChromeExtensionManifest(path.join(dir, "manifest.json"))) {
|
|
5354
|
+
matched = true;
|
|
5355
|
+
break;
|
|
5356
|
+
}
|
|
5357
|
+
if (dir === root) break;
|
|
5358
|
+
dir = path.dirname(dir);
|
|
5359
|
+
}
|
|
5360
|
+
chromeExtensionFileCache.set(cacheKey, matched);
|
|
5361
|
+
return matched;
|
|
5362
|
+
};
|
|
5297
5363
|
const isAmbientFalsePositive = (rule, message, sources) => {
|
|
5298
5364
|
if (rule !== "eslint/no-undef") return false;
|
|
5299
5365
|
const ident = extractNoUndefIdentifier(message);
|
|
@@ -5302,9 +5368,19 @@ const isAmbientFalsePositive = (rule, message, sources) => {
|
|
|
5302
5368
|
if ((sources.has("@types/bun") || sources.has("bun-types")) && ident === "Bun") return true;
|
|
5303
5369
|
return false;
|
|
5304
5370
|
};
|
|
5371
|
+
const isRuntimeGlobalFalsePositive = (rule, message, rootDir, relativeFilePath) => {
|
|
5372
|
+
if (rule !== "eslint/no-undef") return false;
|
|
5373
|
+
const ident = extractNoUndefIdentifier(message);
|
|
5374
|
+
if (!ident) return false;
|
|
5375
|
+
const normalized = relativeFilePath.split(path.sep).join("/");
|
|
5376
|
+
if (ident === "Deno" && SUPABASE_FUNCTION_PATH_RE.test(normalized)) return true;
|
|
5377
|
+
if (ident === "chrome" && isChromeExtensionFile(rootDir, relativeFilePath)) return true;
|
|
5378
|
+
return false;
|
|
5379
|
+
};
|
|
5305
5380
|
const sstReferencedFiles = /* @__PURE__ */ new Map();
|
|
5306
5381
|
const clearSstReferenceCache = () => {
|
|
5307
5382
|
sstReferencedFiles.clear();
|
|
5383
|
+
chromeExtensionFileCache.clear();
|
|
5308
5384
|
};
|
|
5309
5385
|
const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
|
|
5310
5386
|
const cached = sstReferencedFiles.get(relativeFilePath);
|
|
@@ -5356,6 +5432,32 @@ const collectPackageNames = (dir) => {
|
|
|
5356
5432
|
}
|
|
5357
5433
|
return names;
|
|
5358
5434
|
};
|
|
5435
|
+
const readJson = (filePath) => {
|
|
5436
|
+
const raw = readTextFile$1(filePath);
|
|
5437
|
+
if (!raw) return null;
|
|
5438
|
+
try {
|
|
5439
|
+
const parsed = JSON.parse(raw);
|
|
5440
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
5441
|
+
} catch {
|
|
5442
|
+
return null;
|
|
5443
|
+
}
|
|
5444
|
+
};
|
|
5445
|
+
const hasBunRuntime = (rootDir, projectFiles) => {
|
|
5446
|
+
if (fs.existsSync(path.join(rootDir, "bun.lock")) || fs.existsSync(path.join(rootDir, "bun.lockb")) || fs.existsSync(path.join(rootDir, "bunfig.toml"))) return true;
|
|
5447
|
+
const hasBunFiles = projectFiles.some((filePath) => /(?:^|\/)bunfig\.toml$|(?:^|\/)bun\.lockb?$/.test(filePath));
|
|
5448
|
+
const pkg = readJson(path.join(rootDir, "package.json"));
|
|
5449
|
+
if (!pkg) return hasBunFiles;
|
|
5450
|
+
if (typeof pkg.packageManager === "string" && /^bun@/i.test(pkg.packageManager)) return true;
|
|
5451
|
+
const scripts = pkg.scripts;
|
|
5452
|
+
if (scripts && typeof scripts === "object") {
|
|
5453
|
+
for (const command of Object.values(scripts)) if (typeof command === "string" && /(?:^|[;&|()\s])bunx?\s/.test(command)) return true;
|
|
5454
|
+
}
|
|
5455
|
+
return hasBunFiles;
|
|
5456
|
+
};
|
|
5457
|
+
const hasDenoRuntime = (rootDir, projectFiles) => {
|
|
5458
|
+
if (fs.existsSync(path.join(rootDir, "deno.json")) || fs.existsSync(path.join(rootDir, "deno.jsonc"))) return true;
|
|
5459
|
+
return projectFiles.some((filePath) => /(?:^|\/)deno\.jsonc?$/.test(filePath));
|
|
5460
|
+
};
|
|
5359
5461
|
const AMBIENT_GLOBAL_RE = /^\s*(?:declare\s+)?(?:const|let|var|function|class)\s+([A-Za-z_$][\w$]*)/gm;
|
|
5360
5462
|
const collectAmbientGlobals = (rootDir) => {
|
|
5361
5463
|
const globals = /* @__PURE__ */ new Set();
|
|
@@ -5364,12 +5466,11 @@ const collectAmbientGlobals = (rootDir) => {
|
|
|
5364
5466
|
if (!relativePath.endsWith(".d.ts")) continue;
|
|
5365
5467
|
const content = readTextFile$1(path.join(rootDir, relativePath));
|
|
5366
5468
|
if (!content) continue;
|
|
5367
|
-
AMBIENT_GLOBAL_RE.
|
|
5368
|
-
let match;
|
|
5369
|
-
while ((match = AMBIENT_GLOBAL_RE.exec(content)) !== null) globals.add(match[1]);
|
|
5469
|
+
for (const match of content.matchAll(AMBIENT_GLOBAL_RE)) globals.add(match[1]);
|
|
5370
5470
|
}
|
|
5371
5471
|
const deps = collectPackageNames(rootDir);
|
|
5372
|
-
if (deps.has("@types/bun") || deps.has("bun-types")) globals.add("Bun");
|
|
5472
|
+
if (deps.has("@types/bun") || deps.has("bun-types") || hasBunRuntime(rootDir, projectFiles)) globals.add("Bun");
|
|
5473
|
+
if (hasDenoRuntime(rootDir, projectFiles)) globals.add("Deno");
|
|
5373
5474
|
if (projectFiles.some((filePath) => /(?:^|\/)sst\.config\.ts$/.test(filePath))) for (const name of [
|
|
5374
5475
|
"$app",
|
|
5375
5476
|
"$config",
|
|
@@ -5467,6 +5568,37 @@ const detectTestFramework = (rootDir) => {
|
|
|
5467
5568
|
return null;
|
|
5468
5569
|
};
|
|
5469
5570
|
const getOxlintTargets = (context) => getSourceFiles(context).filter((filePath) => OXLINT_EXTENSIONS.has(path.extname(filePath).toLowerCase())).filter((filePath) => !isAutoGenerated(filePath)).map((filePath) => path.relative(context.rootDirectory, filePath).split(path.sep).join("/"));
|
|
5571
|
+
const toDiagnostic = (d) => {
|
|
5572
|
+
const { plugin, rule } = parseRuleCode(d.code);
|
|
5573
|
+
const label = d.labels[0];
|
|
5574
|
+
return {
|
|
5575
|
+
filePath: d.filename,
|
|
5576
|
+
engine: "lint",
|
|
5577
|
+
rule: `${plugin}/${rule}`,
|
|
5578
|
+
severity: d.severity,
|
|
5579
|
+
message: d.message.replace(/\S+\.\w+:\d+:\d+[\s\S]*$/, "").trim() || d.message,
|
|
5580
|
+
help: d.help || "",
|
|
5581
|
+
line: label?.span.line ?? 0,
|
|
5582
|
+
column: label?.span.column ?? 0,
|
|
5583
|
+
category: plugin === "react" ? "React" : plugin === "import" ? "Imports" : "Lint",
|
|
5584
|
+
fixable: false
|
|
5585
|
+
};
|
|
5586
|
+
};
|
|
5587
|
+
const shouldKeepOxlintDiagnostic = (context, ambientSources, seen, d) => {
|
|
5588
|
+
const relativePath = path.isAbsolute(d.filePath) ? path.relative(context.rootDirectory, d.filePath) : d.filePath;
|
|
5589
|
+
if (isExcludedFromScan(relativePath)) return false;
|
|
5590
|
+
if (isViteVirtualImportFalsePositive(d.rule, d.message)) return false;
|
|
5591
|
+
if (isAmbientFalsePositive(d.rule, d.message, ambientSources)) return false;
|
|
5592
|
+
if (isRuntimeGlobalFalsePositive(d.rule, d.message, context.rootDirectory, relativePath)) return false;
|
|
5593
|
+
if (isSolidRefFalsePositive(context, d)) return false;
|
|
5594
|
+
if (isContextualTypeScriptFalsePositive(d)) return false;
|
|
5595
|
+
if (isUnderscoreUnusedVar(d.rule, d.message)) return false;
|
|
5596
|
+
if (d.rule === "eslint/no-undef" && fileReferencesSstPlatform(context.rootDirectory, d.filePath)) return false;
|
|
5597
|
+
const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
|
|
5598
|
+
if (seen.has(key)) return false;
|
|
5599
|
+
seen.add(key);
|
|
5600
|
+
return true;
|
|
5601
|
+
};
|
|
5470
5602
|
const runOxlint = async (context) => {
|
|
5471
5603
|
const configPath = path.join(os.tmpdir(), `aislop-oxlintrc-${process.pid}.json`);
|
|
5472
5604
|
const framework = context.frameworks.find((f) => f !== "none");
|
|
@@ -5503,34 +5635,7 @@ const runOxlint = async (context) => {
|
|
|
5503
5635
|
return [];
|
|
5504
5636
|
}
|
|
5505
5637
|
const seen = /* @__PURE__ */ new Set();
|
|
5506
|
-
return output.diagnostics.map((d) =>
|
|
5507
|
-
const { plugin, rule } = parseRuleCode(d.code);
|
|
5508
|
-
const label = d.labels[0];
|
|
5509
|
-
return {
|
|
5510
|
-
filePath: d.filename,
|
|
5511
|
-
engine: "lint",
|
|
5512
|
-
rule: `${plugin}/${rule}`,
|
|
5513
|
-
severity: d.severity,
|
|
5514
|
-
message: d.message.replace(/\S+\.\w+:\d+:\d+[\s\S]*$/, "").trim() || d.message,
|
|
5515
|
-
help: d.help || "",
|
|
5516
|
-
line: label?.span.line ?? 0,
|
|
5517
|
-
column: label?.span.column ?? 0,
|
|
5518
|
-
category: plugin === "react" ? "React" : plugin === "import" ? "Imports" : "Lint",
|
|
5519
|
-
fixable: false
|
|
5520
|
-
};
|
|
5521
|
-
}).filter((d) => {
|
|
5522
|
-
if (isExcludedFromScan(path.isAbsolute(d.filePath) ? path.relative(context.rootDirectory, d.filePath) : d.filePath)) return false;
|
|
5523
|
-
if (isViteVirtualImportFalsePositive(d.rule, d.message)) return false;
|
|
5524
|
-
if (isAmbientFalsePositive(d.rule, d.message, ambientSources)) return false;
|
|
5525
|
-
if (isSolidRefFalsePositive(context, d)) return false;
|
|
5526
|
-
if (isContextualTypeScriptFalsePositive(d)) return false;
|
|
5527
|
-
if (isUnderscoreUnusedVar(d.rule, d.message)) return false;
|
|
5528
|
-
if (d.rule === "eslint/no-undef" && fileReferencesSstPlatform(context.rootDirectory, d.filePath)) return false;
|
|
5529
|
-
const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
|
|
5530
|
-
if (seen.has(key)) return false;
|
|
5531
|
-
seen.add(key);
|
|
5532
|
-
return true;
|
|
5533
|
-
});
|
|
5638
|
+
return output.diagnostics.map(toDiagnostic).filter((d) => shouldKeepOxlintDiagnostic(context, ambientSources, seen, d));
|
|
5534
5639
|
} finally {
|
|
5535
5640
|
if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
|
|
5536
5641
|
}
|
|
@@ -5581,11 +5686,11 @@ const lintEngine = {
|
|
|
5581
5686
|
promises.push(runOxlint(context));
|
|
5582
5687
|
if (context.config.lint.typecheck) promises.push(import("./typecheck-By967nny.js").then((mod) => mod.runTypecheck(context)));
|
|
5583
5688
|
}
|
|
5584
|
-
if (context.frameworks.includes("expo")) promises.push(import("./expo-doctor-
|
|
5585
|
-
if (languages.includes("python") && installedTools
|
|
5689
|
+
if (context.frameworks.includes("expo")) promises.push(import("./expo-doctor-BM2JR6f6.js").then((mod) => mod.runExpoDoctor(context)));
|
|
5690
|
+
if (languages.includes("python") && installedTools.ruff) promises.push(runRuffLint(context));
|
|
5586
5691
|
if (languages.includes("go") && installedTools["golangci-lint"]) promises.push(runGolangciLint(context));
|
|
5587
|
-
if (languages.includes("rust") && installedTools
|
|
5588
|
-
if (languages.includes("ruby") && installedTools
|
|
5692
|
+
if (languages.includes("rust") && installedTools.cargo) promises.push(runGenericLinter(context, "rust"));
|
|
5693
|
+
if (languages.includes("ruby") && installedTools.rubocop) promises.push(runGenericLinter(context, "ruby"));
|
|
5589
5694
|
const results = await Promise.allSettled(promises);
|
|
5590
5695
|
for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
|
|
5591
5696
|
return {
|
|
@@ -5599,7 +5704,7 @@ const lintEngine = {
|
|
|
5599
5704
|
|
|
5600
5705
|
//#endregion
|
|
5601
5706
|
//#region src/ui/invocation.ts
|
|
5602
|
-
const detectInvocation = () => "
|
|
5707
|
+
const detectInvocation = () => "aislop";
|
|
5603
5708
|
|
|
5604
5709
|
//#endregion
|
|
5605
5710
|
//#region src/engines/security/audit.ts
|
|
@@ -5615,7 +5720,7 @@ const runDependencyAudit = async (context) => {
|
|
|
5615
5720
|
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
5721
|
}
|
|
5617
5722
|
if (context.languages.includes("python") && context.installedTools["pip-audit"]) promises.push(runPipAudit(context.rootDirectory, timeout));
|
|
5618
|
-
if (context.languages.includes("go") && context.installedTools
|
|
5723
|
+
if (context.languages.includes("go") && context.installedTools.govulncheck) promises.push(runGovulncheck(context.rootDirectory, timeout));
|
|
5619
5724
|
if (context.languages.includes("rust")) promises.push(runCargoAudit(context.rootDirectory, timeout));
|
|
5620
5725
|
const results = await Promise.allSettled(promises);
|
|
5621
5726
|
for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
|
|
@@ -5720,9 +5825,12 @@ const parseLegacyAdvisories = (advisories, source) => {
|
|
|
5720
5825
|
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
5826
|
return [...bucket.values()].map((agg) => aggregateToDiagnostic(agg, source));
|
|
5722
5827
|
};
|
|
5828
|
+
const carriesAdvisory = (vulnerability) => Array.isArray(vulnerability.via) && vulnerability.via.some((entry) => entry !== null && typeof entry === "object");
|
|
5723
5829
|
const parseModernVulnerabilities = (vulnerabilities, source) => {
|
|
5724
5830
|
const bucket = /* @__PURE__ */ new Map();
|
|
5831
|
+
const hasRootCauses = Object.values(vulnerabilities).some(carriesAdvisory);
|
|
5725
5832
|
for (const [packageName, vulnerability] of Object.entries(vulnerabilities)) {
|
|
5833
|
+
if (hasRootCauses && !carriesAdvisory(vulnerability)) continue;
|
|
5726
5834
|
const severity = (vulnerability.severity ?? "moderate").toLowerCase();
|
|
5727
5835
|
const fixAvailable = vulnerability.fixAvailable;
|
|
5728
5836
|
const isDirect = vulnerability.isDirect === true;
|
|
@@ -5748,7 +5856,7 @@ const parseJsAudit = (output, source) => {
|
|
|
5748
5856
|
rule: "security/dependency-audit-skipped",
|
|
5749
5857
|
severity: "info",
|
|
5750
5858
|
message: `Dependency audit skipped (${source}): lockfile is missing`,
|
|
5751
|
-
help: error.detail ?? "Generate a lockfile, then re-run `
|
|
5859
|
+
help: error.detail ?? "Generate a lockfile, then re-run `aislop scan` for dependency vulnerability checks.",
|
|
5752
5860
|
line: 0,
|
|
5753
5861
|
column: 0,
|
|
5754
5862
|
category: "Security",
|
|
@@ -5865,6 +5973,194 @@ const runCargoAudit = async (rootDir, timeout) => {
|
|
|
5865
5973
|
}
|
|
5866
5974
|
};
|
|
5867
5975
|
|
|
5976
|
+
//#endregion
|
|
5977
|
+
//#region src/engines/security/html-safety.ts
|
|
5978
|
+
const SAFE_EMPTY_INNER_HTML_RE = /^\.innerHTML\s*=\s*(?:""|''|``)\s*;?/;
|
|
5979
|
+
const SAFE_SANITIZED_INNER_HTML_RE = /^\.innerHTML\s*=\s*(?:escapeHtml|sanitizeHtml|sanitizeHTML|DOMPurify\.sanitize)\s*\([^;\n]*\)\s*;?(?:\n|$)/;
|
|
5980
|
+
const SANITIZER_EXPR_RE = /^(?:escapeHtml|escapeHTML|sanitizeHtml|sanitizeHTML|DOMPurify\.sanitize)\s*\([^;\n]*\)$/;
|
|
5981
|
+
const IDENT_RE = /^[A-Za-z_$][\w$]*$/;
|
|
5982
|
+
const STATIC_STRING_RE = /^(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\$])*`)$/;
|
|
5983
|
+
const NUMERICISH_EXPR_RE = /^(?:[-+]?\d+(?:\.\d+)?|[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*(?:\s*\|\|\s*[-+]?\d+(?:\.\d+)?)?)$/;
|
|
5984
|
+
const NUMERICISH_NAME_RE = /(?:^|\.)(?:count|length|size|width|height|top|right|bottom|left|duration|elapsed|timestamp|time|ms|port|pid|attempt|attempts|index|total|x|y)$|(?:count|length|size|width|height|duration|elapsed|timestamp|time|port|pid|attempt|index|total)$/i;
|
|
5985
|
+
const SAFE_FORMAT_CALL_RE = /^(?:format[A-Z]\w*|fmt[A-Z]?\w*)\s*\((.*)\)$/;
|
|
5986
|
+
const consumeQuotedLiteral = (content, startIndex, quote) => {
|
|
5987
|
+
let i = startIndex + 1;
|
|
5988
|
+
while (i < content.length) {
|
|
5989
|
+
const char = content[i];
|
|
5990
|
+
if (char === "\\") {
|
|
5991
|
+
i += 2;
|
|
5992
|
+
continue;
|
|
5993
|
+
}
|
|
5994
|
+
if (char === quote) return { endIndex: i };
|
|
5995
|
+
if (char === "\n") return null;
|
|
5996
|
+
i++;
|
|
5997
|
+
}
|
|
5998
|
+
return null;
|
|
5999
|
+
};
|
|
6000
|
+
const consumeTemplateLiteral = (content, startIndex) => {
|
|
6001
|
+
const openIndex = content.indexOf("`", startIndex);
|
|
6002
|
+
if (openIndex === -1) return null;
|
|
6003
|
+
let i = openIndex + 1;
|
|
6004
|
+
while (i < content.length) {
|
|
6005
|
+
const char = content[i];
|
|
6006
|
+
if (char === "\\") {
|
|
6007
|
+
i += 2;
|
|
6008
|
+
continue;
|
|
6009
|
+
}
|
|
6010
|
+
if (char === "`") return {
|
|
6011
|
+
body: content.slice(openIndex + 1, i),
|
|
6012
|
+
endIndex: i
|
|
6013
|
+
};
|
|
6014
|
+
i++;
|
|
6015
|
+
}
|
|
6016
|
+
return null;
|
|
6017
|
+
};
|
|
6018
|
+
const assignmentTailIsClosed = (content, endIndex) => /^\s*(?:;[^\n]*)?(?:\n|$)/.test(content.slice(endIndex + 1));
|
|
6019
|
+
const assignmentRhsStart = (content, matchIndex) => {
|
|
6020
|
+
const match = /^\.innerHTML\s*=\s*/.exec(content.slice(matchIndex));
|
|
6021
|
+
return match ? matchIndex + match[0].length : null;
|
|
6022
|
+
};
|
|
6023
|
+
const templateExpressions = (templateBody) => [...templateBody.matchAll(/\$\{\s*([^}]+?)\s*\}/g)].map((match) => match[1].trim());
|
|
6024
|
+
const staticTernaryRe = /^\s*[^?]+\?\s*(?:"[^"]*"|'[^']*'|`[^`$]*`)\s*:\s*(?:"[^"]*"|'[^']*'|`[^`$]*`)\s*$/;
|
|
6025
|
+
const splitTopLevelTernary = (expr) => {
|
|
6026
|
+
let quote = null;
|
|
6027
|
+
let depth = 0;
|
|
6028
|
+
let question = -1;
|
|
6029
|
+
let colon = -1;
|
|
6030
|
+
for (let i = 0; i < expr.length; i++) {
|
|
6031
|
+
const char = expr[i];
|
|
6032
|
+
if (char === "\\") {
|
|
6033
|
+
i++;
|
|
6034
|
+
continue;
|
|
6035
|
+
}
|
|
6036
|
+
if ((char === "'" || char === "\"" || char === "`") && quote === null) {
|
|
6037
|
+
quote = char;
|
|
6038
|
+
continue;
|
|
6039
|
+
}
|
|
6040
|
+
if (char === quote) {
|
|
6041
|
+
quote = null;
|
|
6042
|
+
continue;
|
|
6043
|
+
}
|
|
6044
|
+
if (quote) continue;
|
|
6045
|
+
if (char === "(" || char === "[" || char === "{") depth++;
|
|
6046
|
+
else if (char === ")" || char === "]" || char === "}") depth = Math.max(0, depth - 1);
|
|
6047
|
+
else if (char === "?" && depth === 0 && question === -1) question = i;
|
|
6048
|
+
else if (char === ":" && depth === 0 && question !== -1) {
|
|
6049
|
+
colon = i;
|
|
6050
|
+
break;
|
|
6051
|
+
}
|
|
6052
|
+
}
|
|
6053
|
+
if (question === -1 || colon === -1) return null;
|
|
6054
|
+
return {
|
|
6055
|
+
whenTrue: expr.slice(question + 1, colon).trim(),
|
|
6056
|
+
whenFalse: expr.slice(colon + 1).trim()
|
|
6057
|
+
};
|
|
6058
|
+
};
|
|
6059
|
+
const isNumericishExpression = (expr) => {
|
|
6060
|
+
const normalized = expr.trim();
|
|
6061
|
+
if (/^(?:Math\.\w+|Number|parseInt|parseFloat)\s*\(/.test(normalized)) return true;
|
|
6062
|
+
if (!NUMERICISH_EXPR_RE.test(normalized)) return false;
|
|
6063
|
+
return /\d/.test(normalized) || NUMERICISH_NAME_RE.test(normalized);
|
|
6064
|
+
};
|
|
6065
|
+
const isSafeTemplateLiteralExpression = (expr, safeNames) => {
|
|
6066
|
+
if (!expr.startsWith("`") || !expr.endsWith("`")) return false;
|
|
6067
|
+
return templateExpressions(expr.slice(1, -1)).every((part) => isSafeHtmlExpression(part, safeNames));
|
|
6068
|
+
};
|
|
6069
|
+
const collectSafeHtmlNames = (content, matchIndex) => {
|
|
6070
|
+
const safeNames = /* @__PURE__ */ new Set();
|
|
6071
|
+
const prefix = content.slice(Math.max(0, matchIndex - 8e3), matchIndex);
|
|
6072
|
+
for (const rawLine of prefix.split("\n")) {
|
|
6073
|
+
const line = rawLine.trim();
|
|
6074
|
+
let match = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(.+?)\s*;?$/.exec(line);
|
|
6075
|
+
if (match) {
|
|
6076
|
+
const [, name, expr] = match;
|
|
6077
|
+
if (isSafeHtmlExpression(expr.trim(), safeNames)) safeNames.add(name);
|
|
6078
|
+
else safeNames.delete(name);
|
|
6079
|
+
continue;
|
|
6080
|
+
}
|
|
6081
|
+
match = /^([A-Za-z_$][\w$]*)\s*\+=\s*(.+?)\s*;?$/.exec(line);
|
|
6082
|
+
if (match) {
|
|
6083
|
+
const [, name, expr] = match;
|
|
6084
|
+
if (safeNames.has(name) && isSafeHtmlExpression(expr.trim(), safeNames)) safeNames.add(name);
|
|
6085
|
+
else safeNames.delete(name);
|
|
6086
|
+
continue;
|
|
6087
|
+
}
|
|
6088
|
+
match = /^([A-Za-z_$][\w$]*)\s*=\s*(.+?)\s*;?$/.exec(line);
|
|
6089
|
+
if (match) {
|
|
6090
|
+
const [, name, expr] = match;
|
|
6091
|
+
if (isSafeHtmlExpression(expr.trim(), safeNames)) safeNames.add(name);
|
|
6092
|
+
else safeNames.delete(name);
|
|
6093
|
+
}
|
|
6094
|
+
}
|
|
6095
|
+
return safeNames;
|
|
6096
|
+
};
|
|
6097
|
+
const isSafeHtmlExpression = (expr, safeNames) => {
|
|
6098
|
+
const normalized = expr.trim();
|
|
6099
|
+
if (SANITIZER_EXPR_RE.test(normalized)) return true;
|
|
6100
|
+
if (STATIC_STRING_RE.test(normalized)) return true;
|
|
6101
|
+
if (staticTernaryRe.test(expr)) return true;
|
|
6102
|
+
if (isNumericishExpression(normalized)) return true;
|
|
6103
|
+
if (IDENT_RE.test(normalized) && safeNames.has(normalized)) return true;
|
|
6104
|
+
if (isSafeTemplateLiteralExpression(normalized, safeNames)) return true;
|
|
6105
|
+
const ternary = splitTopLevelTernary(normalized);
|
|
6106
|
+
if (ternary && isSafeHtmlExpression(ternary.whenTrue, safeNames) && isSafeHtmlExpression(ternary.whenFalse, safeNames)) return true;
|
|
6107
|
+
const formatCall = SAFE_FORMAT_CALL_RE.exec(normalized);
|
|
6108
|
+
if (formatCall) return formatCall[1].split(",").map((arg) => arg.trim()).filter((arg) => arg.length > 0).every((arg) => isNumericishExpression(arg) || IDENT_RE.test(arg) && safeNames.has(arg));
|
|
6109
|
+
return false;
|
|
6110
|
+
};
|
|
6111
|
+
const readSingleLineRhs = (content, rhsStart) => {
|
|
6112
|
+
const lineEnd = content.indexOf("\n", rhsStart);
|
|
6113
|
+
const line = content.slice(rhsStart, lineEnd === -1 ? content.length : lineEnd);
|
|
6114
|
+
let quote = null;
|
|
6115
|
+
for (let i = 0; i < line.length; i++) {
|
|
6116
|
+
const char = line[i];
|
|
6117
|
+
if (char === "\\") {
|
|
6118
|
+
i++;
|
|
6119
|
+
continue;
|
|
6120
|
+
}
|
|
6121
|
+
if ((char === "'" || char === "\"" || char === "`") && quote === null) {
|
|
6122
|
+
quote = char;
|
|
6123
|
+
continue;
|
|
6124
|
+
}
|
|
6125
|
+
if (char === quote) {
|
|
6126
|
+
quote = null;
|
|
6127
|
+
continue;
|
|
6128
|
+
}
|
|
6129
|
+
if (char === ";" && quote === null) return line.slice(0, i).trim();
|
|
6130
|
+
}
|
|
6131
|
+
return line.trim();
|
|
6132
|
+
};
|
|
6133
|
+
const isSafeMapJoinHtmlAssignment = (content, rhsStart) => {
|
|
6134
|
+
const head = content.slice(rhsStart);
|
|
6135
|
+
const mapMatch = /^[A-Za-z_$][\w$.]*\.map\(\s*[A-Za-z_$][\w$]*\s*=>\s*`/.exec(head);
|
|
6136
|
+
if (!mapMatch) return false;
|
|
6137
|
+
const template = consumeTemplateLiteral(content, rhsStart + mapMatch[0].length - 1);
|
|
6138
|
+
if (!template) return false;
|
|
6139
|
+
if (!/^\s*\)\.join\(\s*(?:""|'')\s*\)/.test(content.slice(template.endIndex + 1))) return false;
|
|
6140
|
+
const safeNames = collectSafeHtmlNames(content, rhsStart);
|
|
6141
|
+
return templateExpressions(template.body).every((expr) => isSafeHtmlExpression(expr, safeNames));
|
|
6142
|
+
};
|
|
6143
|
+
const isSafeInnerHtmlAssignment = (content, matchIndex) => {
|
|
6144
|
+
const tail = content.slice(matchIndex);
|
|
6145
|
+
if (SAFE_EMPTY_INNER_HTML_RE.test(tail) || SAFE_SANITIZED_INNER_HTML_RE.test(tail)) return true;
|
|
6146
|
+
const rhsStart = assignmentRhsStart(content, matchIndex);
|
|
6147
|
+
if (rhsStart === null) return false;
|
|
6148
|
+
const first = content[rhsStart];
|
|
6149
|
+
const safeNames = collectSafeHtmlNames(content, matchIndex);
|
|
6150
|
+
if (isSafeHtmlExpression(readSingleLineRhs(content, rhsStart), safeNames)) return true;
|
|
6151
|
+
if (isSafeMapJoinHtmlAssignment(content, rhsStart)) return true;
|
|
6152
|
+
if (first === "'" || first === "\"") {
|
|
6153
|
+
const quoted = consumeQuotedLiteral(content, rhsStart, first);
|
|
6154
|
+
return Boolean(quoted && assignmentTailIsClosed(content, quoted.endIndex));
|
|
6155
|
+
}
|
|
6156
|
+
if (first !== "`") return false;
|
|
6157
|
+
const template = consumeTemplateLiteral(content, rhsStart);
|
|
6158
|
+
if (!template || !assignmentTailIsClosed(content, template.endIndex)) return false;
|
|
6159
|
+
const expressions = templateExpressions(template.body);
|
|
6160
|
+
if (expressions.length === 0) return true;
|
|
6161
|
+
return expressions.every((expr) => isSafeHtmlExpression(expr, safeNames));
|
|
6162
|
+
};
|
|
6163
|
+
|
|
5868
6164
|
//#endregion
|
|
5869
6165
|
//#region src/engines/security/risky.ts
|
|
5870
6166
|
const ev = "eval";
|
|
@@ -5990,6 +6286,30 @@ const isStructuredDataScript = (content, matchIndex) => {
|
|
|
5990
6286
|
const after = content.slice(matchIndex, Math.min(content.length, matchIndex + 180));
|
|
5991
6287
|
return /__html\s*:\s*JSON\.stringify\s*\(/.test(after);
|
|
5992
6288
|
};
|
|
6289
|
+
const isSafeShellSpawnArray = (content, matchIndex) => /^spawn\s*\(\s*\[/.test(content.slice(matchIndex)) && !/^\s*spawn\s*\(\s*\[\s*["'](?:sh|bash|zsh|cmd|cmd\.exe|powershell|pwsh)["']\s*,\s*["'](?:-c|\/c|\/C)["']/i.test(content.slice(matchIndex)) && !/shell\s*:\s*true\b/.test(content.slice(matchIndex, matchIndex + 500));
|
|
6290
|
+
const PLACEHOLDER_EXPR_RE = /^(?:placeholders?|placeholderList|bindMarkers?|bindingMarkers?|bindPlaceholders?|bindingPlaceholders?|parameterPlaceholders?|sqlPlaceholders?)(?:\.\w+\([^)]*\))?$/i;
|
|
6291
|
+
const SQL_PLACEHOLDER_LITERAL_RE = /["'](?:\?|\$\d+|\$\{[^}]+\})["']/;
|
|
6292
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6293
|
+
const isGeneratedPlaceholderList = (content, matchIndex, placeholderExpr) => {
|
|
6294
|
+
const name = placeholderExpr.match(/^([A-Za-z_$][\w$]*)/)?.[1];
|
|
6295
|
+
if (!name) return false;
|
|
6296
|
+
const prefix = content.slice(Math.max(0, matchIndex - 4e3), matchIndex);
|
|
6297
|
+
const declarationRe = new RegExp(`\\b(?:const|let|var)\\s+${escapeRegExp(name)}\\s*=\\s*([^;\\n]+)`, "g");
|
|
6298
|
+
const declaration = [...prefix.matchAll(declarationRe)].at(-1);
|
|
6299
|
+
if (!declaration) return false;
|
|
6300
|
+
const expr = declaration[1];
|
|
6301
|
+
if (!/\.join\s*\(/.test(expr)) return false;
|
|
6302
|
+
return /\.map\s*\(/.test(expr) && /=>/.test(expr) && SQL_PLACEHOLDER_LITERAL_RE.test(expr) || /\.fill\s*\(/.test(expr) && SQL_PLACEHOLDER_LITERAL_RE.test(expr);
|
|
6303
|
+
};
|
|
6304
|
+
const isSafeSqlPlaceholderTemplate = (content, matchIndex) => {
|
|
6305
|
+
const template = consumeTemplateLiteral(content, matchIndex);
|
|
6306
|
+
if (!template) return false;
|
|
6307
|
+
const afterTemplate = content.slice(template.endIndex + 1);
|
|
6308
|
+
if (!(/^\s*,/.test(afterTemplate) || /^\s*\)\s*\.(?:all|get|run|values)\s*\(/.test(afterTemplate))) return false;
|
|
6309
|
+
const expressions = [...template.body.matchAll(/\$\{\s*([^}]+?)\s*\}/g)].map((match) => match[1].trim());
|
|
6310
|
+
if (expressions.length === 0) return false;
|
|
6311
|
+
return expressions.every((expr) => PLACEHOLDER_EXPR_RE.test(expr) && isGeneratedPlaceholderList(content, matchIndex, expr));
|
|
6312
|
+
};
|
|
5993
6313
|
const detectRiskyConstructs = async (context) => {
|
|
5994
6314
|
const files = getSourceFiles(context);
|
|
5995
6315
|
const diagnostics = [];
|
|
@@ -6010,13 +6330,15 @@ const detectRiskyConstructs = async (context) => {
|
|
|
6010
6330
|
if (!extensions.includes(ext)) continue;
|
|
6011
6331
|
if (isMigrationOrSeeder && name === "sql-injection") continue;
|
|
6012
6332
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
6013
|
-
|
|
6014
|
-
while ((match = regex.exec(masked)) !== null) {
|
|
6333
|
+
for (const match of masked.matchAll(regex)) {
|
|
6015
6334
|
const line = content.slice(0, match.index).split("\n").length;
|
|
6016
6335
|
if (name === "innerhtml") {
|
|
6017
6336
|
const beforeMatch = content.slice(Math.max(0, match.index - 200), match.index);
|
|
6337
|
+
if (isSafeInnerHtmlAssignment(content, match.index)) continue;
|
|
6018
6338
|
if (/(?:template|tmpl|tpl)$/i.test(beforeMatch.trimEnd()) || /createElement\s*\(\s*['"]template['"]\s*\)$/.test(beforeMatch.trimEnd())) continue;
|
|
6019
6339
|
}
|
|
6340
|
+
if (name === "sql-injection" && isSafeSqlPlaceholderTemplate(content, match.index)) continue;
|
|
6341
|
+
if (name === "shell-injection" && isSafeShellSpawnArray(content, match.index)) continue;
|
|
6020
6342
|
if (name === "dangerously-set-innerhtml") {
|
|
6021
6343
|
if (hasDangerouslySetInnerHtmlIgnore(lines, line - 1)) continue;
|
|
6022
6344
|
if (isStructuredDataScript(content, match.index)) continue;
|
|
@@ -6113,7 +6435,28 @@ const PLACEHOLDER_EXACT = new Set([
|
|
|
6113
6435
|
"todo",
|
|
6114
6436
|
"replace_me"
|
|
6115
6437
|
]);
|
|
6438
|
+
const PLACEHOLDER_URL_PARTS = new Set([
|
|
6439
|
+
"example",
|
|
6440
|
+
"host",
|
|
6441
|
+
"localhost",
|
|
6442
|
+
"pass",
|
|
6443
|
+
"password",
|
|
6444
|
+
"pw",
|
|
6445
|
+
"user",
|
|
6446
|
+
"username"
|
|
6447
|
+
]);
|
|
6448
|
+
const isPlaceholderCredentialUrl = (matchedText) => {
|
|
6449
|
+
const credentialMatch = matchedText.match(/^[a-z]+:\/\/([^:@/\s]+):([^@/\s]+)@/i);
|
|
6450
|
+
if (credentialMatch) return PLACEHOLDER_URL_PARTS.has(credentialMatch[1].toLowerCase()) && PLACEHOLDER_URL_PARTS.has(credentialMatch[2].toLowerCase());
|
|
6451
|
+
try {
|
|
6452
|
+
const parsed = new URL(matchedText);
|
|
6453
|
+
return PLACEHOLDER_URL_PARTS.has(parsed.username.toLowerCase()) && PLACEHOLDER_URL_PARTS.has(parsed.password.toLowerCase()) && PLACEHOLDER_URL_PARTS.has(parsed.hostname.toLowerCase());
|
|
6454
|
+
} catch {
|
|
6455
|
+
return false;
|
|
6456
|
+
}
|
|
6457
|
+
};
|
|
6116
6458
|
const isPlaceholderValue = (matchedText) => {
|
|
6459
|
+
if (isPlaceholderCredentialUrl(matchedText)) return true;
|
|
6117
6460
|
if (/env\(/i.test(matchedText)) return true;
|
|
6118
6461
|
if (matchedText.includes("process.env")) return true;
|
|
6119
6462
|
if (matchedText.includes("os.environ")) return true;
|
|
@@ -6143,8 +6486,7 @@ const scanSecrets = async (context) => {
|
|
|
6143
6486
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
6144
6487
|
for (const { pattern, name, keywordPrefixed } of SECRET_PATTERNS) {
|
|
6145
6488
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
6146
|
-
|
|
6147
|
-
while ((match = regex.exec(content)) !== null) {
|
|
6489
|
+
for (const match of content.matchAll(regex)) {
|
|
6148
6490
|
if (isPlaceholderValue(match[1] ?? match[0])) continue;
|
|
6149
6491
|
if (keywordPrefixed && isInsideStringLiteral(content, match.index)) continue;
|
|
6150
6492
|
const line = content.slice(0, match.index).split("\n").length;
|
|
@@ -6235,23 +6577,36 @@ const STYLE_RULES = new Set([
|
|
|
6235
6577
|
"complexity/function-too-long"
|
|
6236
6578
|
]);
|
|
6237
6579
|
const STYLE_WEIGHT = .5;
|
|
6580
|
+
const COMMENT_STYLE_RULE_CAP = 12;
|
|
6581
|
+
const COMMENT_STYLE_RULES = new Set(["ai-slop/trivial-comment", "ai-slop/narrative-comment"]);
|
|
6238
6582
|
const getEffectiveFileCount = (diagnostics, sourceFileCount) => {
|
|
6239
6583
|
if (typeof sourceFileCount === "number" && sourceFileCount > 0) return sourceFileCount;
|
|
6240
6584
|
const filesWithDiagnostics = new Set(diagnostics.map((d) => d.filePath)).size;
|
|
6241
6585
|
return Math.max(1, filesWithDiagnostics);
|
|
6242
6586
|
};
|
|
6243
|
-
const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoothing) => {
|
|
6587
|
+
const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoothing, maxPerRule) => {
|
|
6244
6588
|
if (diagnostics.length === 0) return {
|
|
6245
6589
|
score: PERFECT_SCORE,
|
|
6246
6590
|
label: "Healthy"
|
|
6247
6591
|
};
|
|
6248
|
-
|
|
6592
|
+
const deductionsByRule = /* @__PURE__ */ new Map();
|
|
6249
6593
|
for (const d of diagnostics) {
|
|
6250
6594
|
const engineWeight = weights[d.engine] ?? 1;
|
|
6251
6595
|
const severityPenalty = d.severity === "error" ? 3 : d.severity === "warning" ? 1 : .25;
|
|
6252
6596
|
const styleFactor = STYLE_RULES.has(d.rule) ? STYLE_WEIGHT : 1;
|
|
6253
|
-
|
|
6254
|
-
|
|
6597
|
+
const key = `${d.engine}:${d.rule}`;
|
|
6598
|
+
deductionsByRule.set(key, (deductionsByRule.get(key) ?? 0) + severityPenalty * engineWeight * styleFactor);
|
|
6599
|
+
}
|
|
6600
|
+
const defaultRuleCap = typeof maxPerRule === "number" && maxPerRule > 0 ? maxPerRule : null;
|
|
6601
|
+
const capForRule = (key) => {
|
|
6602
|
+
const rule = key.slice(key.indexOf(":") + 1);
|
|
6603
|
+
if (COMMENT_STYLE_RULES.has(rule)) return defaultRuleCap ? Math.min(defaultRuleCap, COMMENT_STYLE_RULE_CAP) : COMMENT_STYLE_RULE_CAP;
|
|
6604
|
+
return defaultRuleCap;
|
|
6605
|
+
};
|
|
6606
|
+
const deductions = [...deductionsByRule.entries()].reduce((total, [key, value]) => {
|
|
6607
|
+
const cap = capForRule(key);
|
|
6608
|
+
return total + (cap ? Math.min(value, cap) : value);
|
|
6609
|
+
}, 0);
|
|
6255
6610
|
const effectiveFileCount = getEffectiveFileCount(diagnostics, sourceFileCount);
|
|
6256
6611
|
const smoothingConstant = typeof smoothing === "number" ? smoothing : 10;
|
|
6257
6612
|
const issueDensity = Math.min(1, diagnostics.length / (effectiveFileCount + smoothingConstant));
|
|
@@ -6265,6 +6620,64 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
|
|
|
6265
6620
|
|
|
6266
6621
|
//#endregion
|
|
6267
6622
|
//#region src/utils/discover.ts
|
|
6623
|
+
const UNSUPPORTED_CODE_EXTENSIONS = {
|
|
6624
|
+
".c": "C/C++",
|
|
6625
|
+
".h": "C/C++",
|
|
6626
|
+
".cc": "C/C++",
|
|
6627
|
+
".cpp": "C/C++",
|
|
6628
|
+
".cxx": "C/C++",
|
|
6629
|
+
".hpp": "C/C++",
|
|
6630
|
+
".hh": "C/C++",
|
|
6631
|
+
".hxx": "C/C++",
|
|
6632
|
+
".cs": "C#",
|
|
6633
|
+
".swift": "Swift",
|
|
6634
|
+
".kt": "Kotlin",
|
|
6635
|
+
".kts": "Kotlin",
|
|
6636
|
+
".m": "Objective-C",
|
|
6637
|
+
".mm": "Objective-C",
|
|
6638
|
+
".scala": "Scala",
|
|
6639
|
+
".dart": "Dart",
|
|
6640
|
+
".ex": "Elixir",
|
|
6641
|
+
".exs": "Elixir",
|
|
6642
|
+
".erl": "Erlang",
|
|
6643
|
+
".hs": "Haskell",
|
|
6644
|
+
".clj": "Clojure",
|
|
6645
|
+
".cljs": "Clojure",
|
|
6646
|
+
".lua": "Lua",
|
|
6647
|
+
".jl": "Julia",
|
|
6648
|
+
".zig": "Zig",
|
|
6649
|
+
".nim": "Nim",
|
|
6650
|
+
".ml": "OCaml",
|
|
6651
|
+
".fs": "F#",
|
|
6652
|
+
".sol": "Solidity",
|
|
6653
|
+
".groovy": "Groovy"
|
|
6654
|
+
};
|
|
6655
|
+
const analyzeCoverage = (rootDirectory, excludePatterns = []) => {
|
|
6656
|
+
const allFiles = listProjectFiles(rootDirectory);
|
|
6657
|
+
const supportedFiles = filterProjectFiles(rootDirectory, allFiles, [], excludePatterns).length;
|
|
6658
|
+
const counts = /* @__PURE__ */ new Map();
|
|
6659
|
+
let unsupportedFiles = 0;
|
|
6660
|
+
const candidates = filterProjectFiles(rootDirectory, allFiles, Object.keys(UNSUPPORTED_CODE_EXTENSIONS), excludePatterns);
|
|
6661
|
+
for (const file of candidates) {
|
|
6662
|
+
const lang = UNSUPPORTED_CODE_EXTENSIONS[path.extname(file).toLowerCase()];
|
|
6663
|
+
if (!lang) continue;
|
|
6664
|
+
unsupportedFiles += 1;
|
|
6665
|
+
counts.set(lang, (counts.get(lang) ?? 0) + 1);
|
|
6666
|
+
}
|
|
6667
|
+
let dominantUnsupported = null;
|
|
6668
|
+
let max = 0;
|
|
6669
|
+
for (const [lang, count] of counts) if (count > max) {
|
|
6670
|
+
max = count;
|
|
6671
|
+
dominantUnsupported = lang;
|
|
6672
|
+
}
|
|
6673
|
+
const negligible = supportedFiles === 0 || unsupportedFiles >= 10 && unsupportedFiles > supportedFiles * 3;
|
|
6674
|
+
return {
|
|
6675
|
+
supportedFiles,
|
|
6676
|
+
unsupportedFiles,
|
|
6677
|
+
dominantUnsupported,
|
|
6678
|
+
scoreable: !negligible
|
|
6679
|
+
};
|
|
6680
|
+
};
|
|
6268
6681
|
const LANGUAGE_SIGNALS = {
|
|
6269
6682
|
"tsconfig.json": "typescript",
|
|
6270
6683
|
"go.mod": "go",
|
|
@@ -6384,11 +6797,12 @@ const checkInstalledTools = async () => {
|
|
|
6384
6797
|
}));
|
|
6385
6798
|
return results;
|
|
6386
6799
|
};
|
|
6387
|
-
const discoverProject = async (directory) => {
|
|
6800
|
+
const discoverProject = async (directory, excludePatterns = []) => {
|
|
6388
6801
|
const resolvedDir = path.resolve(directory);
|
|
6389
6802
|
const languages = detectLanguages(resolvedDir);
|
|
6390
6803
|
const frameworks = detectFrameworks(resolvedDir);
|
|
6391
6804
|
const sourceFileCount = countSourceFiles(resolvedDir);
|
|
6805
|
+
const coverage = analyzeCoverage(resolvedDir, excludePatterns);
|
|
6392
6806
|
const installedTools = await checkInstalledTools();
|
|
6393
6807
|
return {
|
|
6394
6808
|
rootDirectory: resolvedDir,
|
|
@@ -6396,6 +6810,7 @@ const discoverProject = async (directory) => {
|
|
|
6396
6810
|
languages,
|
|
6397
6811
|
frameworks,
|
|
6398
6812
|
sourceFileCount,
|
|
6813
|
+
coverage,
|
|
6399
6814
|
installedTools
|
|
6400
6815
|
};
|
|
6401
6816
|
};
|
|
@@ -6434,6 +6849,107 @@ const readBaseline = (cwd) => {
|
|
|
6434
6849
|
}
|
|
6435
6850
|
};
|
|
6436
6851
|
|
|
6852
|
+
//#endregion
|
|
6853
|
+
//#region src/output/finding-assessment.ts
|
|
6854
|
+
const FINDING_KIND_LABELS = {
|
|
6855
|
+
"confirmed-defect": "confirmed defects",
|
|
6856
|
+
"conservative-security": "conservative security",
|
|
6857
|
+
"style-policy": "style/policy",
|
|
6858
|
+
"ai-slop-indicator": "AI-slop indicators"
|
|
6859
|
+
};
|
|
6860
|
+
const STYLE_POLICY_RULES = new Set([
|
|
6861
|
+
"ai-slop/trivial-comment",
|
|
6862
|
+
"ai-slop/narrative-comment",
|
|
6863
|
+
"ai-slop/meta-comment",
|
|
6864
|
+
"ai-slop/console-leftover",
|
|
6865
|
+
"ai-slop/ts-directive",
|
|
6866
|
+
"complexity/file-too-large",
|
|
6867
|
+
"complexity/function-too-long",
|
|
6868
|
+
"complexity/deep-nesting",
|
|
6869
|
+
"complexity/too-many-params",
|
|
6870
|
+
"code-quality/duplicate-block",
|
|
6871
|
+
"eslint/no-empty",
|
|
6872
|
+
"eslint/no-unused-vars",
|
|
6873
|
+
"eslint/no-useless-escape",
|
|
6874
|
+
"eslint/no-unused-expressions",
|
|
6875
|
+
"unicorn/no-useless-fallback-in-spread",
|
|
6876
|
+
"unicorn/prefer-string-starts-ends-with",
|
|
6877
|
+
"unicorn/no-new-array",
|
|
6878
|
+
"unicorn/no-useless-spread"
|
|
6879
|
+
]);
|
|
6880
|
+
const CONFIRMED_DEFECT_RULES = new Set([
|
|
6881
|
+
"ai-slop/hallucinated-import",
|
|
6882
|
+
"eslint/no-undef",
|
|
6883
|
+
"eslint/no-unreachable",
|
|
6884
|
+
"security/vulnerable-dependency"
|
|
6885
|
+
]);
|
|
6886
|
+
const LOW_CONFIDENCE_SECURITY_RULES = new Set(["security/innerhtml", "security/dangerously-set-innerhtml"]);
|
|
6887
|
+
const confidenceFor = (diagnostic, kind) => {
|
|
6888
|
+
if (kind === "confirmed-defect") return "high";
|
|
6889
|
+
if (kind === "style-policy") return "medium";
|
|
6890
|
+
if (kind === "conservative-security") {
|
|
6891
|
+
if (LOW_CONFIDENCE_SECURITY_RULES.has(diagnostic.rule)) return "medium";
|
|
6892
|
+
return diagnostic.severity === "error" ? "high" : "medium";
|
|
6893
|
+
}
|
|
6894
|
+
return diagnostic.severity === "error" ? "high" : "medium";
|
|
6895
|
+
};
|
|
6896
|
+
const classifyKind = (diagnostic) => {
|
|
6897
|
+
if (CONFIRMED_DEFECT_RULES.has(diagnostic.rule)) return "confirmed-defect";
|
|
6898
|
+
if (diagnostic.engine === "security") return "conservative-security";
|
|
6899
|
+
if (STYLE_POLICY_RULES.has(diagnostic.rule)) return "style-policy";
|
|
6900
|
+
if (diagnostic.engine === "format" || diagnostic.engine === "code-quality") return "style-policy";
|
|
6901
|
+
if (diagnostic.engine === "ai-slop") return "ai-slop-indicator";
|
|
6902
|
+
if (diagnostic.severity === "error") return "confirmed-defect";
|
|
6903
|
+
return "style-policy";
|
|
6904
|
+
};
|
|
6905
|
+
const assessDiagnostic = (diagnostic) => {
|
|
6906
|
+
const kind = classifyKind(diagnostic);
|
|
6907
|
+
return {
|
|
6908
|
+
kind,
|
|
6909
|
+
confidence: confidenceFor(diagnostic, kind),
|
|
6910
|
+
label: FINDING_KIND_LABELS[kind]
|
|
6911
|
+
};
|
|
6912
|
+
};
|
|
6913
|
+
const summarizeFindingAssessments = (diagnostics) => {
|
|
6914
|
+
const byKind = {
|
|
6915
|
+
"confirmed-defect": 0,
|
|
6916
|
+
"conservative-security": 0,
|
|
6917
|
+
"style-policy": 0,
|
|
6918
|
+
"ai-slop-indicator": 0
|
|
6919
|
+
};
|
|
6920
|
+
const byConfidence = {
|
|
6921
|
+
high: 0,
|
|
6922
|
+
medium: 0,
|
|
6923
|
+
low: 0
|
|
6924
|
+
};
|
|
6925
|
+
const rows = /* @__PURE__ */ new Map();
|
|
6926
|
+
for (const diagnostic of diagnostics) {
|
|
6927
|
+
const assessment = assessDiagnostic(diagnostic);
|
|
6928
|
+
byKind[assessment.kind]++;
|
|
6929
|
+
byConfidence[assessment.confidence]++;
|
|
6930
|
+
const row = rows.get(assessment.kind) ?? {
|
|
6931
|
+
kind: assessment.kind,
|
|
6932
|
+
label: assessment.label,
|
|
6933
|
+
count: 0,
|
|
6934
|
+
errors: 0,
|
|
6935
|
+
warnings: 0,
|
|
6936
|
+
info: 0,
|
|
6937
|
+
fixable: 0
|
|
6938
|
+
};
|
|
6939
|
+
row.count++;
|
|
6940
|
+
if (diagnostic.severity === "error") row.errors++;
|
|
6941
|
+
else if (diagnostic.severity === "warning") row.warnings++;
|
|
6942
|
+
else row.info++;
|
|
6943
|
+
if (diagnostic.fixable) row.fixable++;
|
|
6944
|
+
rows.set(assessment.kind, row);
|
|
6945
|
+
}
|
|
6946
|
+
return {
|
|
6947
|
+
rows: [...rows.values()].sort((a, b) => b.count - a.count),
|
|
6948
|
+
byKind,
|
|
6949
|
+
byConfidence
|
|
6950
|
+
};
|
|
6951
|
+
};
|
|
6952
|
+
|
|
6437
6953
|
//#endregion
|
|
6438
6954
|
//#region src/mcp/tools.ts
|
|
6439
6955
|
const MAX_FINDINGS = 25;
|
|
@@ -6471,27 +6987,32 @@ const summariseDiagnostic = (d, rootDirectory) => ({
|
|
|
6471
6987
|
column: d.column,
|
|
6472
6988
|
rule: d.rule,
|
|
6473
6989
|
severity: d.severity,
|
|
6990
|
+
assessment: assessDiagnostic(d),
|
|
6474
6991
|
message: d.message,
|
|
6475
6992
|
fixable: d.fixable,
|
|
6476
6993
|
help: d.help || void 0
|
|
6477
6994
|
});
|
|
6478
6995
|
const summariseDiagnostics = (diagnostics, rootDirectory) => {
|
|
6996
|
+
const counts = {
|
|
6997
|
+
error: diagnostics.filter((d) => d.severity === "error").length,
|
|
6998
|
+
warning: diagnostics.filter((d) => d.severity === "warning").length,
|
|
6999
|
+
fixable: diagnostics.filter((d) => d.fixable).length,
|
|
7000
|
+
total: diagnostics.length
|
|
7001
|
+
};
|
|
7002
|
+
const findings = diagnostics.slice(0, MAX_FINDINGS).map((d) => summariseDiagnostic(d, rootDirectory));
|
|
7003
|
+
const elided = diagnostics.length > MAX_FINDINGS ? diagnostics.length - MAX_FINDINGS : 0;
|
|
6479
7004
|
return {
|
|
6480
|
-
counts
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
total: diagnostics.length
|
|
6485
|
-
},
|
|
6486
|
-
findings: diagnostics.slice(0, MAX_FINDINGS).map((d) => summariseDiagnostic(d, rootDirectory)),
|
|
6487
|
-
elided: diagnostics.length > MAX_FINDINGS ? diagnostics.length - MAX_FINDINGS : 0
|
|
7005
|
+
counts,
|
|
7006
|
+
findingAssessment: summarizeFindingAssessments(diagnostics),
|
|
7007
|
+
findings,
|
|
7008
|
+
elided
|
|
6488
7009
|
};
|
|
6489
7010
|
};
|
|
6490
7011
|
const runScan = async (cwd) => {
|
|
6491
7012
|
const project = await discoverProject(cwd);
|
|
6492
7013
|
const config = loadConfig(cwd);
|
|
6493
7014
|
const diagnostics = (await runEngines(buildEngineContext(project.rootDirectory, project, config), enabledEnginesFromConfig(config))).flatMap((r) => r.diagnostics);
|
|
6494
|
-
const { score } = calculateScore(diagnostics, config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing);
|
|
7015
|
+
const { score } = calculateScore(diagnostics, config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing, config.scoring.maxPerRule);
|
|
6495
7016
|
const errorCount = diagnostics.filter((d) => d.severity === "error").length;
|
|
6496
7017
|
const failBelow = config.ci.failBelow;
|
|
6497
7018
|
return {
|
|
@@ -6617,7 +7138,7 @@ const handleAislopBaseline = (input) => {
|
|
|
6617
7138
|
|
|
6618
7139
|
//#endregion
|
|
6619
7140
|
//#region src/version.ts
|
|
6620
|
-
const APP_VERSION = "0.
|
|
7141
|
+
const APP_VERSION = "0.11.0";
|
|
6621
7142
|
|
|
6622
7143
|
//#endregion
|
|
6623
7144
|
//#region src/telemetry/env.ts
|
|
@@ -6813,9 +7334,14 @@ const track = (input) => {
|
|
|
6813
7334
|
pendingRequests.add(request);
|
|
6814
7335
|
return { installCreated };
|
|
6815
7336
|
};
|
|
6816
|
-
const flushTelemetry = async () => {
|
|
7337
|
+
const flushTelemetry = async (timeoutMs) => {
|
|
6817
7338
|
if (pendingRequests.size === 0) return;
|
|
6818
|
-
|
|
7339
|
+
const all = Promise.all(pendingRequests);
|
|
7340
|
+
if (timeoutMs == null) {
|
|
7341
|
+
await all;
|
|
7342
|
+
return;
|
|
7343
|
+
}
|
|
7344
|
+
await Promise.race([all, new Promise((resolve) => setTimeout(resolve, timeoutMs))]);
|
|
6819
7345
|
};
|
|
6820
7346
|
|
|
6821
7347
|
//#endregion
|