slopbrick 0.21.2 → 0.24.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 +72 -0
- package/dist/engine/worker.cjs +1532 -72
- package/dist/engine/worker.js +1532 -72
- package/dist/index.cjs +1826 -33
- package/dist/index.js +1822 -30
- package/package.json +5 -2
package/dist/engine/worker.js
CHANGED
|
@@ -34376,6 +34376,232 @@ var importPathMismatchRule = createRule({
|
|
|
34376
34376
|
}
|
|
34377
34377
|
});
|
|
34378
34378
|
|
|
34379
|
+
// src/rules/cpp/c-style-cast.ts
|
|
34380
|
+
var C_STYLE_CAST_REGEX = /\(\s*(?:int|long|short|char|float|double|bool|unsigned\s+\w+|signed\s+\w+|size_t|[A-Za-z_]\w*(?:\s*[*,&][^)]*)?)\s*\)\s*(\w+|[^a-zA-Z_])/g;
|
|
34381
|
+
var NAMED_CAST_PREFIX_REGEX = /\b(?:static|reinterpret|const|dynamic)_cast\s*<[^>]*>\s*\($/;
|
|
34382
|
+
var cppCStyleCastRule = createRule({
|
|
34383
|
+
id: "cpp/c-style-cast",
|
|
34384
|
+
category: "typo",
|
|
34385
|
+
severity: "low",
|
|
34386
|
+
aiSpecific: true,
|
|
34387
|
+
description: "C-style cast `(int)x` \u2014 use static_cast / reinterpret_cast / const_cast so the kind is greppable.",
|
|
34388
|
+
create(_context) {
|
|
34389
|
+
return {};
|
|
34390
|
+
},
|
|
34391
|
+
analyze(_context, facts) {
|
|
34392
|
+
const issues = [];
|
|
34393
|
+
const source = facts.v2?._source;
|
|
34394
|
+
if (!source) return issues;
|
|
34395
|
+
if (!/\.(cpp|cc|cxx|h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
|
|
34396
|
+
let m;
|
|
34397
|
+
C_STYLE_CAST_REGEX.lastIndex = 0;
|
|
34398
|
+
while ((m = C_STYLE_CAST_REGEX.exec(source)) !== null) {
|
|
34399
|
+
const innerMatch = /\(\s*([^)]+?)\s*\)/.exec(m[0]) ?? [];
|
|
34400
|
+
const inner = (innerMatch[1] ?? "").trim();
|
|
34401
|
+
const looksLikeCast = /\b(?:int|long|short|char|float|double|bool|unsigned|signed|size_t)\b/.test(inner) || /[*&]/.test(inner);
|
|
34402
|
+
if (!looksLikeCast) continue;
|
|
34403
|
+
const before = source.slice(Math.max(0, m.index - 40), m.index);
|
|
34404
|
+
if (NAMED_CAST_PREFIX_REGEX.test(before)) continue;
|
|
34405
|
+
const line = source.slice(0, m.index).split("\n").length;
|
|
34406
|
+
issues.push({
|
|
34407
|
+
ruleId: "cpp/c-style-cast",
|
|
34408
|
+
category: "typo",
|
|
34409
|
+
severity: "low",
|
|
34410
|
+
aiSpecific: true,
|
|
34411
|
+
message: `C-style cast at line ${line} \u2014 use static_cast / reinterpret_cast / const_cast`,
|
|
34412
|
+
line,
|
|
34413
|
+
column: 1,
|
|
34414
|
+
advice: "Use the named cast that matches the intent: `static_cast<int>(x)`, `reinterpret_cast<MyClass*>(p)`, `const_cast<...>(ref)`, or `dynamic_cast<Derived*>(base)` for runtime-checked downcasts. C-style casts silently pick whichever the compiler needs \u2014 `static_cast`, `reinterpret_cast`, OR `const_cast` \u2014 which makes them impossible to grep for and impossible to review. The C++ Core Guidelines (ES.49) call this out by name. AI agents reach for `(int)x` because of training-data C textbooks. Reference: cpp/c-style-cast v0.24."
|
|
34415
|
+
});
|
|
34416
|
+
}
|
|
34417
|
+
return issues;
|
|
34418
|
+
}
|
|
34419
|
+
});
|
|
34420
|
+
|
|
34421
|
+
// src/rules/cpp/magic-numbers.ts
|
|
34422
|
+
var MAGIC_NUMBER_REGEX = /\b(\d+(?:\.\d+)?)\b/g;
|
|
34423
|
+
var COMPARE_OR_RETURN_REGEX = /\b(?:if|while|return|for)\s*[\(\s]?[^;]*[<>=!][^;]*/;
|
|
34424
|
+
var cppMagicNumbersRule = createRule({
|
|
34425
|
+
id: "cpp/magic-numbers",
|
|
34426
|
+
category: "typo",
|
|
34427
|
+
severity: "low",
|
|
34428
|
+
aiSpecific: true,
|
|
34429
|
+
description: "Magic-number literal in a comparison / return \u2014 name it: `constexpr int MAX = 1024;`.",
|
|
34430
|
+
create(_context) {
|
|
34431
|
+
return {};
|
|
34432
|
+
},
|
|
34433
|
+
analyze(_context, facts) {
|
|
34434
|
+
const issues = [];
|
|
34435
|
+
const source = facts.v2?._source;
|
|
34436
|
+
if (!source) return issues;
|
|
34437
|
+
if (!/\.(cpp|cc|cxx|h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
|
|
34438
|
+
const lines = source.split("\n");
|
|
34439
|
+
const allowSet = /* @__PURE__ */ new Set([
|
|
34440
|
+
"1024",
|
|
34441
|
+
"65535",
|
|
34442
|
+
"65536",
|
|
34443
|
+
"86400",
|
|
34444
|
+
"3600",
|
|
34445
|
+
"1000",
|
|
34446
|
+
"100.0",
|
|
34447
|
+
"60",
|
|
34448
|
+
"24",
|
|
34449
|
+
"7",
|
|
34450
|
+
"365",
|
|
34451
|
+
"256",
|
|
34452
|
+
"255",
|
|
34453
|
+
"0",
|
|
34454
|
+
"1",
|
|
34455
|
+
"2",
|
|
34456
|
+
"3",
|
|
34457
|
+
"4",
|
|
34458
|
+
"5"
|
|
34459
|
+
]);
|
|
34460
|
+
for (let i = 0; i < lines.length; i++) {
|
|
34461
|
+
const line = lines[i] ?? "";
|
|
34462
|
+
if (!COMPARE_OR_RETURN_REGEX.test(line)) continue;
|
|
34463
|
+
let m;
|
|
34464
|
+
MAGIC_NUMBER_REGEX.lastIndex = 0;
|
|
34465
|
+
while ((m = MAGIC_NUMBER_REGEX.exec(line)) !== null) {
|
|
34466
|
+
const literal = m[0] ?? "";
|
|
34467
|
+
if (allowSet.has(literal)) continue;
|
|
34468
|
+
if (/\b(?:constexpr|const)\s+\w+\s*=\s*$/.test(line.slice(0, m.index).trimEnd())) continue;
|
|
34469
|
+
const prevLine = i > 0 ? lines[i - 1] ?? "" : "";
|
|
34470
|
+
if (/\b(?:constexpr|const)\s+\w+\s*=\s*$/.test(prevLine.trim())) continue;
|
|
34471
|
+
issues.push({
|
|
34472
|
+
ruleId: "cpp/magic-numbers",
|
|
34473
|
+
category: "typo",
|
|
34474
|
+
severity: "low",
|
|
34475
|
+
aiSpecific: true,
|
|
34476
|
+
message: `magic number ${literal} at line ${i + 1} \u2014 name it: constexpr int MAX = ${literal};`,
|
|
34477
|
+
line: i + 1,
|
|
34478
|
+
column: 1,
|
|
34479
|
+
advice: 'Replace the bare literal with a named constant declared as `constexpr int MAX_SIZE = 1024;` (or `static constexpr`). The named constant lives next to its value, can be searched, and forces the reader to mean what they say. Magic numbers in comparisons hide intent ("7" against what?). AI agents produce magic-number-heavy code because their training-data examples rarely bother to name the constant. Reference: cpp/magic-numbers v0.24.'
|
|
34480
|
+
});
|
|
34481
|
+
}
|
|
34482
|
+
}
|
|
34483
|
+
return issues;
|
|
34484
|
+
}
|
|
34485
|
+
});
|
|
34486
|
+
|
|
34487
|
+
// src/rules/cpp/printf-debug.ts
|
|
34488
|
+
var PRINTF_FAMILY_REGEX = /\b(?:printf|fprintf|sprintf|snprintf)\s*\(/g;
|
|
34489
|
+
var COUT_LITERAL_REGEX = /std\s*::\s*(?:cout|cerr|clog)\s*<<\s*"[^"]*"/g;
|
|
34490
|
+
var STD_COUT_BARE_REGEX = /std\s*::\s*(?:cout|cerr|clog)\s*<<\s*'[^']*'/g;
|
|
34491
|
+
var THRESHOLD_DEFAULT = 1;
|
|
34492
|
+
var cppPrintfDebugRule = createRule({
|
|
34493
|
+
id: "cpp/printf-debug",
|
|
34494
|
+
category: "typo",
|
|
34495
|
+
severity: "low",
|
|
34496
|
+
aiSpecific: true,
|
|
34497
|
+
description: "printf family or std::cout debug printing \u2014 use spdlog / glog / AbslLog for level-controlled output.",
|
|
34498
|
+
create(_context) {
|
|
34499
|
+
return { threshold: THRESHOLD_DEFAULT };
|
|
34500
|
+
},
|
|
34501
|
+
analyze(context, facts) {
|
|
34502
|
+
const issues = [];
|
|
34503
|
+
const source = facts.v2?._source;
|
|
34504
|
+
if (!source) return issues;
|
|
34505
|
+
if (!/\.(cpp|cc|cxx|h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
|
|
34506
|
+
const printfCount = (source.match(PRINTF_FAMILY_REGEX) ?? []).length;
|
|
34507
|
+
let viaPrintf = false;
|
|
34508
|
+
if (printfCount > context.threshold) {
|
|
34509
|
+
const line = source.indexOf("printf");
|
|
34510
|
+
if (line !== -1) viaPrintf = true;
|
|
34511
|
+
}
|
|
34512
|
+
const coutHit = COUT_LITERAL_REGEX.test(source) || STD_COUT_BARE_REGEX.test(source);
|
|
34513
|
+
if (!viaPrintf && !coutHit) return issues;
|
|
34514
|
+
issues.push({
|
|
34515
|
+
ruleId: "cpp/printf-debug",
|
|
34516
|
+
category: "typo",
|
|
34517
|
+
severity: "low",
|
|
34518
|
+
aiSpecific: true,
|
|
34519
|
+
message: viaPrintf ? `${printfCount} printf-family calls \u2014 use spdlog / glog / AbslLog` : "std::cout/cerr/clog with a string literal \u2014 use spdlog / glog / AbslLog",
|
|
34520
|
+
line: 1,
|
|
34521
|
+
column: 1,
|
|
34522
|
+
advice: "Replace with `spdlog::info(...)`, `LOG(INFO) << ...` (glog), or `ABSL_LOG(INFO) << ...` (Abseil). All of these are level-aware and have a configurable sink, and they route to stderr by default. `printf` / `std::cout` have no levels, no redaction, no sink routing, and can't be silenced in release builds. AI agents reach for these because their training data has countless C++ textbook examples with them. Reference: cpp/printf-debug v0.24."
|
|
34523
|
+
});
|
|
34524
|
+
return issues;
|
|
34525
|
+
}
|
|
34526
|
+
});
|
|
34527
|
+
|
|
34528
|
+
// src/rules/cpp/raw-new-delete.ts
|
|
34529
|
+
var NEW_TYPE_REGEX = /\bnew\s+[A-Za-z_]\w*\s*\(/g;
|
|
34530
|
+
var DELETE_REGEX = /\bdelete\s+/g;
|
|
34531
|
+
var MIN_PAIRS_DEFAULT = 1;
|
|
34532
|
+
var cppRawNewDeleteRule = createRule({
|
|
34533
|
+
id: "cpp/raw-new-delete",
|
|
34534
|
+
category: "typo",
|
|
34535
|
+
severity: "low",
|
|
34536
|
+
aiSpecific: true,
|
|
34537
|
+
description: "Manual `new` / `delete` pairing \u2014 use std::make_unique / std::unique_ptr instead.",
|
|
34538
|
+
create(_context) {
|
|
34539
|
+
return { minPairs: MIN_PAIRS_DEFAULT };
|
|
34540
|
+
},
|
|
34541
|
+
analyze(context, facts) {
|
|
34542
|
+
const issues = [];
|
|
34543
|
+
const source = facts.v2?._source;
|
|
34544
|
+
if (!source) return issues;
|
|
34545
|
+
if (!/\.(cpp|cc|cxx|h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
|
|
34546
|
+
let newCount = 0;
|
|
34547
|
+
let m;
|
|
34548
|
+
NEW_TYPE_REGEX.lastIndex = 0;
|
|
34549
|
+
while ((m = NEW_TYPE_REGEX.exec(source)) !== null) newCount++;
|
|
34550
|
+
let deleteCount = 0;
|
|
34551
|
+
DELETE_REGEX.lastIndex = 0;
|
|
34552
|
+
while ((m = DELETE_REGEX.exec(source)) !== null) deleteCount++;
|
|
34553
|
+
if (newCount <= context.minPairs) return issues;
|
|
34554
|
+
if (deleteCount <= context.minPairs) return issues;
|
|
34555
|
+
issues.push({
|
|
34556
|
+
ruleId: "cpp/raw-new-delete",
|
|
34557
|
+
category: "typo",
|
|
34558
|
+
severity: "low",
|
|
34559
|
+
aiSpecific: true,
|
|
34560
|
+
message: `manual new/delete pairing (${newCount} new, ${deleteCount} delete) \u2014 wrap in std::unique_ptr`,
|
|
34561
|
+
line: 1,
|
|
34562
|
+
column: 1,
|
|
34563
|
+
advice: "Replace `T* p = new T(...); ... delete p;` with `auto p = std::make_unique<T>(...);`. Smart pointers delete on scope exit, never leak on early return / exception, never double-free on copy. The C++ Core Guidelines R.20-R.23 call out raw `new` / `delete` as something to avoid in application code. AI agents reach for `new` / `delete` because their training data was written for pre-C++11 idioms. Reference: cpp/raw-new-delete v0.24."
|
|
34564
|
+
});
|
|
34565
|
+
return issues;
|
|
34566
|
+
}
|
|
34567
|
+
});
|
|
34568
|
+
|
|
34569
|
+
// src/rules/cpp/using-namespace-std.ts
|
|
34570
|
+
var USING_NAMESPACE_STD_REGEX = /\busing\s+namespace\s+std\s*;/g;
|
|
34571
|
+
var cppUsingNamespaceStdRule = createRule({
|
|
34572
|
+
id: "cpp/using-namespace-std",
|
|
34573
|
+
category: "logic",
|
|
34574
|
+
severity: "medium",
|
|
34575
|
+
aiSpecific: true,
|
|
34576
|
+
description: "`using namespace std;` in a header \u2014 pollutes every consumer. Use `using std::foo;` or qualify names.",
|
|
34577
|
+
create(_context) {
|
|
34578
|
+
return {};
|
|
34579
|
+
},
|
|
34580
|
+
analyze(_context, facts) {
|
|
34581
|
+
const issues = [];
|
|
34582
|
+
const source = facts.v2?._source;
|
|
34583
|
+
if (!source) return issues;
|
|
34584
|
+
if (!/\.(cpp|cc|cxx|h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
|
|
34585
|
+
if (!/\.(h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
|
|
34586
|
+
let m;
|
|
34587
|
+
USING_NAMESPACE_STD_REGEX.lastIndex = 0;
|
|
34588
|
+
while ((m = USING_NAMESPACE_STD_REGEX.exec(source)) !== null) {
|
|
34589
|
+
const line = source.slice(0, m.index).split("\n").length;
|
|
34590
|
+
issues.push({
|
|
34591
|
+
ruleId: "cpp/using-namespace-std",
|
|
34592
|
+
category: "logic",
|
|
34593
|
+
severity: "medium",
|
|
34594
|
+
aiSpecific: true,
|
|
34595
|
+
message: `using namespace std; at line ${line} \u2014 pollutes every consumer of this header`,
|
|
34596
|
+
line,
|
|
34597
|
+
column: 1,
|
|
34598
|
+
advice: "Remove the directive and qualify names with `std::`, or use a targeted `using std::cout;` / `using std::string;` for the few names that recur. The C++ Core Guidelines (SF.6 / SF.7) explicitly forbid `using namespace` in header files. A single `using namespace std;` in a widely-included header pulls 200+ names into every translation unit \u2014 downstream code, name-lookup, and ADL silently change. AI agents default to `using namespace std;` because their training snippets include countless copy-pasteable textbook examples that start with it. Reference: cpp/using-namespace-std v0.24."
|
|
34599
|
+
});
|
|
34600
|
+
}
|
|
34601
|
+
return issues;
|
|
34602
|
+
}
|
|
34603
|
+
});
|
|
34604
|
+
|
|
34379
34605
|
// src/rules/db/duplicate-index.ts
|
|
34380
34606
|
import { parseSync as parseSql, loadModule as loadSqlModule } from "pgsql-parser";
|
|
34381
34607
|
var moduleReady = loadSqlModule();
|
|
@@ -36108,6 +36334,480 @@ var dupIdenticalBlockRule = createRule({
|
|
|
36108
36334
|
}
|
|
36109
36335
|
});
|
|
36110
36336
|
|
|
36337
|
+
// src/engine/dedup/tokenize.ts
|
|
36338
|
+
import { createHash as createHash3 } from "crypto";
|
|
36339
|
+
var DEFAULT_CONFIG = { k: 5 };
|
|
36340
|
+
function stripComments(source) {
|
|
36341
|
+
let out = "";
|
|
36342
|
+
let i = 0;
|
|
36343
|
+
while (i < source.length) {
|
|
36344
|
+
const ch = source[i];
|
|
36345
|
+
const next = source[i + 1];
|
|
36346
|
+
if (ch === "/" && next === "/") {
|
|
36347
|
+
while (i < source.length && source[i] !== "\n") i++;
|
|
36348
|
+
continue;
|
|
36349
|
+
}
|
|
36350
|
+
if (ch === "/" && next === "*") {
|
|
36351
|
+
i += 2;
|
|
36352
|
+
while (i < source.length && !(source[i] === "*" && source[i + 1] === "/")) i++;
|
|
36353
|
+
i += 2;
|
|
36354
|
+
continue;
|
|
36355
|
+
}
|
|
36356
|
+
out += ch;
|
|
36357
|
+
i++;
|
|
36358
|
+
}
|
|
36359
|
+
return out;
|
|
36360
|
+
}
|
|
36361
|
+
function tokenize(source) {
|
|
36362
|
+
return source.toLowerCase().split(/[^a-z0-9_]+/u).filter((t) => t.length > 0);
|
|
36363
|
+
}
|
|
36364
|
+
function shingleHash(shingle) {
|
|
36365
|
+
const h = createHash3("sha1").update(shingle).digest();
|
|
36366
|
+
return (h[0] ?? 0) << 24 | (h[1] ?? 0) << 16 | (h[2] ?? 0) << 8 | (h[3] ?? 0);
|
|
36367
|
+
}
|
|
36368
|
+
function shingleSet(source, config = {}) {
|
|
36369
|
+
const { k } = { ...DEFAULT_CONFIG, ...config };
|
|
36370
|
+
const tokens = tokenize(stripComments(source));
|
|
36371
|
+
if (tokens.length === 0) return /* @__PURE__ */ new Set();
|
|
36372
|
+
if (tokens.length < k) {
|
|
36373
|
+
return /* @__PURE__ */ new Set([shingleHash(tokens.join(" "))]);
|
|
36374
|
+
}
|
|
36375
|
+
const out = /* @__PURE__ */ new Set();
|
|
36376
|
+
for (let i = 0; i + k <= tokens.length; i++) {
|
|
36377
|
+
out.add(shingleHash(tokens.slice(i, i + k).join(" ")));
|
|
36378
|
+
}
|
|
36379
|
+
return out;
|
|
36380
|
+
}
|
|
36381
|
+
|
|
36382
|
+
// src/engine/dedup/minhash.ts
|
|
36383
|
+
import { createHash as createHash4 } from "crypto";
|
|
36384
|
+
var PRIME = (1n << 61n) - 1n;
|
|
36385
|
+
function mix64(x) {
|
|
36386
|
+
x ^= x >> 33n;
|
|
36387
|
+
x = x * 0xff51afd7ed558ccdn & 0xffffffffffffffffn;
|
|
36388
|
+
x ^= x >> 33n;
|
|
36389
|
+
x = x * 0xc4ceb9fe1a85ec53n & 0xffffffffffffffffn;
|
|
36390
|
+
x ^= x >> 33n;
|
|
36391
|
+
return x;
|
|
36392
|
+
}
|
|
36393
|
+
function bmod(a, m) {
|
|
36394
|
+
const r = a % m;
|
|
36395
|
+
return r < 0n ? r + m : r;
|
|
36396
|
+
}
|
|
36397
|
+
function hashParams(i) {
|
|
36398
|
+
const a = mix64(BigInt(i) * 0x9e3779b97f4a7c15n) & 0xffffffffffffffffn | 1n;
|
|
36399
|
+
const b = mix64(BigInt(i) * 0xbf58476d1ce4e5b9n) & 0xffffffffffffffffn;
|
|
36400
|
+
return [a, b];
|
|
36401
|
+
}
|
|
36402
|
+
function hashAt(i, x) {
|
|
36403
|
+
const [a, b] = hashParams(i);
|
|
36404
|
+
const xb = BigInt(x >>> 0);
|
|
36405
|
+
const combined = a * xb + b;
|
|
36406
|
+
const h = Number(bmod(combined, PRIME) & 0xffffffffn);
|
|
36407
|
+
return h >>> 0;
|
|
36408
|
+
}
|
|
36409
|
+
var DEFAULT_CONFIG2 = {
|
|
36410
|
+
numHashes: 128
|
|
36411
|
+
};
|
|
36412
|
+
function minHash(tokens, config = {}) {
|
|
36413
|
+
const { numHashes } = { ...DEFAULT_CONFIG2, ...config };
|
|
36414
|
+
const sig = new Uint32Array(numHashes);
|
|
36415
|
+
sig.fill(4294967295);
|
|
36416
|
+
const tokenIter = tokens instanceof Set ? tokens : Array.from(tokens);
|
|
36417
|
+
for (const t of tokenIter) {
|
|
36418
|
+
for (let i = 0; i < numHashes; i++) {
|
|
36419
|
+
const h = hashAt(i, t);
|
|
36420
|
+
if (h < (sig[i] ?? 4294967295)) sig[i] = h;
|
|
36421
|
+
}
|
|
36422
|
+
}
|
|
36423
|
+
return sig;
|
|
36424
|
+
}
|
|
36425
|
+
function minHashSimilarity(a, b) {
|
|
36426
|
+
if (a.length !== b.length) {
|
|
36427
|
+
throw new Error(`minHashSimilarity: length mismatch ${a.length} vs ${b.length}`);
|
|
36428
|
+
}
|
|
36429
|
+
if (a.length === 0) return 1;
|
|
36430
|
+
let matches = 0;
|
|
36431
|
+
for (let i = 0; i < a.length; i++) {
|
|
36432
|
+
if (a[i] === b[i]) matches++;
|
|
36433
|
+
}
|
|
36434
|
+
return matches / a.length;
|
|
36435
|
+
}
|
|
36436
|
+
|
|
36437
|
+
// src/rules/dup/near-duplicate.ts
|
|
36438
|
+
var DEFAULT_CONTEXT = {
|
|
36439
|
+
threshold: 0.7,
|
|
36440
|
+
k: 5
|
|
36441
|
+
};
|
|
36442
|
+
var signatureCache = /* @__PURE__ */ new Map();
|
|
36443
|
+
var nearDuplicateRule = createRule({
|
|
36444
|
+
id: "dup/near-duplicate",
|
|
36445
|
+
category: "logic",
|
|
36446
|
+
severity: "medium",
|
|
36447
|
+
aiSpecific: true,
|
|
36448
|
+
description: "Near-duplicate file (Type-2 clone) \u2014 token-set Jaccard similarity > 0.7 after MinHash. Catches AI whitespace/comment refactors.",
|
|
36449
|
+
create(_context) {
|
|
36450
|
+
return { ...DEFAULT_CONTEXT };
|
|
36451
|
+
},
|
|
36452
|
+
analyze(context, facts) {
|
|
36453
|
+
const ctx = { ...DEFAULT_CONTEXT, ...context };
|
|
36454
|
+
const issues = [];
|
|
36455
|
+
const source = facts.v2?._source;
|
|
36456
|
+
if (!source) return issues;
|
|
36457
|
+
const filePath = facts.filePath;
|
|
36458
|
+
if (!filePath) return issues;
|
|
36459
|
+
const shingles = shingleSet(source, { k: ctx.k });
|
|
36460
|
+
if (shingles.size === 0) return issues;
|
|
36461
|
+
const sig = minHash(shingles);
|
|
36462
|
+
const matched = [];
|
|
36463
|
+
for (const [prevPath, prevSig] of signatureCache.entries()) {
|
|
36464
|
+
const sim = minHashSimilarity(sig, prevSig);
|
|
36465
|
+
if (sim >= ctx.threshold) {
|
|
36466
|
+
matched.push({ path: prevPath, similarity: sim });
|
|
36467
|
+
}
|
|
36468
|
+
}
|
|
36469
|
+
if (matched.length > 0) {
|
|
36470
|
+
matched.sort((a, b) => b.similarity - a.similarity);
|
|
36471
|
+
const best = matched[0];
|
|
36472
|
+
issues.push({
|
|
36473
|
+
ruleId: "dup/near-duplicate",
|
|
36474
|
+
category: "logic",
|
|
36475
|
+
severity: "medium",
|
|
36476
|
+
aiSpecific: true,
|
|
36477
|
+
message: `Near-duplicate of ${best.path} (Jaccard ${(best.similarity * 100).toFixed(1)}%, threshold ${(ctx.threshold * 100).toFixed(0)}%)`,
|
|
36478
|
+
line: 1,
|
|
36479
|
+
column: 1,
|
|
36480
|
+
advice: "Refactor to share the implementation, or rename/restructure to make the difference intentional. AI agents produce near-duplicates when iterating \u2014 this is the v0.23.0 Type-2 clone detector. Reference: dup/near-duplicate v0.23."
|
|
36481
|
+
});
|
|
36482
|
+
}
|
|
36483
|
+
signatureCache.set(filePath, sig);
|
|
36484
|
+
return issues;
|
|
36485
|
+
}
|
|
36486
|
+
});
|
|
36487
|
+
|
|
36488
|
+
// src/engine/dedup/lsh.ts
|
|
36489
|
+
import { createHash as createHash5 } from "crypto";
|
|
36490
|
+
|
|
36491
|
+
// src/engine/dedup/structural-clone.ts
|
|
36492
|
+
function stripComments2(source) {
|
|
36493
|
+
let out = "";
|
|
36494
|
+
let i = 0;
|
|
36495
|
+
while (i < source.length) {
|
|
36496
|
+
const ch = source[i];
|
|
36497
|
+
const next = source[i + 1];
|
|
36498
|
+
if (ch === "/" && next === "/") {
|
|
36499
|
+
while (i < source.length && source[i] !== "\n") i++;
|
|
36500
|
+
continue;
|
|
36501
|
+
}
|
|
36502
|
+
if (ch === "/" && next === "*") {
|
|
36503
|
+
i += 2;
|
|
36504
|
+
while (i < source.length && !(source[i] === "*" && source[i + 1] === "/")) i++;
|
|
36505
|
+
i += 2;
|
|
36506
|
+
continue;
|
|
36507
|
+
}
|
|
36508
|
+
out += ch;
|
|
36509
|
+
i++;
|
|
36510
|
+
}
|
|
36511
|
+
return out;
|
|
36512
|
+
}
|
|
36513
|
+
var TOKEN_REGEX = /([A-Za-z_$][A-Za-z0-9_$]*)|(0[xX][0-9A-Fa-f]+|\d+(?:\.\d+)?(?:[eE][+-]?\d+)?[fFdDlL]?)|([(){}\[\];,.<>=+\-*/%!&|^?:])|"((?:[^"\\\n]|\\.)*)"|'((?:[^'\\\n]|\\.)*)'|`((?:[^`\\]|\\.)*)`/g;
|
|
36514
|
+
var BOOL_TOKENS = /* @__PURE__ */ new Set(["true", "false", "null", "none", "nil"]);
|
|
36515
|
+
var KEYWORDS = /* @__PURE__ */ new Set([
|
|
36516
|
+
// Control flow
|
|
36517
|
+
"if",
|
|
36518
|
+
"else",
|
|
36519
|
+
"for",
|
|
36520
|
+
"while",
|
|
36521
|
+
"do",
|
|
36522
|
+
"switch",
|
|
36523
|
+
"case",
|
|
36524
|
+
"default",
|
|
36525
|
+
"break",
|
|
36526
|
+
"continue",
|
|
36527
|
+
"return",
|
|
36528
|
+
"goto",
|
|
36529
|
+
// Declarations
|
|
36530
|
+
"function",
|
|
36531
|
+
"class",
|
|
36532
|
+
"interface",
|
|
36533
|
+
"struct",
|
|
36534
|
+
"enum",
|
|
36535
|
+
"trait",
|
|
36536
|
+
"def",
|
|
36537
|
+
"fn",
|
|
36538
|
+
"fun",
|
|
36539
|
+
"func",
|
|
36540
|
+
"var",
|
|
36541
|
+
"let",
|
|
36542
|
+
"const",
|
|
36543
|
+
"val",
|
|
36544
|
+
"mod",
|
|
36545
|
+
"use",
|
|
36546
|
+
"pub",
|
|
36547
|
+
"impl",
|
|
36548
|
+
"package",
|
|
36549
|
+
"module",
|
|
36550
|
+
"lambda",
|
|
36551
|
+
// Modifiers / visibility
|
|
36552
|
+
"public",
|
|
36553
|
+
"private",
|
|
36554
|
+
"protected",
|
|
36555
|
+
"static",
|
|
36556
|
+
"abstract",
|
|
36557
|
+
"final",
|
|
36558
|
+
"sealed",
|
|
36559
|
+
"override",
|
|
36560
|
+
"virtual",
|
|
36561
|
+
"readonly",
|
|
36562
|
+
"async",
|
|
36563
|
+
"await",
|
|
36564
|
+
"synchronized",
|
|
36565
|
+
"volatile",
|
|
36566
|
+
"transient",
|
|
36567
|
+
"native",
|
|
36568
|
+
"strictfp",
|
|
36569
|
+
"inline",
|
|
36570
|
+
"external",
|
|
36571
|
+
"export",
|
|
36572
|
+
"import",
|
|
36573
|
+
// Type system
|
|
36574
|
+
"type",
|
|
36575
|
+
"interface",
|
|
36576
|
+
"record",
|
|
36577
|
+
"object",
|
|
36578
|
+
"enum",
|
|
36579
|
+
"data",
|
|
36580
|
+
// Pattern / dispatch
|
|
36581
|
+
"match",
|
|
36582
|
+
"select",
|
|
36583
|
+
"when",
|
|
36584
|
+
"where",
|
|
36585
|
+
// Error handling
|
|
36586
|
+
"try",
|
|
36587
|
+
"catch",
|
|
36588
|
+
"finally",
|
|
36589
|
+
"throw",
|
|
36590
|
+
"throws",
|
|
36591
|
+
"raise",
|
|
36592
|
+
"rescue",
|
|
36593
|
+
"ensure",
|
|
36594
|
+
"except",
|
|
36595
|
+
// Logical connectives and tests
|
|
36596
|
+
"and",
|
|
36597
|
+
"or",
|
|
36598
|
+
"not",
|
|
36599
|
+
"in",
|
|
36600
|
+
"is",
|
|
36601
|
+
"as",
|
|
36602
|
+
"instanceof",
|
|
36603
|
+
"typeof",
|
|
36604
|
+
"delete",
|
|
36605
|
+
"void",
|
|
36606
|
+
"new",
|
|
36607
|
+
"this",
|
|
36608
|
+
"super",
|
|
36609
|
+
"self",
|
|
36610
|
+
"yield",
|
|
36611
|
+
// Misc
|
|
36612
|
+
"with",
|
|
36613
|
+
"without",
|
|
36614
|
+
"pass",
|
|
36615
|
+
"assert",
|
|
36616
|
+
"mut",
|
|
36617
|
+
"move",
|
|
36618
|
+
"ref",
|
|
36619
|
+
"out",
|
|
36620
|
+
"params",
|
|
36621
|
+
"operator",
|
|
36622
|
+
"sizeof",
|
|
36623
|
+
"typedef",
|
|
36624
|
+
"union",
|
|
36625
|
+
"register",
|
|
36626
|
+
"extern",
|
|
36627
|
+
"volatile",
|
|
36628
|
+
"goto",
|
|
36629
|
+
// Python-specific
|
|
36630
|
+
"elif",
|
|
36631
|
+
"global",
|
|
36632
|
+
"nonlocal",
|
|
36633
|
+
"del",
|
|
36634
|
+
"with",
|
|
36635
|
+
// Ruby-specific
|
|
36636
|
+
"unless",
|
|
36637
|
+
"until",
|
|
36638
|
+
"begin",
|
|
36639
|
+
"next",
|
|
36640
|
+
"then",
|
|
36641
|
+
"end",
|
|
36642
|
+
"alias",
|
|
36643
|
+
"defined",
|
|
36644
|
+
"super",
|
|
36645
|
+
"yield",
|
|
36646
|
+
"self",
|
|
36647
|
+
// Kotlin-specific
|
|
36648
|
+
"suspend",
|
|
36649
|
+
"lateinit",
|
|
36650
|
+
"companion",
|
|
36651
|
+
"internal",
|
|
36652
|
+
"open",
|
|
36653
|
+
// Rust-specific
|
|
36654
|
+
"unsafe",
|
|
36655
|
+
"dyn",
|
|
36656
|
+
"box",
|
|
36657
|
+
"crate",
|
|
36658
|
+
"where",
|
|
36659
|
+
// Swift-specific
|
|
36660
|
+
"guard",
|
|
36661
|
+
"defer",
|
|
36662
|
+
"repeat",
|
|
36663
|
+
"init",
|
|
36664
|
+
"deinit",
|
|
36665
|
+
"extension",
|
|
36666
|
+
"protocol",
|
|
36667
|
+
"throws",
|
|
36668
|
+
"rethrows",
|
|
36669
|
+
"weak",
|
|
36670
|
+
"unowned",
|
|
36671
|
+
"lazy"
|
|
36672
|
+
]);
|
|
36673
|
+
function canonicalTokens(source) {
|
|
36674
|
+
const stripped = stripComments2(source);
|
|
36675
|
+
const out = [];
|
|
36676
|
+
TOKEN_REGEX.lastIndex = 0;
|
|
36677
|
+
let m;
|
|
36678
|
+
while ((m = TOKEN_REGEX.exec(stripped)) !== null) {
|
|
36679
|
+
const [, ident, num, punct, dq, sq, bq] = m;
|
|
36680
|
+
if (ident !== void 0) {
|
|
36681
|
+
const lower = ident.toLowerCase();
|
|
36682
|
+
if (BOOL_TOKENS.has(lower)) {
|
|
36683
|
+
out.push("bool");
|
|
36684
|
+
} else if (KEYWORDS.has(lower)) {
|
|
36685
|
+
out.push(lower);
|
|
36686
|
+
} else {
|
|
36687
|
+
out.push("id");
|
|
36688
|
+
}
|
|
36689
|
+
continue;
|
|
36690
|
+
}
|
|
36691
|
+
if (num !== void 0) {
|
|
36692
|
+
out.push("num");
|
|
36693
|
+
continue;
|
|
36694
|
+
}
|
|
36695
|
+
if (punct !== void 0) {
|
|
36696
|
+
out.push(punct);
|
|
36697
|
+
continue;
|
|
36698
|
+
}
|
|
36699
|
+
if (dq !== void 0 || sq !== void 0 || bq !== void 0) {
|
|
36700
|
+
out.push("str");
|
|
36701
|
+
continue;
|
|
36702
|
+
}
|
|
36703
|
+
}
|
|
36704
|
+
return out;
|
|
36705
|
+
}
|
|
36706
|
+
function shingleHash2(shingle) {
|
|
36707
|
+
let h = 2166136261;
|
|
36708
|
+
for (let i = 0; i < shingle.length; i++) {
|
|
36709
|
+
h ^= shingle.charCodeAt(i);
|
|
36710
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
36711
|
+
}
|
|
36712
|
+
return h >>> 0;
|
|
36713
|
+
}
|
|
36714
|
+
var DEFAULT_STRUCTURAL_CONFIG = {
|
|
36715
|
+
k: 8,
|
|
36716
|
+
minTokens: 60
|
|
36717
|
+
};
|
|
36718
|
+
function structuralShingles(source, config = {}) {
|
|
36719
|
+
const { k, minTokens } = { ...DEFAULT_STRUCTURAL_CONFIG, ...config };
|
|
36720
|
+
const tokens = canonicalTokens(source);
|
|
36721
|
+
if (tokens.length < minTokens) return /* @__PURE__ */ new Set();
|
|
36722
|
+
if (tokens.length < k) {
|
|
36723
|
+
return /* @__PURE__ */ new Set([shingleHash2(tokens.join(" "))]);
|
|
36724
|
+
}
|
|
36725
|
+
const out = /* @__PURE__ */ new Set();
|
|
36726
|
+
for (let i = 0; i + k <= tokens.length; i++) {
|
|
36727
|
+
out.add(shingleHash2(tokens.slice(i, i + k).join(" ")));
|
|
36728
|
+
}
|
|
36729
|
+
return out;
|
|
36730
|
+
}
|
|
36731
|
+
function structuralSignature(source, config = {}) {
|
|
36732
|
+
return minHash(structuralShingles(source, config));
|
|
36733
|
+
}
|
|
36734
|
+
|
|
36735
|
+
// src/rules/dup/structural-clone.ts
|
|
36736
|
+
var DEFAULT_CONTEXT2 = {
|
|
36737
|
+
structuralThreshold: 0.55,
|
|
36738
|
+
verifyThreshold: 0.45,
|
|
36739
|
+
kStruct: 8,
|
|
36740
|
+
kIdent: 5,
|
|
36741
|
+
minTokens: 60,
|
|
36742
|
+
minHits: 1
|
|
36743
|
+
};
|
|
36744
|
+
var structuralCache = /* @__PURE__ */ new Map();
|
|
36745
|
+
var identifierCache = /* @__PURE__ */ new Map();
|
|
36746
|
+
var structuralCloneRule = createRule({
|
|
36747
|
+
id: "dup/structural-clone",
|
|
36748
|
+
category: "logic",
|
|
36749
|
+
severity: "medium",
|
|
36750
|
+
aiSpecific: true,
|
|
36751
|
+
defaultOff: true,
|
|
36752
|
+
description: "Structural clone (Type-3) \u2014 same shape after identifier canonicalization with possible added/removed statements. Two-stage MinHash: canonical + identifier.",
|
|
36753
|
+
create(_context) {
|
|
36754
|
+
return { ...DEFAULT_CONTEXT2 };
|
|
36755
|
+
},
|
|
36756
|
+
analyze(context, facts) {
|
|
36757
|
+
const ctx = { ...DEFAULT_CONTEXT2, ...context };
|
|
36758
|
+
const issues = [];
|
|
36759
|
+
const source = facts.v2?._source;
|
|
36760
|
+
if (!source) return issues;
|
|
36761
|
+
const filePath = facts.filePath;
|
|
36762
|
+
if (!filePath) return issues;
|
|
36763
|
+
const sigStruct = structuralSignature(source, {
|
|
36764
|
+
k: ctx.kStruct,
|
|
36765
|
+
minTokens: ctx.minTokens
|
|
36766
|
+
});
|
|
36767
|
+
if (sigStruct.every((v) => v === 4294967295)) return issues;
|
|
36768
|
+
const identShingles = shingleSet(source, { k: ctx.kIdent });
|
|
36769
|
+
if (identShingles.size === 0) return issues;
|
|
36770
|
+
const sigIdent = minHash(identShingles);
|
|
36771
|
+
const matched = [];
|
|
36772
|
+
for (const [prevPath, prevStruct] of structuralCache.entries()) {
|
|
36773
|
+
if (prevPath === filePath) continue;
|
|
36774
|
+
const simStruct = minHashSimilarity(sigStruct, prevStruct);
|
|
36775
|
+
if (simStruct < ctx.structuralThreshold) continue;
|
|
36776
|
+
const prevIdent = identifierCache.get(prevPath);
|
|
36777
|
+
if (!prevIdent) continue;
|
|
36778
|
+
const simIdent = minHashSimilarity(sigIdent, prevIdent);
|
|
36779
|
+
if (simIdent <= 0) continue;
|
|
36780
|
+
const score = 2 * simStruct * simIdent / (simStruct + simIdent + 1e-9);
|
|
36781
|
+
if (score >= ctx.verifyThreshold) {
|
|
36782
|
+
matched.push({ path: prevPath, score, simStruct, simIdent });
|
|
36783
|
+
}
|
|
36784
|
+
}
|
|
36785
|
+
if (matched.length >= ctx.minHits) {
|
|
36786
|
+
matched.sort((a, b) => b.score - a.score);
|
|
36787
|
+
const best = matched[0];
|
|
36788
|
+
issues.push({
|
|
36789
|
+
ruleId: "dup/structural-clone",
|
|
36790
|
+
category: "logic",
|
|
36791
|
+
severity: "medium",
|
|
36792
|
+
aiSpecific: true,
|
|
36793
|
+
message: `Structural clone of ${best.path} (canonical Jaccard ${(best.simStruct * 100).toFixed(1)}%, identifier Jaccard ${(best.simIdent * 100).toFixed(1)}%, blend ${(best.score * 100).toFixed(1)}% \u2014 threshold ${(ctx.verifyThreshold * 100).toFixed(0)}%)`,
|
|
36794
|
+
line: 1,
|
|
36795
|
+
column: 1,
|
|
36796
|
+
advice: "Refactor to a shared helper. This is a Type-3 clone \u2014 two files share canonical shape after identifier normalization and likely a few added/removed statements. AI agents produce Type-3 clones when iterating on a function: same control flow, different names, a few lines added. (v0.24.0 \u2014 two-stage MinHash on canonical k=8 + identifier k=5 shingles.)",
|
|
36797
|
+
extras: {
|
|
36798
|
+
structuralDuplicateOf: {
|
|
36799
|
+
file: best.path,
|
|
36800
|
+
score: best.score
|
|
36801
|
+
}
|
|
36802
|
+
}
|
|
36803
|
+
});
|
|
36804
|
+
}
|
|
36805
|
+
structuralCache.set(filePath, sigStruct);
|
|
36806
|
+
identifierCache.set(filePath, sigIdent);
|
|
36807
|
+
return issues;
|
|
36808
|
+
}
|
|
36809
|
+
});
|
|
36810
|
+
|
|
36111
36811
|
// src/rules/go/error-wrap-without-context.ts
|
|
36112
36812
|
var ERR_WRAP_REGEX = /fmt\.Errorf\(\s*"((?:[^"\\]|\\.)*)"\s*,\s*([^)]+)\)/g;
|
|
36113
36813
|
var GENERIC_PREFIX_REGEX = /^\s*(?:error|err|failed|wrap(?:ping)?|invalid)\s*(?::\s*)?%w\b/i;
|
|
@@ -36365,84 +37065,255 @@ var javaLegacyDateApiRule = createRule({
|
|
|
36365
37065
|
advice: "Replace `java.util.Date` / `java.util.Calendar` with `java.time` (`LocalDate`, `LocalDateTime`, `Instant`, `ZonedDateTime`). java.time is immutable, thread-safe, and has a much better API. AI agents default to the legacy API because their training data predates Java 8 (2014). Reference: java/legacy-date-api v0.20."
|
|
36366
37066
|
});
|
|
36367
37067
|
}
|
|
36368
|
-
LEGACY_USAGE_REGEX.lastIndex = 0;
|
|
36369
|
-
while ((m = LEGACY_USAGE_REGEX.exec(source)) !== null) {
|
|
36370
|
-
const line = source.slice(0, m.index).split("\n").length;
|
|
36371
|
-
if (flagged.has(line)) continue;
|
|
37068
|
+
LEGACY_USAGE_REGEX.lastIndex = 0;
|
|
37069
|
+
while ((m = LEGACY_USAGE_REGEX.exec(source)) !== null) {
|
|
37070
|
+
const line = source.slice(0, m.index).split("\n").length;
|
|
37071
|
+
if (flagged.has(line)) continue;
|
|
37072
|
+
issues.push({
|
|
37073
|
+
ruleId: "java/legacy-date-api",
|
|
37074
|
+
category: "typo",
|
|
37075
|
+
severity: "low",
|
|
37076
|
+
aiSpecific: true,
|
|
37077
|
+
message: `new Date() / GregorianCalendar at line ${line} \u2014 use java.time`,
|
|
37078
|
+
line,
|
|
37079
|
+
column: 1,
|
|
37080
|
+
advice: "Replace with `LocalDate.now()`, `Instant.now()`, or `ZonedDateTime.now()`. Reference: java/legacy-date-api v0.20."
|
|
37081
|
+
});
|
|
37082
|
+
}
|
|
37083
|
+
CALENDAR_GET_INSTANCE_REGEX.lastIndex = 0;
|
|
37084
|
+
while ((m = CALENDAR_GET_INSTANCE_REGEX.exec(source)) !== null) {
|
|
37085
|
+
const line = source.slice(0, m.index).split("\n").length;
|
|
37086
|
+
if (flagged.has(line)) continue;
|
|
37087
|
+
issues.push({
|
|
37088
|
+
ruleId: "java/legacy-date-api",
|
|
37089
|
+
category: "typo",
|
|
37090
|
+
severity: "low",
|
|
37091
|
+
aiSpecific: true,
|
|
37092
|
+
message: `Calendar.getInstance() at line ${line} \u2014 use java.time`,
|
|
37093
|
+
line,
|
|
37094
|
+
column: 1,
|
|
37095
|
+
advice: "Replace with `LocalDate.now()` (date-only) or `ZonedDateTime.now()`. Reference: java/legacy-date-api v0.20."
|
|
37096
|
+
});
|
|
37097
|
+
}
|
|
37098
|
+
return issues;
|
|
37099
|
+
}
|
|
37100
|
+
});
|
|
37101
|
+
|
|
37102
|
+
// src/rules/java/raw-type-overuse.ts
|
|
37103
|
+
var RAW_TYPE_REGEX = /\b(List|Map|Set|Collection|Iterable)\s+(?![<A-Z])(\w+)/g;
|
|
37104
|
+
var javaRawTypeOveruseRule = createRule({
|
|
37105
|
+
id: "java/raw-type-overuse",
|
|
37106
|
+
category: "typo",
|
|
37107
|
+
severity: "low",
|
|
37108
|
+
aiSpecific: true,
|
|
37109
|
+
description: "Raw type usage (List, Map, Set) \u2014 use generics (Effective Java, Item 23)",
|
|
37110
|
+
create(_context) {
|
|
37111
|
+
return {};
|
|
37112
|
+
},
|
|
37113
|
+
analyze(_context, facts) {
|
|
37114
|
+
const issues = [];
|
|
37115
|
+
const source = facts.v2?._source;
|
|
37116
|
+
if (!source) return issues;
|
|
37117
|
+
if (!/\.java$/i.test(facts.filePath)) return issues;
|
|
37118
|
+
let m;
|
|
37119
|
+
RAW_TYPE_REGEX.lastIndex = 0;
|
|
37120
|
+
while ((m = RAW_TYPE_REGEX.exec(source)) !== null) {
|
|
37121
|
+
const typeName = m[1];
|
|
37122
|
+
const line = source.slice(0, m.index).split("\n").length;
|
|
37123
|
+
issues.push({
|
|
37124
|
+
ruleId: "java/raw-type-overuse",
|
|
37125
|
+
category: "typo",
|
|
37126
|
+
severity: "low",
|
|
37127
|
+
aiSpecific: true,
|
|
37128
|
+
message: `Raw type ${typeName} at line ${line} \u2014 add type parameters`,
|
|
37129
|
+
line,
|
|
37130
|
+
column: 1,
|
|
37131
|
+
advice: `Replace raw \`${typeName}\` with \`${typeName}<...>\`. Raw types disable generic type checking. Effective Java, Item 23: 'Don't use raw types in new code'. AI agents default to raw types when unsure of the correct generic parameters. Reference: java/raw-type-overuse v0.20.`
|
|
37132
|
+
});
|
|
37133
|
+
}
|
|
37134
|
+
return issues;
|
|
37135
|
+
}
|
|
37136
|
+
});
|
|
37137
|
+
|
|
37138
|
+
// src/rules/java/string-concat-loop.ts
|
|
37139
|
+
var STRING_CONCAT_REGEX = /(\b\w+)\s*=\s*\1\s*\+\s*[^;]+;|(\b\w+)\s*\+=\s*['"`]/g;
|
|
37140
|
+
var javaStringConcatLoopRule = createRule({
|
|
37141
|
+
id: "java/string-concat-loop",
|
|
37142
|
+
category: "perf",
|
|
37143
|
+
severity: "low",
|
|
37144
|
+
aiSpecific: true,
|
|
37145
|
+
description: "String concatenation in a loop \u2014 use StringBuilder",
|
|
37146
|
+
create(_context) {
|
|
37147
|
+
return {};
|
|
37148
|
+
},
|
|
37149
|
+
analyze(_context, facts) {
|
|
37150
|
+
const issues = [];
|
|
37151
|
+
const source = facts.v2?._source;
|
|
37152
|
+
if (!source) return issues;
|
|
37153
|
+
if (!/\.java$/i.test(facts.filePath)) return issues;
|
|
37154
|
+
if (!/\b(for|while|do)\b/.test(source)) return issues;
|
|
37155
|
+
let m;
|
|
37156
|
+
STRING_CONCAT_REGEX.lastIndex = 0;
|
|
37157
|
+
while ((m = STRING_CONCAT_REGEX.exec(source)) !== null) {
|
|
37158
|
+
const line = source.slice(0, m.index).split("\n").length;
|
|
37159
|
+
issues.push({
|
|
37160
|
+
ruleId: "java/string-concat-loop",
|
|
37161
|
+
category: "perf",
|
|
37162
|
+
severity: "low",
|
|
37163
|
+
aiSpecific: true,
|
|
37164
|
+
message: `String concatenation in a loop at line ${line} \u2014 use StringBuilder`,
|
|
37165
|
+
line,
|
|
37166
|
+
column: 1,
|
|
37167
|
+
advice: "Declare a `StringBuilder` outside the loop: `StringBuilder sb = new StringBuilder(); sb.append(...);` then `return sb.toString();` after the loop. String concatenation in a loop is O(n\xB2) \u2014 each iteration copies the prior string. AI agents concatenate strings in loops because of training-data examples. Reference: java/string-concat-loop v0.20."
|
|
37168
|
+
});
|
|
37169
|
+
}
|
|
37170
|
+
return issues;
|
|
37171
|
+
}
|
|
37172
|
+
});
|
|
37173
|
+
|
|
37174
|
+
// src/rules/java/system-out-println.ts
|
|
37175
|
+
var PRINTLN_REGEX = /System\.out\.println\s*\(/g;
|
|
37176
|
+
var DEFAULT_THRESHOLD = 1;
|
|
37177
|
+
var javaSystemOutPrintlnRule = createRule({
|
|
37178
|
+
id: "java/system-out-println",
|
|
37179
|
+
category: "typo",
|
|
37180
|
+
severity: "low",
|
|
37181
|
+
aiSpecific: true,
|
|
37182
|
+
description: "System.out.println in production code \u2014 use a logger (SLF4J, Log4j, etc.)",
|
|
37183
|
+
create(_context) {
|
|
37184
|
+
return { threshold: DEFAULT_THRESHOLD };
|
|
37185
|
+
},
|
|
37186
|
+
analyze(context, facts) {
|
|
37187
|
+
const issues = [];
|
|
37188
|
+
const source = facts.v2?._source;
|
|
37189
|
+
if (!source) return issues;
|
|
37190
|
+
if (!/\.java$/i.test(facts.filePath)) return issues;
|
|
37191
|
+
const matches = [];
|
|
37192
|
+
let m;
|
|
37193
|
+
PRINTLN_REGEX.lastIndex = 0;
|
|
37194
|
+
while ((m = PRINTLN_REGEX.exec(source)) !== null) {
|
|
37195
|
+
matches.push(m.index);
|
|
37196
|
+
}
|
|
37197
|
+
if (matches.length <= context.threshold) return issues;
|
|
37198
|
+
const cap = Math.min(matches.length, 10);
|
|
37199
|
+
for (let i = 0; i < cap; i++) {
|
|
37200
|
+
const idx = matches[i];
|
|
37201
|
+
const line = source.slice(0, idx).split("\n").length;
|
|
36372
37202
|
issues.push({
|
|
36373
|
-
ruleId: "java/
|
|
37203
|
+
ruleId: "java/system-out-println",
|
|
36374
37204
|
category: "typo",
|
|
36375
37205
|
severity: "low",
|
|
36376
37206
|
aiSpecific: true,
|
|
36377
|
-
message: `
|
|
37207
|
+
message: `System.out.println at line ${line} \u2014 use a logger for production output`,
|
|
36378
37208
|
line,
|
|
36379
37209
|
column: 1,
|
|
36380
|
-
advice: "Replace with `
|
|
37210
|
+
advice: "Replace with `private static final Logger log = LoggerFactory.getLogger(...);` then `log.info(...)`. AI agents default to println because their training data has countless textbook examples. Real Java code uses a logger. Reference: java/system-out-println v0.20."
|
|
36381
37211
|
});
|
|
36382
37212
|
}
|
|
36383
|
-
|
|
36384
|
-
|
|
37213
|
+
return issues;
|
|
37214
|
+
}
|
|
37215
|
+
});
|
|
37216
|
+
|
|
37217
|
+
// src/rules/kotlin/coroutine-global-scope.ts
|
|
37218
|
+
var GLOBAL_SCOPE_REGEX = /GlobalScope\s*\.\s*(?:launch|async|runBlocking)\s*[({]/g;
|
|
37219
|
+
var kotlinCoroutineGlobalScopeRule = createRule({
|
|
37220
|
+
id: "kotlin/coroutine-global-scope",
|
|
37221
|
+
category: "logic",
|
|
37222
|
+
severity: "medium",
|
|
37223
|
+
aiSpecific: true,
|
|
37224
|
+
description: "GlobalScope.launch/async/runBlocking \u2014 bypasses structured concurrency. Use viewModelScope / lifecycleScope / coroutineScope {}.",
|
|
37225
|
+
create(_context) {
|
|
37226
|
+
return {};
|
|
37227
|
+
},
|
|
37228
|
+
analyze(_context, facts) {
|
|
37229
|
+
const issues = [];
|
|
37230
|
+
const source = facts.v2?._source;
|
|
37231
|
+
if (!source) return issues;
|
|
37232
|
+
if (!/\.kt$/i.test(facts.filePath)) return issues;
|
|
37233
|
+
let m;
|
|
37234
|
+
GLOBAL_SCOPE_REGEX.lastIndex = 0;
|
|
37235
|
+
while ((m = GLOBAL_SCOPE_REGEX.exec(source)) !== null) {
|
|
36385
37236
|
const line = source.slice(0, m.index).split("\n").length;
|
|
36386
|
-
if (flagged.has(line)) continue;
|
|
36387
37237
|
issues.push({
|
|
36388
|
-
ruleId: "
|
|
36389
|
-
category: "
|
|
36390
|
-
severity: "
|
|
37238
|
+
ruleId: "kotlin/coroutine-global-scope",
|
|
37239
|
+
category: "logic",
|
|
37240
|
+
severity: "medium",
|
|
36391
37241
|
aiSpecific: true,
|
|
36392
|
-
message: `
|
|
37242
|
+
message: `GlobalScope.${m[0].split(".")[1]?.split("(")[0]}() at line ${line} \u2014 bypasses structured concurrency`,
|
|
36393
37243
|
line,
|
|
36394
37244
|
column: 1,
|
|
36395
|
-
advice:
|
|
37245
|
+
advice: 'Replace with `viewModelScope.launch { ... }` (Android view models), `lifecycleScope.launch { ... }` (Android activities / fragments), or `coroutineScope { ... }` inside a suspending function. GlobalScope bypasses cancellation, exception propagation, and lifetime binding. The kotlinx.coroutines docs explicitly call GlobalScope "delicate API". AI agents default to it for "fire-and-forget" work because of training-data snippets. Reference: kotlin/coroutine-global-scope v0.24.'
|
|
36396
37246
|
});
|
|
36397
37247
|
}
|
|
36398
37248
|
return issues;
|
|
36399
37249
|
}
|
|
36400
37250
|
});
|
|
36401
37251
|
|
|
36402
|
-
// src/rules/
|
|
36403
|
-
var
|
|
36404
|
-
var
|
|
36405
|
-
|
|
37252
|
+
// src/rules/kotlin/data-class-defaults-overuse.ts
|
|
37253
|
+
var DATA_CLASS_HEAD_REGEX = /\bdata\s+class\s+\w+\s*\(/g;
|
|
37254
|
+
var DEFAULT_PARAM_REGEX = /=\s*(?:null|true|false|"[^"]*"|'[^']*'|[+-]?\d+(?:\.\d+)?\w*|emptyList\(\)|emptyMap\(\)|emptySet\(\)|listOf\(\)|mapOf\(\)|setOf\(\))/g;
|
|
37255
|
+
var DEFAULT_MIN_DEFAULTS = 3;
|
|
37256
|
+
var kotlinDataClassDefaultsOveruseRule = createRule({
|
|
37257
|
+
id: "kotlin/data-class-defaults-overuse",
|
|
36406
37258
|
category: "typo",
|
|
36407
37259
|
severity: "low",
|
|
36408
37260
|
aiSpecific: true,
|
|
36409
|
-
description: "
|
|
37261
|
+
description: "data class with 3+ literal defaults \u2014 AI defaults every field rather than only the genuinely optional ones",
|
|
36410
37262
|
create(_context) {
|
|
36411
|
-
return {};
|
|
37263
|
+
return { minDefaults: DEFAULT_MIN_DEFAULTS };
|
|
36412
37264
|
},
|
|
36413
|
-
analyze(
|
|
37265
|
+
analyze(context, facts) {
|
|
36414
37266
|
const issues = [];
|
|
36415
37267
|
const source = facts.v2?._source;
|
|
36416
37268
|
if (!source) return issues;
|
|
36417
|
-
if (!/\.
|
|
37269
|
+
if (!/\.kt$/i.test(facts.filePath)) return issues;
|
|
36418
37270
|
let m;
|
|
36419
|
-
|
|
36420
|
-
while ((m =
|
|
36421
|
-
const
|
|
36422
|
-
const
|
|
37271
|
+
DATA_CLASS_HEAD_REGEX.lastIndex = 0;
|
|
37272
|
+
while ((m = DATA_CLASS_HEAD_REGEX.exec(source)) !== null) {
|
|
37273
|
+
const headStart = m.index;
|
|
37274
|
+
const openParen = source.indexOf("(", headStart);
|
|
37275
|
+
if (openParen === -1) continue;
|
|
37276
|
+
let depth = 1;
|
|
37277
|
+
let i = openParen + 1;
|
|
37278
|
+
while (i < source.length && depth > 0) {
|
|
37279
|
+
const ch = source.charAt(i);
|
|
37280
|
+
if (ch === "(") depth++;
|
|
37281
|
+
else if (ch === ")") depth--;
|
|
37282
|
+
i++;
|
|
37283
|
+
}
|
|
37284
|
+
if (depth !== 0) continue;
|
|
37285
|
+
const paramBlock = source.slice(openParen + 1, i - 1);
|
|
37286
|
+
DEFAULT_PARAM_REGEX.lastIndex = 0;
|
|
37287
|
+
let defaults = 0;
|
|
37288
|
+
let dm;
|
|
37289
|
+
while ((dm = DEFAULT_PARAM_REGEX.exec(paramBlock)) !== null) {
|
|
37290
|
+
defaults++;
|
|
37291
|
+
}
|
|
37292
|
+
if (defaults < context.minDefaults) continue;
|
|
37293
|
+
const line = source.slice(0, headStart).split("\n").length;
|
|
36423
37294
|
issues.push({
|
|
36424
|
-
ruleId: "
|
|
37295
|
+
ruleId: "kotlin/data-class-defaults-overuse",
|
|
36425
37296
|
category: "typo",
|
|
36426
37297
|
severity: "low",
|
|
36427
37298
|
aiSpecific: true,
|
|
36428
|
-
message: `
|
|
37299
|
+
message: `data class with ${defaults} literal defaults at line ${line} \u2014 only optional fields should have defaults`,
|
|
36429
37300
|
line,
|
|
36430
37301
|
column: 1,
|
|
36431
|
-
advice:
|
|
37302
|
+
advice: "Audit which fields are genuinely optional. Real Kotlin uses defaults sparingly (1-2 callbacks, the rest required). Defaulting every field makes the no-arg constructor callable for every data class and obscures intent. AI agents default every field because they imitate Java/Swift optional patterns. Reference: kotlin/data-class-defaults-overuse v0.24."
|
|
36432
37303
|
});
|
|
36433
37304
|
}
|
|
36434
37305
|
return issues;
|
|
36435
37306
|
}
|
|
36436
37307
|
});
|
|
36437
37308
|
|
|
36438
|
-
// src/rules/
|
|
36439
|
-
var
|
|
36440
|
-
var
|
|
36441
|
-
id: "
|
|
36442
|
-
category: "
|
|
36443
|
-
severity: "
|
|
37309
|
+
// src/rules/kotlin/object-singleton-misuse.ts
|
|
37310
|
+
var OBJECT_DECL_REGEX = /\bobject\s+(?!\w*Companion\b)(\w+)\s*\{/g;
|
|
37311
|
+
var kotlinObjectSingletonMisuseRule = createRule({
|
|
37312
|
+
id: "kotlin/object-singleton-misuse",
|
|
37313
|
+
category: "logic",
|
|
37314
|
+
severity: "medium",
|
|
36444
37315
|
aiSpecific: true,
|
|
36445
|
-
description: "
|
|
37316
|
+
description: "Kotlin object singleton holding mutable `var` state \u2014 hidden shared state. Use DI or a repository.",
|
|
36446
37317
|
create(_context) {
|
|
36447
37318
|
return {};
|
|
36448
37319
|
},
|
|
@@ -36450,48 +37321,62 @@ var javaStringConcatLoopRule = createRule({
|
|
|
36450
37321
|
const issues = [];
|
|
36451
37322
|
const source = facts.v2?._source;
|
|
36452
37323
|
if (!source) return issues;
|
|
36453
|
-
if (!/\.
|
|
36454
|
-
if (!/\b(for|while|do)\b/.test(source)) return issues;
|
|
37324
|
+
if (!/\.kt$/i.test(facts.filePath)) return issues;
|
|
36455
37325
|
let m;
|
|
36456
|
-
|
|
36457
|
-
while ((m =
|
|
36458
|
-
const
|
|
37326
|
+
OBJECT_DECL_REGEX.lastIndex = 0;
|
|
37327
|
+
while ((m = OBJECT_DECL_REGEX.exec(source)) !== null) {
|
|
37328
|
+
const declStart = m.index;
|
|
37329
|
+
const openBrace = source.indexOf("{", declStart);
|
|
37330
|
+
if (openBrace === -1) continue;
|
|
37331
|
+
let depth = 1;
|
|
37332
|
+
let i = openBrace + 1;
|
|
37333
|
+
while (i < source.length && depth > 0) {
|
|
37334
|
+
const ch = source.charAt(i);
|
|
37335
|
+
if (ch === "{") depth++;
|
|
37336
|
+
else if (ch === "}") depth--;
|
|
37337
|
+
i++;
|
|
37338
|
+
}
|
|
37339
|
+
if (depth !== 0) continue;
|
|
37340
|
+
const body = source.slice(openBrace + 1, i - 1);
|
|
37341
|
+
if (!/\bvar\s+\w+\s*[:=]/m.test(body)) continue;
|
|
37342
|
+
const line = source.slice(0, declStart).split("\n").length;
|
|
37343
|
+
const name = m[1] ?? "<unknown>";
|
|
36459
37344
|
issues.push({
|
|
36460
|
-
ruleId: "
|
|
36461
|
-
category: "
|
|
36462
|
-
severity: "
|
|
37345
|
+
ruleId: "kotlin/object-singleton-misuse",
|
|
37346
|
+
category: "logic",
|
|
37347
|
+
severity: "medium",
|
|
36463
37348
|
aiSpecific: true,
|
|
36464
|
-
message: `
|
|
37349
|
+
message: `object ${name} at line ${line} holds mutable state \u2014 singleton + var = hidden shared state`,
|
|
36465
37350
|
line,
|
|
36466
37351
|
column: 1,
|
|
36467
|
-
advice: "
|
|
37352
|
+
advice: "Replace with a class injected via DI, a Repository, or a `MutableStateFlow` with explicit synchronization. The Kotlin `object` keyword gives you a thread-safe singleton handle, but it does NOT make `var` fields inside that singleton thread-safe \u2014 mutable singleton state is a classic source of race conditions. AI agents reach for `object Cache { var data = ... }` because their training snippets make it look natural. Reference: kotlin/object-singleton-misuse v0.24."
|
|
36468
37353
|
});
|
|
36469
37354
|
}
|
|
36470
37355
|
return issues;
|
|
36471
37356
|
}
|
|
36472
37357
|
});
|
|
36473
37358
|
|
|
36474
|
-
// src/rules/
|
|
36475
|
-
var
|
|
36476
|
-
var
|
|
36477
|
-
var
|
|
36478
|
-
id: "
|
|
37359
|
+
// src/rules/kotlin/println-debug.ts
|
|
37360
|
+
var PRINTLN_REGEX2 = /^\s*println\s*\(/gm;
|
|
37361
|
+
var DEFAULT_THRESHOLD2 = 1;
|
|
37362
|
+
var kotlinPrintlnDebugRule = createRule({
|
|
37363
|
+
id: "kotlin/println-debug",
|
|
36479
37364
|
category: "typo",
|
|
36480
37365
|
severity: "low",
|
|
36481
37366
|
aiSpecific: true,
|
|
36482
|
-
description: "
|
|
37367
|
+
description: "println(...) in production code \u2014 use Timber, android.util.Log, or an SLF4J facade",
|
|
36483
37368
|
create(_context) {
|
|
36484
|
-
return { threshold:
|
|
37369
|
+
return { threshold: DEFAULT_THRESHOLD2 };
|
|
36485
37370
|
},
|
|
36486
37371
|
analyze(context, facts) {
|
|
36487
37372
|
const issues = [];
|
|
36488
37373
|
const source = facts.v2?._source;
|
|
36489
37374
|
if (!source) return issues;
|
|
36490
|
-
if (!/\.
|
|
37375
|
+
if (!/\.kt$/i.test(facts.filePath)) return issues;
|
|
36491
37376
|
const matches = [];
|
|
36492
37377
|
let m;
|
|
36493
|
-
|
|
36494
|
-
while ((m =
|
|
37378
|
+
PRINTLN_REGEX2.lastIndex = 0;
|
|
37379
|
+
while ((m = PRINTLN_REGEX2.exec(source)) !== null) {
|
|
36495
37380
|
matches.push(m.index);
|
|
36496
37381
|
}
|
|
36497
37382
|
if (matches.length <= context.threshold) return issues;
|
|
@@ -36500,14 +37385,52 @@ var javaSystemOutPrintlnRule = createRule({
|
|
|
36500
37385
|
const idx = matches[i];
|
|
36501
37386
|
const line = source.slice(0, idx).split("\n").length;
|
|
36502
37387
|
issues.push({
|
|
36503
|
-
ruleId: "
|
|
37388
|
+
ruleId: "kotlin/println-debug",
|
|
36504
37389
|
category: "typo",
|
|
36505
37390
|
severity: "low",
|
|
36506
37391
|
aiSpecific: true,
|
|
36507
|
-
message: `
|
|
37392
|
+
message: `println(...) at line ${line} \u2014 use Timber or android.util.Log for production output`,
|
|
36508
37393
|
line,
|
|
36509
37394
|
column: 1,
|
|
36510
|
-
advice: "Replace with `
|
|
37395
|
+
advice: "Replace with `Timber.plant(Timber.DebugTree())` then `Timber.d(...)`, or with an SLF4J facade. `println` writes to stdout with no level / no redaction and cannot be silenced in production. Multiple `println` calls in one file is an AI fingerprint \u2014 agents default to it because their training data has many copy-pasteable Kotlin examples. A single println in a `main` function is fine; 2+ is not. Reference: kotlin/println-debug v0.24."
|
|
37396
|
+
});
|
|
37397
|
+
}
|
|
37398
|
+
return issues;
|
|
37399
|
+
}
|
|
37400
|
+
});
|
|
37401
|
+
|
|
37402
|
+
// src/rules/kotlin/string-concat-loop.ts
|
|
37403
|
+
var STRING_CONCAT_REGEX2 = /\b(\w+)\s*=\s*\1\s*\+\s*[^;}]+[;}\n]/g;
|
|
37404
|
+
var kotlinStringConcatLoopRule = createRule({
|
|
37405
|
+
id: "kotlin/string-concat-loop",
|
|
37406
|
+
category: "perf",
|
|
37407
|
+
severity: "low",
|
|
37408
|
+
aiSpecific: true,
|
|
37409
|
+
description: "String concatenation in a loop \u2014 use buildString { ... } or a StringBuilder",
|
|
37410
|
+
create(_context) {
|
|
37411
|
+
return {};
|
|
37412
|
+
},
|
|
37413
|
+
analyze(_context, facts) {
|
|
37414
|
+
const issues = [];
|
|
37415
|
+
const source = facts.v2?._source;
|
|
37416
|
+
if (!source) return issues;
|
|
37417
|
+
if (!/\.kt$/i.test(facts.filePath)) return issues;
|
|
37418
|
+
if (!/\b(?:for|while|repeat|forEach)\b/.test(source)) return issues;
|
|
37419
|
+
let m;
|
|
37420
|
+
STRING_CONCAT_REGEX2.lastIndex = 0;
|
|
37421
|
+
while ((m = STRING_CONCAT_REGEX2.exec(source)) !== null) {
|
|
37422
|
+
const line = source.slice(0, m.index).split("\n").length;
|
|
37423
|
+
const lineText = source.slice(0, m.index).split("\n").pop() ?? "";
|
|
37424
|
+
if (/\.append\s*\(/.test(lineText)) continue;
|
|
37425
|
+
issues.push({
|
|
37426
|
+
ruleId: "kotlin/string-concat-loop",
|
|
37427
|
+
category: "perf",
|
|
37428
|
+
severity: "low",
|
|
37429
|
+
aiSpecific: true,
|
|
37430
|
+
message: `String concatenation in a loop at line ${line} \u2014 use buildString { ... }`,
|
|
37431
|
+
line,
|
|
37432
|
+
column: 1,
|
|
37433
|
+
advice: "Wrap the loop in `buildString { append(...) }` or declare a `StringBuilder` outside the loop. Kotlin cannot optimize `s = s + x` inside a loop \u2014 each iteration allocates a new StringBuilder, making the whole thing O(n\xB2) for n iterations. AI agents concatenate inside loops because of training-data examples. Reference: kotlin/string-concat-loop v0.24."
|
|
36511
37434
|
});
|
|
36512
37435
|
}
|
|
36513
37436
|
return issues;
|
|
@@ -36726,7 +37649,7 @@ var DEFAULT_RULE_CONFIG = {
|
|
|
36726
37649
|
},
|
|
36727
37650
|
genericCenteringMaxInstances: 1
|
|
36728
37651
|
};
|
|
36729
|
-
var
|
|
37652
|
+
var DEFAULT_CONFIG3 = {
|
|
36730
37653
|
include: [
|
|
36731
37654
|
// v0.9.2 — Repository Coherence is language-agnostic. Default include
|
|
36732
37655
|
// covers frontend (TS/JS/Vue/Svelte/Astro) AND backend (Python/Go).
|
|
@@ -36981,8 +37904,8 @@ var STRICTNESS_PRESETS = {
|
|
|
36981
37904
|
}
|
|
36982
37905
|
},
|
|
36983
37906
|
balanced: {
|
|
36984
|
-
thresholds:
|
|
36985
|
-
categoryWeights:
|
|
37907
|
+
thresholds: DEFAULT_CONFIG3.thresholds,
|
|
37908
|
+
categoryWeights: DEFAULT_CONFIG3.categoryWeights
|
|
36986
37909
|
},
|
|
36987
37910
|
permissive: {
|
|
36988
37911
|
thresholds: { meanSlop: 50, p90Slop: 75, individualSlopThreshold: 90 },
|
|
@@ -37926,7 +38849,7 @@ var KEYWORD_OPERATORS = [
|
|
|
37926
38849
|
function stripNoise2(source) {
|
|
37927
38850
|
return source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "").replace(/'(?:\\.|[^'\\])*'/g, "''").replace(/"(?:\\.|[^"\\])*"/g, '""').replace(/`(?:\\.|[^`\\])*`/g, "``");
|
|
37928
38851
|
}
|
|
37929
|
-
function
|
|
38852
|
+
function tokenize2(source) {
|
|
37930
38853
|
const cleaned = stripNoise2(source);
|
|
37931
38854
|
const operators = [];
|
|
37932
38855
|
const operands = [];
|
|
@@ -37947,7 +38870,7 @@ function tokenize(source) {
|
|
|
37947
38870
|
return { operators, operands };
|
|
37948
38871
|
}
|
|
37949
38872
|
function computeHalstead(source) {
|
|
37950
|
-
const { operators, operands } =
|
|
38873
|
+
const { operators, operands } = tokenize2(source);
|
|
37951
38874
|
const uniqueOperators = new Set(operators);
|
|
37952
38875
|
const uniqueOperands = new Set(operands);
|
|
37953
38876
|
const n1 = uniqueOperators.size;
|
|
@@ -39208,7 +40131,226 @@ var unsafeHtmlRenderRule = createRule({
|
|
|
39208
40131
|
message: "dangerouslySetInnerHTML fed a non-literal value (variable, template, or expression) \u2014 unsanitized HTML will be rendered.",
|
|
39209
40132
|
line: lineOfSource(source, m.index),
|
|
39210
40133
|
column: 1,
|
|
39211
|
-
advice: "Sanitize the input with DOMPurify (or your framework's sanitizer) before passing it to dangerouslySetInnerHTML, or replace dangerouslySetInnerHTML with a children prop that React escapes automatically."
|
|
40134
|
+
advice: "Sanitize the input with DOMPurify (or your framework's sanitizer) before passing it to dangerouslySetInnerHTML, or replace dangerouslySetInnerHTML with a children prop that React escapes automatically."
|
|
40135
|
+
});
|
|
40136
|
+
}
|
|
40137
|
+
return issues;
|
|
40138
|
+
}
|
|
40139
|
+
});
|
|
40140
|
+
|
|
40141
|
+
// src/rules/swift/fatal-error-thrown.ts
|
|
40142
|
+
var FATAL_REGEX = /\b(?:fatalError|preconditionFailure)\s*\(/g;
|
|
40143
|
+
var swiftFatalErrorThrownRule = createRule({
|
|
40144
|
+
id: "swift/fatal-error-thrown",
|
|
40145
|
+
category: "logic",
|
|
40146
|
+
severity: "high",
|
|
40147
|
+
aiSpecific: true,
|
|
40148
|
+
description: "fatalError / preconditionFailure in production \u2014 crashes unconditionally. Return an error or a typed default.",
|
|
40149
|
+
create(_context) {
|
|
40150
|
+
return {};
|
|
40151
|
+
},
|
|
40152
|
+
analyze(_context, facts) {
|
|
40153
|
+
const issues = [];
|
|
40154
|
+
const source = facts.v2?._source;
|
|
40155
|
+
if (!source) return issues;
|
|
40156
|
+
if (!/\.swift$/i.test(facts.filePath)) return issues;
|
|
40157
|
+
if (/\bimport\s+XCTest\b/.test(source)) return issues;
|
|
40158
|
+
let m;
|
|
40159
|
+
FATAL_REGEX.lastIndex = 0;
|
|
40160
|
+
while ((m = FATAL_REGEX.exec(source)) !== null) {
|
|
40161
|
+
const line = source.slice(0, m.index).split("\n").length;
|
|
40162
|
+
const matched = m[0].replace(/\s*\($/, "");
|
|
40163
|
+
issues.push({
|
|
40164
|
+
ruleId: "swift/fatal-error-thrown",
|
|
40165
|
+
category: "logic",
|
|
40166
|
+
severity: "high",
|
|
40167
|
+
aiSpecific: true,
|
|
40168
|
+
message: `${matched}() at line ${line} \u2014 crashes unconditionally in release builds`,
|
|
40169
|
+
line,
|
|
40170
|
+
column: 1,
|
|
40171
|
+
advice: 'Replace with `return` of a typed default (e.g. `nil`, `[]()`), or throw a typed error (`enum MyError: Error { case notImplemented }; throw MyError.notImplemented`). Apple\'s docs contrast `fatalError` (survives release) with `precondition` (debug+checked only) and `assert` (debug only). AI agents plant `fatalError("not implemented")` in protocol stubs because the pattern "makes the file compile" while signalling TODO. Survives App Store review. Reference: swift/fatal-error-thrown v0.24.'
|
|
40172
|
+
});
|
|
40173
|
+
}
|
|
40174
|
+
return issues;
|
|
40175
|
+
}
|
|
40176
|
+
});
|
|
40177
|
+
|
|
40178
|
+
// src/rules/swift/force-unwrap.ts
|
|
40179
|
+
var AS_FORCE_REGEX = /\bas!\s+/g;
|
|
40180
|
+
var ACCESS_FORCE_REGEX = /(?:\w|\])\!\s*(?:\.|\(|;|,|\s*$)/gm;
|
|
40181
|
+
var TRY_FORCE_REGEX = /\btry!\s+/g;
|
|
40182
|
+
var swiftForceUnwrapRule = createRule({
|
|
40183
|
+
id: "swift/force-unwrap",
|
|
40184
|
+
category: "logic",
|
|
40185
|
+
severity: "medium",
|
|
40186
|
+
aiSpecific: true,
|
|
40187
|
+
description: "Swift force-unwrap (as!, var!.prop, try!) \u2014 silently crashes if the value is nil. Use as? / guard let / try?.",
|
|
40188
|
+
create(_context) {
|
|
40189
|
+
return {};
|
|
40190
|
+
},
|
|
40191
|
+
analyze(_context, facts) {
|
|
40192
|
+
const issues = [];
|
|
40193
|
+
const source = facts.v2?._source;
|
|
40194
|
+
if (!source) return issues;
|
|
40195
|
+
if (!/\.swift$/i.test(facts.filePath)) return issues;
|
|
40196
|
+
if (/\bimport\s+XCTest\b/.test(source)) return issues;
|
|
40197
|
+
const emit = (idx, label) => {
|
|
40198
|
+
const line = source.slice(0, idx).split("\n").length;
|
|
40199
|
+
issues.push({
|
|
40200
|
+
ruleId: "swift/force-unwrap",
|
|
40201
|
+
category: "logic",
|
|
40202
|
+
severity: "medium",
|
|
40203
|
+
aiSpecific: true,
|
|
40204
|
+
message: `force-unwrap (${label}) at line ${line} \u2014 crashes if the value is nil`,
|
|
40205
|
+
line,
|
|
40206
|
+
column: 1,
|
|
40207
|
+
advice: 'Replace with the safe form: `as?` + guard/if let, `try?` + nil-check, or `guard let x = optional else { return }` instead of `x!`. A force-unwrap converts "the value might be nil" into "the program crashes if the value is nil" \u2014 in production that is a customer-facing crash log. AI agents reach for `!` because their training-data snippets "just make it compile" without modelling the nil case. Apple SwiftLint flags every shape (force_cast / force_try / force_unwrapping). Reference: swift/force-unwrap v0.24.'
|
|
40208
|
+
});
|
|
40209
|
+
};
|
|
40210
|
+
let m;
|
|
40211
|
+
AS_FORCE_REGEX.lastIndex = 0;
|
|
40212
|
+
while ((m = AS_FORCE_REGEX.exec(source)) !== null) emit(m.index, "as!");
|
|
40213
|
+
TRY_FORCE_REGEX.lastIndex = 0;
|
|
40214
|
+
while ((m = TRY_FORCE_REGEX.exec(source)) !== null) emit(m.index, "try!");
|
|
40215
|
+
ACCESS_FORCE_REGEX.lastIndex = 0;
|
|
40216
|
+
while ((m = ACCESS_FORCE_REGEX.exec(source)) !== null) emit(m.index, "!.");
|
|
40217
|
+
return issues;
|
|
40218
|
+
}
|
|
40219
|
+
});
|
|
40220
|
+
|
|
40221
|
+
// src/rules/swift/implicitly-unwrapped-optional.ts
|
|
40222
|
+
var IUO_DECL_REGEX = /\b(?:var|let)\s+\w+\s*:\s*[A-Za-z_][\w<>?,\.\s]*!\s*[=;\n]/g;
|
|
40223
|
+
var swiftImplicitlyUnwrappedOptionalRule = createRule({
|
|
40224
|
+
id: "swift/implicitly-unwrapped-optional",
|
|
40225
|
+
category: "typo",
|
|
40226
|
+
severity: "low",
|
|
40227
|
+
aiSpecific: true,
|
|
40228
|
+
description: "Implicitly unwrapped optional (var x: Type!) \u2014 declare as Type? and unwrap with guard let.",
|
|
40229
|
+
create(_context) {
|
|
40230
|
+
return {};
|
|
40231
|
+
},
|
|
40232
|
+
analyze(_context, facts) {
|
|
40233
|
+
const issues = [];
|
|
40234
|
+
const source = facts.v2?._source;
|
|
40235
|
+
if (!source) return issues;
|
|
40236
|
+
if (!/\.swift$/i.test(facts.filePath)) return issues;
|
|
40237
|
+
let m;
|
|
40238
|
+
IUO_DECL_REGEX.lastIndex = 0;
|
|
40239
|
+
while ((m = IUO_DECL_REGEX.exec(source)) !== null) {
|
|
40240
|
+
const captured = m[0];
|
|
40241
|
+
const bangAt = captured.indexOf("!", captured.indexOf(":"));
|
|
40242
|
+
const between = captured.slice(captured.indexOf(":"), bangAt);
|
|
40243
|
+
if (between.includes("?")) continue;
|
|
40244
|
+
const line = source.slice(0, m.index).split("\n").length;
|
|
40245
|
+
issues.push({
|
|
40246
|
+
ruleId: "swift/implicitly-unwrapped-optional",
|
|
40247
|
+
category: "typo",
|
|
40248
|
+
severity: "low",
|
|
40249
|
+
aiSpecific: true,
|
|
40250
|
+
message: `implicitly unwrapped optional at line ${line} \u2014 declare as Type? and unwrap explicitly`,
|
|
40251
|
+
line,
|
|
40252
|
+
column: 1,
|
|
40253
|
+
advice: 'Declare as `var name: Type?` and unwrap with `guard let name else { ... }` (or `if let`). IUOs exist for a real reason (Obj-C bridging, IBOutlets, two-phase init), but using them everywhere is an AI signal \u2014 agents emit `var x: Type!` because the syntax survives the moment when the agent can\'t decide whether the value is nil. The type system silently says "non-optional" while the runtime says "may be nil". Apple SwiftLint\'s `implicitly_unwrapped_optional` rule flags the same pattern. Reference: swift/implicitly-unwrapped-optional v0.24.'
|
|
40254
|
+
});
|
|
40255
|
+
}
|
|
40256
|
+
return issues;
|
|
40257
|
+
}
|
|
40258
|
+
});
|
|
40259
|
+
|
|
40260
|
+
// src/rules/swift/print-debug.ts
|
|
40261
|
+
var PRINT_REGEX = /\bprint\s*\(/g;
|
|
40262
|
+
var DEFAULT_THRESHOLD3 = 1;
|
|
40263
|
+
var swiftPrintDebugRule = createRule({
|
|
40264
|
+
id: "swift/print-debug",
|
|
40265
|
+
category: "typo",
|
|
40266
|
+
severity: "low",
|
|
40267
|
+
aiSpecific: true,
|
|
40268
|
+
description: "print(...) in production Swift \u2014 use Logger (os.log) for level-controlled output",
|
|
40269
|
+
create(_context) {
|
|
40270
|
+
return { threshold: DEFAULT_THRESHOLD3 };
|
|
40271
|
+
},
|
|
40272
|
+
analyze(context, facts) {
|
|
40273
|
+
const issues = [];
|
|
40274
|
+
const source = facts.v2?._source;
|
|
40275
|
+
if (!source) return issues;
|
|
40276
|
+
if (!/\.swift$/i.test(facts.filePath)) return issues;
|
|
40277
|
+
const matches = [];
|
|
40278
|
+
let m;
|
|
40279
|
+
PRINT_REGEX.lastIndex = 0;
|
|
40280
|
+
while ((m = PRINT_REGEX.exec(source)) !== null) {
|
|
40281
|
+
matches.push(m.index);
|
|
40282
|
+
}
|
|
40283
|
+
if (matches.length <= context.threshold) return issues;
|
|
40284
|
+
const cap = Math.min(matches.length, 10);
|
|
40285
|
+
for (let i = 0; i < cap; i++) {
|
|
40286
|
+
const idx = matches[i];
|
|
40287
|
+
const line = source.slice(0, idx).split("\n").length;
|
|
40288
|
+
issues.push({
|
|
40289
|
+
ruleId: "swift/print-debug",
|
|
40290
|
+
category: "typo",
|
|
40291
|
+
severity: "low",
|
|
40292
|
+
aiSpecific: true,
|
|
40293
|
+
message: `print(...) at line ${line} \u2014 use Logger (os.log) for production output`,
|
|
40294
|
+
line,
|
|
40295
|
+
column: 1,
|
|
40296
|
+
advice: 'Replace with `Logger(subsystem: "...", category: "...").info("...")` (Unified Logging System, os.log). `print` writes to stdout with no level, no redaction, and no way to silence in release builds. The `Logger` API integrates with Console.app and respects the device\'s privacy settings. Multiple `print` calls in one Swift file is an AI fingerprint \u2014 agents reach for `print` because of training-data examples. Reference: swift/print-debug v0.24.'
|
|
40297
|
+
});
|
|
40298
|
+
}
|
|
40299
|
+
return issues;
|
|
40300
|
+
}
|
|
40301
|
+
});
|
|
40302
|
+
|
|
40303
|
+
// src/rules/swift/strong-self-capture.ts
|
|
40304
|
+
var SELF_REF_REGEX = /\bself\.(?:\w+)/g;
|
|
40305
|
+
var CAPTURE_LIST_REGEX = /\[\s*(?:weak|unowned)\s+self\b/;
|
|
40306
|
+
var swiftStrongSelfCaptureRule = createRule({
|
|
40307
|
+
id: "swift/strong-self-capture",
|
|
40308
|
+
category: "logic",
|
|
40309
|
+
severity: "medium",
|
|
40310
|
+
aiSpecific: true,
|
|
40311
|
+
description: "Closure captures `self` strongly \u2014 creates a retain cycle. Use `[weak self]` or `[unowned self]`.",
|
|
40312
|
+
create(_context) {
|
|
40313
|
+
return {};
|
|
40314
|
+
},
|
|
40315
|
+
analyze(_context, facts) {
|
|
40316
|
+
const issues = [];
|
|
40317
|
+
const source = facts.v2?._source;
|
|
40318
|
+
if (!source) return issues;
|
|
40319
|
+
if (!/\.swift$/i.test(facts.filePath)) return issues;
|
|
40320
|
+
const METHOD_HEAD_REGEX = /\b(?:func|init\??|deinit|subscript)\b[^;{}\n]*$/m;
|
|
40321
|
+
let m;
|
|
40322
|
+
SELF_REF_REGEX.lastIndex = 0;
|
|
40323
|
+
while ((m = SELF_REF_REGEX.exec(source)) !== null) {
|
|
40324
|
+
const selfIdx = m.index;
|
|
40325
|
+
let depth = 0;
|
|
40326
|
+
let openBrace = -1;
|
|
40327
|
+
for (let j = selfIdx - 1; j >= 0; j--) {
|
|
40328
|
+
const ch = source.charAt(j);
|
|
40329
|
+
if (ch === "}") depth++;
|
|
40330
|
+
else if (ch === "{") {
|
|
40331
|
+
if (depth === 0) {
|
|
40332
|
+
openBrace = j;
|
|
40333
|
+
break;
|
|
40334
|
+
}
|
|
40335
|
+
depth--;
|
|
40336
|
+
}
|
|
40337
|
+
}
|
|
40338
|
+
if (openBrace === -1) continue;
|
|
40339
|
+
const openLineStart = source.lastIndexOf("\n", openBrace) + 1;
|
|
40340
|
+
const lineBeforeBrace = source.slice(openLineStart, openBrace);
|
|
40341
|
+
if (METHOD_HEAD_REGEX.test(lineBeforeBrace)) continue;
|
|
40342
|
+
const openingSlice = source.slice(openBrace, openBrace + 80);
|
|
40343
|
+
if (CAPTURE_LIST_REGEX.test(openingSlice)) continue;
|
|
40344
|
+
const line = source.slice(0, openBrace).split("\n").length;
|
|
40345
|
+
issues.push({
|
|
40346
|
+
ruleId: "swift/strong-self-capture",
|
|
40347
|
+
category: "logic",
|
|
40348
|
+
severity: "medium",
|
|
40349
|
+
aiSpecific: true,
|
|
40350
|
+
message: `closure at line ${line} captures self strongly \u2014 use [weak self] or [unowned self]`,
|
|
40351
|
+
line,
|
|
40352
|
+
column: 1,
|
|
40353
|
+
advice: 'Capture the closure with `[weak self]` (or `[unowned self]` if you can guarantee non-nil): `[weak self] in self?.foo = bar`. Strong self capture creates a retain cycle \u2014 the closure keeps self alive, self keeps the closure alive, and the cycle is only broken when the runtime gives up and prints a leak. Apple\'s Swift docs use UIKit / UIViewController closures as the canonical example. AI agents write `loadData { self.foo = bar }` because it "just compiles"; the leak is invisible without profiling. Reference: swift/strong-self-capture v0.24.'
|
|
39212
40354
|
});
|
|
39213
40355
|
}
|
|
39214
40356
|
return issues;
|
|
@@ -39216,7 +40358,7 @@ var unsafeHtmlRenderRule = createRule({
|
|
|
39216
40358
|
});
|
|
39217
40359
|
|
|
39218
40360
|
// src/rules/test/utils.ts
|
|
39219
|
-
import { createHash as
|
|
40361
|
+
import { createHash as createHash6 } from "crypto";
|
|
39220
40362
|
function isTestFile(filePath) {
|
|
39221
40363
|
const normalized = filePath.replace(/\\/g, "/");
|
|
39222
40364
|
if (/(^|\/)__tests__\//.test(normalized)) return true;
|
|
@@ -39376,7 +40518,7 @@ function extractSetupBlocks(source, minLines = 3) {
|
|
|
39376
40518
|
}).trim();
|
|
39377
40519
|
const lineCount = rawBody.split("\n").length;
|
|
39378
40520
|
if (lineCount < minLines) continue;
|
|
39379
|
-
const bodyHash =
|
|
40521
|
+
const bodyHash = createHash6("sha1").update(normalizedBody).digest("hex");
|
|
39380
40522
|
const { line, column } = lineColumnOf(source, m.index);
|
|
39381
40523
|
hits.push({
|
|
39382
40524
|
kind,
|
|
@@ -41755,6 +42897,11 @@ var builtinRules = [
|
|
|
41755
42897
|
multipleComponentsPerFileRule,
|
|
41756
42898
|
shadcnPropMismatchRule,
|
|
41757
42899
|
importPathMismatchRule,
|
|
42900
|
+
cppCStyleCastRule,
|
|
42901
|
+
cppMagicNumbersRule,
|
|
42902
|
+
cppPrintfDebugRule,
|
|
42903
|
+
cppRawNewDeleteRule,
|
|
42904
|
+
cppUsingNamespaceStdRule,
|
|
41758
42905
|
duplicateIndexRule,
|
|
41759
42906
|
enumSprawlRule,
|
|
41760
42907
|
missingFkIndexRule,
|
|
@@ -41770,6 +42917,8 @@ var builtinRules = [
|
|
|
41770
42917
|
staleFunctionReferenceRule,
|
|
41771
42918
|
stalePackageReferenceRule,
|
|
41772
42919
|
dupIdenticalBlockRule,
|
|
42920
|
+
nearDuplicateRule,
|
|
42921
|
+
structuralCloneRule,
|
|
41773
42922
|
goErrorWrapWithoutContextRule,
|
|
41774
42923
|
goNilSliceVsEmptyRule,
|
|
41775
42924
|
goStructTagInconsistencyRule,
|
|
@@ -41779,6 +42928,11 @@ var builtinRules = [
|
|
|
41779
42928
|
javaRawTypeOveruseRule,
|
|
41780
42929
|
javaStringConcatLoopRule,
|
|
41781
42930
|
javaSystemOutPrintlnRule,
|
|
42931
|
+
kotlinCoroutineGlobalScopeRule,
|
|
42932
|
+
kotlinDataClassDefaultsOveruseRule,
|
|
42933
|
+
kotlinObjectSingletonMisuseRule,
|
|
42934
|
+
kotlinPrintlnDebugRule,
|
|
42935
|
+
kotlinStringConcatLoopRule,
|
|
41782
42936
|
gapMonopolyRule,
|
|
41783
42937
|
mathElementUniformityRule,
|
|
41784
42938
|
mathGridUniformityRule,
|
|
@@ -41817,6 +42971,11 @@ var builtinRules = [
|
|
|
41817
42971
|
sqlConstructionRule,
|
|
41818
42972
|
targetBlankNoNoopenerRule,
|
|
41819
42973
|
unsafeHtmlRenderRule,
|
|
42974
|
+
swiftFatalErrorThrownRule,
|
|
42975
|
+
swiftForceUnwrapRule,
|
|
42976
|
+
swiftImplicitlyUnwrappedOptionalRule,
|
|
42977
|
+
swiftPrintDebugRule,
|
|
42978
|
+
swiftStrongSelfCaptureRule,
|
|
41820
42979
|
duplicateSetupRule,
|
|
41821
42980
|
fakePlaceholderRule,
|
|
41822
42981
|
missingEdgeCaseRule,
|
|
@@ -42535,9 +43694,9 @@ var signal_strength_default = {
|
|
|
42535
43694
|
ratio: 1,
|
|
42536
43695
|
precision: 0,
|
|
42537
43696
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
42538
|
-
verdict: "
|
|
43697
|
+
verdict: "OK",
|
|
42539
43698
|
defaultOff: false,
|
|
42540
|
-
_calibrationNote: "v0.19: new rule
|
|
43699
|
+
_calibrationNote: "v0.19: new rule. v8.5 calibration does not run dup/* rules (the calibration script analyzes each file in isolation, but dup/* rules require cross-file state \u2014 the module-scope dedup cache is per-process, not per-file, so per-file analysis never produces cross-file matches and the rule's recall reads as 0). Scheduled for v0.22 calibration on the v9 corpus with a per-project dedup cache. v0.21.1 self-scan: 207 fires in src/ after WINDOW_SIZE 10\u219220 (was 575) \u2014 69% reduction. The actual precision/recall of these 207 fires is unverified ground truth. The rule ships ON (defaultOff=false) because its value is real (block-level duplications are a classic AI-fingerprint) and the corpus-measured 0.0 is a methodology artifact, not a real measurement. Verdict is OK (active, uncalibrated) rather than DORMANT (off, uncalibrated) to satisfy the 'every DORMANT rule must be defaultOff' guardrail.",
|
|
42541
43700
|
aiSpecific: false,
|
|
42542
43701
|
_v7Verdict: "DORMANT",
|
|
42543
43702
|
_v7Lift: 1,
|
|
@@ -42545,8 +43704,39 @@ var signal_strength_default = {
|
|
|
42545
43704
|
_v7FpRate: 0,
|
|
42546
43705
|
_v7Precision: 0,
|
|
42547
43706
|
_v8Verdict: "DORMANT",
|
|
42548
|
-
_v8Lift: 1
|
|
42549
|
-
|
|
43707
|
+
_v8Lift: 1
|
|
43708
|
+
},
|
|
43709
|
+
"dup/near-duplicate": {
|
|
43710
|
+
recall: 0,
|
|
43711
|
+
fpRate: 0,
|
|
43712
|
+
ratio: 1,
|
|
43713
|
+
precision: 0,
|
|
43714
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
43715
|
+
verdict: "DORMANT",
|
|
43716
|
+
defaultOff: true,
|
|
43717
|
+
_calibrationNote: "v0.23.0: new rule, not yet calibrated. Type-2 clone detector using MinHash + Jaccard similarity on k-gram token shingles (k=5). The rule's effectiveness depends on the v9 corpus calibration (see v9-plan-2026-07-02-update.md). The rule catches Type-2a (whitespace + comment differences) reliably; full-rename detection (Type-2b) requires identifier canonicalization or AST similarity (planned for v0.27.0 dup/structural-clone). Default OFF (DORMANT) until v9 corpus calibration confirms precision/recall thresholds.",
|
|
43718
|
+
aiSpecific: true,
|
|
43719
|
+
_v7Verdict: "DORMANT",
|
|
43720
|
+
_v7Lift: 1,
|
|
43721
|
+
_v7Recall: 0,
|
|
43722
|
+
_v7FpRate: 0,
|
|
43723
|
+
_v7Precision: 0
|
|
43724
|
+
},
|
|
43725
|
+
"dup/structural-clone": {
|
|
43726
|
+
recall: 0,
|
|
43727
|
+
fpRate: 0,
|
|
43728
|
+
ratio: 1,
|
|
43729
|
+
precision: 0,
|
|
43730
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
43731
|
+
verdict: "DORMANT",
|
|
43732
|
+
defaultOff: true,
|
|
43733
|
+
_calibrationNote: "v0.24.0: new rule, not yet calibrated. Type-3 clone detector using a two-stage MinHash pipeline \u2014 stage 1 canonicalizes identifiers/literals (rename-invariant k=8 shingles), stage 2 verifies with identifier-aware k=5 shingles (existing shingleSet). Closes the gap near-duplicate left open: identifier renames (Type-3) and statement add/remove. Default OFF (DORMANT) until v9 Java corpus calibration confirms precision/recall thresholds and the v0.24.1 patch raises minHits from 1 to 3.",
|
|
43734
|
+
aiSpecific: true,
|
|
43735
|
+
_v7Verdict: "DORMANT",
|
|
43736
|
+
_v7Lift: 1,
|
|
43737
|
+
_v7Recall: 0,
|
|
43738
|
+
_v7FpRate: 0,
|
|
43739
|
+
_v7Precision: 0
|
|
42550
43740
|
},
|
|
42551
43741
|
"go/error-wrap-without-context": {
|
|
42552
43742
|
recall: 0,
|
|
@@ -43952,6 +45142,276 @@ var signal_strength_default = {
|
|
|
43952
45142
|
_v8Verdict: "DORMANT",
|
|
43953
45143
|
_v8Lift: 1,
|
|
43954
45144
|
defaultOff: true
|
|
45145
|
+
},
|
|
45146
|
+
"kotlin/data-class-defaults-overuse": {
|
|
45147
|
+
recall: 0,
|
|
45148
|
+
fpRate: 0,
|
|
45149
|
+
ratio: 1,
|
|
45150
|
+
precision: 0,
|
|
45151
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
45152
|
+
verdict: "DORMANT",
|
|
45153
|
+
_calibrationNote: "v0.24: new rule (data class with 3+ literal defaults). DORMANT until v9 Kotlin corpus calibration.",
|
|
45154
|
+
aiSpecific: true,
|
|
45155
|
+
_v7Verdict: "DORMANT",
|
|
45156
|
+
_v7Lift: 1,
|
|
45157
|
+
_v7Recall: 0,
|
|
45158
|
+
_v7FpRate: 0,
|
|
45159
|
+
_v7Precision: 0,
|
|
45160
|
+
_v8Verdict: "DORMANT",
|
|
45161
|
+
_v8Lift: 1,
|
|
45162
|
+
defaultOff: true
|
|
45163
|
+
},
|
|
45164
|
+
"kotlin/coroutine-global-scope": {
|
|
45165
|
+
recall: 0,
|
|
45166
|
+
fpRate: 0,
|
|
45167
|
+
ratio: 1,
|
|
45168
|
+
precision: 0,
|
|
45169
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
45170
|
+
verdict: "DORMANT",
|
|
45171
|
+
_calibrationNote: "v0.24: new rule (GlobalScope.launch/async/runBlocking \u2014 bypasses structured concurrency). DORMANT until v9 Kotlin corpus calibration.",
|
|
45172
|
+
aiSpecific: true,
|
|
45173
|
+
_v7Verdict: "DORMANT",
|
|
45174
|
+
_v7Lift: 1,
|
|
45175
|
+
_v7Recall: 0,
|
|
45176
|
+
_v7FpRate: 0,
|
|
45177
|
+
_v7Precision: 0,
|
|
45178
|
+
_v8Verdict: "DORMANT",
|
|
45179
|
+
_v8Lift: 1,
|
|
45180
|
+
defaultOff: true
|
|
45181
|
+
},
|
|
45182
|
+
"kotlin/println-debug": {
|
|
45183
|
+
recall: 0,
|
|
45184
|
+
fpRate: 0,
|
|
45185
|
+
ratio: 1,
|
|
45186
|
+
precision: 0,
|
|
45187
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
45188
|
+
verdict: "DORMANT",
|
|
45189
|
+
_calibrationNote: "v0.24: new rule (multi-println in Kotlin source \u2014 use Timber / android.util.Log). DORMANT until v9 Kotlin corpus calibration.",
|
|
45190
|
+
aiSpecific: true,
|
|
45191
|
+
_v7Verdict: "DORMANT",
|
|
45192
|
+
_v7Lift: 1,
|
|
45193
|
+
_v7Recall: 0,
|
|
45194
|
+
_v7FpRate: 0,
|
|
45195
|
+
_v7Precision: 0,
|
|
45196
|
+
_v8Verdict: "DORMANT",
|
|
45197
|
+
_v8Lift: 1,
|
|
45198
|
+
defaultOff: true
|
|
45199
|
+
},
|
|
45200
|
+
"kotlin/object-singleton-misuse": {
|
|
45201
|
+
recall: 0,
|
|
45202
|
+
fpRate: 0,
|
|
45203
|
+
ratio: 1,
|
|
45204
|
+
precision: 0,
|
|
45205
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
45206
|
+
verdict: "DORMANT",
|
|
45207
|
+
_calibrationNote: "v0.24: new rule (Kotlin `object` singleton holding `var` \u2014 hidden shared state). DORMANT until v9 Kotlin corpus calibration.",
|
|
45208
|
+
aiSpecific: true,
|
|
45209
|
+
_v7Verdict: "DORMANT",
|
|
45210
|
+
_v7Lift: 1,
|
|
45211
|
+
_v7Recall: 0,
|
|
45212
|
+
_v7FpRate: 0,
|
|
45213
|
+
_v7Precision: 0,
|
|
45214
|
+
_v8Verdict: "DORMANT",
|
|
45215
|
+
_v8Lift: 1,
|
|
45216
|
+
defaultOff: true
|
|
45217
|
+
},
|
|
45218
|
+
"kotlin/string-concat-loop": {
|
|
45219
|
+
recall: 0,
|
|
45220
|
+
fpRate: 0,
|
|
45221
|
+
ratio: 1,
|
|
45222
|
+
precision: 0,
|
|
45223
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
45224
|
+
verdict: "DORMANT",
|
|
45225
|
+
_calibrationNote: "v0.24: new rule (string concat inside a Kotlin loop \u2014 use buildString { ... }). DORMANT until v9 Kotlin corpus calibration.",
|
|
45226
|
+
aiSpecific: true,
|
|
45227
|
+
_v7Verdict: "DORMANT",
|
|
45228
|
+
_v7Lift: 1,
|
|
45229
|
+
_v7Recall: 0,
|
|
45230
|
+
_v7FpRate: 0,
|
|
45231
|
+
_v7Precision: 0,
|
|
45232
|
+
_v8Verdict: "DORMANT",
|
|
45233
|
+
_v8Lift: 1,
|
|
45234
|
+
defaultOff: true
|
|
45235
|
+
},
|
|
45236
|
+
"swift/force-unwrap": {
|
|
45237
|
+
recall: 0,
|
|
45238
|
+
fpRate: 0,
|
|
45239
|
+
ratio: 1,
|
|
45240
|
+
precision: 0,
|
|
45241
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
45242
|
+
verdict: "DORMANT",
|
|
45243
|
+
_calibrationNote: "v0.24: new rule (Swift force-unwrap \u2014 `as!`, `try!`, `var!.prop` in production). DORMANT until v9 Swift corpus calibration.",
|
|
45244
|
+
aiSpecific: true,
|
|
45245
|
+
_v7Verdict: "DORMANT",
|
|
45246
|
+
_v7Lift: 1,
|
|
45247
|
+
_v7Recall: 0,
|
|
45248
|
+
_v7FpRate: 0,
|
|
45249
|
+
_v7Precision: 0,
|
|
45250
|
+
_v8Verdict: "DORMANT",
|
|
45251
|
+
_v8Lift: 1,
|
|
45252
|
+
defaultOff: true
|
|
45253
|
+
},
|
|
45254
|
+
"swift/print-debug": {
|
|
45255
|
+
recall: 0,
|
|
45256
|
+
fpRate: 0,
|
|
45257
|
+
ratio: 1,
|
|
45258
|
+
precision: 0,
|
|
45259
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
45260
|
+
verdict: "DORMANT",
|
|
45261
|
+
_calibrationNote: "v0.24: new rule (multi-print in Swift source \u2014 use Logger / os.log). DORMANT until v9 Swift corpus calibration.",
|
|
45262
|
+
aiSpecific: true,
|
|
45263
|
+
_v7Verdict: "DORMANT",
|
|
45264
|
+
_v7Lift: 1,
|
|
45265
|
+
_v7Recall: 0,
|
|
45266
|
+
_v7FpRate: 0,
|
|
45267
|
+
_v7Precision: 0,
|
|
45268
|
+
_v8Verdict: "DORMANT",
|
|
45269
|
+
_v8Lift: 1,
|
|
45270
|
+
defaultOff: true
|
|
45271
|
+
},
|
|
45272
|
+
"swift/fatal-error-thrown": {
|
|
45273
|
+
recall: 0,
|
|
45274
|
+
fpRate: 0,
|
|
45275
|
+
ratio: 1,
|
|
45276
|
+
precision: 0,
|
|
45277
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
45278
|
+
verdict: "DORMANT",
|
|
45279
|
+
_calibrationNote: "v0.24: new rule (`fatalError` / `preconditionFailure` in production Swift \u2014 replaces with typed default). DORMANT until v9 Swift corpus calibration.",
|
|
45280
|
+
aiSpecific: true,
|
|
45281
|
+
_v7Verdict: "DORMANT",
|
|
45282
|
+
_v7Lift: 1,
|
|
45283
|
+
_v7Recall: 0,
|
|
45284
|
+
_v7FpRate: 0,
|
|
45285
|
+
_v7Precision: 0,
|
|
45286
|
+
_v8Verdict: "DORMANT",
|
|
45287
|
+
_v8Lift: 1,
|
|
45288
|
+
defaultOff: true
|
|
45289
|
+
},
|
|
45290
|
+
"swift/implicitly-unwrapped-optional": {
|
|
45291
|
+
recall: 0,
|
|
45292
|
+
fpRate: 0,
|
|
45293
|
+
ratio: 1,
|
|
45294
|
+
precision: 0,
|
|
45295
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
45296
|
+
verdict: "DORMANT",
|
|
45297
|
+
_calibrationNote: "v0.24: new rule (Swift implicitly unwrapped optional \u2014 declare as Type? and unwrap with guard let). DORMANT until v9 Swift corpus calibration.",
|
|
45298
|
+
aiSpecific: true,
|
|
45299
|
+
_v7Verdict: "DORMANT",
|
|
45300
|
+
_v7Lift: 1,
|
|
45301
|
+
_v7Recall: 0,
|
|
45302
|
+
_v7FpRate: 0,
|
|
45303
|
+
_v7Precision: 0,
|
|
45304
|
+
_v8Verdict: "DORMANT",
|
|
45305
|
+
_v8Lift: 1,
|
|
45306
|
+
defaultOff: true
|
|
45307
|
+
},
|
|
45308
|
+
"swift/strong-self-capture": {
|
|
45309
|
+
recall: 0,
|
|
45310
|
+
fpRate: 0,
|
|
45311
|
+
ratio: 1,
|
|
45312
|
+
precision: 0,
|
|
45313
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
45314
|
+
verdict: "DORMANT",
|
|
45315
|
+
_calibrationNote: "v0.24: new rule (Swift closure captures `self` strongly \u2014 retain cycle. Use [weak self] / [unowned self]). DORMANT until v9 Swift corpus calibration.",
|
|
45316
|
+
aiSpecific: true,
|
|
45317
|
+
_v7Verdict: "DORMANT",
|
|
45318
|
+
_v7Lift: 1,
|
|
45319
|
+
_v7Recall: 0,
|
|
45320
|
+
_v7FpRate: 0,
|
|
45321
|
+
_v7Precision: 0,
|
|
45322
|
+
_v8Verdict: "DORMANT",
|
|
45323
|
+
_v8Lift: 1,
|
|
45324
|
+
defaultOff: true
|
|
45325
|
+
},
|
|
45326
|
+
"cpp/using-namespace-std": {
|
|
45327
|
+
recall: 0,
|
|
45328
|
+
fpRate: 0,
|
|
45329
|
+
ratio: 1,
|
|
45330
|
+
precision: 0,
|
|
45331
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
45332
|
+
verdict: "DORMANT",
|
|
45333
|
+
_calibrationNote: "v0.24: new rule (`using namespace std;` in C++ header \u2014 pollutes every consumer). DORMANT until v9 C++ corpus calibration.",
|
|
45334
|
+
aiSpecific: true,
|
|
45335
|
+
_v7Verdict: "DORMANT",
|
|
45336
|
+
_v7Lift: 1,
|
|
45337
|
+
_v7Recall: 0,
|
|
45338
|
+
_v7FpRate: 0,
|
|
45339
|
+
_v7Precision: 0,
|
|
45340
|
+
_v8Verdict: "DORMANT",
|
|
45341
|
+
_v8Lift: 1,
|
|
45342
|
+
defaultOff: true
|
|
45343
|
+
},
|
|
45344
|
+
"cpp/raw-new-delete": {
|
|
45345
|
+
recall: 0,
|
|
45346
|
+
fpRate: 0,
|
|
45347
|
+
ratio: 1,
|
|
45348
|
+
precision: 0,
|
|
45349
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
45350
|
+
verdict: "DORMANT",
|
|
45351
|
+
_calibrationNote: "v0.24: new rule (manual `new` / `delete` pairing in C++ \u2014 use std::make_unique / std::unique_ptr). DORMANT until v9 C++ corpus calibration.",
|
|
45352
|
+
aiSpecific: true,
|
|
45353
|
+
_v7Verdict: "DORMANT",
|
|
45354
|
+
_v7Lift: 1,
|
|
45355
|
+
_v7Recall: 0,
|
|
45356
|
+
_v7FpRate: 0,
|
|
45357
|
+
_v7Precision: 0,
|
|
45358
|
+
_v8Verdict: "DORMANT",
|
|
45359
|
+
_v8Lift: 1,
|
|
45360
|
+
defaultOff: true
|
|
45361
|
+
},
|
|
45362
|
+
"cpp/c-style-cast": {
|
|
45363
|
+
recall: 0,
|
|
45364
|
+
fpRate: 0,
|
|
45365
|
+
ratio: 1,
|
|
45366
|
+
precision: 0,
|
|
45367
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
45368
|
+
verdict: "DORMANT",
|
|
45369
|
+
_calibrationNote: "v0.24: new rule (C-style cast `(int)x` in C++ \u2014 use static_cast / reinterpret_cast / const_cast). DORMANT until v9 C++ corpus calibration.",
|
|
45370
|
+
aiSpecific: true,
|
|
45371
|
+
_v7Verdict: "DORMANT",
|
|
45372
|
+
_v7Lift: 1,
|
|
45373
|
+
_v7Recall: 0,
|
|
45374
|
+
_v7FpRate: 0,
|
|
45375
|
+
_v7Precision: 0,
|
|
45376
|
+
_v8Verdict: "DORMANT",
|
|
45377
|
+
_v8Lift: 1,
|
|
45378
|
+
defaultOff: true
|
|
45379
|
+
},
|
|
45380
|
+
"cpp/printf-debug": {
|
|
45381
|
+
recall: 0,
|
|
45382
|
+
fpRate: 0,
|
|
45383
|
+
ratio: 1,
|
|
45384
|
+
precision: 0,
|
|
45385
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
45386
|
+
verdict: "DORMANT",
|
|
45387
|
+
_calibrationNote: "v0.24: new rule (printf family / std::cout debug printing in C++ \u2014 use spdlog / glog / AbslLog). DORMANT until v9 C++ corpus calibration.",
|
|
45388
|
+
aiSpecific: true,
|
|
45389
|
+
_v7Verdict: "DORMANT",
|
|
45390
|
+
_v7Lift: 1,
|
|
45391
|
+
_v7Recall: 0,
|
|
45392
|
+
_v7FpRate: 0,
|
|
45393
|
+
_v7Precision: 0,
|
|
45394
|
+
_v8Verdict: "DORMANT",
|
|
45395
|
+
_v8Lift: 1,
|
|
45396
|
+
defaultOff: true
|
|
45397
|
+
},
|
|
45398
|
+
"cpp/magic-numbers": {
|
|
45399
|
+
recall: 0,
|
|
45400
|
+
fpRate: 0,
|
|
45401
|
+
ratio: 1,
|
|
45402
|
+
precision: 0,
|
|
45403
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
45404
|
+
verdict: "DORMANT",
|
|
45405
|
+
_calibrationNote: "v0.24: new rule (magic-number literal in C++ comparison / return \u2014 name it `constexpr int MAX = ...`). DORMANT until v9 C++ corpus calibration.",
|
|
45406
|
+
aiSpecific: true,
|
|
45407
|
+
_v7Verdict: "DORMANT",
|
|
45408
|
+
_v7Lift: 1,
|
|
45409
|
+
_v7Recall: 0,
|
|
45410
|
+
_v7FpRate: 0,
|
|
45411
|
+
_v7Precision: 0,
|
|
45412
|
+
_v8Verdict: "DORMANT",
|
|
45413
|
+
_v8Lift: 1,
|
|
45414
|
+
defaultOff: true
|
|
43955
45415
|
}
|
|
43956
45416
|
};
|
|
43957
45417
|
|