llm-relay 0.54.0 → 0.55.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +23 -1
- package/dist/cli.js.map +1 -1
- package/dist/delegate-gate/cast-necessity.d.ts +14 -0
- package/dist/delegate-gate/cast-necessity.js +150 -0
- package/dist/delegate-gate/cast-necessity.js.map +1 -0
- package/dist/delegate-gate/cli.d.ts +20 -0
- package/dist/delegate-gate/cli.js +51 -0
- package/dist/delegate-gate/cli.js.map +1 -0
- package/dist/delegate-gate/diff-parser.d.ts +59 -0
- package/dist/delegate-gate/diff-parser.js +147 -0
- package/dist/delegate-gate/diff-parser.js.map +1 -0
- package/dist/delegate-gate/fix.d.ts +16 -0
- package/dist/delegate-gate/fix.js +43 -0
- package/dist/delegate-gate/fix.js.map +1 -0
- package/dist/delegate-gate/gate.d.ts +18 -0
- package/dist/delegate-gate/gate.js +42 -0
- package/dist/delegate-gate/gate.js.map +1 -0
- package/dist/delegate-gate/minimality.d.ts +30 -0
- package/dist/delegate-gate/minimality.js +120 -0
- package/dist/delegate-gate/minimality.js.map +1 -0
- package/dist/delegate-gate/post-image.d.ts +10 -0
- package/dist/delegate-gate/post-image.js +45 -0
- package/dist/delegate-gate/post-image.js.map +1 -0
- package/dist/delegate-gate/shared-state.d.ts +15 -0
- package/dist/delegate-gate/shared-state.js +158 -0
- package/dist/delegate-gate/shared-state.js.map +1 -0
- package/dist/delegate-gate/test-assertions.d.ts +18 -0
- package/dist/delegate-gate/test-assertions.js +273 -0
- package/dist/delegate-gate/test-assertions.js.map +1 -0
- package/dist/delegate-gate/types.d.ts +26 -0
- package/dist/delegate-gate/types.js +19 -0
- package/dist/delegate-gate/types.js.map +1 -0
- package/dist/refusal-interpretation.d.ts +3 -0
- package/dist/refusal-interpretation.js +7 -1
- package/dist/refusal-interpretation.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { reportedPath } from "./diff-parser.js";
|
|
2
|
+
/** Two lines carry the same code but differ only in leading whitespace. */
|
|
3
|
+
function isWhitespaceOnlyChange(removed, added) {
|
|
4
|
+
return removed !== added && removed.trimStart() === added.trimStart();
|
|
5
|
+
}
|
|
6
|
+
/** Two lines are byte-identical once surrounding whitespace is trimmed — a change that reads as
|
|
7
|
+
* "different bytes, same code" (re-indentation, a trailing-space touch, a blank line shuffled). */
|
|
8
|
+
function hasNoSemanticDelta(removed, added) {
|
|
9
|
+
return removed !== added && removed.trim() === added.trim();
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Pair a hunk's removed lines against its added lines by position.
|
|
13
|
+
*
|
|
14
|
+
* This is deliberately naive — position pairing, not a diff-of-diffs — because a delegate-diff
|
|
15
|
+
* gate is reading output a lane already produced, not re-deriving the edit. It is precise for the
|
|
16
|
+
* common single-block replacement shape (a contiguous run of `-` lines immediately followed by a
|
|
17
|
+
* contiguous run of `+` lines, equal in count) and says nothing when counts differ rather than
|
|
18
|
+
* guessing a pairing.
|
|
19
|
+
*/
|
|
20
|
+
function pairedRemoveAddRuns(hunk) {
|
|
21
|
+
const pairs = [];
|
|
22
|
+
const lines = hunk.lines;
|
|
23
|
+
let i = 0;
|
|
24
|
+
while (i < lines.length) {
|
|
25
|
+
if (lines[i].kind !== "remove") {
|
|
26
|
+
i += 1;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const removeStart = i;
|
|
30
|
+
while (i < lines.length && lines[i].kind === "remove")
|
|
31
|
+
i += 1;
|
|
32
|
+
const removeRun = lines.slice(removeStart, i);
|
|
33
|
+
const addStart = i;
|
|
34
|
+
while (i < lines.length && lines[i].kind === "add")
|
|
35
|
+
i += 1;
|
|
36
|
+
const addRun = lines.slice(addStart, i);
|
|
37
|
+
if (removeRun.length === addRun.length) {
|
|
38
|
+
for (let k = 0; k < removeRun.length; k++) {
|
|
39
|
+
const removed = removeRun[k];
|
|
40
|
+
const added = addRun[k];
|
|
41
|
+
pairs.push({ removed: removed.content, added: added.content, newLine: added.newLine });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return pairs;
|
|
46
|
+
}
|
|
47
|
+
const NON_MINIMAL_MIN_CHANGED_LINES = 6;
|
|
48
|
+
const NON_MINIMAL_NO_DELTA_RATIO = 0.5;
|
|
49
|
+
function totalOfKind(hunk, kind) {
|
|
50
|
+
return hunk.lines.reduce((count, line) => (line.kind === kind ? count + 1 : count), 0);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Is this hunk PURE indentation churn: every removed and every added line accounted for by a
|
|
54
|
+
* whitespace-only pair, with nothing left unpaired?
|
|
55
|
+
*
|
|
56
|
+
* The "fully paired" requirement (every remove and every add line in the hunk is consumed by a
|
|
57
|
+
* 1:1 pair) is what makes the auto-fix in `fix.ts` safe: dropping a fully-paired churn hunk
|
|
58
|
+
* removes an equal number of old-side and new-side lines, so it can never shift another hunk's
|
|
59
|
+
* line numbers. A hunk containing an unequal-length remove/add run alongside pure churn is NOT
|
|
60
|
+
* flagged here — `pairedRemoveAddRuns` silently skips unequal runs, and treating the REST of the
|
|
61
|
+
* hunk as "100% churn" while ignoring lines it could not pair would both misreport the hunk and
|
|
62
|
+
* make an auto-fix unsafe.
|
|
63
|
+
*/
|
|
64
|
+
export function isPureIndentationChurnHunk(hunk) {
|
|
65
|
+
const pairs = pairedRemoveAddRuns(hunk);
|
|
66
|
+
if (pairs.length === 0)
|
|
67
|
+
return false;
|
|
68
|
+
if (pairs.length !== totalOfKind(hunk, "remove") || pairs.length !== totalOfKind(hunk, "add"))
|
|
69
|
+
return false;
|
|
70
|
+
return pairs.every((p) => isWhitespaceOnlyChange(p.removed, p.added));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Flag indentation-only churn and count changed lines with no semantic delta.
|
|
74
|
+
*
|
|
75
|
+
* Two finding shapes:
|
|
76
|
+
* - `indentation-churn`: a hunk whose every line is accounted for by a whitespace-only
|
|
77
|
+
* remove/add pair — the hunk changed formatting and nothing else. Auto-fixable: dropping the
|
|
78
|
+
* whole hunk is safe (see `isPureIndentationChurnHunk`).
|
|
79
|
+
* - `non-minimal-diff`: a hunk that is NOT pure churn but where at least half its PAIRED changed
|
|
80
|
+
* lines (≥6 of them) carry no semantic delta — evidence of a diff that reformats lines it did
|
|
81
|
+
* not need to touch alongside a real change, i.e. class (e), "non-minimal diffs touching
|
|
82
|
+
* unrelated lines". Not auto-fixable: a partial hunk mixes a real edit with the noise, and
|
|
83
|
+
* removing only the noise without re-deriving the hunk boundaries risks corrupting the real
|
|
84
|
+
* change.
|
|
85
|
+
*/
|
|
86
|
+
export function analyzeDiffMinimality(files) {
|
|
87
|
+
const findings = [];
|
|
88
|
+
for (const file of files) {
|
|
89
|
+
const path = reportedPath(file);
|
|
90
|
+
for (const hunk of file.hunks) {
|
|
91
|
+
if (isPureIndentationChurnHunk(hunk)) {
|
|
92
|
+
const pairs = pairedRemoveAddRuns(hunk);
|
|
93
|
+
findings.push({
|
|
94
|
+
class: "indentation-churn",
|
|
95
|
+
file: path,
|
|
96
|
+
line: pairs[0].newLine,
|
|
97
|
+
detail: `hunk @@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@ changes ${pairs.length} line(s) by leading whitespace only`,
|
|
98
|
+
autoFixable: true,
|
|
99
|
+
});
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const pairs = pairedRemoveAddRuns(hunk);
|
|
103
|
+
if (pairs.length === 0)
|
|
104
|
+
continue;
|
|
105
|
+
const noDeltaPairs = pairs.filter((p) => hasNoSemanticDelta(p.removed, p.added));
|
|
106
|
+
if (pairs.length >= NON_MINIMAL_MIN_CHANGED_LINES
|
|
107
|
+
&& noDeltaPairs.length / pairs.length >= NON_MINIMAL_NO_DELTA_RATIO) {
|
|
108
|
+
findings.push({
|
|
109
|
+
class: "non-minimal-diff",
|
|
110
|
+
file: path,
|
|
111
|
+
line: noDeltaPairs[0].newLine,
|
|
112
|
+
detail: `hunk @@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@ has no semantic delta on ${noDeltaPairs.length}/${pairs.length} changed lines — likely touches lines unrelated to the actual edit`,
|
|
113
|
+
autoFixable: false,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return findings;
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=minimality.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"minimality.js","sourceRoot":"","sources":["../../src/delegate-gate/minimality.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DiffFile } from "./diff-parser.js";
|
|
2
|
+
export interface PostImage {
|
|
3
|
+
/** The reconstructed post-patch file content, one line per array entry (no trailing newline
|
|
4
|
+
* entries). */
|
|
5
|
+
readonly lines: readonly string[];
|
|
6
|
+
/** 1-based post-image line numbers the diff actually ADDED — the only lines an AST detector
|
|
7
|
+
* may report a finding against ("introduced by the diff"). */
|
|
8
|
+
readonly addedLines: ReadonlySet<number>;
|
|
9
|
+
}
|
|
10
|
+
export declare function reconstructPostImage(original: string | null, file: DiffFile): PostImage;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconstruct a file's post-patch content from its pre-image plus the diff's hunks.
|
|
3
|
+
*
|
|
4
|
+
* `original` is the pre-image content (read from `--repo`), or `null` for a newly created file —
|
|
5
|
+
* a `null` pre-image is only valid when every hunk's removed-line count is zero, which is what a
|
|
6
|
+
* `diff --git` "new file" section always produces.
|
|
7
|
+
*
|
|
8
|
+
* Lines outside any hunk are copied through unchanged from the pre-image; a hunk's `context`
|
|
9
|
+
* lines are trusted as given (this is a diff CONSUMER, not a patch verifier — a hunk that lies
|
|
10
|
+
* about context is the delegate's problem, not this gate's to detect).
|
|
11
|
+
*/
|
|
12
|
+
function applyHunkLines(hunk, out, added, startOldCursor) {
|
|
13
|
+
let oldCursor = startOldCursor;
|
|
14
|
+
for (const line of hunk.lines) {
|
|
15
|
+
if (line.kind === "remove") {
|
|
16
|
+
oldCursor += 1;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
out.push(line.content);
|
|
20
|
+
if (line.kind === "add")
|
|
21
|
+
added.add(out.length);
|
|
22
|
+
if (line.kind === "context")
|
|
23
|
+
oldCursor += 1;
|
|
24
|
+
}
|
|
25
|
+
return oldCursor;
|
|
26
|
+
}
|
|
27
|
+
export function reconstructPostImage(original, file) {
|
|
28
|
+
const originalLines = original === null ? [] : original.replace(/\r\n/g, "\n").split("\n");
|
|
29
|
+
const out = [];
|
|
30
|
+
const added = new Set();
|
|
31
|
+
let oldCursor = 0; // 0-based index into originalLines, next unconsumed line
|
|
32
|
+
for (const hunk of file.hunks) {
|
|
33
|
+
const hunkStartIndex = hunk.oldStart === 0 ? 0 : hunk.oldStart - 1;
|
|
34
|
+
// Copy the untouched span between the previous hunk (or file start) and this one.
|
|
35
|
+
for (; oldCursor < hunkStartIndex; oldCursor++) {
|
|
36
|
+
out.push(originalLines[oldCursor] ?? "");
|
|
37
|
+
}
|
|
38
|
+
oldCursor = applyHunkLines(hunk, out, added, oldCursor);
|
|
39
|
+
}
|
|
40
|
+
for (; oldCursor < originalLines.length; oldCursor++) {
|
|
41
|
+
out.push(originalLines[oldCursor] ?? "");
|
|
42
|
+
}
|
|
43
|
+
return { lines: out, addedLines: added };
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=post-image.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"post-image.js","sourceRoot":"","sources":["../../src/delegate-gate/post-image.ts"],"names":[],"mappings":"AAWA;;;;;;;;;;GAUG;AACH,SAAS,cAAc,CAAC,IAA+B,EAAE,GAAa,EAAE,KAAkB,EAAE,cAAsB;IAChH,IAAI,SAAS,GAAG,cAAc,CAAC;IAC/B,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QAC9B,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC3B,SAAS,IAAI,CAAC,CAAC;YACf,SAAS;QACX,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACvB,IAAI,IAAI,CAAC,IAAI,KAAK,KAAK;YAAE,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC/C,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS;YAAE,SAAS,IAAI,CAAC,CAAC;IAC9C,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,QAAuB,EAAE,IAAc;IAC1E,MAAM,aAAa,GAAG,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC3F,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAEhC,IAAI,SAAS,GAAG,CAAC,CAAC,CAAC,yDAAyD;IAC5E,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QAC9B,MAAM,cAAc,GAAG,IAAI,CAAC,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC;QACnE,kFAAkF;QAClF,OAAO,SAAS,GAAG,cAAc,EAAE,SAAS,EAAE,EAAE,CAAC;YAC/C,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC;QAC3C,CAAC;QACD,SAAS,GAAG,cAAc,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC;IAC1D,CAAC;IACD,OAAO,SAAS,GAAG,aAAa,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,CAAC;QACrD,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC;IAC3C,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;AAC3C,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DiffFile } from "./diff-parser.js";
|
|
2
|
+
import type { Finding } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Flag statements the diff ADDS that assign into module-level (top-level, exported, or
|
|
5
|
+
* object-shaped) state from inside a function body.
|
|
6
|
+
*
|
|
7
|
+
* A tracked binding is either exported, or a top-level `const`/`let`/`var` initialized to an
|
|
8
|
+
* object/array literal or a `new X(...)` instance — the shapes a mutation actually changes for
|
|
9
|
+
* every future reader of the module, as opposed to a local primitive that happens to sit at
|
|
10
|
+
* module scope. Both a direct assignment (`state.x = …`, compound operators included) and a call
|
|
11
|
+
* to a known mutating method (`push`, `set`, `delete`, …) on a property/element-access chain
|
|
12
|
+
* rooted at that binding count. Only sites on lines the diff ADDED are reported — a mutation the
|
|
13
|
+
* delegate did not touch is not this diff's defect.
|
|
14
|
+
*/
|
|
15
|
+
export declare function analyzeSharedStateMutation(files: readonly DiffFile[], readOriginal: (path: string) => string | null): Finding[];
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { reportedPath } from "./diff-parser.js";
|
|
3
|
+
import { reconstructPostImage } from "./post-image.js";
|
|
4
|
+
const TS_FILE = /\.(ts|tsx|mts|cts)$/;
|
|
5
|
+
/** Top-level bindings whose value shape makes an in-place mutation meaningful: object/array
|
|
6
|
+
* literals and `new X(...)` instances (Map, Set, a class instance holding its own state) — plus
|
|
7
|
+
* every EXPORTED top-level binding regardless of shape, since exporting it makes it part of the
|
|
8
|
+
* module's shared surface by declaration. */
|
|
9
|
+
function collectModuleScopeBindings(sourceFile) {
|
|
10
|
+
const names = new Set();
|
|
11
|
+
for (const statement of sourceFile.statements) {
|
|
12
|
+
if (!ts.isVariableStatement(statement))
|
|
13
|
+
continue;
|
|
14
|
+
const isExported = statement.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
15
|
+
for (const decl of statement.declarationList.declarations) {
|
|
16
|
+
if (!ts.isIdentifier(decl.name))
|
|
17
|
+
continue;
|
|
18
|
+
const initializer = decl.initializer;
|
|
19
|
+
const isStateShaped = initializer !== undefined
|
|
20
|
+
&& (ts.isObjectLiteralExpression(initializer)
|
|
21
|
+
|| ts.isArrayLiteralExpression(initializer)
|
|
22
|
+
|| ts.isNewExpression(initializer));
|
|
23
|
+
if (isExported || isStateShaped)
|
|
24
|
+
names.add(decl.name.text);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return names;
|
|
28
|
+
}
|
|
29
|
+
/** Walk up from an expression to the root identifier of a member/index-access chain:
|
|
30
|
+
* `state.rows[0].value` -> `state`. Returns null for anything else (a call result, `this`, …). */
|
|
31
|
+
function rootIdentifier(node) {
|
|
32
|
+
let current = node;
|
|
33
|
+
while (ts.isPropertyAccessExpression(current) || ts.isElementAccessExpression(current)) {
|
|
34
|
+
current = current.expression;
|
|
35
|
+
}
|
|
36
|
+
return ts.isIdentifier(current) ? current.text : null;
|
|
37
|
+
}
|
|
38
|
+
const MUTATING_METHODS = new Set([
|
|
39
|
+
"push", "pop", "shift", "unshift", "splice", "sort", "reverse", "fill", "copyWithin",
|
|
40
|
+
"set", "delete", "clear", "add",
|
|
41
|
+
]);
|
|
42
|
+
function isAssignmentTarget(node) {
|
|
43
|
+
if (ts.isBinaryExpression(node) && isAssignmentOperator(node.operatorToken.kind)) {
|
|
44
|
+
return node.left;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function isAssignmentOperator(kind) {
|
|
49
|
+
switch (kind) {
|
|
50
|
+
case ts.SyntaxKind.EqualsToken:
|
|
51
|
+
case ts.SyntaxKind.PlusEqualsToken:
|
|
52
|
+
case ts.SyntaxKind.MinusEqualsToken:
|
|
53
|
+
case ts.SyntaxKind.AsteriskEqualsToken:
|
|
54
|
+
case ts.SyntaxKind.SlashEqualsToken:
|
|
55
|
+
case ts.SyntaxKind.AmpersandEqualsToken:
|
|
56
|
+
case ts.SyntaxKind.BarEqualsToken:
|
|
57
|
+
case ts.SyntaxKind.CaretEqualsToken:
|
|
58
|
+
case ts.SyntaxKind.QuestionQuestionEqualsToken:
|
|
59
|
+
return true;
|
|
60
|
+
default:
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** `state.rows.push(x)` / `cache.set(k, v)` / `seen.delete(k)` — a call to a known mutating
|
|
65
|
+
* built-in method on a member-access chain rooted at a tracked binding. */
|
|
66
|
+
function mutatingCallTarget(node) {
|
|
67
|
+
if (!ts.isCallExpression(node))
|
|
68
|
+
return null;
|
|
69
|
+
const callee = node.expression;
|
|
70
|
+
if (!ts.isPropertyAccessExpression(callee))
|
|
71
|
+
return null;
|
|
72
|
+
if (!MUTATING_METHODS.has(callee.name.text))
|
|
73
|
+
return null;
|
|
74
|
+
return callee.expression;
|
|
75
|
+
}
|
|
76
|
+
function isFunctionLikeContainer(node) {
|
|
77
|
+
return ts.isFunctionDeclaration(node)
|
|
78
|
+
|| ts.isFunctionExpression(node)
|
|
79
|
+
|| ts.isArrowFunction(node)
|
|
80
|
+
|| ts.isMethodDeclaration(node)
|
|
81
|
+
|| ts.isGetAccessor(node)
|
|
82
|
+
|| ts.isSetAccessor(node)
|
|
83
|
+
|| ts.isConstructorDeclaration(node);
|
|
84
|
+
}
|
|
85
|
+
/** Is `node` lexically inside a function body (as opposed to sitting directly among a source
|
|
86
|
+
* file's top-level statements)? `node` itself is never function-like here (it is an assignment or
|
|
87
|
+
* call expression), so this only ever matches an ANCESTOR. */
|
|
88
|
+
function isInsideFunctionBody(node) {
|
|
89
|
+
return ts.findAncestor(node, isFunctionLikeContainer) !== undefined;
|
|
90
|
+
}
|
|
91
|
+
function findMutationSites(sourceFile) {
|
|
92
|
+
const sites = [];
|
|
93
|
+
const visit = (node) => {
|
|
94
|
+
const assignmentTarget = isAssignmentTarget(node);
|
|
95
|
+
if (assignmentTarget !== null && isInsideFunctionBody(node)) {
|
|
96
|
+
sites.push({ node, target: assignmentTarget });
|
|
97
|
+
}
|
|
98
|
+
const mutatingTarget = mutatingCallTarget(node);
|
|
99
|
+
if (mutatingTarget !== null && isInsideFunctionBody(node)) {
|
|
100
|
+
sites.push({ node, target: mutatingTarget });
|
|
101
|
+
}
|
|
102
|
+
ts.forEachChild(node, visit);
|
|
103
|
+
};
|
|
104
|
+
visit(sourceFile);
|
|
105
|
+
return sites;
|
|
106
|
+
}
|
|
107
|
+
function findingsForFile(file, path, readOriginal) {
|
|
108
|
+
const original = file.isNew ? null : readOriginal(path);
|
|
109
|
+
const { lines, addedLines } = reconstructPostImage(original, file);
|
|
110
|
+
if (addedLines.size === 0)
|
|
111
|
+
return [];
|
|
112
|
+
const content = lines.join("\n");
|
|
113
|
+
const sourceFile = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true, path.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
114
|
+
const moduleScope = collectModuleScopeBindings(sourceFile);
|
|
115
|
+
if (moduleScope.size === 0)
|
|
116
|
+
return [];
|
|
117
|
+
const findings = [];
|
|
118
|
+
for (const site of findMutationSites(sourceFile)) {
|
|
119
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(site.node.getStart(sourceFile));
|
|
120
|
+
const lineNo = line + 1;
|
|
121
|
+
if (!addedLines.has(lineNo))
|
|
122
|
+
continue;
|
|
123
|
+
const root = rootIdentifier(site.target);
|
|
124
|
+
if (root === null || !moduleScope.has(root))
|
|
125
|
+
continue;
|
|
126
|
+
findings.push({
|
|
127
|
+
class: "shared-state-mutation",
|
|
128
|
+
file: path,
|
|
129
|
+
line: lineNo,
|
|
130
|
+
detail: `\`${site.node.getText().slice(0, 120)}\` mutates module-level state \`${root}\` from inside a function`,
|
|
131
|
+
autoFixable: false,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return findings;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Flag statements the diff ADDS that assign into module-level (top-level, exported, or
|
|
138
|
+
* object-shaped) state from inside a function body.
|
|
139
|
+
*
|
|
140
|
+
* A tracked binding is either exported, or a top-level `const`/`let`/`var` initialized to an
|
|
141
|
+
* object/array literal or a `new X(...)` instance — the shapes a mutation actually changes for
|
|
142
|
+
* every future reader of the module, as opposed to a local primitive that happens to sit at
|
|
143
|
+
* module scope. Both a direct assignment (`state.x = …`, compound operators included) and a call
|
|
144
|
+
* to a known mutating method (`push`, `set`, `delete`, …) on a property/element-access chain
|
|
145
|
+
* rooted at that binding count. Only sites on lines the diff ADDED are reported — a mutation the
|
|
146
|
+
* delegate did not touch is not this diff's defect.
|
|
147
|
+
*/
|
|
148
|
+
export function analyzeSharedStateMutation(files, readOriginal) {
|
|
149
|
+
const findings = [];
|
|
150
|
+
for (const file of files) {
|
|
151
|
+
const path = reportedPath(file);
|
|
152
|
+
if (!TS_FILE.test(path) || file.hunks.length === 0)
|
|
153
|
+
continue;
|
|
154
|
+
findings.push(...findingsForFile(file, path, readOriginal));
|
|
155
|
+
}
|
|
156
|
+
return findings;
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=shared-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared-state.js","sourceRoot":"","sources":["../../src/delegate-gate/shared-state.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DiffFile } from "./diff-parser.js";
|
|
2
|
+
import type { Finding } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Flag assertions added by the diff, in changed TEST files, that cannot fail or that show no
|
|
5
|
+
* traceable connection to the code under test.
|
|
6
|
+
*
|
|
7
|
+
* Sharp (always correct) checks: `expect(<literal>).toBeDefined()`, `expect(true).toBe(true)` /
|
|
8
|
+
* `expect(false).toBe(false)`, `expect(x).toBe(x)` on the identical identifier, and the
|
|
9
|
+
* `toBeTruthy`/`toBeFalsy` equivalents on a constant.
|
|
10
|
+
*
|
|
11
|
+
* Best-effort (may under-report on purpose, per the "prefer false negatives" rule): an assertion
|
|
12
|
+
* whose value is produced by calling a function DECLARED LOCALLY in the same test file rather
|
|
13
|
+
* than one it imported — the "hand-copied replica of production control flow, asserted against
|
|
14
|
+
* the replica" shape from `docs/delegate-gate.md`'s cited real defect. Narrower than "no reference
|
|
15
|
+
* to any import at all" on purpose: that broader form flags ordinary local-variable assertions
|
|
16
|
+
* (`const a = 1; expect(a + 1).toBe(2)`), which is legitimate and common.
|
|
17
|
+
*/
|
|
18
|
+
export declare function analyzeTestAssertions(files: readonly DiffFile[], readOriginal: (path: string) => string | null): Finding[];
|