@avi2dg/checks 0.20.0 → 0.22.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/CHANGELOG.md +65 -39
- package/CONTRIBUTING.md +11 -8
- package/README.md +15 -24
- package/bunfig.toml +1 -1
- package/docs/configs/commit-messages.md +5 -1
- package/docs/configs/dependency-rules.md +5 -2
- package/docs/configs/effect-rules.md +32 -33
- package/docs/configs/native-settings.md +74 -0
- package/docs/configs/typescript-rules.md +4 -0
- package/docs/design.md +24 -25
- package/docs/gates/checks-backtest.md +4 -0
- package/docs/gates/checks-ci-wiring.md +30 -93
- package/docs/gates/checks-comment-gate.md +4 -0
- package/docs/gates/checks-commit-identity.md +23 -27
- package/docs/gates/checks-docs.md +23 -21
- package/docs/gates/checks-flake.md +4 -10
- package/docs/gates/checks-lint-coverage.md +5 -1
- package/docs/gates/checks-lint.md +22 -104
- package/docs/gates/checks-mutation-compare.md +4 -0
- package/docs/gates/checks-quarantine-clock.md +4 -0
- package/docs/gates/checks-repetition.md +26 -41
- package/docs/gates/checks-subsumed-tests.md +4 -0
- package/docs/gates/checks-suppressions-ratchet.md +4 -0
- package/docs/gates/checks-test-layout.md +16 -8
- package/docs/gates/checks-test.md +68 -36
- package/docs/gates/checks-vendor.md +20 -13
- package/package.json +8 -21
- package/scripts/ci-wiring.ts +28 -97
- package/scripts/commit-identity.ts +32 -4
- package/scripts/doc-rules.ts +26 -11
- package/scripts/doc-templates.ts +2 -1
- package/scripts/docs.ts +4 -7
- package/scripts/gates.ts +0 -29
- package/scripts/git.ts +24 -1
- package/scripts/lint.ts +15 -34
- package/scripts/range-gate.ts +1 -2
- package/scripts/repetition.ts +40 -40
- package/scripts/shell-command.ts +7 -1
- package/scripts/swc.ts +46 -0
- package/scripts/test-layout.ts +40 -56
- package/scripts/test-skips.ts +180 -0
- package/scripts/test.ts +33 -38
- package/scripts/vendor.ts +55 -11
- package/dist/feature-rules.js +0 -349
- package/docs/configs/quality-file.md +0 -101
- package/docs/gates/checks-feature-owners.md +0 -113
- package/docs/gates/checks-quality.md +0 -109
- package/docs/gates/checks-size-budget.md +0 -107
- package/quality.schema.json +0 -496
- package/scripts/feature-owners.ts +0 -139
- package/scripts/quality-file.ts +0 -347
- package/scripts/quality.ts +0 -342
- package/scripts/size-budget.ts +0 -285
- package/scripts/size-rules.ts +0 -126
package/scripts/size-budget.ts
DELETED
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
import { Console, Effect, FileSystem, Path, Schema } from "effect";
|
|
3
|
-
import { changedPaths, collect, git, pathsAt, rangeEnds, type Change } from "./git.ts";
|
|
4
|
-
import { runMain } from "./main.ts";
|
|
5
|
-
import { renderJson } from "./quality-file.ts";
|
|
6
|
-
import { rangeGateInputs } from "./range-gate.ts";
|
|
7
|
-
import {
|
|
8
|
-
budgetOf,
|
|
9
|
-
budgetsOf,
|
|
10
|
-
diagnosticCode,
|
|
11
|
-
qualifiedName,
|
|
12
|
-
SIZE_DEFAULTS,
|
|
13
|
-
SIZE_RULES,
|
|
14
|
-
TESTS_DIRECTORY,
|
|
15
|
-
type Applies,
|
|
16
|
-
type Budget,
|
|
17
|
-
type Budgets,
|
|
18
|
-
type LimitKey,
|
|
19
|
-
type Size,
|
|
20
|
-
type SizeRule,
|
|
21
|
-
} from "./size-rules.ts";
|
|
22
|
-
|
|
23
|
-
export type Held = { readonly path: string; readonly from: string };
|
|
24
|
-
|
|
25
|
-
export type Site = {
|
|
26
|
-
readonly file: string;
|
|
27
|
-
readonly line: number | undefined;
|
|
28
|
-
readonly rule: SizeRule["rule"];
|
|
29
|
-
readonly overrun: number;
|
|
30
|
-
readonly message: string;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export type Growth = {
|
|
34
|
-
readonly file: string;
|
|
35
|
-
readonly rule: SizeRule["rule"];
|
|
36
|
-
readonly base: number;
|
|
37
|
-
readonly head: number;
|
|
38
|
-
readonly sites: readonly Site[];
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export type Verdict =
|
|
42
|
-
| { readonly applies: "all"; readonly held: number; readonly overruns: readonly Site[]; readonly advisory: readonly Site[] }
|
|
43
|
-
| { readonly applies: "ratchet"; readonly held: number; readonly growths: readonly Growth[]; readonly advisory: readonly Site[] };
|
|
44
|
-
|
|
45
|
-
class OxlintUnreadable extends Schema.TaggedError<OxlintUnreadable>()("OxlintUnreadable", {
|
|
46
|
-
message: Schema.String,
|
|
47
|
-
}) {}
|
|
48
|
-
|
|
49
|
-
const NAME = "size-budget";
|
|
50
|
-
const USAGE = "usage: size-budget.ts <ref> | <base-ref> <head-ref>";
|
|
51
|
-
const DECLARATIONS = ":(exclude,glob)**/*.d.ts";
|
|
52
|
-
const TYPESCRIPT = [":(glob)**/*.ts", ":(glob)**/*.tsx", DECLARATIONS];
|
|
53
|
-
const TESTS = [`:(glob)${TESTS_DIRECTORY}/**/*.ts`, `:(glob)${TESTS_DIRECTORY}/**/*.tsx`];
|
|
54
|
-
const CONFIG = ".size-budget.oxlintrc.json";
|
|
55
|
-
const WHOLE_FILE: LimitKey = "fileLines";
|
|
56
|
-
const OXLINT_FOUND_NOTHING = 0;
|
|
57
|
-
const OXLINT_FOUND_ERRORS = 1;
|
|
58
|
-
const SCOPE = {
|
|
59
|
-
ratchet: "the production and test files the range adds or changes",
|
|
60
|
-
all: "every production and test file",
|
|
61
|
-
} satisfies Record<Applies, string>;
|
|
62
|
-
|
|
63
|
-
const Diagnostic = Schema.Struct({
|
|
64
|
-
code: Schema.String,
|
|
65
|
-
message: Schema.String,
|
|
66
|
-
filename: Schema.String,
|
|
67
|
-
labels: Schema.Array(Schema.Struct({ span: Schema.Struct({ line: Schema.Int }) })),
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const decodeReport = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Struct({ diagnostics: Schema.Array(Diagnostic) })));
|
|
71
|
-
|
|
72
|
-
type Rules = Readonly<Record<string, unknown>>;
|
|
73
|
-
|
|
74
|
-
export type SizeConfig = {
|
|
75
|
-
readonly plugins: readonly string[];
|
|
76
|
-
readonly jsPlugins: readonly string[];
|
|
77
|
-
readonly categories: Readonly<Record<string, string>>;
|
|
78
|
-
readonly rules: Rules;
|
|
79
|
-
readonly overrides: readonly { readonly files: readonly string[]; readonly rules: Rules }[];
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
function rulesOf(budget: Budget): Rules {
|
|
83
|
-
return Object.fromEntries(
|
|
84
|
-
SIZE_RULES.map((entry) => {
|
|
85
|
-
const max = budget[entry.key];
|
|
86
|
-
return [qualifiedName(entry), max === undefined ? "off" : ["error", { max, ...entry.options }]];
|
|
87
|
-
}),
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function sizeConfig({ production, tests }: Budgets, plugin: string): SizeConfig {
|
|
92
|
-
return {
|
|
93
|
-
plugins: [],
|
|
94
|
-
jsPlugins: [plugin],
|
|
95
|
-
categories: { correctness: "off" },
|
|
96
|
-
rules: rulesOf(production),
|
|
97
|
-
overrides: [{ files: [`${TESTS_DIRECTORY}/**`], rules: rulesOf(tests) }],
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function heldByChange(changes: readonly Change[]): readonly Held[] {
|
|
102
|
-
return changes.flatMap((change) => {
|
|
103
|
-
if (change.kind === "written") return [{ path: change.path, from: change.path }];
|
|
104
|
-
if (change.kind === "renamed" && change.edited) return [{ path: change.path, from: change.from }];
|
|
105
|
-
return [];
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// oxlint reads files from disk, and the working tree need not hold the head: a merge checkout or an uncommitted edit.
|
|
110
|
-
const materializeHead = Effect.fn("materializeHead")(function* (root: string, head: string, files: readonly string[], tree: string) {
|
|
111
|
-
const env = { GIT_INDEX_FILE: `${tree}.index` };
|
|
112
|
-
yield* git(["read-tree", head], root, { env });
|
|
113
|
-
yield* git(["checkout-index", "-z", "--stdin", `--prefix=${tree}/`], root, { env, input: files.map((file) => `${file}\0`).join("") });
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// Each file is written at its head path, so a rename is measured against the budget it is held to now.
|
|
117
|
-
const materializeBase = Effect.fn("materializeBase")(function* (root: string, base: string, held: readonly Held[], tree: string) {
|
|
118
|
-
const env = { GIT_INDEX_FILE: `${tree}.index` };
|
|
119
|
-
const found = (yield* git(["cat-file", "--batch-check=%(objecttype) %(objectname)"], root, {
|
|
120
|
-
input: held.map(({ from }) => `${base}:${from}\n`).join(""),
|
|
121
|
-
})).split("\n");
|
|
122
|
-
const entries = held.flatMap(({ path }, index) => {
|
|
123
|
-
const [type, blob] = (found[index] ?? "").split(" ");
|
|
124
|
-
return type === "blob" ? [`100644 ${blob}\t${path}\0`] : [];
|
|
125
|
-
});
|
|
126
|
-
if (entries.length === 0) return;
|
|
127
|
-
yield* git(["update-index", "-z", "--index-info"], root, { env, input: entries.join("") });
|
|
128
|
-
yield* git(["checkout-index", "--all", `--prefix=${tree}/`], root, { env });
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
export const siteOf = (
|
|
132
|
-
budgets: Budgets,
|
|
133
|
-
): ((diagnostic: typeof Diagnostic.Type) => Effect.Effect<readonly Site[], OxlintUnreadable>) =>
|
|
134
|
-
Effect.fn("siteOf")(function* ({ code, message, filename, labels }: typeof Diagnostic.Type) {
|
|
135
|
-
const rule = SIZE_RULES.find((candidate) => code === diagnosticCode(candidate));
|
|
136
|
-
if (rule === undefined) return [];
|
|
137
|
-
const measured = rule.measured.exec(message)?.[1];
|
|
138
|
-
const max = budgetOf(budgets, filename)[rule.key];
|
|
139
|
-
if (measured === undefined || max === undefined) {
|
|
140
|
-
return yield* new OxlintUnreadable({ message: `cannot read ${rule.rule} for ${filename} from oxlint: ${message}` });
|
|
141
|
-
}
|
|
142
|
-
const wholeFile = rule.key === WHOLE_FILE;
|
|
143
|
-
return [
|
|
144
|
-
{
|
|
145
|
-
file: filename,
|
|
146
|
-
line: wholeFile ? undefined : labels[0]?.span.line,
|
|
147
|
-
rule: rule.rule,
|
|
148
|
-
overrun: Number(measured) - max,
|
|
149
|
-
message: wholeFile ? `${message} Maximum allowed is ${max}.` : message,
|
|
150
|
-
} satisfies Site,
|
|
151
|
-
];
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
const measure = Effect.fn("measure")(function* (tree: string, budgets: Budgets, plugin: string) {
|
|
155
|
-
const fs = yield* FileSystem.FileSystem;
|
|
156
|
-
if (!(yield* fs.exists(tree))) return [];
|
|
157
|
-
// oxlint reads an override's glob from the directory of the config that holds it, so the config sits in the tree.
|
|
158
|
-
yield* fs.writeFileString((yield* Path.Path).join(tree, CONFIG), renderJson(sizeConfig(budgets, plugin)));
|
|
159
|
-
|
|
160
|
-
const { stdout, stderr, exitCode } = yield* collect("oxlint", ["-c", CONFIG, "-f", "json", "."], tree).pipe(
|
|
161
|
-
Effect.mapError((cause) => new OxlintUnreadable({ message: `cannot run oxlint: ${cause.message}` })),
|
|
162
|
-
);
|
|
163
|
-
if (exitCode !== OXLINT_FOUND_NOTHING && exitCode !== OXLINT_FOUND_ERRORS) {
|
|
164
|
-
return yield* new OxlintUnreadable({ message: `oxlint exited ${exitCode}: ${stderr.trim() || stdout.trim()}` });
|
|
165
|
-
}
|
|
166
|
-
const { diagnostics } = yield* decodeReport(stdout).pipe(
|
|
167
|
-
Effect.mapError((cause) => new OxlintUnreadable({ message: `cannot read oxlint's report: ${cause.message}` })),
|
|
168
|
-
);
|
|
169
|
-
const sites = (yield* Effect.forEach(diagnostics, siteOf(budgets))).flat();
|
|
170
|
-
return sites.toSorted((a, b) => a.file.localeCompare(b.file) || (a.line ?? 0) - (b.line ?? 0));
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
function byFileAndRule(sites: readonly Site[]): ReadonlyMap<string, readonly Site[]> {
|
|
174
|
-
return Map.groupBy(sites, ({ file, rule }) => `${file}\0${rule}`);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function total(sites: readonly Site[]): number {
|
|
178
|
-
return sites.reduce((sum, { overrun }) => sum + overrun, 0);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Sums each rule per file separately, so a shrink in one rule or file never offsets a growth in another.
|
|
182
|
-
export function growthsOf(head: readonly Site[], base: readonly Site[]): readonly Growth[] {
|
|
183
|
-
const before = byFileAndRule(base);
|
|
184
|
-
return [...byFileAndRule(head).entries()].flatMap(([group, sites]) => {
|
|
185
|
-
const [first] = sites;
|
|
186
|
-
const growth = { base: total(before.get(group) ?? []), head: total(sites), sites };
|
|
187
|
-
return first !== undefined && growth.head > growth.base ? [{ file: first.file, rule: first.rule, ...growth }] : [];
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Decides the verdict from sites oxlint already measured: which are held to the range or the
|
|
192
|
-
// whole tree, and, under ratchet, which grew past what the base measured for the same file and rule.
|
|
193
|
-
export function verdictOf(
|
|
194
|
-
applies: Applies,
|
|
195
|
-
held: number,
|
|
196
|
-
holds: ReadonlySet<string>,
|
|
197
|
-
sites: readonly Site[],
|
|
198
|
-
baseSites: readonly Site[],
|
|
199
|
-
): Verdict {
|
|
200
|
-
const heldSites = sites.filter((site) => holds.has(site.file));
|
|
201
|
-
if (applies !== "ratchet") {
|
|
202
|
-
return { applies, held, overruns: heldSites, advisory: sites.filter((site) => !holds.has(site.file)) };
|
|
203
|
-
}
|
|
204
|
-
const growths = growthsOf(heldSites, baseSites);
|
|
205
|
-
const failing = new Set(growths.flatMap((growth) => growth.sites));
|
|
206
|
-
return { applies, held, growths, advisory: sites.filter((site) => !failing.has(site)) };
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const heldFiles = Effect.fn("heldFiles")(function* (root: string, applies: Applies, pathspecs: readonly string[], base: string, head: string) {
|
|
210
|
-
if (applies === "all") return (yield* pathsAt(head, pathspecs, root)).map((path): Held => ({ path, from: path }));
|
|
211
|
-
return heldByChange(yield* changedPaths(base, head, pathspecs, root));
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
const runBudget = Effect.fn("runBudget")(
|
|
215
|
-
function* (root: string, size: Size, production: readonly string[], base: string, head: string) {
|
|
216
|
-
const fs = yield* FileSystem.FileSystem;
|
|
217
|
-
const path = yield* Path.Path;
|
|
218
|
-
const applies = size.applies ?? SIZE_DEFAULTS.applies;
|
|
219
|
-
const budgets = budgetsOf(size);
|
|
220
|
-
const pathspecs = [...production.map((glob) => `:(glob)${glob}`), ...TESTS, DECLARATIONS];
|
|
221
|
-
const held = yield* heldFiles(root, applies, pathspecs, base, head);
|
|
222
|
-
const holds = new Set(held.map((file) => file.path));
|
|
223
|
-
const others = (yield* pathsAt(head, TYPESCRIPT, root)).filter((file) => !holds.has(file));
|
|
224
|
-
const scratch = yield* fs.makeTempDirectoryScoped({ prefix: "checks-size-budget-" });
|
|
225
|
-
|
|
226
|
-
const headTree = path.join(scratch, "head");
|
|
227
|
-
if (held.length + others.length > 0) yield* materializeHead(root, head, [...holds, ...others], headTree);
|
|
228
|
-
const plugin = path.join(import.meta.dir, "..", "dist", "index.js");
|
|
229
|
-
const sites = yield* measure(headTree, budgets, plugin);
|
|
230
|
-
if (applies !== "ratchet") return verdictOf(applies, held.length, holds, sites, []);
|
|
231
|
-
|
|
232
|
-
const baseTree = path.join(scratch, "base");
|
|
233
|
-
if (held.length > 0) yield* materializeBase(root, base, held, baseTree);
|
|
234
|
-
return verdictOf(applies, held.length, holds, sites, yield* measure(baseTree, budgets, plugin));
|
|
235
|
-
},
|
|
236
|
-
Effect.scoped,
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
export function describe({ file, line, message }: Site, indent = " "): string {
|
|
240
|
-
return `${indent}${file}${line === undefined ? "" : `:${line}`}: ${message}`;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
export function verdictLines(verdict: Verdict): readonly string[] {
|
|
244
|
-
const scope = SCOPE[verdict.applies];
|
|
245
|
-
if (verdict.applies === "ratchet") {
|
|
246
|
-
if (verdict.growths.length === 0) return [`${NAME}: ${verdict.held} file(s), ${scope}, raise no overrun past the base`];
|
|
247
|
-
return [
|
|
248
|
-
`${NAME}: ${verdict.growths.length} overrun(s) grew past the base in ${scope}:`,
|
|
249
|
-
...verdict.growths.flatMap(({ file, rule, base, head, sites }) => [
|
|
250
|
-
` ${file}: ${rule} over by ${head} in total, up from ${base}`,
|
|
251
|
-
...sites.map((site) => describe(site, " ")),
|
|
252
|
-
]),
|
|
253
|
-
];
|
|
254
|
-
}
|
|
255
|
-
if (verdict.overruns.length === 0) return [`${NAME}: ${verdict.held} file(s), ${scope}, keep within the budget`];
|
|
256
|
-
return [`${NAME}: ${verdict.overruns.length} overrun(s) of the budget in ${scope}:`, ...verdict.overruns.map((site) => describe(site))];
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export function report(verdict: Verdict): string {
|
|
260
|
-
const { advisory } = verdict;
|
|
261
|
-
const notice =
|
|
262
|
-
advisory.length === 0
|
|
263
|
-
? []
|
|
264
|
-
: [`${NAME}: advisory, ${advisory.length} overrun(s) where the budget does not hold yet:`, ...advisory.map((site) => describe(site))];
|
|
265
|
-
return [...verdictLines(verdict), ...notice].join("\n");
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
export function passes(verdict: Verdict): boolean {
|
|
269
|
-
return verdict.applies === "ratchet" ? verdict.growths.length === 0 : verdict.overruns.length === 0;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const budget = Effect.gen(function* () {
|
|
273
|
-
const { refs, root, source, quality } = yield* rangeGateInputs(USAGE);
|
|
274
|
-
if (quality.size === undefined) {
|
|
275
|
-
yield* Console.log(`${NAME}: ${source} declares no size budget`);
|
|
276
|
-
return true;
|
|
277
|
-
}
|
|
278
|
-
const { base, head } = yield* rangeEnds(refs.first, refs.second, root);
|
|
279
|
-
const verdict = yield* runBudget(root, quality.size, quality.sources?.production ?? [], base, head);
|
|
280
|
-
|
|
281
|
-
yield* Console.log(report(verdict));
|
|
282
|
-
return passes(verdict);
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
if (import.meta.main) runMain(NAME, budget);
|
package/scripts/size-rules.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
2
|
-
|
|
3
|
-
export const TESTS_DIRECTORY = "tests";
|
|
4
|
-
|
|
5
|
-
const COUNTED = { skipBlankLines: false, skipComments: false };
|
|
6
|
-
|
|
7
|
-
const FILE_LINES = {
|
|
8
|
-
key: "fileLines",
|
|
9
|
-
rule: "max-lines",
|
|
10
|
-
options: COUNTED,
|
|
11
|
-
measured: /has too many lines \((\d+)\)/,
|
|
12
|
-
limits: "The most lines a file may hold, blank and comment lines counted",
|
|
13
|
-
} as const;
|
|
14
|
-
|
|
15
|
-
const FUNCTION_LINES = {
|
|
16
|
-
key: "functionLines",
|
|
17
|
-
rule: "max-lines-per-function",
|
|
18
|
-
options: COUNTED,
|
|
19
|
-
measured: /has too many lines \((\d+)\)/,
|
|
20
|
-
limits: "The most lines a function may span, blank and comment lines counted",
|
|
21
|
-
} as const;
|
|
22
|
-
|
|
23
|
-
const STATEMENTS = {
|
|
24
|
-
key: "statements",
|
|
25
|
-
rule: "max-statements",
|
|
26
|
-
options: {},
|
|
27
|
-
measured: /has too many statements \((\d+)\)/,
|
|
28
|
-
limits: "The most statements a function may hold",
|
|
29
|
-
} as const;
|
|
30
|
-
|
|
31
|
-
const COMPLEXITY = {
|
|
32
|
-
key: "complexity",
|
|
33
|
-
rule: "cognitive-complexity",
|
|
34
|
-
plugin: "effect-channel",
|
|
35
|
-
options: {},
|
|
36
|
-
measured: /has a cognitive complexity of (\d+)/,
|
|
37
|
-
limits: "The highest cognitive complexity a function may reach, a switch counted once",
|
|
38
|
-
} as const;
|
|
39
|
-
|
|
40
|
-
const DEPTH = {
|
|
41
|
-
key: "depth",
|
|
42
|
-
rule: "max-depth",
|
|
43
|
-
options: {},
|
|
44
|
-
measured: /nested too deeply \((\d+)\)/,
|
|
45
|
-
limits: "The deepest a block may nest inside a function",
|
|
46
|
-
} as const;
|
|
47
|
-
|
|
48
|
-
export const SIZE_RULES = [FILE_LINES, FUNCTION_LINES, STATEMENTS, COMPLEXITY, DEPTH] as const;
|
|
49
|
-
|
|
50
|
-
export type SizeRule = (typeof SIZE_RULES)[number];
|
|
51
|
-
export type LimitKey = SizeRule["key"];
|
|
52
|
-
|
|
53
|
-
export function qualifiedName(rule: SizeRule): string {
|
|
54
|
-
return "plugin" in rule ? `${rule.plugin}/${rule.rule}` : rule.rule;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function diagnosticCode(rule: SizeRule): string {
|
|
58
|
-
return "plugin" in rule ? `${rule.plugin}(${rule.rule})` : `eslint(${rule.rule})`;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// An absent limit turns its rule off.
|
|
62
|
-
export type Budget = { readonly [K in LimitKey]?: number };
|
|
63
|
-
|
|
64
|
-
export type Budgets = { readonly production: Budget; readonly tests: Budget };
|
|
65
|
-
|
|
66
|
-
const APPLIES = ["ratchet", "all"] as const;
|
|
67
|
-
export type Applies = (typeof APPLIES)[number];
|
|
68
|
-
|
|
69
|
-
export const SIZE_DEFAULTS = {
|
|
70
|
-
applies: "ratchet",
|
|
71
|
-
production: { fileLines: 400, functionLines: 100, statements: 30, complexity: 15, depth: 4 },
|
|
72
|
-
tests: { fileLines: 600, statements: 50, complexity: 15, depth: 4 },
|
|
73
|
-
} as const satisfies Budgets & { readonly applies: Applies };
|
|
74
|
-
|
|
75
|
-
const Limit = Schema.Int.check(Schema.isGreaterThan(0));
|
|
76
|
-
|
|
77
|
-
function limit({ limits }: SizeRule, fallback: number) {
|
|
78
|
-
return Schema.optionalKey(Limit.annotate({ description: `${limits}; ${fallback} when absent` }));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const ProductionBudget = Schema.Struct({
|
|
82
|
-
fileLines: limit(FILE_LINES, SIZE_DEFAULTS.production.fileLines),
|
|
83
|
-
functionLines: limit(FUNCTION_LINES, SIZE_DEFAULTS.production.functionLines),
|
|
84
|
-
statements: limit(STATEMENTS, SIZE_DEFAULTS.production.statements),
|
|
85
|
-
complexity: limit(COMPLEXITY, SIZE_DEFAULTS.production.complexity),
|
|
86
|
-
depth: limit(DEPTH, SIZE_DEFAULTS.production.depth),
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
const TestBudget = Schema.Struct({
|
|
90
|
-
fileLines: limit(FILE_LINES, SIZE_DEFAULTS.tests.fileLines),
|
|
91
|
-
statements: limit(STATEMENTS, SIZE_DEFAULTS.tests.statements),
|
|
92
|
-
complexity: limit(COMPLEXITY, SIZE_DEFAULTS.tests.complexity),
|
|
93
|
-
depth: limit(DEPTH, SIZE_DEFAULTS.tests.depth),
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
export const Size = Schema.Struct({
|
|
97
|
-
applies: Schema.optionalKey(
|
|
98
|
-
Schema.Literals(APPLIES).annotate({
|
|
99
|
-
description: `Which production and test files the budget holds: ratchet, the ones a range adds or changes, to no more overrun per rule than at the range's base; all, every one. ${SIZE_DEFAULTS.applies} when absent. The rest are listed as advisory`,
|
|
100
|
-
message: "Expected ratchet or all, and ratchet replaces changed",
|
|
101
|
-
}),
|
|
102
|
-
),
|
|
103
|
-
production: Schema.optionalKey(
|
|
104
|
-
ProductionBudget.annotate({
|
|
105
|
-
description: `The budget of the files under sources.production, and of the files listed as advisory outside ${TESTS_DIRECTORY}/`,
|
|
106
|
-
}),
|
|
107
|
-
),
|
|
108
|
-
tests: Schema.optionalKey(
|
|
109
|
-
TestBudget.annotate({ description: `The budget of the files under ${TESTS_DIRECTORY}/, which sets no limit on a function's lines` }),
|
|
110
|
-
),
|
|
111
|
-
}).annotate({
|
|
112
|
-
description: "The size budget oxlint holds production and test files to, read by checks-size-budget",
|
|
113
|
-
messageUnexpectedKey: "Expected only applies, production and tests, since each limit is set inside production or tests",
|
|
114
|
-
});
|
|
115
|
-
export type Size = typeof Size.Type;
|
|
116
|
-
|
|
117
|
-
export function budgetsOf(size: Size): Budgets {
|
|
118
|
-
return {
|
|
119
|
-
production: { ...SIZE_DEFAULTS.production, ...size.production },
|
|
120
|
-
tests: { ...SIZE_DEFAULTS.tests, ...size.tests },
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function budgetOf({ production, tests }: Budgets, file: string): Budget {
|
|
125
|
-
return file.startsWith(`${TESTS_DIRECTORY}/`) ? tests : production;
|
|
126
|
-
}
|