@trofimyukdev/repo-truth 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +273 -2
- package/dist/action.d.ts +52 -0
- package/dist/action.js +182 -0
- package/dist/action.js.map +1 -0
- package/dist/checks/action-pins.d.ts +52 -0
- package/dist/checks/action-pins.js +196 -0
- package/dist/checks/action-pins.js.map +1 -0
- package/dist/checks/autosquash.d.ts +23 -0
- package/dist/checks/autosquash.js +85 -0
- package/dist/checks/autosquash.js.map +1 -0
- package/dist/checks/bidi-controls.d.ts +58 -0
- package/dist/checks/bidi-controls.js +172 -0
- package/dist/checks/bidi-controls.js.map +1 -0
- package/dist/checks/binary-files.d.ts +53 -0
- package/dist/checks/binary-files.js +157 -0
- package/dist/checks/binary-files.js.map +1 -0
- package/dist/checks/config-changes.d.ts +57 -0
- package/dist/checks/config-changes.js +175 -0
- package/dist/checks/config-changes.js.map +1 -0
- package/dist/checks/credentials.d.ts +35 -0
- package/dist/checks/credentials.js +138 -0
- package/dist/checks/credentials.js.map +1 -0
- package/dist/checks/install-scripts.d.ts +39 -0
- package/dist/checks/install-scripts.js +173 -0
- package/dist/checks/install-scripts.js.map +1 -0
- package/dist/checks/lint-rules.d.ts +49 -0
- package/dist/checks/lint-rules.js +184 -0
- package/dist/checks/lint-rules.js.map +1 -0
- package/dist/checks/path-policy.d.ts +40 -0
- package/dist/checks/path-policy.js +195 -0
- package/dist/checks/path-policy.js.map +1 -0
- package/dist/checks/raised-timeouts.d.ts +66 -0
- package/dist/checks/raised-timeouts.js +248 -0
- package/dist/checks/raised-timeouts.js.map +1 -0
- package/dist/checks/script-changes.d.ts +51 -0
- package/dist/checks/script-changes.js +199 -0
- package/dist/checks/script-changes.js.map +1 -0
- package/dist/checks/secret-files.d.ts +58 -0
- package/dist/checks/secret-files.js +165 -0
- package/dist/checks/secret-files.js.map +1 -0
- package/dist/checks/strictness.d.ts +59 -0
- package/dist/checks/strictness.js +260 -0
- package/dist/checks/strictness.js.map +1 -0
- package/dist/checks/submodules.d.ts +51 -0
- package/dist/checks/submodules.js +172 -0
- package/dist/checks/submodules.js.map +1 -0
- package/dist/checks/suppressions.d.ts +53 -0
- package/dist/checks/suppressions.js +203 -0
- package/dist/checks/suppressions.js.map +1 -0
- package/dist/checks/workflow-permissions.d.ts +56 -0
- package/dist/checks/workflow-permissions.js +237 -0
- package/dist/checks/workflow-permissions.js.map +1 -0
- package/dist/checks/workflow-weakening.d.ts +38 -0
- package/dist/checks/workflow-weakening.js +172 -0
- package/dist/checks/workflow-weakening.js.map +1 -0
- package/dist/cli.d.ts +31 -0
- package/dist/cli.js +300 -0
- package/dist/cli.js.map +1 -0
- package/dist/commit-range.d.ts +28 -0
- package/dist/commit-range.js +152 -0
- package/dist/commit-range.js.map +1 -0
- package/dist/config.d.ts +35 -0
- package/dist/config.js +235 -0
- package/dist/config.js.map +1 -0
- package/dist/conflict-markers.d.ts +40 -0
- package/dist/conflict-markers.js +234 -0
- package/dist/conflict-markers.js.map +1 -0
- package/dist/dependency-sources.d.ts +32 -0
- package/dist/dependency-sources.js +260 -0
- package/dist/dependency-sources.js.map +1 -0
- package/dist/figure-claims.d.ts +35 -0
- package/dist/figure-claims.js +139 -0
- package/dist/figure-claims.js.map +1 -0
- package/dist/figure-text.d.ts +22 -0
- package/dist/figure-text.js +119 -0
- package/dist/figure-text.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/large-blobs.d.ts +35 -0
- package/dist/large-blobs.js +163 -0
- package/dist/large-blobs.js.map +1 -0
- package/dist/lockfile-drift.d.ts +36 -0
- package/dist/lockfile-drift.js +206 -0
- package/dist/lockfile-drift.js.map +1 -0
- package/dist/lowered-thresholds.d.ts +70 -0
- package/dist/lowered-thresholds.js +281 -0
- package/dist/lowered-thresholds.js.map +1 -0
- package/dist/measurement-rule.d.ts +39 -0
- package/dist/measurement-rule.js +213 -0
- package/dist/measurement-rule.js.map +1 -0
- package/dist/portable-paths.d.ts +37 -0
- package/dist/portable-paths.js +215 -0
- package/dist/portable-paths.js.map +1 -0
- package/dist/registry.d.ts +43 -0
- package/dist/registry.js +144 -0
- package/dist/registry.js.map +1 -0
- package/dist/replay-plan.d.ts +36 -0
- package/dist/replay-plan.js +196 -0
- package/dist/replay-plan.js.map +1 -0
- package/dist/replayed-figures.d.ts +36 -0
- package/dist/replayed-figures.js +159 -0
- package/dist/replayed-figures.js.map +1 -0
- package/dist/snapshot-refreshes.d.ts +36 -0
- package/dist/snapshot-refreshes.js +202 -0
- package/dist/snapshot-refreshes.js.map +1 -0
- package/dist/stray-files.d.ts +52 -0
- package/dist/stray-files.js +234 -0
- package/dist/stray-files.js.map +1 -0
- package/dist/symlinks.d.ts +28 -0
- package/dist/symlinks.js +193 -0
- package/dist/symlinks.js.map +1 -0
- package/dist/trailers.d.ts +36 -0
- package/dist/trailers.js +110 -0
- package/dist/trailers.js.map +1 -0
- package/dist/weakened-tests.d.ts +94 -0
- package/dist/weakened-tests.js +458 -0
- package/dist/weakened-tests.js.map +1 -0
- package/package.json +31 -4
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install scripts added.
|
|
3
|
+
*
|
|
4
|
+
* `npm ci` and `npm install` run a package's `preinstall`, `install` and
|
|
5
|
+
* `postinstall` scripts on the machine that installs it, and npm's lockfile
|
|
6
|
+
* says which packages have one: the entry of such a package under `packages`
|
|
7
|
+
* holds `"hasInstallScript": true`. This check reads every file the range adds,
|
|
8
|
+
* changes or renames onto whose last path segment is exactly `package-lock.json`
|
|
9
|
+
* or `npm-shrinkwrap.json`, at any depth, whole through git at the merge base
|
|
10
|
+
* of `base` and `candidate` and at `candidate` (never the working tree, renames
|
|
11
|
+
* detected), and parses each with `JSON.parse`. The source is the file at the
|
|
12
|
+
* merge base, the one a rename came from, or none for a file the range adds.
|
|
13
|
+
*
|
|
14
|
+
* A finding is a key of the candidate's `packages` object whose entry holds
|
|
15
|
+
* `hasInstallScript` equal to `true` while the source's `packages` does not
|
|
16
|
+
* hold that key with such an entry (whatever its version). A lockfile of
|
|
17
|
+
* version 1 has no `packages` object and yields nothing. A lockfile, candidate
|
|
18
|
+
* or source, that `JSON.parse` refuses fails the record and names the file.
|
|
19
|
+
*
|
|
20
|
+
* A finding's id is `install-script:<file>`, which sanctions every finding of
|
|
21
|
+
* that file, by a `Sanctioned-Weakening: <id> <reason>` line in a commit
|
|
22
|
+
* message of the range or by the caller's `sanctions`.
|
|
23
|
+
*/
|
|
24
|
+
import { execFile } from "node:child_process";
|
|
25
|
+
import { SANCTION_KEY } from "../weakened-tests.js";
|
|
26
|
+
export const name = "RT-94";
|
|
27
|
+
function runGit(cwd, args) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
execFile("git", args, { cwd, maxBuffer: 1024 * 1024 * 1024, encoding: "utf8" }, (error, stdout, stderr) => {
|
|
30
|
+
if (error)
|
|
31
|
+
reject(new Error(stderr.trim() || error.message));
|
|
32
|
+
else
|
|
33
|
+
resolve(stdout);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/** Escape everything outside printable ASCII, so evidence cannot replay a control sequence. */
|
|
38
|
+
function escapeText(text) {
|
|
39
|
+
let escaped = "";
|
|
40
|
+
for (const char of text) {
|
|
41
|
+
const codePoint = char.codePointAt(0) ?? 0;
|
|
42
|
+
escaped += codePoint >= 0x20 && codePoint <= 0x7e ? char : `\\u{${codePoint.toString(16).padStart(4, "0")}}`;
|
|
43
|
+
}
|
|
44
|
+
return escaped;
|
|
45
|
+
}
|
|
46
|
+
async function resolveCommit(cwd, revision) {
|
|
47
|
+
const fail = (cause) => new Error(`revision "${escapeText(String(revision))}" does not name a commit${cause}`);
|
|
48
|
+
// A revision that begins with a dash would be read as an option.
|
|
49
|
+
if (typeof revision !== "string" || revision.length === 0 || revision.startsWith("-"))
|
|
50
|
+
throw fail("");
|
|
51
|
+
let hash = "";
|
|
52
|
+
try {
|
|
53
|
+
hash = (await runGit(cwd, ["rev-parse", "--verify", "--quiet", `${revision}^{commit}`])).trim();
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
throw fail(`: ${error.message}`);
|
|
57
|
+
}
|
|
58
|
+
if (hash.length === 0)
|
|
59
|
+
throw fail("");
|
|
60
|
+
return hash;
|
|
61
|
+
}
|
|
62
|
+
const isLockfile = (path) => /(^|\/)(package-lock|npm-shrinkwrap)\.json$/.test(path);
|
|
63
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
64
|
+
async function readPackages(cwd, mode, sha) {
|
|
65
|
+
if (!mode.startsWith("100"))
|
|
66
|
+
return { problem: "is not a regular file" };
|
|
67
|
+
let parsed;
|
|
68
|
+
try {
|
|
69
|
+
parsed = JSON.parse(await runGit(cwd, ["cat-file", "blob", sha]));
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
return { problem: `is not JSON: ${error.message}` };
|
|
73
|
+
}
|
|
74
|
+
const packages = isObject(parsed) && Object.hasOwn(parsed, "packages") ? parsed["packages"] : null;
|
|
75
|
+
return { packages: isObject(packages) ? packages : null };
|
|
76
|
+
}
|
|
77
|
+
/** Whether an entry of `packages` is an object holding `hasInstallScript: true`. */
|
|
78
|
+
const flagged = (entry) => isObject(entry) && entry["hasInstallScript"] === true;
|
|
79
|
+
async function compare(cwd, from, to) {
|
|
80
|
+
const out = await runGit(cwd, ["diff", "--raw", "-z", "--no-abbrev", "--find-renames", "--no-relative", from, to, "--"]);
|
|
81
|
+
const fields = out.split("\0");
|
|
82
|
+
const drafts = [];
|
|
83
|
+
const problems = [];
|
|
84
|
+
for (let i = 0; i < fields.length; i++) {
|
|
85
|
+
const head = fields[i] ?? "";
|
|
86
|
+
if (!head.startsWith(":"))
|
|
87
|
+
continue;
|
|
88
|
+
const [oldMode = "", newMode = "", oldSha = "", newSha = "", status = ""] = head.slice(1).split(" ");
|
|
89
|
+
const first = fields[++i] ?? "";
|
|
90
|
+
const renamed = status.startsWith("R") || status.startsWith("C");
|
|
91
|
+
const second = renamed ? (fields[++i] ?? "") : first;
|
|
92
|
+
if (status.startsWith("D"))
|
|
93
|
+
continue;
|
|
94
|
+
const oldPath = status.startsWith("A") ? null : first;
|
|
95
|
+
const file = second;
|
|
96
|
+
if (!isLockfile(file))
|
|
97
|
+
continue;
|
|
98
|
+
const after = await readPackages(cwd, newMode, newSha);
|
|
99
|
+
const before = oldPath === null ? { packages: null } : await readPackages(cwd, oldMode, oldSha);
|
|
100
|
+
if ("problem" in after)
|
|
101
|
+
problems.push(`${escapeText(file)}: at the candidate the lockfile ${escapeText(after.problem)}`);
|
|
102
|
+
if ("problem" in before)
|
|
103
|
+
problems.push(`${escapeText(oldPath ?? file)}: at the merge base the lockfile ${escapeText(before.problem)}`);
|
|
104
|
+
if ("problem" in after || "problem" in before || after.packages === null)
|
|
105
|
+
continue;
|
|
106
|
+
for (const [key, entry] of Object.entries(after.packages)) {
|
|
107
|
+
if (!flagged(entry))
|
|
108
|
+
continue;
|
|
109
|
+
if (before.packages !== null && Object.hasOwn(before.packages, key) && flagged(before.packages[key]))
|
|
110
|
+
continue;
|
|
111
|
+
const version = entry["version"];
|
|
112
|
+
drafts.push({ file, text: `${escapeText(file)}: install-script ${escapeText(key)}${typeof version === "string" ? `@${escapeText(version)}` : ""}` });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { drafts, problems };
|
|
116
|
+
}
|
|
117
|
+
/** The text after the sanction key on every such line of the range's commit messages. */
|
|
118
|
+
async function readSanctionLines(cwd, from, to) {
|
|
119
|
+
const out = await runGit(cwd, ["log", "--format=%B%x00", `${from}..${to}`, "--"]);
|
|
120
|
+
const prefix = `${SANCTION_KEY}:`;
|
|
121
|
+
return out
|
|
122
|
+
.split(/[\n\0]/)
|
|
123
|
+
.map((line) => line.trim())
|
|
124
|
+
.filter((line) => line.startsWith(prefix))
|
|
125
|
+
.map((line) => line.slice(prefix.length).trim());
|
|
126
|
+
}
|
|
127
|
+
function sanctionFor(id, lines, given) {
|
|
128
|
+
const reason = Object.hasOwn(given, id) ? given[id] : undefined;
|
|
129
|
+
if (typeof reason === "string" && reason.trim().length > 0)
|
|
130
|
+
return reason.trim();
|
|
131
|
+
for (const line of lines) {
|
|
132
|
+
const rest = line.slice(id.length);
|
|
133
|
+
if (line.startsWith(id) && /^\s+\S/.test(rest))
|
|
134
|
+
return rest.trim();
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Report each package `base..candidate` adds to a lockfile with an install
|
|
140
|
+
* script. Fails while any finding is unsanctioned or a lockfile cannot be read,
|
|
141
|
+
* and fails closed: a revision that names no commit, or any git invocation that
|
|
142
|
+
* fails, is a `fail` record naming the cause - never a `pass`, never a throw.
|
|
143
|
+
*/
|
|
144
|
+
export async function check(options) {
|
|
145
|
+
try {
|
|
146
|
+
const { cwd, base, candidate } = options;
|
|
147
|
+
const baseCommit = await resolveCommit(cwd, base);
|
|
148
|
+
const candidateCommit = await resolveCommit(cwd, candidate);
|
|
149
|
+
let ancestor;
|
|
150
|
+
try {
|
|
151
|
+
ancestor = (await runGit(cwd, ["merge-base", baseCommit, candidateCommit])).trim();
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
throw new Error(`no merge base of "${escapeText(base)}" and "${escapeText(candidate)}": ${error.message}`);
|
|
155
|
+
}
|
|
156
|
+
const { drafts, problems } = await compare(cwd, ancestor, candidateCommit);
|
|
157
|
+
if (drafts.length === 0 && problems.length === 0)
|
|
158
|
+
return { name, status: "pass", evidence: ["no install script added"] };
|
|
159
|
+
const lines = drafts.length === 0 ? [] : await readSanctionLines(cwd, ancestor, candidateCommit);
|
|
160
|
+
let open = problems.length > 0;
|
|
161
|
+
const evidence = drafts.map((draft) => {
|
|
162
|
+
const reason = sanctionFor(`install-script:${draft.file}`, lines, options.sanctions ?? {});
|
|
163
|
+
if (reason === null)
|
|
164
|
+
open = true;
|
|
165
|
+
return reason === null ? draft.text : `${draft.text} - sanctioned: ${escapeText(reason)}`;
|
|
166
|
+
});
|
|
167
|
+
return { name, status: open ? "fail" : "pass", evidence: [...problems, ...evidence] };
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
return { name, status: "fail", evidence: [escapeText(error instanceof Error ? error.message : String(error))] };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=install-scripts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install-scripts.js","sourceRoot":"","sources":["../../src/checks/install-scripts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lint rules turned off.
|
|
3
|
+
*
|
|
4
|
+
* A lint run goes green as surely when a rule is turned off in the lint
|
|
5
|
+
* configuration as when the code is fixed. This check reads every path the range
|
|
6
|
+
* adds, changes or renames onto whose last path segment is exactly one of
|
|
7
|
+
* `LINT_FILES` (`.eslintrc*`, `eslint.config.*`, `biome.json`, `biome.jsonc`,
|
|
8
|
+
* `.oxlintrc.json`), at any depth, whole and through git at the merge base of
|
|
9
|
+
* `base` and `candidate` and at `candidate`, renames detected. The working tree
|
|
10
|
+
* is never read, and no file is parsed or run.
|
|
11
|
+
*
|
|
12
|
+
* An off pair is, in a file's text, a rule name, white space or none, a colon,
|
|
13
|
+
* white space or none, then an off value (`"off"`, `'off'`, `off` or `0`, or
|
|
14
|
+
* `[` and white space before one of them), the value not followed by a letter, a
|
|
15
|
+
* digit, `_` or `.`. A name is written in double quotes, in single quotes or
|
|
16
|
+
* bare. A pair is counted wherever it stands, a comment included. For each file,
|
|
17
|
+
* each name's pairs are counted at the candidate and in the source file (the
|
|
18
|
+
* file at the merge base, the one a rename came from, or none for a file the
|
|
19
|
+
* range adds); a name counted more often at the candidate is one finding
|
|
20
|
+
* `rule-off`, whatever the count. A rule moved or a line re-indented says
|
|
21
|
+
* nothing; a rule changed from `error` or `warn` to `off` is a finding.
|
|
22
|
+
*
|
|
23
|
+
* Each finding is the evidence line `<file>: rule-off <name>`. A finding's id is
|
|
24
|
+
* `rule-off:<file>`, which sanctions every finding of that file by a
|
|
25
|
+
* `Sanctioned-Weakening: <id> <reason>` line in a commit message of the range or
|
|
26
|
+
* by the caller's `sanctions`; the evidence line then ends ` - sanctioned:
|
|
27
|
+
* <reason>`.
|
|
28
|
+
*
|
|
29
|
+
* What it does not see: a rule set to `warn`, a rule deleted, a rule turned off
|
|
30
|
+
* by a configuration named in `extends`, `package.json`'s `eslintConfig`, ignore
|
|
31
|
+
* patterns, biome's `"recommended": false` and `"enabled": false`, and other
|
|
32
|
+
* linters' files.
|
|
33
|
+
*/
|
|
34
|
+
import type { CheckRecord } from "../index.js";
|
|
35
|
+
export declare const name = "RT-92";
|
|
36
|
+
export interface LintRulesOptions {
|
|
37
|
+
readonly cwd: string;
|
|
38
|
+
readonly base: string;
|
|
39
|
+
readonly candidate: string;
|
|
40
|
+
/** Ids (`rule-off:<file>`) mapped to reasons. */
|
|
41
|
+
readonly sanctions?: Readonly<Record<string, string>> | undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Report each rule `base..candidate` turns off in a lint configuration file.
|
|
45
|
+
* Fails while any finding is unsanctioned, and fails closed: a revision that
|
|
46
|
+
* names no commit, or any git invocation that fails, is a `fail` record naming
|
|
47
|
+
* the cause - never a `pass`, never a throw.
|
|
48
|
+
*/
|
|
49
|
+
export declare function check(options: LintRulesOptions): Promise<CheckRecord>;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lint rules turned off.
|
|
3
|
+
*
|
|
4
|
+
* A lint run goes green as surely when a rule is turned off in the lint
|
|
5
|
+
* configuration as when the code is fixed. This check reads every path the range
|
|
6
|
+
* adds, changes or renames onto whose last path segment is exactly one of
|
|
7
|
+
* `LINT_FILES` (`.eslintrc*`, `eslint.config.*`, `biome.json`, `biome.jsonc`,
|
|
8
|
+
* `.oxlintrc.json`), at any depth, whole and through git at the merge base of
|
|
9
|
+
* `base` and `candidate` and at `candidate`, renames detected. The working tree
|
|
10
|
+
* is never read, and no file is parsed or run.
|
|
11
|
+
*
|
|
12
|
+
* An off pair is, in a file's text, a rule name, white space or none, a colon,
|
|
13
|
+
* white space or none, then an off value (`"off"`, `'off'`, `off` or `0`, or
|
|
14
|
+
* `[` and white space before one of them), the value not followed by a letter, a
|
|
15
|
+
* digit, `_` or `.`. A name is written in double quotes, in single quotes or
|
|
16
|
+
* bare. A pair is counted wherever it stands, a comment included. For each file,
|
|
17
|
+
* each name's pairs are counted at the candidate and in the source file (the
|
|
18
|
+
* file at the merge base, the one a rename came from, or none for a file the
|
|
19
|
+
* range adds); a name counted more often at the candidate is one finding
|
|
20
|
+
* `rule-off`, whatever the count. A rule moved or a line re-indented says
|
|
21
|
+
* nothing; a rule changed from `error` or `warn` to `off` is a finding.
|
|
22
|
+
*
|
|
23
|
+
* Each finding is the evidence line `<file>: rule-off <name>`. A finding's id is
|
|
24
|
+
* `rule-off:<file>`, which sanctions every finding of that file by a
|
|
25
|
+
* `Sanctioned-Weakening: <id> <reason>` line in a commit message of the range or
|
|
26
|
+
* by the caller's `sanctions`; the evidence line then ends ` - sanctioned:
|
|
27
|
+
* <reason>`.
|
|
28
|
+
*
|
|
29
|
+
* What it does not see: a rule set to `warn`, a rule deleted, a rule turned off
|
|
30
|
+
* by a configuration named in `extends`, `package.json`'s `eslintConfig`, ignore
|
|
31
|
+
* patterns, biome's `"recommended": false` and `"enabled": false`, and other
|
|
32
|
+
* linters' files.
|
|
33
|
+
*/
|
|
34
|
+
import { execFile } from "node:child_process";
|
|
35
|
+
import { SANCTION_KEY } from "../weakened-tests.js";
|
|
36
|
+
export const name = "RT-92";
|
|
37
|
+
const LINT_FILES = new Set([
|
|
38
|
+
".eslintrc",
|
|
39
|
+
".eslintrc.json",
|
|
40
|
+
".eslintrc.yml",
|
|
41
|
+
".eslintrc.yaml",
|
|
42
|
+
".eslintrc.js",
|
|
43
|
+
".eslintrc.cjs",
|
|
44
|
+
"eslint.config.js",
|
|
45
|
+
"eslint.config.mjs",
|
|
46
|
+
"eslint.config.cjs",
|
|
47
|
+
"eslint.config.ts",
|
|
48
|
+
"eslint.config.mts",
|
|
49
|
+
"eslint.config.cts",
|
|
50
|
+
"biome.json",
|
|
51
|
+
"biome.jsonc",
|
|
52
|
+
".oxlintrc.json",
|
|
53
|
+
]);
|
|
54
|
+
const OFF_PAIR = new RegExp(String.raw `(?:"([^"]*)"|'([^']*)'|([A-Za-z@_$][A-Za-z0-9@/\-_.$]*))\s*:\s*(?:\[\s*)?(?:"off"|'off'|off|0)(?![A-Za-z0-9_.])`, "g");
|
|
55
|
+
function runGit(cwd, args) {
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
execFile("git", args, { cwd, maxBuffer: 1024 * 1024 * 1024, encoding: "buffer" }, (error, stdout, stderr) => {
|
|
58
|
+
if (error)
|
|
59
|
+
reject(new Error(stderr.toString("utf8").trim() || error.message));
|
|
60
|
+
else
|
|
61
|
+
resolve(stdout.toString("utf8"));
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/** Escape everything outside printable ASCII, so evidence cannot replay a control sequence. */
|
|
66
|
+
function escapeText(text) {
|
|
67
|
+
let escaped = "";
|
|
68
|
+
for (const char of text) {
|
|
69
|
+
const codePoint = char.codePointAt(0) ?? 0;
|
|
70
|
+
escaped += codePoint >= 0x20 && codePoint <= 0x7e ? char : `\\u{${codePoint.toString(16).padStart(4, "0")}}`;
|
|
71
|
+
}
|
|
72
|
+
return escaped;
|
|
73
|
+
}
|
|
74
|
+
async function resolveCommit(cwd, revision) {
|
|
75
|
+
const fail = (cause) => new Error(`revision "${escapeText(String(revision))}" does not name a commit${cause}`);
|
|
76
|
+
// A revision that begins with a dash would be read as an option.
|
|
77
|
+
if (typeof revision !== "string" || revision.length === 0 || revision.startsWith("-"))
|
|
78
|
+
throw fail("");
|
|
79
|
+
let hash = "";
|
|
80
|
+
try {
|
|
81
|
+
hash = (await runGit(cwd, ["rev-parse", "--verify", "--quiet", `${revision}^{commit}`])).trim();
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
throw fail(`: ${error.message}`);
|
|
85
|
+
}
|
|
86
|
+
if (hash.length === 0)
|
|
87
|
+
throw fail("");
|
|
88
|
+
return hash;
|
|
89
|
+
}
|
|
90
|
+
const isLintPath = (path) => LINT_FILES.has(path.slice(path.lastIndexOf("/") + 1));
|
|
91
|
+
const isNull = (sha) => /^0+$/.test(sha);
|
|
92
|
+
/** A blob read whole through git; an absent blob is an empty file. */
|
|
93
|
+
const readBlob = async (cwd, sha) => isNull(sha) ? "" : runGit(cwd, ["cat-file", "blob", sha]);
|
|
94
|
+
/** How many off pairs each rule name has in a file's text. */
|
|
95
|
+
function countOffPairs(content) {
|
|
96
|
+
const counts = new Map();
|
|
97
|
+
for (const match of content.matchAll(OFF_PAIR)) {
|
|
98
|
+
const rule = match[1] ?? match[2] ?? match[3] ?? "";
|
|
99
|
+
counts.set(rule, (counts.get(rule) ?? 0) + 1);
|
|
100
|
+
}
|
|
101
|
+
return counts;
|
|
102
|
+
}
|
|
103
|
+
async function compare(cwd, from, to) {
|
|
104
|
+
const out = await runGit(cwd, ["diff", "--raw", "-z", "-M", "--no-abbrev", "--no-ext-diff", "--no-relative", from, to, "--"]);
|
|
105
|
+
const fields = out.split("\0");
|
|
106
|
+
const drafts = [];
|
|
107
|
+
for (let i = 0; i < fields.length; i++) {
|
|
108
|
+
const head = fields[i] ?? "";
|
|
109
|
+
if (!head.startsWith(":"))
|
|
110
|
+
continue;
|
|
111
|
+
const [, , oldSha = "", newSha = "", status = ""] = head.slice(1).split(" ");
|
|
112
|
+
const renamed = status.startsWith("R") || status.startsWith("C");
|
|
113
|
+
i++;
|
|
114
|
+
const newPath = renamed ? (fields[++i] ?? "") : (fields[i] ?? "");
|
|
115
|
+
const letter = status.charAt(0);
|
|
116
|
+
if (!["A", "M", "R", "C", "T"].includes(letter) || !isLintPath(newPath))
|
|
117
|
+
continue;
|
|
118
|
+
const before = countOffPairs(letter === "A" ? "" : await readBlob(cwd, oldSha));
|
|
119
|
+
const after = countOffPairs(await readBlob(cwd, newSha));
|
|
120
|
+
for (const [rule, count] of after) {
|
|
121
|
+
if (count > (before.get(rule) ?? 0))
|
|
122
|
+
drafts.push({ file: newPath, rule });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return drafts;
|
|
126
|
+
}
|
|
127
|
+
/** The text after the sanction key on every such line of the range's commit messages. */
|
|
128
|
+
async function readSanctionLines(cwd, from, to) {
|
|
129
|
+
const out = await runGit(cwd, ["log", "--format=%B%x00", `${from}..${to}`, "--"]);
|
|
130
|
+
const prefix = `${SANCTION_KEY}:`;
|
|
131
|
+
return out
|
|
132
|
+
.split(/[\n\0]/)
|
|
133
|
+
.map((line) => line.trim())
|
|
134
|
+
.filter((line) => line.startsWith(prefix))
|
|
135
|
+
.map((line) => line.slice(prefix.length).trim());
|
|
136
|
+
}
|
|
137
|
+
function sanctionFor(id, lines, given) {
|
|
138
|
+
const reason = Object.hasOwn(given, id) ? given[id] : undefined;
|
|
139
|
+
if (typeof reason === "string" && reason.trim().length > 0)
|
|
140
|
+
return reason.trim();
|
|
141
|
+
for (const line of lines) {
|
|
142
|
+
const rest = line.slice(id.length);
|
|
143
|
+
if (line.startsWith(id) && /^\s+\S/.test(rest))
|
|
144
|
+
return rest.trim();
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Report each rule `base..candidate` turns off in a lint configuration file.
|
|
150
|
+
* Fails while any finding is unsanctioned, and fails closed: a revision that
|
|
151
|
+
* names no commit, or any git invocation that fails, is a `fail` record naming
|
|
152
|
+
* the cause - never a `pass`, never a throw.
|
|
153
|
+
*/
|
|
154
|
+
export async function check(options) {
|
|
155
|
+
try {
|
|
156
|
+
const { cwd, base, candidate } = options;
|
|
157
|
+
const baseCommit = await resolveCommit(cwd, base);
|
|
158
|
+
const candidateCommit = await resolveCommit(cwd, candidate);
|
|
159
|
+
let ancestor;
|
|
160
|
+
try {
|
|
161
|
+
ancestor = (await runGit(cwd, ["merge-base", baseCommit, candidateCommit])).trim();
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
throw new Error(`no merge base of "${escapeText(base)}" and "${escapeText(candidate)}": ${error.message}`);
|
|
165
|
+
}
|
|
166
|
+
const drafts = await compare(cwd, ancestor, candidateCommit);
|
|
167
|
+
if (drafts.length === 0)
|
|
168
|
+
return { name, status: "pass", evidence: ["no lint rule turned off"] };
|
|
169
|
+
const lines = await readSanctionLines(cwd, ancestor, candidateCommit);
|
|
170
|
+
let open = false;
|
|
171
|
+
const evidence = drafts.map((draft) => {
|
|
172
|
+
const reason = sanctionFor(`rule-off:${draft.file}`, lines, options.sanctions ?? {});
|
|
173
|
+
if (reason === null)
|
|
174
|
+
open = true;
|
|
175
|
+
const tail = reason === null ? "" : ` - sanctioned: ${escapeText(reason)}`;
|
|
176
|
+
return `${escapeText(draft.file)}: rule-off ${escapeText(draft.rule)}${tail}`;
|
|
177
|
+
});
|
|
178
|
+
return { name, status: open ? "fail" : "pass", evidence };
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
return { name, status: "fail", evidence: [escapeText(error instanceof Error ? error.message : String(error))] };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=lint-rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lint-rules.js","sourceRoot":"","sources":["../../src/checks/lint-rules.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path policy.
|
|
3
|
+
*
|
|
4
|
+
* Which paths a change should not reach without a second look is the
|
|
5
|
+
* repository's to say, so this check holds no list: it reads `forbidden` and
|
|
6
|
+
* `allowed` from its options and reports each path the range adds, changes,
|
|
7
|
+
* deletes or renames - both ends of a rename - that a `forbidden` pattern
|
|
8
|
+
* matches or, when `allowed` is given, that no `allowed` pattern matches.
|
|
9
|
+
*
|
|
10
|
+
* Paths come from git's list of the changes between the merge base and the
|
|
11
|
+
* candidate, never from the working tree. A pattern is matched against a whole
|
|
12
|
+
* path: `?` is one character but `/`, `*` is any run but `/`, a segment that
|
|
13
|
+
* is exactly `**` is any number of whole segments (none included), and every
|
|
14
|
+
* other character - case included - matches only itself.
|
|
15
|
+
*/
|
|
16
|
+
import type { CheckRecord } from "../index.js";
|
|
17
|
+
export declare const name = "RT-57";
|
|
18
|
+
/** The options a configuration file may set for this check. */
|
|
19
|
+
export declare const optionKinds: {
|
|
20
|
+
forbidden: string;
|
|
21
|
+
allowed: string;
|
|
22
|
+
};
|
|
23
|
+
export interface PathPolicyOptions {
|
|
24
|
+
readonly cwd: string;
|
|
25
|
+
readonly base: string;
|
|
26
|
+
readonly candidate: string;
|
|
27
|
+
/** Ids (`<category>:<path>`) mapped to reasons. */
|
|
28
|
+
readonly sanctions?: Readonly<Record<string, string>> | undefined;
|
|
29
|
+
readonly forbidden?: readonly string[] | undefined;
|
|
30
|
+
readonly allowed?: readonly string[] | undefined;
|
|
31
|
+
}
|
|
32
|
+
/** Whether one pattern matches one whole path. */
|
|
33
|
+
export declare function matchesPattern(pattern: string, path: string): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Report each path `base..candidate` touches that the policy forbids or does
|
|
36
|
+
* not allow. Fails while any finding is unsanctioned, and fails closed: a bad
|
|
37
|
+
* revision, a bad option or a failing git invocation is a `fail` record naming
|
|
38
|
+
* the cause - never a `pass`, never a throw.
|
|
39
|
+
*/
|
|
40
|
+
export declare function check(options: PathPolicyOptions): Promise<CheckRecord>;
|