@avi2dg/checks 0.17.0 → 0.18.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 +12 -0
- package/docs/configs/commit-messages.md +2 -18
- package/docs/configs/quality-file.md +2 -2
- package/docs/gates/checks-quality.md +23 -8
- package/package.json +3 -1
- package/scripts/backtest.ts +57 -41
- package/scripts/ci-wiring.ts +2 -63
- package/scripts/comment-gate.ts +3 -4
- package/scripts/comment-matchers.ts +54 -57
- package/scripts/commit-identity.ts +3 -4
- package/scripts/doc-outline.ts +34 -16
- package/scripts/doc-rules.ts +34 -21
- package/scripts/docs.ts +3 -4
- package/scripts/feature-owners.ts +5 -8
- package/scripts/git.ts +31 -26
- package/scripts/prose-matchers.ts +93 -83
- package/scripts/quality.ts +157 -10
- package/scripts/range-gate.ts +9 -0
- package/scripts/shell-command.ts +74 -0
- package/scripts/size-budget.ts +5 -8
- package/scripts/suppressions-ratchet.ts +22 -24
- package/scripts/test-layout.ts +37 -35
- package/scripts/test-report.ts +18 -13
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export type Command = readonly string[];
|
|
2
|
+
|
|
3
|
+
type Span = {
|
|
4
|
+
readonly text: string;
|
|
5
|
+
readonly end: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const VARIABLE = /^\$(?:[A-Za-z_][A-Za-z0-9_]*|\{[A-Za-z_][A-Za-z0-9_]*\})/;
|
|
9
|
+
const UNPLAIN = new Set(["|", "&", ";", "<", ">", "(", ")", "`", "\\", "#", "\n"]);
|
|
10
|
+
|
|
11
|
+
function variable(line: string, from: number): Span | undefined {
|
|
12
|
+
const match = VARIABLE.exec(line.slice(from));
|
|
13
|
+
return match === null ? undefined : { text: match[0], end: from + match[0].length };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function singleQuoted(line: string, open: number): Span | undefined {
|
|
17
|
+
const close = line.indexOf("'", open + 1);
|
|
18
|
+
return close === -1 ? undefined : { text: line.slice(open + 1, close), end: close + 1 };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function doubleQuoted(line: string, open: number): Span | undefined {
|
|
22
|
+
let text = "";
|
|
23
|
+
let index = open + 1;
|
|
24
|
+
while (line.charAt(index) !== '"') {
|
|
25
|
+
const inner = line.charAt(index);
|
|
26
|
+
if (inner === "" || inner === "\\" || inner === "`") return undefined;
|
|
27
|
+
const part = inner === "$" ? variable(line, index) : { text: inner, end: index + 1 };
|
|
28
|
+
if (part === undefined) return undefined;
|
|
29
|
+
text += part.text;
|
|
30
|
+
index = part.end;
|
|
31
|
+
}
|
|
32
|
+
return { text, end: index + 1 };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function wordPart(line: string, index: number): Span | undefined {
|
|
36
|
+
const char = line.charAt(index);
|
|
37
|
+
if (char === "'") return singleQuoted(line, index);
|
|
38
|
+
if (char === '"') return doubleQuoted(line, index);
|
|
39
|
+
if (char === "$") return variable(line, index);
|
|
40
|
+
return UNPLAIN.has(char) ? undefined : { text: char, end: index + 1 };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// A script counts only when it is one line of plain words: any shell control, redirection or
|
|
44
|
+
// substitution can run the gate without its failure failing the step.
|
|
45
|
+
export function plainCommand(script: string): Command | undefined {
|
|
46
|
+
const line = script.trim();
|
|
47
|
+
const words: string[] = [];
|
|
48
|
+
let word: string | undefined;
|
|
49
|
+
let index = 0;
|
|
50
|
+
while (index < line.length) {
|
|
51
|
+
const char = line.charAt(index);
|
|
52
|
+
if (char === " " || char === "\t") {
|
|
53
|
+
if (word !== undefined) words.push(word);
|
|
54
|
+
word = undefined;
|
|
55
|
+
index += 1;
|
|
56
|
+
} else {
|
|
57
|
+
const part = wordPart(line, index);
|
|
58
|
+
if (part === undefined) return undefined;
|
|
59
|
+
word = (word ?? "") + part.text;
|
|
60
|
+
index = part.end;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (word !== undefined) words.push(word);
|
|
64
|
+
return words.length === 0 ? undefined : words;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function invokes(command: Command | undefined, gate: Command): boolean {
|
|
68
|
+
return command !== undefined && gate.length <= command.length && gate.every((word, index) => command[index] === word);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function mentions(script: string, gate: Command): boolean {
|
|
72
|
+
const tokens = script.split(/[\s|&;<>()`]+/);
|
|
73
|
+
return tokens.some((_, start) => invokes(tokens.slice(start), gate));
|
|
74
|
+
}
|
package/scripts/size-budget.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Console, Effect, FileSystem, Path, Schema } from "effect";
|
|
3
3
|
import { changedPaths, collect, git, pathsAt, rangeEnds, type Change } from "./git.ts";
|
|
4
|
-
import { runMain
|
|
5
|
-
import {
|
|
4
|
+
import { runMain } from "./main.ts";
|
|
5
|
+
import { renderJson } from "./quality-file.ts";
|
|
6
|
+
import { rangeGateInputs } from "./range-gate.ts";
|
|
6
7
|
import {
|
|
7
8
|
budgetOf,
|
|
8
9
|
budgetsOf,
|
|
@@ -243,16 +244,12 @@ function passes(verdict: Verdict): boolean {
|
|
|
243
244
|
}
|
|
244
245
|
|
|
245
246
|
const budget = Effect.gen(function* () {
|
|
246
|
-
const
|
|
247
|
-
if (first === undefined || extra.length > 0) return yield* new Usage({ message: USAGE });
|
|
248
|
-
|
|
249
|
-
const root = (yield* git(["rev-parse", "--show-toplevel"])).trim();
|
|
250
|
-
const { source, quality } = yield* readQuality(root);
|
|
247
|
+
const { refs, root, source, quality } = yield* rangeGateInputs(USAGE);
|
|
251
248
|
if (quality.size === undefined) {
|
|
252
249
|
yield* Console.log(`${NAME}: ${source} declares no size budget`);
|
|
253
250
|
return true;
|
|
254
251
|
}
|
|
255
|
-
const { base, head } = yield* rangeEnds(first, second, root);
|
|
252
|
+
const { base, head } = yield* rangeEnds(refs.first, refs.second, root);
|
|
256
253
|
const verdict = yield* runBudget(root, quality.size, quality.sources?.production ?? [], base, head);
|
|
257
254
|
|
|
258
255
|
yield* Console.log(report(verdict));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Console, Effect, Schema } from "effect";
|
|
3
|
-
import { git, parentOrEmptyTree } from "./git.ts";
|
|
4
|
-
import { runMain
|
|
3
|
+
import { git, parentOrEmptyTree, refArgs } from "./git.ts";
|
|
4
|
+
import { runMain } from "./main.ts";
|
|
5
5
|
|
|
6
6
|
export const SUPPRESSIONS = "oxlint-suppressions.json";
|
|
7
7
|
|
|
@@ -49,28 +49,27 @@ export const parseSuppressions = Effect.fn("parseSuppressions")(function* (
|
|
|
49
49
|
return suppressions;
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
+
type Count = {
|
|
53
|
+
readonly file: string;
|
|
54
|
+
readonly rule: string;
|
|
55
|
+
readonly count: number;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function countsOf(suppressions: Suppressions): readonly Count[] {
|
|
59
|
+
return [...suppressions].flatMap(([file, rules]) => [...rules].map(([rule, count]) => ({ file, rule, count })));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function riseOf({ file, rule, count }: Count, before: number | undefined): readonly Rise[] {
|
|
63
|
+
if (before === undefined) return count > 0 ? [{ kind: "appeared", file, rule, head: count }] : [];
|
|
64
|
+
return count > before ? [{ kind: "rose", file, rule, base: before, head: count }] : [];
|
|
65
|
+
}
|
|
66
|
+
|
|
52
67
|
export function compareSuppressions(base: Suppressions, head: Suppressions): Ratchet {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
for (const [rule, count] of rules) {
|
|
57
|
-
counted += 1;
|
|
58
|
-
const before = base.get(file)?.get(rule);
|
|
59
|
-
if (before === undefined) {
|
|
60
|
-
if (count > 0) rises.push({ kind: "appeared", file, rule, head: count });
|
|
61
|
-
} else if (count > before) {
|
|
62
|
-
rises.push({ kind: "rose", file, rule, base: before, head: count });
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
let lowered = 0;
|
|
67
|
-
for (const [file, rules] of base) {
|
|
68
|
-
for (const [rule, count] of rules) {
|
|
69
|
-
if ((head.get(file)?.get(rule) ?? 0) < count) lowered += 1;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
68
|
+
const counts = countsOf(head);
|
|
69
|
+
const rises = counts.flatMap((counted) => riseOf(counted, base.get(counted.file)?.get(counted.rule)));
|
|
70
|
+
const lowered = countsOf(base).filter(({ file, rule, count }) => (head.get(file)?.get(rule) ?? 0) < count).length;
|
|
72
71
|
rises.sort((a, b) => a.file.localeCompare(b.file) || a.rule.localeCompare(b.rule));
|
|
73
|
-
return { counted, lowered, rises };
|
|
72
|
+
return { counted: counts.length, lowered, rises };
|
|
74
73
|
}
|
|
75
74
|
|
|
76
75
|
function describeRise(rise: Rise): string {
|
|
@@ -118,8 +117,7 @@ export const runTip = Effect.fn("runTip")(function* (tip: string) {
|
|
|
118
117
|
});
|
|
119
118
|
|
|
120
119
|
const ratchet = Effect.gen(function* () {
|
|
121
|
-
const
|
|
122
|
-
if (first === undefined || extra.length > 0) return yield* new Usage({ message: USAGE });
|
|
120
|
+
const { first, second } = yield* refArgs(process.argv.slice(2), USAGE);
|
|
123
121
|
|
|
124
122
|
const result = second !== undefined ? yield* runRange(first, second) : yield* runTip(first);
|
|
125
123
|
|
package/scripts/test-layout.ts
CHANGED
|
@@ -147,45 +147,47 @@ function importedNames(node: Record<string, unknown>): readonly string[] {
|
|
|
147
147
|
return names;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
type UseCheck = (node: Record<string, unknown>) => string | undefined;
|
|
151
|
+
|
|
152
|
+
function moduleUse(node: Record<string, unknown>): string | undefined {
|
|
153
|
+
const source = stringValue(node["source"]);
|
|
154
|
+
if (source === undefined) return undefined;
|
|
155
|
+
const banned = bannedModule(source);
|
|
156
|
+
if (banned !== undefined) return `imports node:${banned}`;
|
|
157
|
+
if (source !== "bun") return undefined;
|
|
158
|
+
const name = importedNames(node).find((candidate) => BANNED_BUN_NAMES.includes(candidate));
|
|
159
|
+
return name === undefined ? undefined : `imports ${name} from bun`;
|
|
160
|
+
}
|
|
152
161
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (source === "bun") {
|
|
159
|
-
const name = importedNames(node).find((candidate) => BANNED_BUN_NAMES.includes(candidate));
|
|
160
|
-
if (name !== undefined) return `imports ${name} from bun`;
|
|
161
|
-
}
|
|
162
|
-
return undefined;
|
|
163
|
-
}
|
|
162
|
+
function bunMemberUse(node: Record<string, unknown>): string | undefined {
|
|
163
|
+
if (identifierName(node["object"]) !== "Bun") return undefined;
|
|
164
|
+
const property = identifierName(node["property"]);
|
|
165
|
+
return property !== undefined && BANNED_BUN_NAMES.includes(property) ? `uses Bun.${property}` : undefined;
|
|
166
|
+
}
|
|
164
167
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
function callUse(node: Record<string, unknown>): string | undefined {
|
|
169
|
+
const callee = node["callee"];
|
|
170
|
+
const calleeName = identifierName(callee);
|
|
171
|
+
if (calleeName !== undefined && BANNED_GLOBAL_CALLS.includes(calleeName)) return `calls ${calleeName}`;
|
|
172
|
+
const argument = Array.isArray(node["arguments"]) ? node["arguments"][0] : undefined;
|
|
173
|
+
const specifier = isRecord(argument) ? stringValue(argument["expression"]) : undefined;
|
|
174
|
+
const banned = specifier === undefined ? undefined : bannedModule(specifier);
|
|
175
|
+
if (banned === undefined) return undefined;
|
|
176
|
+
if (isRecord(callee) && callee["type"] === "Import") return `imports node:${banned}`;
|
|
177
|
+
return calleeName === "require" ? `requires node:${banned}` : undefined;
|
|
178
|
+
}
|
|
171
179
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const specifier = isRecord(argument) ? stringValue(argument["expression"]) : undefined;
|
|
180
|
-
if (specifier === undefined) return undefined;
|
|
181
|
-
const banned = bannedModule(specifier);
|
|
182
|
-
if (banned === undefined) return undefined;
|
|
183
|
-
if (isRecord(callee) && callee["type"] === "Import") return `imports node:${banned}`;
|
|
184
|
-
if (calleeName === "require") return `requires node:${banned}`;
|
|
185
|
-
return undefined;
|
|
186
|
-
}
|
|
180
|
+
const USE_CHECKS = new Map<string, UseCheck>([
|
|
181
|
+
["ImportDeclaration", moduleUse],
|
|
182
|
+
["ExportNamedDeclaration", moduleUse],
|
|
183
|
+
["ExportAllDeclaration", moduleUse],
|
|
184
|
+
["MemberExpression", bunMemberUse],
|
|
185
|
+
["CallExpression", callUse],
|
|
186
|
+
]);
|
|
187
187
|
|
|
188
|
-
|
|
188
|
+
function outOfProcessUse(node: Record<string, unknown>): string | undefined {
|
|
189
|
+
const type = node["type"];
|
|
190
|
+
return typeof type === "string" ? USE_CHECKS.get(type)?.(node) : undefined;
|
|
189
191
|
}
|
|
190
192
|
|
|
191
193
|
export const isolationViolations = Effect.fn("isolationViolations")(function* (file: string, source: string) {
|
package/scripts/test-report.ts
CHANGED
|
@@ -52,26 +52,31 @@ function attributesOf(source: string): Readonly<Record<string, string>> {
|
|
|
52
52
|
return attributes;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
const malformed = (reason: string) => new ReportError({ message: `the junit report is malformed: ${reason}` });
|
|
56
|
+
|
|
57
|
+
const takeTag = Effect.fnUntraced(function* (open: Element[], match: RegExpExecArray): Effect.fn.Return<void, ReportError> {
|
|
58
|
+
const [, closing, tag, attributes, selfClosing] = match;
|
|
59
|
+
if (tag === undefined) return;
|
|
60
|
+
const parent = open.at(-1);
|
|
61
|
+
if (parent === undefined) return yield* malformed(`<${tag}> after the root closed`);
|
|
62
|
+
if (closing === "/") {
|
|
63
|
+
if (parent.tag !== tag) return yield* malformed(`</${tag}> closes <${parent.tag}>`);
|
|
64
|
+
open.pop();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const element: Element = { tag, attributes: attributesOf(attributes ?? ""), children: [] };
|
|
68
|
+
parent.children.push(element);
|
|
69
|
+
if (selfClosing !== "/") open.push(element);
|
|
70
|
+
});
|
|
71
|
+
|
|
55
72
|
const parseElements = Effect.fnUntraced(function* (xml: string): Effect.fn.Return<Element, ReportError> {
|
|
56
|
-
const malformed = (reason: string) => new ReportError({ message: `the junit report is malformed: ${reason}` });
|
|
57
73
|
const document: Element = { tag: "", attributes: {}, children: [] };
|
|
58
74
|
const open: Element[] = [document];
|
|
59
75
|
let consumed = 0;
|
|
60
76
|
for (const match of xml.matchAll(MARKUP)) {
|
|
61
77
|
if (xml.slice(consumed, match.index).includes("<")) return yield* malformed(`stray < before offset ${match.index}`);
|
|
62
78
|
consumed = match.index + match[0].length;
|
|
63
|
-
|
|
64
|
-
if (tag === undefined) continue;
|
|
65
|
-
const parent = open.at(-1);
|
|
66
|
-
if (parent === undefined) return yield* malformed(`<${tag}> after the root closed`);
|
|
67
|
-
if (closing === "/") {
|
|
68
|
-
if (parent.tag !== tag) return yield* malformed(`</${tag}> closes <${parent.tag}>`);
|
|
69
|
-
open.pop();
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
const element: Element = { tag, attributes: attributesOf(attributes ?? ""), children: [] };
|
|
73
|
-
parent.children.push(element);
|
|
74
|
-
if (selfClosing !== "/") open.push(element);
|
|
79
|
+
yield* takeTag(open, match);
|
|
75
80
|
}
|
|
76
81
|
if (xml.slice(consumed).includes("<")) return yield* malformed("stray < after the last element");
|
|
77
82
|
if (open.length !== 1) return yield* malformed(`<${open.at(-1)?.tag}> never closes`);
|