agent-roast 0.17.0 → 0.19.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 +14 -5
- package/dist/{chunk-IKXGHCWB.js → chunk-OAMVMB5N.js} +418 -54
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -80,7 +80,7 @@ Policy is read from the **base** branch, so a pull request cannot switch off the
|
|
|
80
80
|
| --- | --- | :-: |
|
|
81
81
|
| `assertion-weakened` | an exact check is replaced by a weaker one: `toBe(42)` → `toBeDefined()`, `toBe(2)` → `toBeLessThanOrEqual(2)`, `toEqual(x)` → `toMatchObject(x)`, `assert x == 1` → `assert x` | blocks |
|
|
82
82
|
| `guard-weakened` | a coverage threshold is lowered, `strict` is switched off, a lint rule is downgraded, a CI step may fail, a workflow stops running tests, a `conftest.py` hook sets a report to `passed` | blocks |
|
|
83
|
-
| `test-skip` | a test is switched off or expected to fail: `it.skip`, `@pytest.mark.skip`, `@pytest.mark.xfail`, `it.fails`, `test.fixme`, `t.Skip`, `#[ignore]`, `@Disabled`, `[Ignore]`, `xit`, ... | blocks |
|
|
83
|
+
| `test-skip` | a test that was already there is switched off or expected to fail: `it.skip`, `@pytest.mark.skip`, `@pytest.mark.xfail`, `it.fails`, `test.fixme`, `t.Skip`, `#[ignore]`, `@Disabled`, `[Ignore]`, `xit`, ... (a new test that is skipped is not reported) | blocks |
|
|
84
84
|
| `test-focus`, `trivial-assert` | `.only` / `fit`; `expect(true).toBe(true)` and its relatives in ten languages; a test that asserts on a function it mocked itself | blocks |
|
|
85
85
|
| `assertion-removed`, `test-removed`, `expectation-rewritten` | a test loses assertions, disappears, or its expected value is edited next to production code | warns |
|
|
86
86
|
| `swallowed-error`, `type-escape`, `stub-abandonment` | `catch {}` / `except: pass`, `as any` / `# type: ignore` / `# pragma: no cover` / `istanbul ignore`, `throw new Error("Not implemented")` | warns |
|
|
@@ -109,17 +109,26 @@ old debt with a baseline: [docs/reference.md](docs/reference.md#configuration).
|
|
|
109
109
|
## Does it work?
|
|
110
110
|
|
|
111
111
|
Measured, with the denominators and the caveats in [docs/benchmarks.md](docs/benchmarks.md). **Weakened checks are rare, mostly
|
|
112
|
-
deliberate, and not specific to AI. agent-roast is a tripwire that
|
|
112
|
+
deliberate, and not specific to AI. agent-roast is a tripwire that asks about them. On repositories it had not seen, between a quarter and
|
|
113
|
+
two thirds of its blocks were questions with a harmless answer, depending on the sample.**
|
|
113
114
|
|
|
114
|
-
- **
|
|
115
|
+
- **145 of 145** hand-labeled cheating edits are caught and **127 of 127** harmless look-alikes (reformats, moves, raised
|
|
115
116
|
thresholds, environment-gated skips) stay silent. The cases were written with the detector, so they guard against regressions;
|
|
116
117
|
they do not prove accuracy. One gap is documented and still open.
|
|
117
118
|
- **12,645 commits** that touch tests or CI/config in 166 public repositories: blocking rules fired on 0.3% of AI-signed commits
|
|
118
119
|
(Claude, Copilot, Cursor, Codex, Devin, aider) and on 0.3% of the others. Inside the same repositories the odds for an
|
|
119
120
|
AI-signed commit were 0.81 times those of another commit (95% CI 0.40–1.67). There is no sign that AI-signed commits weaken
|
|
120
121
|
checks more often.
|
|
121
|
-
- **
|
|
122
|
-
|
|
122
|
+
- **On repositories it was not written from, between a quarter and two thirds of the blocked commits were false alarms.** Six samples of 39
|
|
123
|
+
or 40 public repositories (238 in all, 67,843 commits) were replayed and every blocked commit was read. The share was 69% for 0.17.0 on the
|
|
124
|
+
first sample and on the fourth, 51% for 0.18.0 on the fourth and 27% on the fifth, which it had not seen either. This version, frozen before the
|
|
125
|
+
sixth sample was drawn, blocked 100 of its 11,335 commits: 34 changed a check a reviewer should see (an assertion that got weaker, a gate
|
|
126
|
+
that was relaxed, a test switched off or left focused) and 66 were false alarms, but 51 of the 66 were one defect in one repository, an array of
|
|
127
|
+
mypy overrides read as one table, which is fixed in this release; the released build blocks 44 of those commits, 34 and 10. A new
|
|
128
|
+
test that asserts nothing counts as a block for a reviewer; counted as false alarms, 13 of 33 on the fifth sample and 72 of 100 on the sixth. Most of the rest, in the
|
|
129
|
+
earlier measurements too, were deliberate and explained. One reader (an AI assistant) sorted them, and the fixes were made from the same
|
|
130
|
+
sorting, so treat the figures as a first estimate ([section 9](docs/benchmarks.md#9-repositories-nobody-had-tuned-on),
|
|
131
|
+
[section 10](docs/benchmarks.md#10-two-more-samples-and-what-the-fixes-from-sample-4-did)).
|
|
123
132
|
- **9,732 commits compared in full** from 164 public repositories that use coding agents (5,454 of them AI-signed) were reviewed again for the
|
|
124
133
|
last two releases, and every new finding was read. 0.15.0's rules reported 30 new findings there and 7 were false alarms (fixed in 0.16.0);
|
|
125
134
|
0.17.0's rule for an exact check turned into a bound reported 18 in 14 commits, all of them loosened checks, after two false alarms were
|
|
@@ -1166,7 +1166,8 @@ var PYTHON_TEST_SKIP_PATTERNS = [
|
|
|
1166
1166
|
{ regex: /\bself\s*\.\s*skipTest\s*\(/, name: "imperative unittest skip (self.skipTest)", imperative: true },
|
|
1167
1167
|
// A test that is expected to fail cannot fail the run, which is what a skip does too.
|
|
1168
1168
|
{ regex: /(?<![\w.])pytest\s*\.\s*xfail\s*\(/, name: "imperative pytest xfail (pytest.xfail)", imperative: true },
|
|
1169
|
-
|
|
1169
|
+
// `@unittest.expectedFailure`, and the same decorator under another module's name (`@test.expectedFailure` of a test framework that re-exports it).
|
|
1170
|
+
{ regex: /@(?:[A-Za-z_]\w*\s*\.\s*)*expectedFailure\b/, name: "test expected to fail (@unittest.expectedFailure)", imperative: false }
|
|
1170
1171
|
];
|
|
1171
1172
|
var PYTHON_MARKER = /(@\s*|(?<![\w.])marks\s*=\s*\[?\s*|^[ \t]*pytestmark\s*=\s*\[?\s*)pytest\s*\.\s*mark\s*\.\s*(skip|xfail)\b/gm;
|
|
1172
1173
|
function isConditionalXfail(args) {
|
|
@@ -1317,13 +1318,55 @@ function isGuardedBlockSkip(lines, profile) {
|
|
|
1317
1318
|
return compiledConditionally(i + 1);
|
|
1318
1319
|
}
|
|
1319
1320
|
var JS_TEST_API = /(?<![\w$.])(?:(?:it|test|describe|suite)(?:\s*\.\s*(?:concurrent|serial|parallel|describe|failing|sequential))*\s*\.\s*(skip|todo|only|fixme|failing|fails|fail)|((?:xit|xtest|xdescribe|fit|fdescribe)))\b((?:\s*\.\s*(?:each|failing|concurrent|serial|parallel|sequential))*)\s*(?=\(|\u0001)/g;
|
|
1320
|
-
|
|
1321
|
+
var MENTIONS_CI = /\bCI\b|\bisCI\b|GITHUB_ACTIONS/;
|
|
1322
|
+
function isConditionalSkip(source, raw, open2, start) {
|
|
1321
1323
|
const args = callArguments(source, raw, open2);
|
|
1322
1324
|
if (!args) return false;
|
|
1323
1325
|
const isString = (arg) => /^(['"`])[\s\S]*\1$/.test(arg);
|
|
1324
|
-
if (!args.length ||
|
|
1326
|
+
if (!args.length || args[0] === "true") return isUnderRuntimeCondition(source, raw, start);
|
|
1327
|
+
if (isString(args[0])) return false;
|
|
1325
1328
|
if (args.length > 1 && !isString(args.at(-1))) return false;
|
|
1326
|
-
return
|
|
1329
|
+
return !MENTIONS_CI.test(args[0]);
|
|
1330
|
+
}
|
|
1331
|
+
var TEST_REGISTRATION = /^(?:await\s+)?(?:test|it|describe|suite|beforeAll|beforeEach|afterAll|afterEach)\b/;
|
|
1332
|
+
function isUnderRuntimeCondition(source, raw, start) {
|
|
1333
|
+
const alwaysTrue = (text) => /^\s*(?:true|1|!\s*0|!\s*false)\s*$/.test(text);
|
|
1334
|
+
const indentOf = (text) => text.length - text.trimStart().length;
|
|
1335
|
+
const lineStartOf = (offset) => Math.max(source.lastIndexOf("\n", offset - 1), source.lastIndexOf("\0", offset - 1)) + 1;
|
|
1336
|
+
const mentionsCi = (from, to) => MENTIONS_CI.test(raw.slice(from, to));
|
|
1337
|
+
const lineStart = lineStartOf(start);
|
|
1338
|
+
const head = source.slice(lineStart, start).trimEnd();
|
|
1339
|
+
if (head.endsWith(")")) {
|
|
1340
|
+
let depth = 0, opening = -1;
|
|
1341
|
+
for (let i = head.length - 1; i >= 0; i--) {
|
|
1342
|
+
if (head[i] === ")") depth++;
|
|
1343
|
+
else if (head[i] === "(" && --depth === 0) {
|
|
1344
|
+
opening = i;
|
|
1345
|
+
break;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
if (opening >= 0 && /(?:^|[\s}])if\s*$/.test(head.slice(0, opening))) {
|
|
1349
|
+
return !mentionsCi(lineStart + opening + 1, lineStart + head.length - 1) && !alwaysTrue(head.slice(opening + 1, -1));
|
|
1350
|
+
}
|
|
1351
|
+
} else if (/(?:^|[\s}])else$/.test(head)) return true;
|
|
1352
|
+
else if (head.endsWith("&&")) return !mentionsCi(lineStart, start) && !alwaysTrue(head.slice(0, -2).replace(/^[\s{}]+/, ""));
|
|
1353
|
+
const lineEnd = source.slice(start).search(/[\n\0]/);
|
|
1354
|
+
let limit = indentOf(source.slice(lineStart, lineEnd < 0 ? void 0 : start + lineEnd)), at = lineStart;
|
|
1355
|
+
for (let steps = 0; steps < 200 && at > 0; steps++) {
|
|
1356
|
+
if (source[at - 1] === "\0") return false;
|
|
1357
|
+
const previous = lineStartOf(at - 1), line = source.slice(previous, at - 1);
|
|
1358
|
+
const lineRaw = raw.slice(previous, at - 1);
|
|
1359
|
+
at = previous;
|
|
1360
|
+
if (!line.trim() || indentOf(line) >= limit) continue;
|
|
1361
|
+
const header = line.trim();
|
|
1362
|
+
if (/^(?:\}\s*)?(?:else\s+)?if\s*\(/.test(header)) {
|
|
1363
|
+
return !MENTIONS_CI.test(lineRaw) && !alwaysTrue(/\bif\s*\((.*)\)\s*\{?\s*$/.exec(header)?.[1] ?? "");
|
|
1364
|
+
}
|
|
1365
|
+
if (/^(?:\}\s*)?(?:else|catch)\b|^(?:case\b|default\s*:)|^\)\s*\{?\s*$/.test(header)) return true;
|
|
1366
|
+
if (TEST_REGISTRATION.test(header)) return false;
|
|
1367
|
+
limit = indentOf(line);
|
|
1368
|
+
}
|
|
1369
|
+
return false;
|
|
1327
1370
|
}
|
|
1328
1371
|
function callArguments(source, raw, open2) {
|
|
1329
1372
|
if (source[open2] !== "(") return null;
|
|
@@ -1623,9 +1666,11 @@ function auditFileDiffs(files, commitHash = "HEAD", commitDate = (/* @__PURE__ *
|
|
|
1623
1666
|
while (previous >= 0 && /\s/.test(source[previous])) previous--;
|
|
1624
1667
|
if (source[previous] === ".") continue;
|
|
1625
1668
|
if (match[2] && !isTestFile(file.path)) continue;
|
|
1669
|
+
if (match[2] && callArguments(source, raw, match.index + match[0].length)?.length === 0) continue;
|
|
1670
|
+
if (match[2] && previous >= 0 && (/[(,=:?[&|!+\-*%<]/.test(source[previous]) || /(?:await|return|yield|typeof|void)$/.test(source.slice(Math.max(0, previous - 6), previous + 1)))) continue;
|
|
1626
1671
|
if (/\bfunction$/.test(source.slice(Math.max(0, previous - 8), previous + 1))) continue;
|
|
1627
1672
|
if (source[match.index + match[0].length] === "" && !/\.\s*each\s*$/.test(match[3])) continue;
|
|
1628
|
-
if ((match[1] === "skip" || match[1] === "fixme" || match[1] === "fail") && !match[3] && isConditionalSkip(source, raw, match.index + match[0].length)) continue;
|
|
1673
|
+
if ((match[1] === "skip" || match[1] === "fixme" || match[1] === "fail") && !match[3] && isConditionalSkip(source, raw, match.index + match[0].length, match.index)) continue;
|
|
1629
1674
|
const focus = match[1] === "only" || match[2] === "fit" || match[2] === "fdescribe";
|
|
1630
1675
|
const expectedToFail = match[1] === "failing" || match[1] === "fails" || match[1] === "fail";
|
|
1631
1676
|
infractions.push({
|
|
@@ -3206,6 +3251,12 @@ function javascript(text) {
|
|
|
3206
3251
|
const end = closing(code, open2);
|
|
3207
3252
|
if (end > 0) callbacks.push([open2, end]);
|
|
3208
3253
|
}
|
|
3254
|
+
const tests = [];
|
|
3255
|
+
for (const match of code.matchAll(/(?<![\w$.])(?:it|test|describe)(?:\s*\.\s*\w+)*\s*\(/g)) {
|
|
3256
|
+
const open2 = match.index + match[0].length - 1;
|
|
3257
|
+
const end = closing(code, open2);
|
|
3258
|
+
if (end > 0) tests.push([open2, end]);
|
|
3259
|
+
}
|
|
3209
3260
|
const programLevel = (index) => {
|
|
3210
3261
|
let depth = 0;
|
|
3211
3262
|
for (let i = 0; i < index; i++) {
|
|
@@ -3224,6 +3275,9 @@ function javascript(text) {
|
|
|
3224
3275
|
const end = closing(code, open2);
|
|
3225
3276
|
const args = end > 0 ? raw.slice(open2, end) : "";
|
|
3226
3277
|
if (/requireActual|importActual|importOriginal/.test(args)) continue;
|
|
3278
|
+
const factoryHead = /^\(\s*(['"`])[^'"`\n]+\1\s*,\s*(?:async\s+)?(?:function\s*[\w$]*\s*)?(?:\(\s*([A-Za-z_$][\w$]*)\s*[,)]|([A-Za-z_$][\w$]*)\s*=>)/.exec(args);
|
|
3279
|
+
const loader = factoryHead?.[2] ?? factoryHead?.[3];
|
|
3280
|
+
if (loader && new RegExp(`(?<![\\w$])(?:(?<=\\.\\.\\.)|(?<!\\.))${loader.replace(/\$/g, "\\$")}\\s*(?:<[^\\n]*?>)?\\s*\\(`).test(args.slice(args.indexOf(loader) + loader.length))) continue;
|
|
3227
3281
|
const factory = /^\(\s*(['"`])[^'"`\n]+\1\s*,/.test(args) && !/,\s*\{\s*spy\s*:\s*true\b/.test(args);
|
|
3228
3282
|
if (!mocked.has(specifier)) mocked.set(specifier, { index: match.index, fake: factory });
|
|
3229
3283
|
}
|
|
@@ -3252,7 +3306,8 @@ function javascript(text) {
|
|
|
3252
3306
|
if (match[1].startsWith("{")) bind(match[1], specifier);
|
|
3253
3307
|
else bindings.set(match[1], { specifier, name: "*", namespace: true });
|
|
3254
3308
|
}
|
|
3255
|
-
const
|
|
3309
|
+
const candidates = [];
|
|
3310
|
+
const mockedNames = [...bindings].filter(([, binding]) => mocked.has(binding.specifier)).map(([local]) => local.replace(/\$/g, "\\$"));
|
|
3256
3311
|
for (const match of code.matchAll(/(?<![\w$.])expect\s*\(/g)) {
|
|
3257
3312
|
const open2 = match.index + match[0].length - 1;
|
|
3258
3313
|
const end = closing(code, open2);
|
|
@@ -3273,18 +3328,32 @@ function javascript(text) {
|
|
|
3273
3328
|
const name = binding.namespace ? call[2] : call[2] ? void 0 : binding.name;
|
|
3274
3329
|
if (!name) continue;
|
|
3275
3330
|
const identifier = call[1].replace(/\$/g, "\\$");
|
|
3276
|
-
const
|
|
3331
|
+
const configures = new RegExp(`(?<![\\w$.])${identifier}\\b[^;]{0,80}?\\.\\s*mock(?:Return|Resolved|Rejected|Implementation)\\w*\\s*\\(`, "g");
|
|
3332
|
+
const configured = [...raw.matchAll(configures)].some((config) => tests.every((range) => !within(range, config.index) || within(range, match.index)));
|
|
3277
3333
|
if (!mock.fake && !configured) continue;
|
|
3278
3334
|
const key = `${binding.specifier}::${name}`;
|
|
3279
|
-
if (found.has(key)) continue;
|
|
3280
3335
|
const line = lineOf2(mock.index);
|
|
3281
|
-
|
|
3336
|
+
candidates.push({
|
|
3282
3337
|
key,
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3338
|
+
at: match.index,
|
|
3339
|
+
range: innermost(callbacks, match.index),
|
|
3340
|
+
subject: {
|
|
3341
|
+
key,
|
|
3342
|
+
line,
|
|
3343
|
+
snippet: lines[line - 1].trim().slice(0, 120),
|
|
3344
|
+
reason: `assertion on the output of ${name}(), which this test replaces with a mock of "${binding.specifier}"`
|
|
3345
|
+
}
|
|
3286
3346
|
});
|
|
3287
3347
|
}
|
|
3348
|
+
const candidateAt = new Set(candidates.map((candidate) => candidate.at));
|
|
3349
|
+
const mentionsMock = mockedNames.length ? new RegExp(`(?<![\\w$.])(?:${mockedNames.join("|")})(?![\\w$])`) : null;
|
|
3350
|
+
const checksSomethingElse = (range) => !!range && [...code.matchAll(/(?<![\w$.])expect\s*\(/g)].some((other) => {
|
|
3351
|
+
if (!within(range, other.index) || candidateAt.has(other.index)) return false;
|
|
3352
|
+
const open2 = other.index + other[0].length - 1, end = closing(code, open2);
|
|
3353
|
+
return end > 0 && !mentionsMock?.test(code.slice(open2 + 1, end));
|
|
3354
|
+
});
|
|
3355
|
+
const found = /* @__PURE__ */ new Map();
|
|
3356
|
+
for (const candidate of candidates) if (!found.has(candidate.key) && !checksSomethingElse(candidate.range)) found.set(candidate.key, candidate.subject);
|
|
3288
3357
|
return [...found.values()];
|
|
3289
3358
|
}
|
|
3290
3359
|
var DOTTED = /^[A-Za-z_][\w.]*$/;
|
|
@@ -3339,6 +3408,15 @@ function python(text) {
|
|
|
3339
3408
|
}
|
|
3340
3409
|
return [line, end];
|
|
3341
3410
|
};
|
|
3411
|
+
const headerLine = (line) => {
|
|
3412
|
+
for (let k = line; k >= 0 && k > line - 8; k--) {
|
|
3413
|
+
if (!/^\s*with\b/.test(codeLines2[k] ?? "")) continue;
|
|
3414
|
+
const head = codeLines2.slice(k, line).join("\n");
|
|
3415
|
+
const open2 = (head.match(/[([{]/g) ?? []).length - (head.match(/[)\]}]/g) ?? []).length;
|
|
3416
|
+
return k === line || open2 > 0 || codeLines2.slice(k, line).every((text2) => /\\\s*$/.test(text2)) ? k : null;
|
|
3417
|
+
}
|
|
3418
|
+
return null;
|
|
3419
|
+
};
|
|
3342
3420
|
const patched = /* @__PURE__ */ new Map();
|
|
3343
3421
|
const patchCall = /(?<![\w.])(?:(?:mock|unittest\s*\.\s*mock)\s*\.\s*)?patch(\s*\.\s*object)?\s*\(|(?<![\w.])mocker\s*\.\s*patch(\s*\.\s*object)?\s*\(|(?<![\w.])monkeypatch\s*\.\s*setattr\s*\(/g;
|
|
3344
3422
|
for (const match of code.matchAll(patchCall)) {
|
|
@@ -3352,7 +3430,11 @@ function python(text) {
|
|
|
3352
3430
|
const key = target ? target[2] : parts ? `${expandObject(parts[1])}.${parts[3]}` : void 0;
|
|
3353
3431
|
if (!key || !DOTTED.test(key)) continue;
|
|
3354
3432
|
const line = lineIndex(match.index);
|
|
3355
|
-
|
|
3433
|
+
const fixture = /^(?:mocker|monkeypatch)/.test(match[0]);
|
|
3434
|
+
const header = headerLine(line);
|
|
3435
|
+
const inForce = fixture || /^\s*@/.test(codeLines2[line] ?? "") || header !== null || /^\s*\.\s*start\s*\(/.test(code.slice(end + 1)) || /enter_context\s*\(\s*$/.test(code.slice(Math.max(0, match.index - 40), match.index));
|
|
3436
|
+
if (!inForce) continue;
|
|
3437
|
+
patched.set(key, [...patched.get(key) ?? [], { index: match.index, end, object: parts ? parts[1] : null, scope: (header !== null ? blockOf(header) : null) ?? blockOf(line) ?? scopeOf(line) }]);
|
|
3356
3438
|
}
|
|
3357
3439
|
if (!patched.size) return [];
|
|
3358
3440
|
const found = /* @__PURE__ */ new Map();
|
|
@@ -3372,6 +3454,16 @@ function python(text) {
|
|
|
3372
3454
|
const at = lineIndex(match.index);
|
|
3373
3455
|
const patch = patched.get(key)?.find((item) => item.scope && within(item.scope, at) && (at >= lineIndex(item.index) || /^\s*@/.test(codeLines2[lineIndex(item.index)] ?? "")));
|
|
3374
3456
|
if (!patch) continue;
|
|
3457
|
+
if (patch.object) {
|
|
3458
|
+
const handed = new RegExp(`(?<![\\w.])(?!(?:[\\w.]*\\.)?(?:patch|object|setattr)\\b)[A-Za-z_][\\w.]*\\(\\s*(?:[^()]*,\\s*)?${patch.object.replace(/[.$]/g, "\\$&")}\\s*[,)]`);
|
|
3459
|
+
if (handed.test(code.slice(patch.end + 1, match.index))) continue;
|
|
3460
|
+
}
|
|
3461
|
+
if (patch.object) {
|
|
3462
|
+
const attribute = key.slice(key.lastIndexOf(".") + 1);
|
|
3463
|
+
const receiver = new RegExp(`(?<![\\w.])${patch.object.replace(/[.$]/g, "\\$&")}\\s*\\.\\s*(?!${attribute}\\b)\\w+\\s*\\(`);
|
|
3464
|
+
if (receiver.test(code.slice(patch.end + 1, match.index))) continue;
|
|
3465
|
+
}
|
|
3466
|
+
if (/(?<![\w$])(?:[A-Za-z_]\w*\s*\.\s*)*_*(?:install|wrap|instrument|inject|hook|swap|rewire|reload)\w*\s*\(/.test(code.slice(patch.end + 1, match.index))) continue;
|
|
3375
3467
|
const line = lineOf2(patch.index);
|
|
3376
3468
|
found.set(key, {
|
|
3377
3469
|
key,
|
|
@@ -3402,13 +3494,15 @@ function openBrackets(text) {
|
|
|
3402
3494
|
function configEntries(text, toml) {
|
|
3403
3495
|
const lines = text.split(/\r?\n/);
|
|
3404
3496
|
const entries = [];
|
|
3405
|
-
|
|
3497
|
+
const elements = /* @__PURE__ */ new Map();
|
|
3498
|
+
let section = "", block = 0;
|
|
3406
3499
|
for (let i = 0; i < lines.length; i++) {
|
|
3407
3500
|
const trimmed = lines[i].trim();
|
|
3408
3501
|
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";")) continue;
|
|
3409
|
-
const header =
|
|
3502
|
+
const header = /^(\[\[?)\s*([^\]]+?)\s*\]\]?\s*(?:[#;].*)?$/.exec(trimmed);
|
|
3410
3503
|
if (header) {
|
|
3411
|
-
section = header[
|
|
3504
|
+
section = header[2].replace(/["']/g, "");
|
|
3505
|
+
block = header[1] === "[[" ? (elements.set(section, (elements.get(section) ?? 0) + 1), elements.get(section)) : 0;
|
|
3412
3506
|
continue;
|
|
3413
3507
|
}
|
|
3414
3508
|
const pair = /^("[^"]*"|'[^']*'|[\w.-]+)\s*=\s*(.*)$/.exec(trimmed);
|
|
@@ -3420,7 +3514,7 @@ function configEntries(text, toml) {
|
|
|
3420
3514
|
} else {
|
|
3421
3515
|
while (i + 1 < lines.length && /^\s+\S/.test(lines[i + 1]) && !/^\s*[#;]/.test(lines[i + 1])) value += "\n" + lines[++i].trim();
|
|
3422
3516
|
}
|
|
3423
|
-
entries.push({ section, key: pair[1].replace(/["']/g, ""), value, line: start + 1 });
|
|
3517
|
+
entries.push({ section, key: pair[1].replace(/["']/g, ""), value, line: start + 1, block });
|
|
3424
3518
|
}
|
|
3425
3519
|
return entries;
|
|
3426
3520
|
}
|
|
@@ -3494,10 +3588,16 @@ function analyzePythonConfig(file, oldText, newText, report) {
|
|
|
3494
3588
|
report(line, `${tool} select: ${dropped.slice(0, 4).map((code) => "-" + code).join(", ")}${dropped.length > 4 ? ", \u2026" : ""}`, "Lint rules removed from the selected set");
|
|
3495
3589
|
}
|
|
3496
3590
|
}
|
|
3497
|
-
const
|
|
3591
|
+
const tableOf = (entries) => {
|
|
3592
|
+
const modules = /* @__PURE__ */ new Map();
|
|
3593
|
+
for (const entry of entries) if (entry.block && entry.key === "module") modules.set(`${entry.section}#${entry.block}`, members(entry.value).join(","));
|
|
3594
|
+
return (entry) => entry.block ? `${entry.section}#${modules.get(`${entry.section}#${entry.block}`) ?? entry.block}` : entry.section;
|
|
3595
|
+
};
|
|
3596
|
+
const tableBefore = tableOf(before), tableAfter = tableOf(after);
|
|
3597
|
+
const switches = new Map(before.filter((entry) => toolOf(file, entry.section) === "mypy" && MYPY_SWITCHES.test(entry.key)).map((entry) => [`${tableBefore(entry)}|${entry.key}`, truthy(entry.value)]));
|
|
3498
3598
|
for (const entry of after) {
|
|
3499
3599
|
if (toolOf(file, entry.section) !== "mypy" || !MYPY_SWITCHES.test(entry.key)) continue;
|
|
3500
|
-
if (switches.get(`${entry
|
|
3600
|
+
if (switches.get(`${tableAfter(entry)}|${entry.key}`) === true && !truthy(entry.value)) report(entry.line, `mypy ${entry.key}: true \u2192 false`, "Type checker strictness switched off");
|
|
3501
3601
|
}
|
|
3502
3602
|
const ignoring = (entries) => entries.filter((entry) => toolOf(file, entry.section) === "mypy" && entry.key === "ignore_errors" && truthy(entry.value));
|
|
3503
3603
|
const before_ = ignoring(before), after_ = ignoring(after);
|
|
@@ -3593,20 +3693,113 @@ function testBodies(text, language) {
|
|
|
3593
3693
|
};
|
|
3594
3694
|
});
|
|
3595
3695
|
}
|
|
3696
|
+
function maskRegexLiterals(text) {
|
|
3697
|
+
let out = "", quote = "";
|
|
3698
|
+
for (let i = 0; i < text.length; i++) {
|
|
3699
|
+
const char = text[i];
|
|
3700
|
+
if (quote) {
|
|
3701
|
+
out += char;
|
|
3702
|
+
if (char === "\\" && i + 1 < text.length) out += text[++i];
|
|
3703
|
+
else if (char === quote) quote = "";
|
|
3704
|
+
continue;
|
|
3705
|
+
}
|
|
3706
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
3707
|
+
quote = char;
|
|
3708
|
+
out += char;
|
|
3709
|
+
continue;
|
|
3710
|
+
}
|
|
3711
|
+
if (char === "/" && text[i + 1] !== "/" && text[i + 1] !== "*") {
|
|
3712
|
+
let p = out.length - 1;
|
|
3713
|
+
while (p >= 0 && /\s/.test(out[p])) p--;
|
|
3714
|
+
if (p < 0 || /[(,=:[!&|?{};+\-*%~^]/.test(out[p]) || /(?:return|typeof|case)$/.test(out.slice(0, p + 1))) {
|
|
3715
|
+
let j = i + 1, inClass = false, closed = false;
|
|
3716
|
+
for (; j < text.length; j++) {
|
|
3717
|
+
const c = text[j];
|
|
3718
|
+
if (c === "\n") break;
|
|
3719
|
+
if (c === "\\") {
|
|
3720
|
+
j++;
|
|
3721
|
+
continue;
|
|
3722
|
+
}
|
|
3723
|
+
if (c === "[") inClass = true;
|
|
3724
|
+
else if (c === "]") inClass = false;
|
|
3725
|
+
else if (c === "/" && !inClass) {
|
|
3726
|
+
closed = true;
|
|
3727
|
+
break;
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
if (closed) {
|
|
3731
|
+
out += "/" + "_".repeat(j - i - 1) + "/";
|
|
3732
|
+
i = j;
|
|
3733
|
+
continue;
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
out += char;
|
|
3738
|
+
}
|
|
3739
|
+
return out;
|
|
3740
|
+
}
|
|
3596
3741
|
function balance(line) {
|
|
3597
|
-
const bare = line.replace(/(['"`])(?:\\.|(?!\1)[^\\])*\1/g, "");
|
|
3742
|
+
const bare = maskRegexLiterals(line).replace(/(['"`])(?:\\.|(?!\1)[^\\])*\1/g, "").replace(/\\./g, "");
|
|
3598
3743
|
let net = 0;
|
|
3599
3744
|
for (const char of bare) net += "([{".includes(char) ? 1 : ")]}".includes(char) ? -1 : 0;
|
|
3600
3745
|
return net;
|
|
3601
3746
|
}
|
|
3747
|
+
function withoutMessageParens(statement) {
|
|
3748
|
+
let depth = 0, quote = "";
|
|
3749
|
+
for (let i = 0; i < statement.length; i++) {
|
|
3750
|
+
const char = statement[i];
|
|
3751
|
+
if (quote) {
|
|
3752
|
+
if (char === "\\") i++;
|
|
3753
|
+
else if (char === quote) quote = "";
|
|
3754
|
+
} else if (char === '"' || char === "'") quote = char;
|
|
3755
|
+
else if ("([{".includes(char)) depth++;
|
|
3756
|
+
else if (")]}".includes(char)) depth--;
|
|
3757
|
+
else if (char === "," && depth === 0) {
|
|
3758
|
+
const message = statement.slice(i + 1).trim();
|
|
3759
|
+
if (!message.startsWith("(") || !message.endsWith(")")) return statement;
|
|
3760
|
+
let inner = 0, q = "";
|
|
3761
|
+
for (let j = 0; j < message.length; j++) {
|
|
3762
|
+
const c = message[j];
|
|
3763
|
+
if (q) {
|
|
3764
|
+
if (c === "\\") j++;
|
|
3765
|
+
else if (c === q) q = "";
|
|
3766
|
+
} else if (c === '"' || c === "'") q = c;
|
|
3767
|
+
else if (c === "(") inner++;
|
|
3768
|
+
else if (c === ")" && --inner === 0 && j < message.length - 1) return statement;
|
|
3769
|
+
}
|
|
3770
|
+
return `${statement.slice(0, i + 1)} ${message.slice(1, -1).trim()}`;
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
return statement;
|
|
3774
|
+
}
|
|
3775
|
+
function withoutOuterParens(statement) {
|
|
3776
|
+
const start = /^\s*assert\s+/.exec(statement);
|
|
3777
|
+
if (!start) return statement;
|
|
3778
|
+
if (statement[start[0].length] !== "(") return withoutMessageParens(statement);
|
|
3779
|
+
let depth = 0, quote = "";
|
|
3780
|
+
for (let i = start[0].length; i < statement.length; i++) {
|
|
3781
|
+
const char = statement[i];
|
|
3782
|
+
if (quote) {
|
|
3783
|
+
if (char === "\\") i++;
|
|
3784
|
+
else if (char === quote) quote = "";
|
|
3785
|
+
} else if (char === '"' || char === "'") quote = char;
|
|
3786
|
+
else if (char === "(") depth++;
|
|
3787
|
+
else if (char === ")" && --depth === 0) {
|
|
3788
|
+
const rest = statement.slice(i + 1);
|
|
3789
|
+
return /^\s*(?:,|$)/.test(rest) ? withoutMessageParens(`${statement.slice(0, start[0].length)}${statement.slice(start[0].length + 1, i)}${rest}`) : statement;
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
return statement;
|
|
3793
|
+
}
|
|
3602
3794
|
function normalize(statement) {
|
|
3603
3795
|
return statement.replace(/\s+/g, "").replace(/'/g, '"').replace(/,([)\]}])/g, "$1").replace(/;$/, "");
|
|
3604
3796
|
}
|
|
3605
3797
|
function splitStatements(text) {
|
|
3606
3798
|
const pieces = [];
|
|
3799
|
+
const scanned = maskRegexLiterals(text);
|
|
3607
3800
|
let quote = "", start = 0;
|
|
3608
|
-
for (let i = 0; i <
|
|
3609
|
-
const char =
|
|
3801
|
+
for (let i = 0; i < scanned.length; i++) {
|
|
3802
|
+
const char = scanned[i];
|
|
3610
3803
|
if (quote) {
|
|
3611
3804
|
if (char === "\\") i++;
|
|
3612
3805
|
else if (char === quote) quote = "";
|
|
@@ -3654,7 +3847,7 @@ function parseTests(text, language) {
|
|
|
3654
3847
|
const clean = piece.replace(CALLBACK_HEADER, "");
|
|
3655
3848
|
if (assertionPattern.test(clean)) {
|
|
3656
3849
|
raw.push(clean);
|
|
3657
|
-
assertions.push(normalize(clean));
|
|
3850
|
+
assertions.push(normalize(language === "py" ? withoutOuterParens(clean) : clean));
|
|
3658
3851
|
} else setupLines.push(clean);
|
|
3659
3852
|
}
|
|
3660
3853
|
}
|
|
@@ -3668,15 +3861,21 @@ function parseTests(text, language) {
|
|
|
3668
3861
|
function skeleton(assertion) {
|
|
3669
3862
|
return assertion.replace(/(['"`])(?:\\.|(?!\1)[^\\])*\1/g, "S").replace(/\b\d+(?:\.\d+)?\b/g, "N").replace(/\b(?:true|false|null|undefined|None|True|False|nil)\b/g, "V").replace(/\s+/g, "");
|
|
3670
3863
|
}
|
|
3671
|
-
var WEAK_JS = /\.(?:toBeDefined|toBeTruthy|toBeFalsy
|
|
3864
|
+
var WEAK_JS = /\.(?:toBeDefined|toBeTruthy|toBeFalsy)\s*\(|\.not\s*\.\s*(?:toBeNull|toBeUndefined|toThrow)\s*\(\s*\)|\.toBeGreaterThanOrEqual\s*\(\s*0\s*\)|(?<!\.not)\.(?:toHaveBeenCalled|toBeCalled)\s*\(\s*\)|\.(?:toBe|toEqual|toStrictEqual)\s*\(\s*expect\s*\.\s*(?:anything\s*\(\s*\)|any\s*\(\s*\w+\s*\))\s*\)/;
|
|
3672
3865
|
var BOUND_JS = /\.toBeGreaterThan\s*\(\s*(?:0|-1)\s*\)|\.toBeGreaterThanOrEqual\s*\(\s*[01]\s*\)/;
|
|
3673
3866
|
var ZERO_JS = /\.(?:toBe|toEqual|toStrictEqual|toHaveLength|toHaveBeenCalledTimes)\s*\(\s*(?:0|\[\s*\]|\{\s*\}|null|undefined|false|"")\s*\)/;
|
|
3674
|
-
var WEAK_PY = /^\s*assert\s+[\w
|
|
3867
|
+
var WEAK_PY = /^\s*assert\s+[\w.]+(?:\[[^\]]*\])*\s*(?:,.*)?$|^\s*assert\s+.+\bis\s+not\s+None\s*(?:,.*)?$|^\s*assert\s+True\b/;
|
|
3675
3868
|
var BOUND_PY = /^\s*assert\s+.+(?:>\s*0|>=\s*[01])\s*(?:,.*)?$/;
|
|
3676
3869
|
var EMPTY_PY = /==\s*(?:0(?:\.0+)?(?![\w.])|\[\s*\]|\{\s*\}|""|''|\(\s*\)|(?:set|dict|list|tuple|frozenset)\(\s*\)|None\b|False\b)|\bis\s+None\b|\bnot\b|!=\s*(?:0|None)\b/;
|
|
3677
3870
|
var PY_SPECIFIC = /==|!=|<=|>=|\bin\b|\bis\b|\bisinstance\s*\(|\.startswith\s*\(|\.endswith\s*\(|\bcallable\s*\(/;
|
|
3678
3871
|
var JS_SPECIFIC = /\.(?:toBe|toEqual|toStrictEqual|toHaveLength|toContain|toContainEqual|toMatch|toMatchObject|toHaveProperty|toHaveBeenCalledWith|toHaveBeenLastCalledWith|toHaveBeenNthCalledWith|toBeCalledWith|toHaveBeenCalledTimes|toHaveBeenCalledOnce|toHaveTextContent|toHaveAttribute|toHaveValue|toThrow\w*|toBeGreaterThan\w*|toBeLessThan\w*)\s*\(/;
|
|
3679
3872
|
var THROWING_QUERY = /\b(?:get|find)(?:All)?By\w*\s*\(/;
|
|
3873
|
+
var PRESENCE_PREDICATE = /\.(?:exists|isVisible|isExisting|isDisplayed)\(\)$/;
|
|
3874
|
+
function samePresence(oldAssertion, newAssertion) {
|
|
3875
|
+
if (!/\.toBe\s*\(\s*true\s*\)\s*;?\s*$/.test(oldAssertion) || !/\.(?:not\s*\.\s*toBeNull|toBeTruthy|toBeDefined|toBeInTheDocument)\s*\(\s*\)\s*;?\s*$/.test(newAssertion)) return false;
|
|
3876
|
+
const was = checkedExpression(oldAssertion, "js"), now = checkedExpression(newAssertion, "js");
|
|
3877
|
+
return !!was && !!now && PRESENCE_PREDICATE.test(was) && (PRESENCE_PREDICATE.test(now) || /(?:querySelector|queryBy\w*|getBy\w*|findBy\w*|\.find|\.get|\$)\(/.test(now));
|
|
3878
|
+
}
|
|
3680
3879
|
function checkedExpression(assertion, language) {
|
|
3681
3880
|
const squash = (text) => text.replace(/\s+/g, "").replace(/^len\((.*)\)$/, "$1").replace(/\.length$/, "");
|
|
3682
3881
|
if (language === "py") {
|
|
@@ -3713,6 +3912,20 @@ function isExact(assertion, language) {
|
|
|
3713
3912
|
if (language === "py") return /^\s*assert\s+.+?\s*==\s*\S/.test(assertion) || /\bself\s*\.\s*assertEqual\s*\(/.test(assertion);
|
|
3714
3913
|
return false;
|
|
3715
3914
|
}
|
|
3915
|
+
function admits(value, operator, limit) {
|
|
3916
|
+
switch (operator) {
|
|
3917
|
+
case ">":
|
|
3918
|
+
return value > limit;
|
|
3919
|
+
case ">=":
|
|
3920
|
+
return value >= limit;
|
|
3921
|
+
case "<":
|
|
3922
|
+
return value < limit;
|
|
3923
|
+
case "<=":
|
|
3924
|
+
return value <= limit;
|
|
3925
|
+
default:
|
|
3926
|
+
return true;
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3716
3929
|
function relaxes(oldAssertion, newAssertion, language, siblings = []) {
|
|
3717
3930
|
const subject = checkedExpression(oldAssertion, language);
|
|
3718
3931
|
if (!subject || subject !== checkedExpression(newAssertion, language)) return false;
|
|
@@ -3721,18 +3934,67 @@ function relaxes(oldAssertion, newAssertion, language, siblings = []) {
|
|
|
3721
3934
|
if (/\.\s*not\s*\./.test(oldAssertion) || /\.\s*not\s*\./.test(newAssertion)) return false;
|
|
3722
3935
|
const exact = /\.(?:toBe|toEqual|toStrictEqual|toHaveLength)\s*\(\s*(-?\d+(?:\.\d+)?)\s*\)/.exec(oldAssertion)?.[1] ?? /assert\s*\.\s*(?:equal|strictEqual|deepEqual|deepStrictEqual)\s*\([^,]+,\s*(-?\d+(?:\.\d+)?)\s*[,)]/.exec(oldAssertion)?.[1];
|
|
3723
3936
|
const bound = /\.(?:toBeGreaterThan|toBeGreaterThanOrEqual|toBeLessThan|toBeLessThanOrEqual)\s*\(/.test(newAssertion) || /assert\s*\.\s*ok\s*\([^;]*(?:>=|<=|>|<)/.test(newAssertion);
|
|
3724
|
-
if (exact !== void 0 && Number(exact) !== 0 && bound)
|
|
3937
|
+
if (exact !== void 0 && Number(exact) !== 0 && bound) {
|
|
3938
|
+
const matcherLimit = /\.(toBeGreaterThan|toBeGreaterThanOrEqual|toBeLessThan|toBeLessThanOrEqual)\s*\(\s*(-?\d+(?:\.\d+)?)\s*\)/.exec(newAssertion);
|
|
3939
|
+
const okLimit = /assert\s*\.\s*ok\s*\([^;]*?(>=|<=|>|<)\s*(-?\d+(?:\.\d+)?)\s*\)/.exec(newAssertion);
|
|
3940
|
+
const operators = { toBeGreaterThan: ">", toBeGreaterThanOrEqual: ">=", toBeLessThan: "<", toBeLessThanOrEqual: "<=" };
|
|
3941
|
+
if (matcherLimit && !admits(Number(exact), operators[matcherLimit[1]], Number(matcherLimit[2]))) return false;
|
|
3942
|
+
if (okLimit && !admits(Number(exact), okLimit[1], Number(okLimit[2]))) return false;
|
|
3943
|
+
return true;
|
|
3944
|
+
}
|
|
3725
3945
|
const same = matcherArgument(oldAssertion, "toEqual") ?? matcherArgument(oldAssertion, "toStrictEqual");
|
|
3726
3946
|
return same !== null && same !== "" && same === matcherArgument(newAssertion, "toMatchObject");
|
|
3727
3947
|
}
|
|
3728
3948
|
if (language === "py") {
|
|
3729
3949
|
if (EMPTY_PY.test(oldAssertion)) return false;
|
|
3730
|
-
const
|
|
3731
|
-
const
|
|
3732
|
-
|
|
3950
|
+
const plain = (text) => !/\b(?:and|or)\b/.test(text.replace(/(['"`])(?:\\.|(?!\1)[^\\])*\1/g, ""));
|
|
3951
|
+
const operator = (text) => /^\s*assert\s+(?:not\s+)?.+?\s*(==|!=|>=|<=|>|<|\bin\b|\bis\b)/.exec(text)?.[1] ?? "";
|
|
3952
|
+
const exact = operator(oldAssertion) === "==" && plain(oldAssertion) || /\bself\s*\.\s*assertEqual\s*\(/.test(oldAssertion);
|
|
3953
|
+
const bound = [">=", "<=", ">", "<"].includes(operator(newAssertion)) && plain(newAssertion) || /\bself\s*\.\s*assert(?:Greater|Less)(?:Equal)?\s*\(/.test(newAssertion);
|
|
3954
|
+
if (!exact || !bound) return false;
|
|
3955
|
+
const value = (text) => /==\s*(-?\d+(?:\.\d+)?|[\w.]+(?:\([^)]*\))?)\s*(?:,|$)/.exec(text)?.[1] ?? /\bassertEqual\s*\([^,]+,\s*(-?\d+(?:\.\d+)?|[\w.]+)\s*[,)]/.exec(text)?.[1];
|
|
3956
|
+
const limit = (text) => {
|
|
3957
|
+
const symbol = /^\s*assert\s+(?:not\s+)?.+?\s*(>=|<=|>|<)\s*(-?\d+(?:\.\d+)?|[\w.]+(?:\([^)]*\))?)\s*(?:,|$)/.exec(text);
|
|
3958
|
+
if (symbol) return [symbol[1], symbol[2]];
|
|
3959
|
+
const named = /\bassert(Greater|GreaterEqual|Less|LessEqual)\s*\([^,]+,\s*(-?\d+(?:\.\d+)?|[\w.]+)\s*[,)]/.exec(text);
|
|
3960
|
+
return named ? [{ Greater: ">", GreaterEqual: ">=", Less: "<", LessEqual: "<=" }[named[1]], named[2]] : null;
|
|
3961
|
+
};
|
|
3962
|
+
const pinned = value(oldAssertion), bounded = limit(newAssertion);
|
|
3963
|
+
if (pinned === void 0 || !bounded) return true;
|
|
3964
|
+
const [operator2, edge] = bounded;
|
|
3965
|
+
if (/^-?\d/.test(pinned) && /^-?\d/.test(edge)) return admits(Number(pinned), operator2, Number(edge));
|
|
3966
|
+
return !(pinned === edge && (operator2 === ">" || operator2 === "<"));
|
|
3733
3967
|
}
|
|
3734
3968
|
return false;
|
|
3735
3969
|
}
|
|
3970
|
+
function splitArguments(text) {
|
|
3971
|
+
const parts = [];
|
|
3972
|
+
let depth = 0, quote = "", start = 0;
|
|
3973
|
+
for (let i = 0; i < text.length; i++) {
|
|
3974
|
+
const char = text[i];
|
|
3975
|
+
if (quote) {
|
|
3976
|
+
if (char === "\\") i++;
|
|
3977
|
+
else if (char === quote) quote = "";
|
|
3978
|
+
} else if (char === '"' || char === "'") quote = char;
|
|
3979
|
+
else if ("([{".includes(char)) depth++;
|
|
3980
|
+
else if (")]}".includes(char)) depth--;
|
|
3981
|
+
else if (char === "," && depth === 0) {
|
|
3982
|
+
parts.push(text.slice(start, i).trim());
|
|
3983
|
+
start = i + 1;
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
parts.push(text.slice(start).trim());
|
|
3987
|
+
return parts.filter(Boolean);
|
|
3988
|
+
}
|
|
3989
|
+
function unittestAsAssert(statement) {
|
|
3990
|
+
const call = /^\s*self\s*\.\s*assert(True|IsNotNone|GreaterEqual|Greater)\s*\(([\s\S]*)\)\s*;?\s*$/.exec(statement);
|
|
3991
|
+
if (!call) return null;
|
|
3992
|
+
const [first, second] = splitArguments(call[2]);
|
|
3993
|
+
if (!first) return null;
|
|
3994
|
+
if (call[1] === "True") return `assert ${first}`;
|
|
3995
|
+
if (call[1] === "IsNotNone") return `assert ${first} is not None`;
|
|
3996
|
+
return second === void 0 ? null : `assert ${first} ${call[1] === "Greater" ? ">" : ">="} ${second}`;
|
|
3997
|
+
}
|
|
3736
3998
|
function pinsLess(oldAssertion, newAssertion, language) {
|
|
3737
3999
|
if (language === "js") {
|
|
3738
4000
|
const was = /\.toThrow(?:Error)?\s*\(\s*([^)\s][^)]*)\)/.exec(oldAssertion);
|
|
@@ -3753,15 +4015,20 @@ function pinsLess(oldAssertion, newAssertion, language) {
|
|
|
3753
4015
|
}
|
|
3754
4016
|
return false;
|
|
3755
4017
|
}
|
|
3756
|
-
function isWeaker(
|
|
4018
|
+
function isWeaker(rawOld, rawNew, language) {
|
|
4019
|
+
const oldAssertion = language === "py" ? withoutOuterParens(rawOld) : rawOld, newAssertion = language === "py" ? withoutOuterParens(rawNew) : rawNew;
|
|
4020
|
+
if (language === "js" && samePresence(oldAssertion, newAssertion)) return false;
|
|
3757
4021
|
if (pinsLess(oldAssertion, newAssertion, language) || relaxes(oldAssertion, newAssertion, language)) return true;
|
|
3758
4022
|
if (language === "js") {
|
|
3759
4023
|
if (THROWING_QUERY.test(newAssertion) || !JS_SPECIFIC.test(oldAssertion) || WEAK_JS.test(oldAssertion) || BOUND_JS.test(oldAssertion)) return false;
|
|
3760
|
-
|
|
4024
|
+
const limit = /\.(?:toBeGreaterThan|toBeGreaterThanOrEqual|toBeLessThan|toBeLessThanOrEqual)\s*\(\s*([^)]*)\)/.exec(oldAssertion)?.[1];
|
|
4025
|
+
return WEAK_JS.test(newAssertion) || BOUND_JS.test(newAssertion) && !ZERO_JS.test(oldAssertion) && (limit === void 0 || /^-?\d+(?:\.\d+)?$/.test(limit.trim()));
|
|
3761
4026
|
}
|
|
3762
4027
|
if (language === "py") {
|
|
3763
|
-
|
|
3764
|
-
|
|
4028
|
+
const oldBare = unittestAsAssert(oldAssertion) ?? oldAssertion;
|
|
4029
|
+
if (WEAK_PY.test(oldAssertion) || BOUND_PY.test(oldAssertion) || WEAK_PY.test(oldBare) || BOUND_PY.test(oldBare) || !PY_SPECIFIC.test(oldAssertion)) return false;
|
|
4030
|
+
const limit = /(?:>=|<=|>|<)\s*([^\s,)]+)/.exec(oldAssertion)?.[1];
|
|
4031
|
+
return WEAK_PY.test(newAssertion) || BOUND_PY.test(newAssertion) && !EMPTY_PY.test(oldAssertion) && /==|[<>]=?/.test(oldAssertion) && (limit === void 0 || /^-?\d+(?:\.\d+)?$/.test(limit));
|
|
3765
4032
|
}
|
|
3766
4033
|
return false;
|
|
3767
4034
|
}
|
|
@@ -3939,8 +4206,8 @@ function resolveTampering(files, productionChanged) {
|
|
|
3939
4206
|
...productionChanged ? files.flatMap((file) => file.rewritten) : []
|
|
3940
4207
|
];
|
|
3941
4208
|
}
|
|
3942
|
-
function
|
|
3943
|
-
return finding2.type === "test-skip" &&
|
|
4209
|
+
function isTestMarker(finding2) {
|
|
4210
|
+
return finding2.type === "test-skip" && /\.(?:py|[cm]?[jt]sx?|rs|go)$/i.test(finding2.file ?? "");
|
|
3944
4211
|
}
|
|
3945
4212
|
function markedTest(finding2, lines) {
|
|
3946
4213
|
const snippet = finding2.lineSnippet ?? "";
|
|
@@ -3961,31 +4228,62 @@ function markedTest(finding2, lines) {
|
|
|
3961
4228
|
}
|
|
3962
4229
|
return null;
|
|
3963
4230
|
}
|
|
4231
|
+
if (/\.rs$/i.test(finding2.file ?? "")) {
|
|
4232
|
+
for (let i = index; i < lines.length && i < index + 12; i++) {
|
|
4233
|
+
const fn = /^\s*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?fn\s+(\w+)\s*[<(]/.exec(lines[i]);
|
|
4234
|
+
if (fn) return { key: `rs:${fn[1]}`, title: fn[1] };
|
|
4235
|
+
}
|
|
4236
|
+
return null;
|
|
4237
|
+
}
|
|
4238
|
+
if (/\.go$/i.test(finding2.file ?? "")) {
|
|
4239
|
+
for (let i = index; i >= 0 && i > index - 400; i--) {
|
|
4240
|
+
const fn = /^func\s+(?:\([^)]*\)\s*)?((?:Test|Benchmark|Fuzz|Example)\w*)\s*\(/.exec(lines[i]);
|
|
4241
|
+
if (fn) return { key: `go:${fn[1]}`, title: fn[1] };
|
|
4242
|
+
}
|
|
4243
|
+
return null;
|
|
4244
|
+
}
|
|
3964
4245
|
if (/\.each\b/.test(snippet)) return null;
|
|
3965
|
-
const title =
|
|
3966
|
-
|
|
4246
|
+
const title = /\b(?:it|test|describe|suite|xit|xtest|xdescribe|fit|fdescribe)\b[\w$.\s]*\(\s*(['"`])((?:\\.|(?!\1)[^\\\n])+)\1\s*(?:[,)]|$)/.exec(titleOnNextLine(snippet, lines, index));
|
|
4247
|
+
if (title) return { key: `js:${title[1]}${title[2]}${title[1]}`, title: `${title[1]}${title[2]}${title[1]}` };
|
|
4248
|
+
let limit = (lines[index] ?? "").search(/\S/);
|
|
4249
|
+
if (limit > 0 && /\b(?:test|it)\s*\.\s*(?:skip|fixme|fail)\s*\(/.test(snippet)) {
|
|
4250
|
+
for (let i = index - 1; i >= 0 && i > index - 200 && limit > 0; i--) {
|
|
4251
|
+
if (!lines[i].trim() || lines[i].search(/\S/) >= limit) continue;
|
|
4252
|
+
limit = lines[i].search(/\S/);
|
|
4253
|
+
const owner = /^\s*(?:test|it)(?:\s*\.\s*(?!describe\b|step\b)\w+)*\s*\(\s*(['"`])((?:\\.|(?!\1)[^\\\n])+)\1/.exec(titleOnNextLine(lines[i], lines, i));
|
|
4254
|
+
if (owner) return { key: `js:${owner[1]}${owner[2]}${owner[1]}`, title: `${owner[1]}${owner[2]}${owner[1]}` };
|
|
4255
|
+
if (/^\s*(?:await\s+)?(?:test|it)\s*\.\s*describe\b|^\s*(?:describe|suite)\b/.test(lines[i])) return null;
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
return null;
|
|
3967
4259
|
}
|
|
3968
|
-
function
|
|
3969
|
-
|
|
4260
|
+
function titleOnNextLine(text, lines, index) {
|
|
4261
|
+
if (!/\(\s*$/.test(text)) return text;
|
|
4262
|
+
const next = (lines[index + 1] ?? "").trim();
|
|
4263
|
+
return /^(['"`])/.test(next) ? `${text} ${next}` : text;
|
|
4264
|
+
}
|
|
4265
|
+
function newTestMarkers(before, after, oldText, newText) {
|
|
4266
|
+
const markers = after.filter(isTestMarker);
|
|
3970
4267
|
if (!markers.length || oldText === null) return [];
|
|
3971
4268
|
const oldLines = oldText.split(/\r?\n/), newLines = (newText ?? "").split(/\r?\n/);
|
|
3972
|
-
const existed = (test) => test.key.startsWith("py:") ? new RegExp(`^\\s*(?:async\\s+)?def\\s+${test.title}\\s*\\(`, "m").test(oldText) : oldText.includes(test.title);
|
|
3973
|
-
const markedBefore = /* @__PURE__ */ new
|
|
4269
|
+
const existed = (test) => test.key.startsWith("py:") ? new RegExp(`^\\s*(?:async\\s+)?def\\s+${test.title}\\s*\\(`, "m").test(oldText) : test.key.startsWith("rs:") ? new RegExp(`\\bfn\\s+${test.title}\\s*[<(]`).test(oldText) : test.key.startsWith("go:") ? new RegExp(`^func\\s+(?:\\([^)]*\\)\\s*)?${test.title}\\s*\\(`, "m").test(oldText) : oldText.includes(test.title);
|
|
4270
|
+
const markedBefore = /* @__PURE__ */ new Map();
|
|
3974
4271
|
let unnamedBefore = 0;
|
|
3975
|
-
for (const finding2 of before.filter(
|
|
4272
|
+
for (const finding2 of before.filter(isTestMarker)) {
|
|
3976
4273
|
const test = markedTest(finding2, oldLines);
|
|
3977
|
-
if (test) markedBefore.
|
|
4274
|
+
if (test) markedBefore.set(test.key, (markedBefore.get(test.key) ?? 0) + 1);
|
|
3978
4275
|
else unnamedBefore++;
|
|
3979
4276
|
}
|
|
3980
|
-
const found = [], unnamed = [], seen = /* @__PURE__ */ new
|
|
4277
|
+
const found = [], unnamed = [], seen = /* @__PURE__ */ new Map();
|
|
3981
4278
|
for (const finding2 of markers) {
|
|
3982
4279
|
const test = markedTest(finding2, newLines);
|
|
3983
4280
|
if (!test) {
|
|
3984
4281
|
unnamed.push(finding2);
|
|
3985
4282
|
continue;
|
|
3986
4283
|
}
|
|
3987
|
-
|
|
3988
|
-
seen.
|
|
4284
|
+
const nth = (seen.get(test.key) ?? 0) + 1, had = markedBefore.get(test.key) ?? 0;
|
|
4285
|
+
seen.set(test.key, nth);
|
|
4286
|
+
if (nth <= had || had === 0 && (nth > 1 && !test.key.startsWith("js:") || !existed(test))) continue;
|
|
3989
4287
|
found.push(finding2);
|
|
3990
4288
|
}
|
|
3991
4289
|
return [...found, ...unnamed.slice(unnamedBefore)];
|
|
@@ -4019,22 +4317,60 @@ var THRESHOLD_PATTERNS = [
|
|
|
4019
4317
|
var SWITCHES = ["strict", "noImplicitAny", "strictNullChecks", "noImplicitReturns", "useUnknownInCatchVariables"];
|
|
4020
4318
|
var GATE_SCRIPT = /^(?:pre|post)?(?:test|lint|typecheck|type-check|check|ci|verify|validate)(?::[\w:.-]+)?$/;
|
|
4021
4319
|
var CI_RUNNER = new RegExp(
|
|
4022
|
-
String.raw`(?<![\w./-])(?:pytest|vitest|jest|mocha|tox|nox)(?![\w/-])` + String.raw`|(?<![\w./-])(?:npm|yarn|pnpm|bun|go|cargo|mvn|gradle|make|dotnet|flutter)\s+(?:run\s+)?(?:-\S+\s+)*test(?![\w/])` +
|
|
4320
|
+
String.raw`(?<![\w./-])(?:pytest|vitest|jest|mocha|tox|nox)(?![\w/-])` + String.raw`|(?<![\w./-])(?:npm|yarn|pnpm|bun|go|cargo|mvn|gradle|make|dotnet|flutter)\s+(?:run\s+)?(?:-\S+\s+)*test(?![\w/])` + // A package script that runs the tests with coverage: `yarn coverage`, `npm run coverage`.
|
|
4321
|
+
String.raw`|(?<![\w./-])(?:npm|yarn|pnpm|bun)\s+(?:run\s+)?coverage(?![\w/-])` + // Monorepo runners: `nx affected --target=test`, `turbo run test`, `lerna run test`, `bazel test //...`.
|
|
4322
|
+
String.raw`|(?<![\w./-])(?:nx|turbo|lerna|rush)\s+(?:run-many|affected|run|test)\b[^\n]*(?<![\w-])(?:test|tests)(?![\w-])` + String.raw`|(?<![\w./-])(?:bazel\s+test|ctest|phpunit|rspec|deno\s+test)(?![\w/-])` + // Python's own runners and the browser runners: `python3 -m unittest`, `nosetests`, `playwright test`, `cypress run`, `karma start`.
|
|
4323
|
+
String.raw`|(?<![\w./-])python[\d.]*\s+-m\s+(?:unittest|pytest|nose2?)(?![\w/-])` + String.raw`|(?<![\w./-])(?:nosetests|nose2|py\.test|stestr|behave|jasmine|playwright\s+test|cypress\s+run|karma\s+start|hatch\s+(?:run\s+)?test|swift\s+test)(?![\w/-])` + String.raw`|\brun:.*(?<=\s)(?:tests?|unit|e2e)(?![\w/.-])`
|
|
4023
4324
|
);
|
|
4325
|
+
var SHELL_TEST = /(?:^|run:\s*|[;&|({]\s*|\b(?:if|then|do|else|elif|while|until)\s+|!\s+)test\s+!?\s*-[bcdefghkLnOGprsStuwxzN](?=\s)/g;
|
|
4326
|
+
var ciRuns = (line) => CI_RUNNER.test(line.replace(SHELL_TEST, " ").replace(/\s#.*$/, ""));
|
|
4024
4327
|
var CI_FILE = /(?:\.github\/workflows\/|\.gitlab-ci)/;
|
|
4025
4328
|
var CI_REUSABLE = /^\s*-?\s*uses:\s*\S*\.github\/workflows\/\S+/;
|
|
4026
4329
|
var CI_GATE = /^\s*(?:pull_request|pull_request_target|push|merge_group)\s*:|^\s*on\s*:\s*(?:\[[^\]]*\b(?:push|pull_request|pull_request_target|merge_group)\b[^\]]*\]|(?:push|pull_request|pull_request_target|merge_group)\s*$)|^\s*-\s*(?:push|pull_request|pull_request_target|merge_group)\s*$/m;
|
|
4330
|
+
var CI_NON_GATE = /^\s*(?:schedule|release|workflow_dispatch|workflow_run|repository_dispatch)\s*:/m;
|
|
4331
|
+
function gatesChanges(file, text) {
|
|
4332
|
+
const section = triggerSection(text), triggers = gatingTriggers(text) ?? text;
|
|
4333
|
+
if (/\.gitlab-ci/.test(file) || CI_GATE.test(triggers) || /\bworkflow_call\b/.test(triggers)) return true;
|
|
4334
|
+
return section !== null && triggers !== section ? false : !CI_NON_GATE.test(triggers);
|
|
4335
|
+
}
|
|
4336
|
+
function gatingTriggers(text) {
|
|
4337
|
+
const section = triggerSection(text);
|
|
4338
|
+
if (section === null) return null;
|
|
4339
|
+
const lines = section.split(/\r?\n/), kept = [];
|
|
4340
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4341
|
+
const head = /^(\s*)push\s*:\s*(?:#.*)?$/.exec(lines[i]);
|
|
4342
|
+
if (!head) {
|
|
4343
|
+
kept.push(lines[i]);
|
|
4344
|
+
continue;
|
|
4345
|
+
}
|
|
4346
|
+
let end = i + 1;
|
|
4347
|
+
while (end < lines.length && (!lines[end].trim() || lines[end].search(/\S/) > head[1].length)) end++;
|
|
4348
|
+
const block = lines.slice(i + 1, end).join("\n");
|
|
4349
|
+
if (!(/^\s*tags\s*:/m.test(block) && !/^\s*branches(?:-ignore)?\s*:/m.test(block))) kept.push(...lines.slice(i, end));
|
|
4350
|
+
i = end - 1;
|
|
4351
|
+
}
|
|
4352
|
+
return kept.join("\n");
|
|
4353
|
+
}
|
|
4354
|
+
function triggerSection(text) {
|
|
4355
|
+
const lines = text.split(/\r?\n/);
|
|
4356
|
+
const start = lines.findIndex((line) => /^["']?on["']?\s*:/.test(line));
|
|
4357
|
+
if (start < 0) return null;
|
|
4358
|
+
let end = start + 1;
|
|
4359
|
+
while (end < lines.length && (!lines[end].trim() || /^[\s#-]/.test(lines[end]))) end++;
|
|
4360
|
+
return lines.slice(start, end).join("\n");
|
|
4361
|
+
}
|
|
4027
4362
|
function gatingTestLines(file, text) {
|
|
4028
|
-
return /\.gitlab-ci/.test(file) || CI_GATE.test(text) ? matchingLines(text,
|
|
4363
|
+
return /\.gitlab-ci/.test(file) || CI_GATE.test(gatingTriggers(text) ?? text) ? matchingLines(text, ciRuns) : 0;
|
|
4029
4364
|
}
|
|
4030
4365
|
function matchingLines(text, pattern) {
|
|
4031
|
-
|
|
4366
|
+
const matches = typeof pattern === "function" ? pattern : (line) => pattern.test(line);
|
|
4367
|
+
return text.split(/\r?\n/).filter((line) => !line.trim().startsWith("#") && matches(line)).length;
|
|
4032
4368
|
}
|
|
4033
4369
|
function ciTestSteps(file, oldText, newText) {
|
|
4034
4370
|
if (!CI_FILE.test(file) || oldText === null && newText === null) return null;
|
|
4035
4371
|
const was = oldText ?? "", now = newText ?? "";
|
|
4036
4372
|
const after = gatingTestLines(file, now);
|
|
4037
|
-
return { file, before: gatingTestLines(file, was), after, reusableAfter: matchingLines(now, CI_REUSABLE), ungatedAfter: after === 0 ? matchingLines(now,
|
|
4373
|
+
return { file, before: gatingTestLines(file, was), after, reusableAfter: matchingLines(now, CI_REUSABLE), ungatedAfter: after === 0 ? matchingLines(now, ciRuns) : 0 };
|
|
4038
4374
|
}
|
|
4039
4375
|
function resolveCiTestSteps(counts, commit) {
|
|
4040
4376
|
const before = counts.reduce((sum, item) => sum + item.before, 0);
|
|
@@ -4044,6 +4380,23 @@ function resolveCiTestSteps(counts, commit) {
|
|
|
4044
4380
|
}
|
|
4045
4381
|
var LENIENT = /continue-on-error:\s*true|allow_failure:\s*true|^\s*(?:-\s+)?if:\s*(?:\$\{\{\s*)?false\s*(?:\}\})?\s*(?:#.*)?$/;
|
|
4046
4382
|
var SHELL_SWALLOW = /\|\|\s*(?:true|exit\s+0|:|echo\b)|;\s*(?:true|exit\s+0)\b/;
|
|
4383
|
+
var NON_CHECK_ACTION = new RegExp(String.raw`^\s*(?:-\s+)?uses:\s*["']?(?:` + [
|
|
4384
|
+
String.raw`actions/(?:upload-artifact|download-artifact|upload-pages-artifact|deploy-pages|cache|attest[\w-]*|checkout|setup-[\w-]+)`,
|
|
4385
|
+
String.raw`docker/(?:login-action|metadata-action|build-push-action|setup-[\w-]+)`,
|
|
4386
|
+
String.raw`azure/login`,
|
|
4387
|
+
String.raw`aws-actions/(?:configure-aws-credentials|amazon-ecr-login)`,
|
|
4388
|
+
String.raw`google-github-actions/(?:auth|setup-gcloud)`,
|
|
4389
|
+
String.raw`softprops/action-gh-release`,
|
|
4390
|
+
String.raw`ncipollo/release-action`,
|
|
4391
|
+
String.raw`pypa/gh-action-pypi-publish`,
|
|
4392
|
+
String.raw`JS-DevTools/npm-publish`,
|
|
4393
|
+
String.raw`peaceiris/actions-gh-pages`,
|
|
4394
|
+
String.raw`peter-evans/[\w-]+`,
|
|
4395
|
+
String.raw`marocchino/sticky-pull-request-comment`,
|
|
4396
|
+
String.raw`slackapi/slack-github-action`,
|
|
4397
|
+
String.raw`codecov/codecov-action`,
|
|
4398
|
+
String.raw`coverallsapp/github-action`
|
|
4399
|
+
].join("|") + String.raw`)(?:/[\w./-]+)?@`, "i");
|
|
4047
4400
|
function lenientSteps(text) {
|
|
4048
4401
|
const lines = text.split(/\r?\n/);
|
|
4049
4402
|
const indentOf = (line) => line.length - line.trimStart().length;
|
|
@@ -4060,9 +4413,18 @@ function lenientSteps(text) {
|
|
|
4060
4413
|
while (j >= 0 && (!lines[j].trim() || lines[j].trim().startsWith("#") || indentOf(lines[j]) >= indentOf(line))) j--;
|
|
4061
4414
|
identity = j >= 0 ? lines[j].trim() : "";
|
|
4062
4415
|
}
|
|
4416
|
+
const start = /^\s*-\s+if:/.test(line) ? i : Math.max(j, 0), level = indentOf(lines[start]);
|
|
4417
|
+
let end = start + 1;
|
|
4418
|
+
while (end < lines.length && (!lines[end].trim() || lines[end].trim().startsWith("#") || indentOf(lines[end]) > level)) end++;
|
|
4419
|
+
const scope = lines.slice(start, end), runsChecks = matchingLines(scope.join("\n"), ciRuns) > 0;
|
|
4420
|
+
const uses = scope.filter((item) => /^\s*(?:-\s+)?uses:/.test(item));
|
|
4421
|
+
const nonCheck = !runsChecks && uses.length === 1 && NON_CHECK_ACTION.test(uses[0]) && !scope.some((item) => /^\s*(?:-\s+)?run:/.test(item));
|
|
4063
4422
|
const entry = found.get(identity);
|
|
4064
|
-
if (entry)
|
|
4065
|
-
|
|
4423
|
+
if (entry) {
|
|
4424
|
+
entry.count++;
|
|
4425
|
+
entry.runsChecks ||= runsChecks;
|
|
4426
|
+
entry.nonCheck &&= nonCheck;
|
|
4427
|
+
} else found.set(identity, { count: 1, line: i + 1, runsChecks, nonCheck });
|
|
4066
4428
|
});
|
|
4067
4429
|
return found;
|
|
4068
4430
|
}
|
|
@@ -4124,16 +4486,18 @@ function analyzeGuardFile(file, oldText, newText, commit) {
|
|
|
4124
4486
|
});
|
|
4125
4487
|
}
|
|
4126
4488
|
if (CI_FILE.test(file)) {
|
|
4489
|
+
const gating = gatesChanges(file, oldText) || gatesChanges(file, newText);
|
|
4127
4490
|
const before = lenientSteps(oldText), existing = new Set(oldText.split(/\r?\n/).flatMap((line) => [line.trim(), line.trim().replace(/^-\s+/, "")]));
|
|
4128
4491
|
const newLines = newText.split(/\r?\n/);
|
|
4129
4492
|
for (const [identity, now2] of lenientSteps(newText)) {
|
|
4493
|
+
if (now2.nonCheck || !gating && !now2.runsChecks) continue;
|
|
4130
4494
|
if (identity && existing.has(identity) && now2.count > (before.get(identity)?.count ?? 0)) {
|
|
4131
4495
|
const marker = (newLines[now2.line - 1] ?? "").trim();
|
|
4132
4496
|
out.push(finding("guard-weakened", file, now2.line, marker, /^(?:-\s+)?if:/.test(marker) ? "CI step is now switched off" : "CI step is now allowed to fail", commit));
|
|
4133
4497
|
break;
|
|
4134
4498
|
}
|
|
4135
4499
|
}
|
|
4136
|
-
const swallowing = (text) => text.split(/\r?\n/).map((line, index) => ({ line: line.trim(), index })).filter((item) => !item.line.startsWith("#") &&
|
|
4500
|
+
const swallowing = (text) => text.split(/\r?\n/).map((line, index) => ({ line: line.trim(), index })).filter((item) => !item.line.startsWith("#") && ciRuns(item.line) && SHELL_SWALLOW.test(item.line));
|
|
4137
4501
|
const was = swallowing(oldText), now = swallowing(newText);
|
|
4138
4502
|
const tolerance = (text) => text.split(/\r?\n/).filter((line) => !line.trim().startsWith("#") && /continue-on-error:\s*true|allow_failure:\s*true/.test(line)).length;
|
|
4139
4503
|
const traded = Math.max(0, tolerance(oldText) - tolerance(newText));
|
|
@@ -4321,7 +4685,7 @@ async function compareSnapshot(options, cwd, snapshot) {
|
|
|
4321
4685
|
const fingerprint = findingFingerprint(finding2);
|
|
4322
4686
|
existingCounts.set(fingerprint, (existingCounts.get(fingerprint) ?? 0) + 1);
|
|
4323
4687
|
}
|
|
4324
|
-
result.infractions.push(...compared.introduced.filter((finding2) => !
|
|
4688
|
+
result.infractions.push(...compared.introduced.filter((finding2) => !isTestMarker(finding2)), ...newTestMarkers(before, after, oldText, newText));
|
|
4325
4689
|
result.removedFindings.push(...compared.removed);
|
|
4326
4690
|
result.unchangedCount += compared.unchanged;
|
|
4327
4691
|
result.beforeFindings += before.length;
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-roast",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Stops AI coding agents (and rushed humans) from turning a build green by weakening tests, coverage gates or CI. Hook for Claude Code, Codex and Cursor, pre-commit, GitHub Action. Offline.",
|
|
5
5
|
"mcpName": "io.github.Nari-Ab/agent-roast",
|
|
6
6
|
"type": "module",
|