@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.
@@ -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
+ }
@@ -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, Usage } from "./main.ts";
5
- import { readQuality, renderJson } from "./quality-file.ts";
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 [first, second, ...extra] = process.argv.slice(2);
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, Usage } from "./main.ts";
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 rises: Rise[] = [];
54
- let counted = 0;
55
- for (const [file, rules] of head) {
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 [first, second, ...extra] = process.argv.slice(2);
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
 
@@ -147,45 +147,47 @@ function importedNames(node: Record<string, unknown>): readonly string[] {
147
147
  return names;
148
148
  }
149
149
 
150
- function outOfProcessUse(node: Record<string, unknown>): string | undefined {
151
- const type = node["type"];
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
- if (type === "ImportDeclaration" || type === "ExportNamedDeclaration" || type === "ExportAllDeclaration") {
154
- const source = stringValue(node["source"]);
155
- if (source === undefined) return undefined;
156
- const banned = bannedModule(source);
157
- if (banned !== undefined) return `imports node:${banned}`;
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
- if (type === "MemberExpression") {
166
- if (identifierName(node["object"]) !== "Bun") return undefined;
167
- const property = identifierName(node["property"]);
168
- if (property !== undefined && BANNED_BUN_NAMES.includes(property)) return `uses Bun.${property}`;
169
- return undefined;
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
- if (type === "CallExpression") {
173
- const callee = node["callee"];
174
- const calleeName = identifierName(callee);
175
- if (calleeName !== undefined && BANNED_GLOBAL_CALLS.includes(calleeName)) {
176
- return `calls ${calleeName}`;
177
- }
178
- const argument = Array.isArray(node["arguments"]) ? node["arguments"][0] : undefined;
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
- return undefined;
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) {
@@ -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
- const [, closing, tag, attributes, selfClosing] = match;
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`);