aislop 0.9.6 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +523 -293
- package/dist/index.js +440 -291
- package/dist/{json-CxiErSgX.js → json-Bqkcl1DF.js} +1 -1
- package/dist/mcp.js +438 -289
- package/dist/{sarif-CLVijBAO.js → sarif-C-vh4wcC.js} +1 -1
- package/dist/version-rlhQD8Qh.js +5 -0
- package/package.json +1 -1
- package/dist/version-CPpO6jbj.js +0 -5
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as getEngineLabel, t as ENGINE_INFO } from "./engine-info-DCvIfZ0f.js";
|
|
2
2
|
import { n as runSubprocess, t as isToolInstalled } from "./subprocess-CQUJDGgn.js";
|
|
3
|
-
import { t as APP_VERSION } from "./version-
|
|
3
|
+
import { t as APP_VERSION } from "./version-rlhQD8Qh.js";
|
|
4
4
|
import { r as runGenericLinter, t as fixRubyLint } from "./generic-D_T4cUaC.js";
|
|
5
5
|
import { n as runExpoDoctor } from "./expo-doctor-BcIkOte5.js";
|
|
6
6
|
import { createRequire, isBuiltin } from "node:module";
|
|
@@ -1254,7 +1254,7 @@ const doctorCommand = async (directory, options = {}) => {
|
|
|
1254
1254
|
};
|
|
1255
1255
|
|
|
1256
1256
|
//#endregion
|
|
1257
|
-
//#region src/
|
|
1257
|
+
//#region src/utils/source-masker.ts
|
|
1258
1258
|
const JS_EXTS$2 = new Set([
|
|
1259
1259
|
".ts",
|
|
1260
1260
|
".tsx",
|
|
@@ -1263,14 +1263,226 @@ const JS_EXTS$2 = new Set([
|
|
|
1263
1263
|
".mjs",
|
|
1264
1264
|
".cjs"
|
|
1265
1265
|
]);
|
|
1266
|
+
const PY_EXTS = new Set([".py"]);
|
|
1267
|
+
const RB_EXTS = new Set([".rb"]);
|
|
1268
|
+
const PHP_EXTS = new Set([".php"]);
|
|
1269
|
+
const familyForExt = (ext) => {
|
|
1270
|
+
if (JS_EXTS$2.has(ext)) return "js";
|
|
1271
|
+
if (PY_EXTS.has(ext)) return "py";
|
|
1272
|
+
if (RB_EXTS.has(ext)) return "rb";
|
|
1273
|
+
if (PHP_EXTS.has(ext)) return "php";
|
|
1274
|
+
return "none";
|
|
1275
|
+
};
|
|
1276
|
+
const maskStringsAndComments = (content, ext) => {
|
|
1277
|
+
const family = familyForExt(ext);
|
|
1278
|
+
if (family === "none") return content;
|
|
1279
|
+
if (family === "js") return maskJs(content, true);
|
|
1280
|
+
return maskSimple(content, family, true);
|
|
1281
|
+
};
|
|
1282
|
+
const maskComments = (content, ext) => {
|
|
1283
|
+
const family = familyForExt(ext);
|
|
1284
|
+
if (family === "none") return content;
|
|
1285
|
+
if (family === "js") return maskJs(content, false);
|
|
1286
|
+
return maskSimple(content, family, false);
|
|
1287
|
+
};
|
|
1288
|
+
const handleQuotesAndComments = (content, i, tplStack, mask, maskStrings) => {
|
|
1289
|
+
const len = content.length;
|
|
1290
|
+
const c = content[i];
|
|
1291
|
+
const next = content[i + 1];
|
|
1292
|
+
if (c === "\"" || c === "'") {
|
|
1293
|
+
const strStart = i;
|
|
1294
|
+
const end = consumeQuotedString(content, i, c);
|
|
1295
|
+
if (maskStrings) mask(strStart + 1, end - 1);
|
|
1296
|
+
return {
|
|
1297
|
+
handled: true,
|
|
1298
|
+
nextI: end
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
if (c === "`") {
|
|
1302
|
+
const scan = consumeTemplateString(content, i + 1);
|
|
1303
|
+
if (maskStrings) mask(i + 1, scan.maskEnd);
|
|
1304
|
+
if (scan.openedInterp) tplStack.push(0);
|
|
1305
|
+
return {
|
|
1306
|
+
handled: true,
|
|
1307
|
+
nextI: scan.resumeAt
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
if (c === "/" && next === "/") {
|
|
1311
|
+
const strStart = i;
|
|
1312
|
+
let k = i;
|
|
1313
|
+
while (k < len && content[k] !== "\n") k++;
|
|
1314
|
+
mask(strStart, k);
|
|
1315
|
+
return {
|
|
1316
|
+
handled: true,
|
|
1317
|
+
nextI: k
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
if (c === "/" && next === "*") {
|
|
1321
|
+
const strStart = i;
|
|
1322
|
+
let k = i + 2;
|
|
1323
|
+
while (k < len - 1 && !(content[k] === "*" && content[k + 1] === "/")) k++;
|
|
1324
|
+
if (k < len - 1) k += 2;
|
|
1325
|
+
mask(strStart, k);
|
|
1326
|
+
return {
|
|
1327
|
+
handled: true,
|
|
1328
|
+
nextI: k
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
return {
|
|
1332
|
+
handled: false,
|
|
1333
|
+
nextI: i
|
|
1334
|
+
};
|
|
1335
|
+
};
|
|
1336
|
+
const maskJs = (content, maskStrings) => {
|
|
1337
|
+
const out = content.split("");
|
|
1338
|
+
const len = content.length;
|
|
1339
|
+
const tplStack = [];
|
|
1340
|
+
let i = 0;
|
|
1341
|
+
const mask = (start, end) => {
|
|
1342
|
+
for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
|
|
1343
|
+
};
|
|
1344
|
+
while (i < len) {
|
|
1345
|
+
const c = content[i];
|
|
1346
|
+
if (tplStack.length > 0) {
|
|
1347
|
+
if (c === "{") {
|
|
1348
|
+
tplStack[tplStack.length - 1]++;
|
|
1349
|
+
i++;
|
|
1350
|
+
continue;
|
|
1351
|
+
}
|
|
1352
|
+
if (c === "}") {
|
|
1353
|
+
if (tplStack[tplStack.length - 1] === 0) {
|
|
1354
|
+
tplStack.pop();
|
|
1355
|
+
const scan = consumeTemplateString(content, i + 1);
|
|
1356
|
+
if (maskStrings) mask(i + 1, scan.maskEnd);
|
|
1357
|
+
if (scan.openedInterp) tplStack.push(0);
|
|
1358
|
+
i = scan.resumeAt;
|
|
1359
|
+
continue;
|
|
1360
|
+
}
|
|
1361
|
+
tplStack[tplStack.length - 1]--;
|
|
1362
|
+
i++;
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
const handled = handleQuotesAndComments(content, i, tplStack, mask, maskStrings);
|
|
1367
|
+
if (handled.handled) {
|
|
1368
|
+
i = handled.nextI;
|
|
1369
|
+
continue;
|
|
1370
|
+
}
|
|
1371
|
+
i++;
|
|
1372
|
+
}
|
|
1373
|
+
return out.join("");
|
|
1374
|
+
};
|
|
1375
|
+
const consumeQuotedString = (content, start, quote) => {
|
|
1376
|
+
const len = content.length;
|
|
1377
|
+
let i = start + 1;
|
|
1378
|
+
while (i < len) {
|
|
1379
|
+
const c = content[i];
|
|
1380
|
+
if (c === "\\" && i + 1 < len) {
|
|
1381
|
+
i += 2;
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1384
|
+
if (c === quote) return i + 1;
|
|
1385
|
+
if (c === "\n") return i;
|
|
1386
|
+
i++;
|
|
1387
|
+
}
|
|
1388
|
+
return i;
|
|
1389
|
+
};
|
|
1390
|
+
const consumeTemplateString = (content, start) => {
|
|
1391
|
+
const len = content.length;
|
|
1392
|
+
let i = start;
|
|
1393
|
+
while (i < len) {
|
|
1394
|
+
const c = content[i];
|
|
1395
|
+
if (c === "\\" && i + 1 < len) {
|
|
1396
|
+
i += 2;
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
if (c === "`") return {
|
|
1400
|
+
maskEnd: i,
|
|
1401
|
+
resumeAt: i + 1,
|
|
1402
|
+
openedInterp: false
|
|
1403
|
+
};
|
|
1404
|
+
if (c === "$" && content[i + 1] === "{") return {
|
|
1405
|
+
maskEnd: i,
|
|
1406
|
+
resumeAt: i + 2,
|
|
1407
|
+
openedInterp: true
|
|
1408
|
+
};
|
|
1409
|
+
i++;
|
|
1410
|
+
}
|
|
1411
|
+
return {
|
|
1412
|
+
maskEnd: i,
|
|
1413
|
+
resumeAt: i,
|
|
1414
|
+
openedInterp: false
|
|
1415
|
+
};
|
|
1416
|
+
};
|
|
1417
|
+
const maskSimple = (content, family, maskStrings) => {
|
|
1418
|
+
const out = content.split("");
|
|
1419
|
+
const len = content.length;
|
|
1420
|
+
let i = 0;
|
|
1421
|
+
const mask = (start, end) => {
|
|
1422
|
+
for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
|
|
1423
|
+
};
|
|
1424
|
+
while (i < len) {
|
|
1425
|
+
const c = content[i];
|
|
1426
|
+
const next = content[i + 1];
|
|
1427
|
+
if (family === "py" && (c === "\"" || c === "'")) {
|
|
1428
|
+
if (content[i + 1] === c && content[i + 2] === c) {
|
|
1429
|
+
const triple = c + c + c;
|
|
1430
|
+
const end = content.indexOf(triple, i + 3);
|
|
1431
|
+
const stop = end === -1 ? len : end + 3;
|
|
1432
|
+
if (maskStrings) mask(i + 3, stop - 3);
|
|
1433
|
+
i = stop;
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
if (c === "\"" || c === "'") {
|
|
1438
|
+
const strStart = i;
|
|
1439
|
+
i = consumeQuotedString(content, i, c);
|
|
1440
|
+
if (maskStrings) mask(strStart + 1, i - 1);
|
|
1441
|
+
continue;
|
|
1442
|
+
}
|
|
1443
|
+
if ((family === "py" || family === "rb" || family === "php") && c === "#") {
|
|
1444
|
+
const strStart = i;
|
|
1445
|
+
while (i < len && content[i] !== "\n") i++;
|
|
1446
|
+
mask(strStart, i);
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
if (family === "php" && c === "/" && next === "/") {
|
|
1450
|
+
const strStart = i;
|
|
1451
|
+
while (i < len && content[i] !== "\n") i++;
|
|
1452
|
+
mask(strStart, i);
|
|
1453
|
+
continue;
|
|
1454
|
+
}
|
|
1455
|
+
if (family === "php" && c === "/" && next === "*") {
|
|
1456
|
+
const strStart = i;
|
|
1457
|
+
i += 2;
|
|
1458
|
+
while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
|
|
1459
|
+
if (i < len - 1) i += 2;
|
|
1460
|
+
mask(strStart, i);
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
i++;
|
|
1464
|
+
}
|
|
1465
|
+
return out.join("");
|
|
1466
|
+
};
|
|
1467
|
+
|
|
1468
|
+
//#endregion
|
|
1469
|
+
//#region src/engines/ai-slop/abstractions.ts
|
|
1470
|
+
const JS_EXTS$1 = new Set([
|
|
1471
|
+
".ts",
|
|
1472
|
+
".tsx",
|
|
1473
|
+
".js",
|
|
1474
|
+
".jsx",
|
|
1475
|
+
".mjs",
|
|
1476
|
+
".cjs"
|
|
1477
|
+
]);
|
|
1266
1478
|
const THIN_WRAPPER_PATTERNS = [
|
|
1267
1479
|
{
|
|
1268
1480
|
pattern: /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
|
|
1269
|
-
extensions: JS_EXTS$
|
|
1481
|
+
extensions: JS_EXTS$1
|
|
1270
1482
|
},
|
|
1271
1483
|
{
|
|
1272
1484
|
pattern: /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
|
|
1273
|
-
extensions: JS_EXTS$
|
|
1485
|
+
extensions: JS_EXTS$1
|
|
1274
1486
|
},
|
|
1275
1487
|
{
|
|
1276
1488
|
pattern: /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm,
|
|
@@ -1280,14 +1492,16 @@ const THIN_WRAPPER_PATTERNS = [
|
|
|
1280
1492
|
const AI_NAMING_PATTERNS = [/(?:helper|util|handler|process|do|handle|execute|perform)_?\d+/i, /(?:data|temp|result|value|item|obj|arr|str|num|val)\d+/];
|
|
1281
1493
|
const FRAMEWORK_METHOD_NAMES = /^(?:setUp|tearDown|setUpClass|tearDownClass|setUpModule|tearDownModule)$/;
|
|
1282
1494
|
const DUNDER_PATTERN = /^__\w+__$/;
|
|
1283
|
-
const
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1495
|
+
const stripParam = (p) => p.trim().split(/[:=]/)[0].trim().replace(/^[*&]+/, "");
|
|
1496
|
+
const paramNames = (paramsText) => new Set(paramsText.split(",").map(stripParam).filter((p) => p && p !== "self" && p !== "cls"));
|
|
1497
|
+
const isIdentityForward = (matchText) => {
|
|
1498
|
+
const paramsMatch = matchText.match(/\(([^)]*)\)/);
|
|
1499
|
+
const innerMatch = matchText.match(/(?:return\s+\w+|=>\s*\w+)\s*\(([^)]*)\)/);
|
|
1500
|
+
if (!paramsMatch || !innerMatch) return false;
|
|
1501
|
+
const params = paramNames(paramsMatch[1]);
|
|
1502
|
+
const args = innerMatch[1].split(",").map((a) => a.trim()).filter((a) => a.length > 0);
|
|
1503
|
+
if (args.length === 0) return false;
|
|
1504
|
+
return args.every((a) => /^[A-Za-z_$][\w$]*$/.test(a) && params.has(a));
|
|
1291
1505
|
};
|
|
1292
1506
|
const isUseContextWrapper = (matchText) => /\buse\w+/.test(matchText) && /useContext\s*\(/.test(matchText);
|
|
1293
1507
|
const detectThinWrappers = (content, relativePath, ext) => {
|
|
@@ -1307,7 +1521,7 @@ const detectThinWrappers = (content, relativePath, ext) => {
|
|
|
1307
1521
|
const prevLine = lines[lineNumber - 2]?.trim();
|
|
1308
1522
|
if (prevLine && prevLine.startsWith("@")) continue;
|
|
1309
1523
|
}
|
|
1310
|
-
if (
|
|
1524
|
+
if (!isIdentityForward(matchText)) continue;
|
|
1311
1525
|
if (isUseContextWrapper(matchText)) continue;
|
|
1312
1526
|
diagnostics.push({
|
|
1313
1527
|
filePath: relativePath,
|
|
@@ -1361,8 +1575,9 @@ const detectOverAbstraction = async (context) => {
|
|
|
1361
1575
|
}
|
|
1362
1576
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
1363
1577
|
const ext = path.extname(filePath);
|
|
1364
|
-
|
|
1365
|
-
diagnostics.push(...
|
|
1578
|
+
const codeOnly = maskComments(content, ext);
|
|
1579
|
+
diagnostics.push(...detectThinWrappers(codeOnly, relativePath, ext));
|
|
1580
|
+
diagnostics.push(...detectAiNaming(codeOnly, relativePath));
|
|
1366
1581
|
}
|
|
1367
1582
|
return diagnostics;
|
|
1368
1583
|
};
|
|
@@ -1392,8 +1607,7 @@ const JUSTIFICATION_OPENERS = [
|
|
|
1392
1607
|
/^(?:First|Then|Finally|Next|Lastly|Subsequently),?\s+(?:it|we|the\s+(?:function|method|class))\b/i
|
|
1393
1608
|
];
|
|
1394
1609
|
const EXPLANATORY_OPENERS = /^(Matches|Detects|Represents|Holds|Stores|Tracks|Handles|Manages|Controls|Contains|Captures|Encapsulates|Wraps|Describes)\s+[A-Za-z`'"]/;
|
|
1395
|
-
const
|
|
1396
|
-
const EXPLANATORY_WHY_MARKERS = /\b(?:because|since|otherwise|workaround|caveat|warning|important|assumes?|note:|bug|issue|see\s+(?:issue|above|below)|in\s+prod|in\s+production|breaks?\s+when|fails?\s+when|must\s+run|must\s+be|has\s+to\s+be|hack\s+for|fix\s+for|reason:|to\s+avoid|to\s+ensure|to\s+prevent|in\s+order\s+to|necessary|guarantee[sd]?|prevents?|regardless\s+of|required\s+(?:for|to|by)|for\s+example|e\.g\.|i\.e\.|useful\s+(?:for|when)|intended\s+to|on\s+purpose|by\s+design)\b/i;
|
|
1610
|
+
const EXPLANATORY_WHY_MARKERS = /\b(?:because|since|otherwise|workaround|caveat|warning|important|assumes?|note:|bug|issue|see\s+(?:issue|above|below)|in\s+prod|in\s+production|breaks?\s+when|fails?\s+when|must\s+run|must\s+be|has\s+to\s+be|hack\s+for|fix\s+for|reason:|to\s+avoid|to\s+ensure|to\s+prevent|in\s+order\s+to|necessary|guarantee[sd]?|prevents?|regardless\s+of|required\s+(?:for|to|by)|for\s+example|e\.g\.|i\.e\.|useful\s+(?:for|when)|intended\s+to|on\s+purpose|by\s+design|ideally|however|although|even\s+though|despite|whereas|unfortunately|trade-?off|first\s+need)\b/i;
|
|
1397
1611
|
const MEANINGFUL_JSDOC_TAGS = new Set([
|
|
1398
1612
|
"deprecated",
|
|
1399
1613
|
"see",
|
|
@@ -1489,7 +1703,7 @@ const PHP_DECL_START = /^\s*(?:(?:public|private|protected|static|final|abstract
|
|
|
1489
1703
|
|
|
1490
1704
|
//#endregion
|
|
1491
1705
|
//#region src/engines/ai-slop/non-production-paths.ts
|
|
1492
|
-
const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
|
|
1706
|
+
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;
|
|
1493
1707
|
const BASENAME_PATTERN = /(?:^|\/)(?:benchmark|bench|demo|example|script|seed|migrate|profile|smoke|stress|load|debug|repro)[-_.][^/]*\.[mc]?[jt]sx?$|(?:^|\/)[^/]+[-_](?:benchmark|bench|demo|example)\.[mc]?[jt]sx?$/i;
|
|
1494
1708
|
const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) || BASENAME_PATTERN.test(relativePath);
|
|
1495
1709
|
|
|
@@ -1498,7 +1712,7 @@ const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) ||
|
|
|
1498
1712
|
const TRIVIAL_VERB_STEMS = "Import|Defin|Initializ|Setting|Set\\s+up|Setup|Return|Check|Loop|Iterat|Creat|Updat|Delet|Remov|Handl|Get|Fetch|Increment|Decrement|Writ|Runn|Run|Pars|Execut|Extract|Sav|Load|Build|Start|Stopp|Stop|Clean(?:up|\\s+up)?|Configur|Validat|Process|Queue|Fire|Emit|Dispatch|Log|Print|Render";
|
|
1499
1713
|
const TRIVIAL_JS_COMMENT_PATTERNS = [/\/\/\s*This (?:function|method|class|variable|constant) (?:will |is used to |is responsible for )?/i, new RegExp(`\\/\\/\\s*(?:${TRIVIAL_VERB_STEMS})(?:e|es|ing|s)?\\b`, "i")];
|
|
1500
1714
|
const TRIVIAL_PYTHON_COMMENT_PATTERNS = [/^#\s*This (?:function|method|class) (?:will |is used to )?/i, new RegExp(`^#\\s*(?:${TRIVIAL_VERB_STEMS})(?:e|es|ing|s)?\\b`, "i")];
|
|
1501
|
-
const EXPLANATORY_KEYWORDS = /\b(?:because|since|note|todo|fixme|hack|warn|warning|workaround|caveat|important|assumes?)\b/i;
|
|
1715
|
+
const EXPLANATORY_KEYWORDS = /\b(?:because|since|note|todo|fixme|hack|warn|warning|workaround|caveat|important|assumes?|if|when|unless|until|only|except|otherwise|needs?|must|should|ensure|avoid|prevent|requires?)\b/i;
|
|
1502
1716
|
const COMMENTED_CODE_CHARS = /[({=;}\]>]/;
|
|
1503
1717
|
const MAX_TRIVIAL_COMMENT_LENGTH = 60;
|
|
1504
1718
|
const isJsComment = (trimmed) => trimmed.startsWith("//") && !trimmed.startsWith("///") && !trimmed.startsWith("//!");
|
|
@@ -1647,6 +1861,7 @@ const TODO_PATTERN = new RegExp(`\\b(?:${[
|
|
|
1647
1861
|
"PLACEHOLDER",
|
|
1648
1862
|
"STUB"
|
|
1649
1863
|
].join("|")})[:\\s]`);
|
|
1864
|
+
const TODO_TRACKING_RE = /https?:\/\/|#\d+|\bgh-\d+\b|\b[A-Z][A-Z0-9]+-\d+\b|\b(?:issue|ticket|jira)\b/i;
|
|
1650
1865
|
const isBlockCloserAfterReturn = (line) => line.startsWith("}") || line.startsWith("};") || line.startsWith("),") || line.startsWith(");") || line.startsWith("],") || line.startsWith("]);");
|
|
1651
1866
|
const isGuardedSingleLineExit = (lines, lineIndex) => {
|
|
1652
1867
|
const contextLines = [];
|
|
@@ -1666,7 +1881,10 @@ const detectTodoStubs = (content, relativePath) => {
|
|
|
1666
1881
|
for (let i = 0; i < lines.length; i++) {
|
|
1667
1882
|
const trimmed = lines[i].trim();
|
|
1668
1883
|
if (!trimmed.startsWith("//") && !trimmed.startsWith("#") && !trimmed.startsWith("*") && !trimmed.startsWith("/*")) continue;
|
|
1669
|
-
if (TODO_PATTERN.test(trimmed))
|
|
1884
|
+
if (TODO_PATTERN.test(trimmed)) {
|
|
1885
|
+
if (TODO_TRACKING_RE.test(trimmed)) continue;
|
|
1886
|
+
diagnostics.push(slop(relativePath, i + 1, "ai-slop/todo-stub", "info", "Unresolved TODO/FIXME/HACK comment indicates incomplete code", "Resolve the TODO or create a tracked issue for it", false));
|
|
1887
|
+
}
|
|
1670
1888
|
}
|
|
1671
1889
|
return diagnostics;
|
|
1672
1890
|
};
|
|
@@ -1715,9 +1933,10 @@ const detectDeadPatterns = async (context) => {
|
|
|
1715
1933
|
}
|
|
1716
1934
|
const ext = path.extname(filePath);
|
|
1717
1935
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
1718
|
-
|
|
1936
|
+
const codeOnly = maskComments(content, ext);
|
|
1937
|
+
diagnostics.push(...detectConsoleLeftovers(codeOnly, relativePath, ext));
|
|
1719
1938
|
diagnostics.push(...detectTodoStubs(content, relativePath));
|
|
1720
|
-
diagnostics.push(...detectDeadCodePatterns(
|
|
1939
|
+
diagnostics.push(...detectDeadCodePatterns(codeOnly, relativePath, ext));
|
|
1721
1940
|
diagnostics.push(...detectUnsafeTypePatterns(content, relativePath, ext));
|
|
1722
1941
|
}
|
|
1723
1942
|
return diagnostics;
|
|
@@ -1907,6 +2126,7 @@ const JS_EXTENSIONS$3 = new Set([
|
|
|
1907
2126
|
const IMPORT_FROM_RE$1 = /^\s*import\s+([^;]*?)\s+from\s+["']([^"']+)["']/;
|
|
1908
2127
|
const TYPE_ONLY_RE = /^\s*type\b/;
|
|
1909
2128
|
const VALUE_BINDING_RE = /\{([^}]*)\}/;
|
|
2129
|
+
const NAMESPACE_RE = /\*\s+as\s+/;
|
|
1910
2130
|
const isTypeOnly = (clause) => {
|
|
1911
2131
|
if (TYPE_ONLY_RE.test(clause)) return true;
|
|
1912
2132
|
const braces = VALUE_BINDING_RE.exec(clause);
|
|
@@ -1924,7 +2144,8 @@ const extractImportLines = (content) => {
|
|
|
1924
2144
|
results.push({
|
|
1925
2145
|
spec: match[2],
|
|
1926
2146
|
line: i + 1,
|
|
1927
|
-
typeOnly: isTypeOnly(match[1])
|
|
2147
|
+
typeOnly: isTypeOnly(match[1]),
|
|
2148
|
+
namespace: NAMESPACE_RE.test(match[1])
|
|
1928
2149
|
});
|
|
1929
2150
|
}
|
|
1930
2151
|
return results;
|
|
@@ -1941,11 +2162,11 @@ const detectDuplicateImports = async (context) => {
|
|
|
1941
2162
|
} catch {
|
|
1942
2163
|
continue;
|
|
1943
2164
|
}
|
|
1944
|
-
const imports = extractImportLines(content);
|
|
2165
|
+
const imports = extractImportLines(maskComments(content, path.extname(filePath)));
|
|
1945
2166
|
if (imports.length < 2) continue;
|
|
1946
2167
|
const byBucket = /* @__PURE__ */ new Map();
|
|
1947
2168
|
for (const imp of imports) {
|
|
1948
|
-
const key = `${imp.typeOnly ? "type" : "value"}\0${imp.spec}`;
|
|
2169
|
+
const key = `${imp.namespace ? "ns" : imp.typeOnly ? "type" : "value"}\0${imp.spec}`;
|
|
1949
2170
|
const list = byBucket.get(key) ?? [];
|
|
1950
2171
|
list.push(imp);
|
|
1951
2172
|
byBucket.set(key, list);
|
|
@@ -2192,6 +2413,30 @@ const LOOPBACK_HOSTS = new Set([
|
|
|
2192
2413
|
"0.0.0.0",
|
|
2193
2414
|
"::1"
|
|
2194
2415
|
]);
|
|
2416
|
+
const VENDOR_API_DOMAINS = [
|
|
2417
|
+
"github.com",
|
|
2418
|
+
"githubusercontent.com",
|
|
2419
|
+
"googleapis.com",
|
|
2420
|
+
"accounts.google.com",
|
|
2421
|
+
"stripe.com",
|
|
2422
|
+
"openai.com",
|
|
2423
|
+
"anthropic.com",
|
|
2424
|
+
"slack.com",
|
|
2425
|
+
"twilio.com",
|
|
2426
|
+
"sendgrid.com",
|
|
2427
|
+
"mailgun.net",
|
|
2428
|
+
"cloudflare.com",
|
|
2429
|
+
"discord.com",
|
|
2430
|
+
"telegram.org",
|
|
2431
|
+
"login.microsoftonline.com",
|
|
2432
|
+
"graph.microsoft.com",
|
|
2433
|
+
"twitter.com",
|
|
2434
|
+
"x.com",
|
|
2435
|
+
"twimg.com",
|
|
2436
|
+
"t.co",
|
|
2437
|
+
"api.telegram.org"
|
|
2438
|
+
];
|
|
2439
|
+
const isVendorApiHost = (host) => VENDOR_API_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
|
|
2195
2440
|
const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
|
|
2196
2441
|
const HARDCODED_URL_FINDING = {
|
|
2197
2442
|
rule: "ai-slop/hardcoded-url",
|
|
@@ -2236,6 +2481,7 @@ const shouldFlagUrlLiteral = (line, urlText) => {
|
|
|
2236
2481
|
if (!host) return false;
|
|
2237
2482
|
if (PLACEHOLDER_HOSTS.has(host)) return false;
|
|
2238
2483
|
if (LOOPBACK_HOSTS.has(host)) return false;
|
|
2484
|
+
if (isVendorApiHost(host)) return false;
|
|
2239
2485
|
if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
|
|
2240
2486
|
return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
|
|
2241
2487
|
};
|
|
@@ -2287,7 +2533,7 @@ const detectHardcodedConfigLiterals = async (context) => {
|
|
|
2287
2533
|
}
|
|
2288
2534
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
2289
2535
|
const ext = path.extname(filePath);
|
|
2290
|
-
diagnostics.push(...scanFileForConfigLiterals(content, relativePath, ext));
|
|
2536
|
+
diagnostics.push(...scanFileForConfigLiterals(maskComments(content, ext), relativePath, ext));
|
|
2291
2537
|
}
|
|
2292
2538
|
return diagnostics;
|
|
2293
2539
|
};
|
|
@@ -2420,7 +2666,9 @@ const PYTHON_STDLIB = new Set([
|
|
|
2420
2666
|
"builtins",
|
|
2421
2667
|
"bz2",
|
|
2422
2668
|
"calendar",
|
|
2669
|
+
"code",
|
|
2423
2670
|
"codecs",
|
|
2671
|
+
"codeop",
|
|
2424
2672
|
"collections",
|
|
2425
2673
|
"concurrent",
|
|
2426
2674
|
"configparser",
|
|
@@ -2493,6 +2741,7 @@ const PYTHON_STDLIB = new Set([
|
|
|
2493
2741
|
"readline",
|
|
2494
2742
|
"reprlib",
|
|
2495
2743
|
"resource",
|
|
2744
|
+
"rlcompleter",
|
|
2496
2745
|
"secrets",
|
|
2497
2746
|
"select",
|
|
2498
2747
|
"selectors",
|
|
@@ -2681,6 +2930,8 @@ const collectFromPyproject = (rootDir, pyDeps) => {
|
|
|
2681
2930
|
}
|
|
2682
2931
|
const extras = content.match(/\[project\.optional-dependencies\]([\s\S]*?)(?=\n\[|$)/);
|
|
2683
2932
|
if (extras) for (const m of extras[1].matchAll(/["']\s*([a-zA-Z][a-zA-Z0-9_\-.]+)/g)) addPyDep(pyDeps, m[1]);
|
|
2933
|
+
const groups = content.match(/\[dependency-groups\]([\s\S]*?)(?=\n\[[^[]|$)/);
|
|
2934
|
+
if (groups) for (const m of groups[1].matchAll(/["']\s*([a-zA-Z][a-zA-Z0-9_\-.]+)/g)) addPyDep(pyDeps, m[1]);
|
|
2684
2935
|
const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
|
|
2685
2936
|
let match = poetryRe.exec(content);
|
|
2686
2937
|
while (match !== null) {
|
|
@@ -2913,9 +3164,28 @@ const extractJsImports = (content) => {
|
|
|
2913
3164
|
const extractPyImports = (content) => {
|
|
2914
3165
|
const lines = content.split("\n");
|
|
2915
3166
|
const results = [];
|
|
3167
|
+
let inDoc = null;
|
|
3168
|
+
let typeCheckIndent = -1;
|
|
2916
3169
|
for (let i = 0; i < lines.length; i++) {
|
|
2917
|
-
const
|
|
2918
|
-
|
|
3170
|
+
const raw = lines[i];
|
|
3171
|
+
const line = raw.trim();
|
|
3172
|
+
if (inDoc) {
|
|
3173
|
+
if (line.includes(inDoc)) inDoc = null;
|
|
3174
|
+
continue;
|
|
3175
|
+
}
|
|
3176
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
3177
|
+
const triples = line.match(/"""|'''/g);
|
|
3178
|
+
if (triples) {
|
|
3179
|
+
if (triples.length % 2 === 1) inDoc = triples[triples.length - 1];
|
|
3180
|
+
continue;
|
|
3181
|
+
}
|
|
3182
|
+
const indent = raw.length - raw.trimStart().length;
|
|
3183
|
+
if (typeCheckIndent >= 0 && indent <= typeCheckIndent) typeCheckIndent = -1;
|
|
3184
|
+
if (/^if\s+(?:[\w.]+\.)?TYPE_CHECKING\b/.test(line)) {
|
|
3185
|
+
typeCheckIndent = indent;
|
|
3186
|
+
continue;
|
|
3187
|
+
}
|
|
3188
|
+
if (typeCheckIndent >= 0) continue;
|
|
2919
3189
|
const fromMatch = line.match(/^from\s+([\w.]+)\s+import\b/);
|
|
2920
3190
|
if (fromMatch && !fromMatch[1].startsWith(".")) {
|
|
2921
3191
|
results.push({
|
|
@@ -2925,8 +3195,8 @@ const extractPyImports = (content) => {
|
|
|
2925
3195
|
continue;
|
|
2926
3196
|
}
|
|
2927
3197
|
const importMatch = line.match(/^import\s+([\w.,\s]+?)(?:\s+as\s+\w+)?\s*$/);
|
|
2928
|
-
if (importMatch) for (const
|
|
2929
|
-
const cleaned =
|
|
3198
|
+
if (importMatch) for (const part of importMatch[1].split(",")) {
|
|
3199
|
+
const cleaned = part.trim().split(/\s+as\s+/)[0];
|
|
2930
3200
|
if (cleaned && !cleaned.startsWith(".")) results.push({
|
|
2931
3201
|
spec: cleaned,
|
|
2932
3202
|
line: i + 1
|
|
@@ -2983,6 +3253,7 @@ const detectHallucinatedImports = async (context) => {
|
|
|
2983
3253
|
continue;
|
|
2984
3254
|
}
|
|
2985
3255
|
const relPath = path.relative(context.rootDirectory, filePath);
|
|
3256
|
+
if (isNonProductionPath(relPath)) continue;
|
|
2986
3257
|
const imports = isJs ? extractJsImports(content) : extractPyImports(content);
|
|
2987
3258
|
for (const { spec, line } of imports) {
|
|
2988
3259
|
const hallucinated = isJs ? checkJsImport(spec, manifest, tsAliasMatchers) : checkPyImport(spec, manifest);
|
|
@@ -3110,7 +3381,7 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
3110
3381
|
//#endregion
|
|
3111
3382
|
//#region src/engines/ai-slop/meta-comment.ts
|
|
3112
3383
|
const PLAN_REFERENCE_RES = [
|
|
3113
|
-
|
|
3384
|
+
/^(?:stage|step|phase)\s+\d+\s*[:.\-–—]/i,
|
|
3114
3385
|
/\bstep\s+\d+\s+of\s+the\s+plan\b/i,
|
|
3115
3386
|
/\bas\s+(?:per|requested)\s+(?:the\s+)?(?:requirements?|spec|task|ticket|prompt|instructions?)\b/i,
|
|
3116
3387
|
/\bper\s+the\s+(?:spec|requirements?|task|ticket|plan|prompt|instructions?)\b/i,
|
|
@@ -3212,24 +3483,6 @@ const looksLikeLicenseHeader = (block) => {
|
|
|
3212
3483
|
const text = block.rawLines.join(" ").toLowerCase();
|
|
3213
3484
|
return text.includes("copyright") || text.includes("license") || text.includes("spdx-license-identifier");
|
|
3214
3485
|
};
|
|
3215
|
-
const BARE_LABEL_RE = /^[A-Z][A-Za-z0-9 ]{1,28}$/;
|
|
3216
|
-
const isBareSectionLabel = (prose) => {
|
|
3217
|
-
if (!BARE_LABEL_RE.test(prose)) return false;
|
|
3218
|
-
if (prose.endsWith(".")) return false;
|
|
3219
|
-
if (prose.split(/\s+/).length > 3) return false;
|
|
3220
|
-
if (STEP_COMMENT_VERB_RE.test(prose)) return false;
|
|
3221
|
-
return true;
|
|
3222
|
-
};
|
|
3223
|
-
const DATA_ENTRY_START = /^\s*(?:\{|\[|["'`]|\d|\w+:\s|case\s)/;
|
|
3224
|
-
const nextLineLooksLikeDataEntry = (nextLine) => {
|
|
3225
|
-
if (nextLine === null) return false;
|
|
3226
|
-
if (!DATA_ENTRY_START.test(nextLine)) return false;
|
|
3227
|
-
const trimmed = nextLine.trim();
|
|
3228
|
-
if (trimmed.startsWith("case ")) return true;
|
|
3229
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith("\"") || trimmed.startsWith("'") || trimmed.startsWith("`")) return true;
|
|
3230
|
-
if (/^\w+\s*:/.test(trimmed)) return true;
|
|
3231
|
-
return false;
|
|
3232
|
-
};
|
|
3233
3486
|
const looksLikeSuppressDirective = (block) => block.rawLines.some((l) => /\b(biome-ignore|eslint-disable|ts-ignore|ts-expect-error|@ts-\w+|noqa|pylint:\s*disable|rubocop:disable|noinspection|phpcs:disable)\b/.test(l));
|
|
3234
3487
|
const GO_DECL_NAME_RE = /^(?:func|type|var|const)\s+(?:\([^)]*\)\s*)?(\w+)/;
|
|
3235
3488
|
const GO_FIELD_LEAD_RE = /^(\w+)\s+/;
|
|
@@ -3318,10 +3571,6 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
3318
3571
|
matched: true,
|
|
3319
3572
|
reason: "phase/section header"
|
|
3320
3573
|
};
|
|
3321
|
-
if (block.kind === "line" && block.prose.length === 1 && isBareSectionLabel(block.prose[0]) && !nextLineLooksLikeDataEntry(block.nextNonBlankLine) && !looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
|
|
3322
|
-
matched: true,
|
|
3323
|
-
reason: "bare section label"
|
|
3324
|
-
};
|
|
3325
3574
|
const joined = block.prose.join(" ");
|
|
3326
3575
|
const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joined);
|
|
3327
3576
|
if (hasWhyMarker || hasDocIndicator(block)) return {
|
|
@@ -3352,17 +3601,11 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
3352
3601
|
};
|
|
3353
3602
|
const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
|
|
3354
3603
|
const isAboveDeclaration = looksLikeDeclarationPreamble(block.nextNonBlankLine, ext);
|
|
3355
|
-
if (nonEmptyProseCount >= 5) {
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
return {
|
|
3361
|
-
matched: true,
|
|
3362
|
-
reason: "long narrative block"
|
|
3363
|
-
};
|
|
3364
|
-
}
|
|
3365
|
-
if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line" && !isAboveDeclaration) return {
|
|
3604
|
+
if (nonEmptyProseCount >= 5 && !isAboveDeclaration && hasPreambleSlopSignal(block)) return {
|
|
3605
|
+
matched: true,
|
|
3606
|
+
reason: "long narrative block"
|
|
3607
|
+
};
|
|
3608
|
+
if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line" && !isAboveDeclaration && hasPreambleSlopSignal(block)) return {
|
|
3366
3609
|
matched: true,
|
|
3367
3610
|
reason: "multi-line narrative prose"
|
|
3368
3611
|
};
|
|
@@ -3796,7 +4039,7 @@ const detectRustPatterns = async (context) => {
|
|
|
3796
4039
|
|
|
3797
4040
|
//#endregion
|
|
3798
4041
|
//#region src/engines/ai-slop/silent-recovery.ts
|
|
3799
|
-
const JS_EXTS
|
|
4042
|
+
const JS_EXTS = new Set([
|
|
3800
4043
|
".ts",
|
|
3801
4044
|
".tsx",
|
|
3802
4045
|
".js",
|
|
@@ -3804,7 +4047,14 @@ const JS_EXTS$1 = new Set([
|
|
|
3804
4047
|
".mjs",
|
|
3805
4048
|
".cjs"
|
|
3806
4049
|
]);
|
|
3807
|
-
const CATCH_HEAD_RE = /\bcatch\s*(?:\([^)]*\))?\s*\{/g;
|
|
4050
|
+
const CATCH_HEAD_RE = /\bcatch\s*(?:\(\s*([^)]*?)\s*\))?\s*\{/g;
|
|
4051
|
+
const isIdentifier = (s) => /^[A-Za-z_$][\w$]*$/.test(s);
|
|
4052
|
+
const recoveryDropsError = (binding, body) => {
|
|
4053
|
+
const name = binding?.trim() ?? "";
|
|
4054
|
+
if (name === "") return true;
|
|
4055
|
+
if (!isIdentifier(name)) return false;
|
|
4056
|
+
return !new RegExp(`\\b${name}\\b`).test(body);
|
|
4057
|
+
};
|
|
3808
4058
|
const LOG_STATEMENT_RE = /^(?:console|[\w$]+(?:\.[\w$]+)*)\.(?:log|info|warn|warning|error|debug|trace)\s*\(/;
|
|
3809
4059
|
const HANDLING_TOKEN_RE = /\b(?:throw|return|reject|next|process\.exit|continue|break)\b/;
|
|
3810
4060
|
const stripBlockComments = (text) => text.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
@@ -3854,14 +4104,15 @@ const detectJsSilentRecovery = (content, relPath) => {
|
|
|
3854
4104
|
const body = extractCatchBody(content, match.index + match[0].length - 1);
|
|
3855
4105
|
if (body === null) continue;
|
|
3856
4106
|
if (!isLogOnlyBody(body)) continue;
|
|
4107
|
+
if (!recoveryDropsError(match[1], body)) continue;
|
|
3857
4108
|
const line = content.slice(0, match.index).split("\n").length;
|
|
3858
4109
|
out.push({
|
|
3859
4110
|
filePath: relPath,
|
|
3860
4111
|
engine: "ai-slop",
|
|
3861
4112
|
rule: "ai-slop/silent-recovery",
|
|
3862
4113
|
severity: "warning",
|
|
3863
|
-
message: "Catch
|
|
3864
|
-
help: "
|
|
4114
|
+
message: "Catch logs without the caught error then continues; the failure cause is lost",
|
|
4115
|
+
help: "Include the caught error in the log, or rethrow / recover explicitly, so the failure stays diagnosable.",
|
|
3865
4116
|
line,
|
|
3866
4117
|
column: 0,
|
|
3867
4118
|
category: "AI Slop",
|
|
@@ -3871,6 +4122,7 @@ const detectJsSilentRecovery = (content, relPath) => {
|
|
|
3871
4122
|
return out;
|
|
3872
4123
|
};
|
|
3873
4124
|
const PY_EXCEPT_RE = /^(\s*)except\b[^\n]*:\s*(?:#.*)?$/;
|
|
4125
|
+
const PY_EXCEPT_BINDING_RE = /\bas\s+(\w+)\s*:/;
|
|
3874
4126
|
const PY_LOG_STATEMENT_RE = /^(?:logging|logger|log|self\.log|self\.logger|print)(?:\.(?:debug|info|warning|warn|error|exception|critical))?\s*\(/;
|
|
3875
4127
|
const PY_HANDLING_TOKEN_RE = /^(?:raise\b|return\b|continue\b|break\b|self\.|[\w.]+\s*=)/;
|
|
3876
4128
|
const detectPySilentRecovery = (content, relPath) => {
|
|
@@ -3894,13 +4146,14 @@ const detectPySilentRecovery = (content, relPath) => {
|
|
|
3894
4146
|
const allLogs = bodyLines.every((line) => PY_LOG_STATEMENT_RE.test(line) || /^[\w"'(),.\s+:%{}[\]-]+$/.test(line));
|
|
3895
4147
|
const sawLog = bodyLines.some((line) => PY_LOG_STATEMENT_RE.test(line));
|
|
3896
4148
|
if (!allLogs || !sawLog) continue;
|
|
4149
|
+
if (!recoveryDropsError(PY_EXCEPT_BINDING_RE.exec(lines[i])?.[1], bodyLines.join(" "))) continue;
|
|
3897
4150
|
out.push({
|
|
3898
4151
|
filePath: relPath,
|
|
3899
4152
|
engine: "ai-slop",
|
|
3900
4153
|
rule: "ai-slop/silent-recovery",
|
|
3901
4154
|
severity: "warning",
|
|
3902
|
-
message: "except
|
|
3903
|
-
help: "
|
|
4155
|
+
message: "except logs without the caught error then continues; the failure cause is lost",
|
|
4156
|
+
help: "Include the caught error in the log, or re-raise / recover explicitly, so the failure stays diagnosable.",
|
|
3904
4157
|
line: i + 1,
|
|
3905
4158
|
column: 0,
|
|
3906
4159
|
category: "AI Slop",
|
|
@@ -3915,7 +4168,7 @@ const detectSilentRecovery = async (context) => {
|
|
|
3915
4168
|
for (const filePath of files) {
|
|
3916
4169
|
if (isAutoGenerated(filePath)) continue;
|
|
3917
4170
|
const ext = path.extname(filePath);
|
|
3918
|
-
const isJs = JS_EXTS
|
|
4171
|
+
const isJs = JS_EXTS.has(ext);
|
|
3919
4172
|
if (!isJs && !(ext === ".py")) continue;
|
|
3920
4173
|
const relPath = path.relative(context.rootDirectory, filePath);
|
|
3921
4174
|
if (isNonProductionPath(relPath)) continue;
|
|
@@ -4003,10 +4256,11 @@ const extractPyImportedSymbols = (lines) => {
|
|
|
4003
4256
|
const importLines = /* @__PURE__ */ new Set();
|
|
4004
4257
|
for (let i = 0; i < lines.length; i++) {
|
|
4005
4258
|
const trimmed = lines[i].trim();
|
|
4006
|
-
const fromMatch = trimmed.match(/^from\s+[\w.]
|
|
4259
|
+
const fromMatch = trimmed.match(/^from\s+([\w.]+)\s+import\s+(.+)/);
|
|
4007
4260
|
if (fromMatch) {
|
|
4008
4261
|
importLines.add(i);
|
|
4009
|
-
|
|
4262
|
+
if (fromMatch[1] === "__future__") continue;
|
|
4263
|
+
const importPart = fromMatch[2].replace(/#.*$/, "").trim();
|
|
4010
4264
|
if (importPart === "*") continue;
|
|
4011
4265
|
const cleaned = importPart.replace(/[()]/g, "");
|
|
4012
4266
|
for (const item of cleaned.split(",")) {
|
|
@@ -4382,12 +4636,92 @@ const findBraceFunctionEnd = (lines, startIndex) => {
|
|
|
4382
4636
|
maxNesting
|
|
4383
4637
|
};
|
|
4384
4638
|
};
|
|
4385
|
-
const
|
|
4386
|
-
|
|
4387
|
-
let
|
|
4639
|
+
const extractPythonSignature = (lines, startIndex) => {
|
|
4640
|
+
let depth = 0;
|
|
4641
|
+
let started = false;
|
|
4642
|
+
let params = "";
|
|
4643
|
+
for (let j = startIndex; j < lines.length; j++) {
|
|
4644
|
+
const l = lines[j];
|
|
4645
|
+
for (let ci = 0; ci < l.length; ci++) {
|
|
4646
|
+
const ch = l[ci];
|
|
4647
|
+
if (ch === "(") {
|
|
4648
|
+
depth++;
|
|
4649
|
+
if (depth === 1 && !started) {
|
|
4650
|
+
started = true;
|
|
4651
|
+
continue;
|
|
4652
|
+
}
|
|
4653
|
+
} else if (ch === ")") {
|
|
4654
|
+
depth--;
|
|
4655
|
+
if (depth === 0) return {
|
|
4656
|
+
params,
|
|
4657
|
+
sigEndIndex: j
|
|
4658
|
+
};
|
|
4659
|
+
}
|
|
4660
|
+
if (started) params += ch;
|
|
4661
|
+
}
|
|
4662
|
+
if (started) params += " ";
|
|
4663
|
+
}
|
|
4664
|
+
return {
|
|
4665
|
+
params,
|
|
4666
|
+
sigEndIndex: startIndex
|
|
4667
|
+
};
|
|
4668
|
+
};
|
|
4669
|
+
const countPythonParams = (signature) => {
|
|
4670
|
+
let depth = 0;
|
|
4671
|
+
const parts = [];
|
|
4672
|
+
let current = "";
|
|
4673
|
+
for (const ch of signature) {
|
|
4674
|
+
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
4675
|
+
else if (ch === ")" || ch === "]" || ch === "}") depth--;
|
|
4676
|
+
if (ch === "," && depth === 0) {
|
|
4677
|
+
parts.push(current);
|
|
4678
|
+
current = "";
|
|
4679
|
+
continue;
|
|
4680
|
+
}
|
|
4681
|
+
current += ch;
|
|
4682
|
+
}
|
|
4683
|
+
parts.push(current);
|
|
4684
|
+
let count = 0;
|
|
4685
|
+
for (const raw of parts) {
|
|
4686
|
+
const p = raw.trim();
|
|
4687
|
+
if (p.length === 0 || p === "*" || p === "/") continue;
|
|
4688
|
+
if (p.startsWith("*")) continue;
|
|
4689
|
+
if (p.includes("=")) continue;
|
|
4690
|
+
const name = p.split(":")[0].trim();
|
|
4691
|
+
if (name === "self" || name === "cls") continue;
|
|
4692
|
+
count++;
|
|
4693
|
+
}
|
|
4694
|
+
return count;
|
|
4695
|
+
};
|
|
4696
|
+
const countPythonBodyCodeLines = (lines, sigEndIndex, endLine) => {
|
|
4697
|
+
let count = 0;
|
|
4698
|
+
let inDoc = false;
|
|
4699
|
+
let delim = "";
|
|
4700
|
+
for (let j = sigEndIndex + 1; j <= endLine && j < lines.length; j++) {
|
|
4701
|
+
const t = lines[j].trim();
|
|
4702
|
+
if (inDoc) {
|
|
4703
|
+
if (t.includes(delim)) inDoc = false;
|
|
4704
|
+
continue;
|
|
4705
|
+
}
|
|
4706
|
+
if (t === "" || t.startsWith("#")) continue;
|
|
4707
|
+
const opener = t.startsWith("\"\"\"") ? "\"\"\"" : t.startsWith("'''") ? "'''" : "";
|
|
4708
|
+
if (opener) {
|
|
4709
|
+
if (!t.slice(3).includes(opener)) {
|
|
4710
|
+
inDoc = true;
|
|
4711
|
+
delim = opener;
|
|
4712
|
+
}
|
|
4713
|
+
continue;
|
|
4714
|
+
}
|
|
4715
|
+
count++;
|
|
4716
|
+
}
|
|
4717
|
+
return count;
|
|
4718
|
+
};
|
|
4719
|
+
const findPythonFunctionEnd = (lines, defIndex, bodyStartIndex) => {
|
|
4720
|
+
const baseIndent = lines[defIndex].match(/^(\s*)/)?.[1].length ?? 0;
|
|
4721
|
+
let endLine = bodyStartIndex;
|
|
4388
4722
|
let maxNesting = 0;
|
|
4389
4723
|
const controlIndentStack = [];
|
|
4390
|
-
for (let j =
|
|
4724
|
+
for (let j = bodyStartIndex + 1; j < lines.length; j++) {
|
|
4391
4725
|
const l = lines[j];
|
|
4392
4726
|
if (l.trim() === "") {
|
|
4393
4727
|
endLine = j;
|
|
@@ -4409,7 +4743,10 @@ const findPythonFunctionEnd = (lines, startIndex) => {
|
|
|
4409
4743
|
};
|
|
4410
4744
|
};
|
|
4411
4745
|
const findFunctionEnd = (lines, startIndex, isPython) => {
|
|
4412
|
-
if (isPython)
|
|
4746
|
+
if (isPython) {
|
|
4747
|
+
const { sigEndIndex } = extractPythonSignature(lines, startIndex);
|
|
4748
|
+
return findPythonFunctionEnd(lines, startIndex, sigEndIndex);
|
|
4749
|
+
}
|
|
4413
4750
|
return findBraceFunctionEnd(lines, startIndex);
|
|
4414
4751
|
};
|
|
4415
4752
|
const isBlockArrow = (lines, startIndex) => {
|
|
@@ -4474,7 +4811,7 @@ const FUNCTION_PATTERNS = [
|
|
|
4474
4811
|
]
|
|
4475
4812
|
},
|
|
4476
4813
|
{
|
|
4477
|
-
regex: /^\s*def\s+(\w+)\s*\(
|
|
4814
|
+
regex: /^\s*(?:async\s+)?def\s+(\w+)\s*\(/,
|
|
4478
4815
|
langFilter: [".py"]
|
|
4479
4816
|
},
|
|
4480
4817
|
{
|
|
@@ -4531,14 +4868,23 @@ const analyzeFunctions = (content, ext) => {
|
|
|
4531
4868
|
const isPython = fnMatch.patternIndex === 2;
|
|
4532
4869
|
if (fnMatch.patternIndex === 1 && !isBlockArrow(lines, i)) continue;
|
|
4533
4870
|
const { endLine, maxNesting } = findFunctionEnd(lines, i, isPython);
|
|
4534
|
-
|
|
4535
|
-
|
|
4871
|
+
let templateLines;
|
|
4872
|
+
let paramCount;
|
|
4873
|
+
if (isPython) {
|
|
4874
|
+
const sig = extractPythonSignature(lines, i);
|
|
4875
|
+
const codeLines = countPythonBodyCodeLines(lines, sig.sigEndIndex, endLine);
|
|
4876
|
+
templateLines = endLine - i + 1 - codeLines;
|
|
4877
|
+
paramCount = countPythonParams(sig.params);
|
|
4878
|
+
} else {
|
|
4879
|
+
templateLines = countTemplateLines(lines.slice(i + 1, endLine));
|
|
4880
|
+
paramCount = countParams(fnMatch.params);
|
|
4881
|
+
}
|
|
4536
4882
|
functions.push({
|
|
4537
4883
|
name: fnMatch.name,
|
|
4538
4884
|
startLine: i + 1,
|
|
4539
4885
|
lineCount: endLine - i + 1,
|
|
4540
4886
|
maxNesting,
|
|
4541
|
-
paramCount
|
|
4887
|
+
paramCount,
|
|
4542
4888
|
templateLines
|
|
4543
4889
|
});
|
|
4544
4890
|
}
|
|
@@ -6550,212 +6896,6 @@ const runCargoAudit = async (rootDir, timeout) => {
|
|
|
6550
6896
|
}
|
|
6551
6897
|
};
|
|
6552
6898
|
|
|
6553
|
-
//#endregion
|
|
6554
|
-
//#region src/utils/source-masker.ts
|
|
6555
|
-
const JS_EXTS = new Set([
|
|
6556
|
-
".ts",
|
|
6557
|
-
".tsx",
|
|
6558
|
-
".js",
|
|
6559
|
-
".jsx",
|
|
6560
|
-
".mjs",
|
|
6561
|
-
".cjs"
|
|
6562
|
-
]);
|
|
6563
|
-
const PY_EXTS = new Set([".py"]);
|
|
6564
|
-
const RB_EXTS = new Set([".rb"]);
|
|
6565
|
-
const PHP_EXTS = new Set([".php"]);
|
|
6566
|
-
const familyForExt = (ext) => {
|
|
6567
|
-
if (JS_EXTS.has(ext)) return "js";
|
|
6568
|
-
if (PY_EXTS.has(ext)) return "py";
|
|
6569
|
-
if (RB_EXTS.has(ext)) return "rb";
|
|
6570
|
-
if (PHP_EXTS.has(ext)) return "php";
|
|
6571
|
-
return "none";
|
|
6572
|
-
};
|
|
6573
|
-
const maskStringsAndComments = (content, ext) => {
|
|
6574
|
-
const family = familyForExt(ext);
|
|
6575
|
-
if (family === "none") return content;
|
|
6576
|
-
if (family === "js") return maskJs(content);
|
|
6577
|
-
return maskSimple(content, family);
|
|
6578
|
-
};
|
|
6579
|
-
const handleQuotesAndComments = (content, i, tplStack, mask) => {
|
|
6580
|
-
const len = content.length;
|
|
6581
|
-
const c = content[i];
|
|
6582
|
-
const next = content[i + 1];
|
|
6583
|
-
if (c === "\"" || c === "'") {
|
|
6584
|
-
const strStart = i;
|
|
6585
|
-
const end = consumeQuotedString(content, i, c);
|
|
6586
|
-
mask(strStart + 1, end - 1);
|
|
6587
|
-
return {
|
|
6588
|
-
handled: true,
|
|
6589
|
-
nextI: end
|
|
6590
|
-
};
|
|
6591
|
-
}
|
|
6592
|
-
if (c === "`") {
|
|
6593
|
-
const scan = consumeTemplateString(content, i + 1);
|
|
6594
|
-
mask(i + 1, scan.maskEnd);
|
|
6595
|
-
if (scan.openedInterp) tplStack.push(0);
|
|
6596
|
-
return {
|
|
6597
|
-
handled: true,
|
|
6598
|
-
nextI: scan.resumeAt
|
|
6599
|
-
};
|
|
6600
|
-
}
|
|
6601
|
-
if (c === "/" && next === "/") {
|
|
6602
|
-
const strStart = i;
|
|
6603
|
-
let k = i;
|
|
6604
|
-
while (k < len && content[k] !== "\n") k++;
|
|
6605
|
-
mask(strStart, k);
|
|
6606
|
-
return {
|
|
6607
|
-
handled: true,
|
|
6608
|
-
nextI: k
|
|
6609
|
-
};
|
|
6610
|
-
}
|
|
6611
|
-
if (c === "/" && next === "*") {
|
|
6612
|
-
const strStart = i;
|
|
6613
|
-
let k = i + 2;
|
|
6614
|
-
while (k < len - 1 && !(content[k] === "*" && content[k + 1] === "/")) k++;
|
|
6615
|
-
if (k < len - 1) k += 2;
|
|
6616
|
-
mask(strStart, k);
|
|
6617
|
-
return {
|
|
6618
|
-
handled: true,
|
|
6619
|
-
nextI: k
|
|
6620
|
-
};
|
|
6621
|
-
}
|
|
6622
|
-
return {
|
|
6623
|
-
handled: false,
|
|
6624
|
-
nextI: i
|
|
6625
|
-
};
|
|
6626
|
-
};
|
|
6627
|
-
const maskJs = (content) => {
|
|
6628
|
-
const out = content.split("");
|
|
6629
|
-
const len = content.length;
|
|
6630
|
-
const tplStack = [];
|
|
6631
|
-
let i = 0;
|
|
6632
|
-
const mask = (start, end) => {
|
|
6633
|
-
for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
|
|
6634
|
-
};
|
|
6635
|
-
while (i < len) {
|
|
6636
|
-
const c = content[i];
|
|
6637
|
-
if (tplStack.length > 0) {
|
|
6638
|
-
if (c === "{") {
|
|
6639
|
-
tplStack[tplStack.length - 1]++;
|
|
6640
|
-
i++;
|
|
6641
|
-
continue;
|
|
6642
|
-
}
|
|
6643
|
-
if (c === "}") {
|
|
6644
|
-
if (tplStack[tplStack.length - 1] === 0) {
|
|
6645
|
-
tplStack.pop();
|
|
6646
|
-
const scan = consumeTemplateString(content, i + 1);
|
|
6647
|
-
mask(i + 1, scan.maskEnd);
|
|
6648
|
-
if (scan.openedInterp) tplStack.push(0);
|
|
6649
|
-
i = scan.resumeAt;
|
|
6650
|
-
continue;
|
|
6651
|
-
}
|
|
6652
|
-
tplStack[tplStack.length - 1]--;
|
|
6653
|
-
i++;
|
|
6654
|
-
continue;
|
|
6655
|
-
}
|
|
6656
|
-
}
|
|
6657
|
-
const handled = handleQuotesAndComments(content, i, tplStack, mask);
|
|
6658
|
-
if (handled.handled) {
|
|
6659
|
-
i = handled.nextI;
|
|
6660
|
-
continue;
|
|
6661
|
-
}
|
|
6662
|
-
i++;
|
|
6663
|
-
}
|
|
6664
|
-
return out.join("");
|
|
6665
|
-
};
|
|
6666
|
-
const consumeQuotedString = (content, start, quote) => {
|
|
6667
|
-
const len = content.length;
|
|
6668
|
-
let i = start + 1;
|
|
6669
|
-
while (i < len) {
|
|
6670
|
-
const c = content[i];
|
|
6671
|
-
if (c === "\\" && i + 1 < len) {
|
|
6672
|
-
i += 2;
|
|
6673
|
-
continue;
|
|
6674
|
-
}
|
|
6675
|
-
if (c === quote) return i + 1;
|
|
6676
|
-
if (c === "\n") return i;
|
|
6677
|
-
i++;
|
|
6678
|
-
}
|
|
6679
|
-
return i;
|
|
6680
|
-
};
|
|
6681
|
-
const consumeTemplateString = (content, start) => {
|
|
6682
|
-
const len = content.length;
|
|
6683
|
-
let i = start;
|
|
6684
|
-
while (i < len) {
|
|
6685
|
-
const c = content[i];
|
|
6686
|
-
if (c === "\\" && i + 1 < len) {
|
|
6687
|
-
i += 2;
|
|
6688
|
-
continue;
|
|
6689
|
-
}
|
|
6690
|
-
if (c === "`") return {
|
|
6691
|
-
maskEnd: i,
|
|
6692
|
-
resumeAt: i + 1,
|
|
6693
|
-
openedInterp: false
|
|
6694
|
-
};
|
|
6695
|
-
if (c === "$" && content[i + 1] === "{") return {
|
|
6696
|
-
maskEnd: i,
|
|
6697
|
-
resumeAt: i + 2,
|
|
6698
|
-
openedInterp: true
|
|
6699
|
-
};
|
|
6700
|
-
i++;
|
|
6701
|
-
}
|
|
6702
|
-
return {
|
|
6703
|
-
maskEnd: i,
|
|
6704
|
-
resumeAt: i,
|
|
6705
|
-
openedInterp: false
|
|
6706
|
-
};
|
|
6707
|
-
};
|
|
6708
|
-
const maskSimple = (content, family) => {
|
|
6709
|
-
const out = content.split("");
|
|
6710
|
-
const len = content.length;
|
|
6711
|
-
let i = 0;
|
|
6712
|
-
const mask = (start, end) => {
|
|
6713
|
-
for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
|
|
6714
|
-
};
|
|
6715
|
-
while (i < len) {
|
|
6716
|
-
const c = content[i];
|
|
6717
|
-
const next = content[i + 1];
|
|
6718
|
-
if (family === "py" && (c === "\"" || c === "'")) {
|
|
6719
|
-
if (content[i + 1] === c && content[i + 2] === c) {
|
|
6720
|
-
const triple = c + c + c;
|
|
6721
|
-
const end = content.indexOf(triple, i + 3);
|
|
6722
|
-
const stop = end === -1 ? len : end + 3;
|
|
6723
|
-
mask(i + 3, stop - 3);
|
|
6724
|
-
i = stop;
|
|
6725
|
-
continue;
|
|
6726
|
-
}
|
|
6727
|
-
}
|
|
6728
|
-
if (c === "\"" || c === "'") {
|
|
6729
|
-
const strStart = i;
|
|
6730
|
-
i = consumeQuotedString(content, i, c);
|
|
6731
|
-
mask(strStart + 1, i - 1);
|
|
6732
|
-
continue;
|
|
6733
|
-
}
|
|
6734
|
-
if ((family === "py" || family === "rb" || family === "php") && c === "#") {
|
|
6735
|
-
const strStart = i;
|
|
6736
|
-
while (i < len && content[i] !== "\n") i++;
|
|
6737
|
-
mask(strStart, i);
|
|
6738
|
-
continue;
|
|
6739
|
-
}
|
|
6740
|
-
if (family === "php" && c === "/" && next === "/") {
|
|
6741
|
-
const strStart = i;
|
|
6742
|
-
while (i < len && content[i] !== "\n") i++;
|
|
6743
|
-
mask(strStart, i);
|
|
6744
|
-
continue;
|
|
6745
|
-
}
|
|
6746
|
-
if (family === "php" && c === "/" && next === "*") {
|
|
6747
|
-
const strStart = i;
|
|
6748
|
-
i += 2;
|
|
6749
|
-
while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
|
|
6750
|
-
if (i < len - 1) i += 2;
|
|
6751
|
-
mask(strStart, i);
|
|
6752
|
-
continue;
|
|
6753
|
-
}
|
|
6754
|
-
i++;
|
|
6755
|
-
}
|
|
6756
|
-
return out.join("");
|
|
6757
|
-
};
|
|
6758
|
-
|
|
6759
6899
|
//#endregion
|
|
6760
6900
|
//#region src/engines/security/risky.ts
|
|
6761
6901
|
const ev = "eval";
|
|
@@ -7030,6 +7170,7 @@ const scanSecrets = async (context) => {
|
|
|
7030
7170
|
} catch {
|
|
7031
7171
|
continue;
|
|
7032
7172
|
}
|
|
7173
|
+
content = maskComments(content, path.extname(filePath));
|
|
7033
7174
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
7034
7175
|
for (const { pattern, name, keywordPrefixed } of SECRET_PATTERNS) {
|
|
7035
7176
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
@@ -7118,6 +7259,13 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
|
|
|
7118
7259
|
//#endregion
|
|
7119
7260
|
//#region src/scoring/index.ts
|
|
7120
7261
|
const PERFECT_SCORE = 100;
|
|
7262
|
+
const STYLE_RULES = new Set([
|
|
7263
|
+
"ai-slop/trivial-comment",
|
|
7264
|
+
"ai-slop/narrative-comment",
|
|
7265
|
+
"complexity/file-too-large",
|
|
7266
|
+
"complexity/function-too-long"
|
|
7267
|
+
]);
|
|
7268
|
+
const STYLE_WEIGHT = .5;
|
|
7121
7269
|
const getEffectiveFileCount = (diagnostics, sourceFileCount) => {
|
|
7122
7270
|
if (typeof sourceFileCount === "number" && sourceFileCount > 0) return sourceFileCount;
|
|
7123
7271
|
const filesWithDiagnostics = new Set(diagnostics.map((d) => d.filePath)).size;
|
|
@@ -7132,7 +7280,8 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
|
|
|
7132
7280
|
for (const d of diagnostics) {
|
|
7133
7281
|
const engineWeight = weights[d.engine] ?? 1;
|
|
7134
7282
|
const severityPenalty = d.severity === "error" ? 3 : d.severity === "warning" ? 1 : .25;
|
|
7135
|
-
|
|
7283
|
+
const styleFactor = STYLE_RULES.has(d.rule) ? STYLE_WEIGHT : 1;
|
|
7284
|
+
deductions += severityPenalty * engineWeight * styleFactor;
|
|
7136
7285
|
}
|
|
7137
7286
|
const effectiveFileCount = getEffectiveFileCount(diagnostics, sourceFileCount);
|
|
7138
7287
|
const smoothingConstant = typeof smoothing === "number" ? smoothing : 10;
|
|
@@ -8313,12 +8462,12 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
8313
8462
|
engineTimings
|
|
8314
8463
|
};
|
|
8315
8464
|
if (options.sarif) {
|
|
8316
|
-
const { buildSarifLog } = await import("./sarif-
|
|
8465
|
+
const { buildSarifLog } = await import("./sarif-C-vh4wcC.js");
|
|
8317
8466
|
console.log(JSON.stringify(buildSarifLog(results), null, 2));
|
|
8318
8467
|
return completion;
|
|
8319
8468
|
}
|
|
8320
8469
|
if (options.json) {
|
|
8321
|
-
const { buildJsonOutput } = await import("./json-
|
|
8470
|
+
const { buildJsonOutput } = await import("./json-Bqkcl1DF.js");
|
|
8322
8471
|
const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
|
|
8323
8472
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
8324
8473
|
return completion;
|