@avi2dg/checks 0.56.0 → 0.57.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 +16 -0
- package/docs/bins.md +7 -5
- package/docs/configs/dependency-rules.md +3 -3
- package/docs/configs/native-settings.md +5 -5
- package/docs/design.md +4 -2
- package/docs/gates/checks-frontend-syntax.md +2 -2
- package/docs/gates/checks-graphwalk.md +126 -0
- package/docs/gates/checks-lint-coverage.md +2 -1
- package/docs/gates/checks-lint.md +10 -5
- package/docs/gates/checks-night-run.md +38 -9
- package/docs/gates/checks-no-plain-js.md +1 -1
- package/docs/gates/checks-quarantine-clock.md +4 -2
- package/docs/gates/checks-repetition.md +12 -7
- package/docs/gates/checks-suppressions-ratchet.md +5 -2
- package/package.json +4 -7
- package/src/complexity/graphwalk.ts +374 -0
- package/src/complexity/knip.ts +2 -17
- package/src/complexity/repetition.ts +51 -16
- package/src/complexity/suppressions-ratchet.ts +14 -5
- package/src/core/gates.ts +13 -8
- package/src/core/git.ts +3 -1
- package/src/core/lint.ts +54 -20
- package/src/core/main.ts +12 -8
- package/src/testing/night-run-findings.ts +20 -3
- package/src/testing/night-run.ts +13 -7
- package/src/testing/quarantine-clock.ts +10 -2
- package/docs/gates/checks-exports.md +0 -104
- package/docs/gates/checks-imports.md +0 -64
- package/docs/gates/checks-unused.md +0 -66
- package/src/complexity/exports.ts +0 -189
- package/src/complexity/unused.ts +0 -49
- package/src/dependencies/imports.ts +0 -94
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
import { Console, Effect, FileSystem, Path, Schema } from "effect";
|
|
3
|
-
import { TYPESCRIPT_SOURCE } from "../core/gates.ts";
|
|
4
|
-
import { checkoutFiles, commitOf, git, pathsAt, rangeEnds, refArgs } from "../core/git.ts";
|
|
5
|
-
import { runMain } from "../core/main.ts";
|
|
6
|
-
import { knipReport, scanTree } from "./knip.ts";
|
|
7
|
-
|
|
8
|
-
const UnusedSymbolSchema = Schema.Struct({
|
|
9
|
-
file: Schema.String,
|
|
10
|
-
kind: Schema.Literals(["export", "type"]),
|
|
11
|
-
name: Schema.String,
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
export type UnusedSymbol = typeof UnusedSymbolSchema.Type;
|
|
15
|
-
|
|
16
|
-
export type Baseline = readonly UnusedSymbol[];
|
|
17
|
-
|
|
18
|
-
export type Held = {
|
|
19
|
-
readonly reported: readonly UnusedSymbol[];
|
|
20
|
-
readonly base: Baseline;
|
|
21
|
-
readonly head: Baseline;
|
|
22
|
-
readonly unusedAtBase: readonly UnusedSymbol[];
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export type Drift = {
|
|
26
|
-
readonly unlisted: readonly UnusedSymbol[];
|
|
27
|
-
readonly added: readonly UnusedSymbol[];
|
|
28
|
-
readonly stale: readonly UnusedSymbol[];
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const NAME = "exports";
|
|
32
|
-
export const BASELINE_FILE = "exports-baseline.json";
|
|
33
|
-
const INCLUDED = ["--include", "exports,types"];
|
|
34
|
-
const WRITE = "--write";
|
|
35
|
-
const USAGE = `usage: exports.ts <ref> | <base-ref> <head-ref> | ${WRITE}`;
|
|
36
|
-
|
|
37
|
-
class ExportsError extends Schema.TaggedError<ExportsError>()("ExportsError", {
|
|
38
|
-
message: Schema.String,
|
|
39
|
-
}) {}
|
|
40
|
-
|
|
41
|
-
const KnipSymbol = Schema.Struct({ name: Schema.String });
|
|
42
|
-
|
|
43
|
-
const decodeKnipReport = Schema.decodeUnknownEffect(
|
|
44
|
-
Schema.fromJsonString(
|
|
45
|
-
Schema.Struct({
|
|
46
|
-
issues: Schema.Array(
|
|
47
|
-
Schema.Struct({
|
|
48
|
-
file: Schema.String,
|
|
49
|
-
exports: Schema.optional(Schema.Array(KnipSymbol)),
|
|
50
|
-
types: Schema.optional(Schema.Array(KnipSymbol)),
|
|
51
|
-
}),
|
|
52
|
-
),
|
|
53
|
-
}),
|
|
54
|
-
),
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
const decodeBaseline = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Array(UnusedSymbolSchema)));
|
|
58
|
-
|
|
59
|
-
function byPosition(a: UnusedSymbol, b: UnusedSymbol): number {
|
|
60
|
-
return a.file.localeCompare(b.file) || a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export const symbolsOf = Effect.fn("symbolsOf")(function* (stdout: string) {
|
|
64
|
-
const { issues } = yield* decodeKnipReport(stdout).pipe(
|
|
65
|
-
Effect.mapError((cause) => new ExportsError({ message: `knip's JSON report does not decode: ${cause.message}` })),
|
|
66
|
-
);
|
|
67
|
-
return issues
|
|
68
|
-
.flatMap(({ file, exports = [], types = [] }): readonly UnusedSymbol[] => [
|
|
69
|
-
...exports.map(({ name }): UnusedSymbol => ({ file, kind: "export", name })),
|
|
70
|
-
...types.map(({ name }): UnusedSymbol => ({ file, kind: "type", name })),
|
|
71
|
-
])
|
|
72
|
-
.toSorted(byPosition);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
export const baselineOf = Effect.fn("baselineOf")(function* (text: string, where: string) {
|
|
76
|
-
return yield* decodeBaseline(text).pipe(
|
|
77
|
-
Effect.mapError((cause) => new ExportsError({ message: `${where} does not decode: ${cause.message}` })),
|
|
78
|
-
);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const baselinePath = Effect.fn("baselinePath")(function* (root: string) {
|
|
82
|
-
return (yield* Path.Path).join(root, BASELINE_FILE);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const baselineInTree = Effect.fn("baselineInTree")(function* (root: string) {
|
|
86
|
-
const fs = yield* FileSystem.FileSystem;
|
|
87
|
-
const at = yield* baselinePath(root);
|
|
88
|
-
if (!(yield* fs.exists(at))) return [] satisfies Baseline;
|
|
89
|
-
return yield* fs.readFileString(at).pipe(Effect.flatMap((text) => baselineOf(text, BASELINE_FILE)));
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
const baselineAt = Effect.fn("baselineAt")(function* (treeish: string, root: string) {
|
|
93
|
-
const blob = (yield* git(["ls-tree", "--object-only", treeish, "--", BASELINE_FILE], root)).trim();
|
|
94
|
-
if (blob === "") return [] satisfies Baseline;
|
|
95
|
-
return yield* baselineOf(yield* git(["cat-file", "blob", blob], root), `${BASELINE_FILE} at ${treeish}`);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
const keyOf = ({ file, kind, name }: UnusedSymbol): string => `${kind}\0${file}\0${name}`;
|
|
99
|
-
|
|
100
|
-
function outside(symbols: readonly UnusedSymbol[], others: readonly UnusedSymbol[]): readonly UnusedSymbol[] {
|
|
101
|
-
const known = new Set(others.map(keyOf));
|
|
102
|
-
return symbols.filter((symbol) => !known.has(keyOf(symbol))).toSorted(byPosition);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function driftOf({ reported, base, head, unusedAtBase }: Held): Drift {
|
|
106
|
-
return {
|
|
107
|
-
unlisted: outside(reported, head),
|
|
108
|
-
added: outside(head, [...base, ...unusedAtBase]),
|
|
109
|
-
stale: outside(head, reported),
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function describe(symbol: UnusedSymbol): string {
|
|
114
|
-
return ` ${symbol.file}: ${symbol.name} (${symbol.kind})`;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export function report(head: Baseline, { unlisted, added, stale }: Drift): string {
|
|
118
|
-
const sections = [
|
|
119
|
-
{ symbols: unlisted, what: `unused export(s) not in ${BASELINE_FILE}:` },
|
|
120
|
-
{ symbols: added, what: `${BASELINE_FILE} export(s) the range adds that its base did not leave unused, remove the export instead:` },
|
|
121
|
-
{ symbols: stale, what: `${BASELINE_FILE} export(s) no longer reported, remove them:` },
|
|
122
|
-
].filter(({ symbols }) => symbols.length > 0);
|
|
123
|
-
if (sections.length > 0) {
|
|
124
|
-
return sections.flatMap(({ symbols, what }) => [`${NAME}: ${symbols.length} ${what}`, ...symbols.map(describe)]).join("\n");
|
|
125
|
-
}
|
|
126
|
-
return head.length === 0
|
|
127
|
-
? `${NAME}: no unused exports or types`
|
|
128
|
-
: `${NAME}: ${head.length} unused export(s) in ${BASELINE_FILE}, and no new ones`;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const scan = scanTree(NAME, INCLUDED, TYPESCRIPT_SOURCE).pipe(Effect.mapError((cause) => new ExportsError({ message: cause.message })));
|
|
132
|
-
|
|
133
|
-
// Knip loads the configuration's imports, such as the kit's knip builder, from a node_modules the checkout lacks.
|
|
134
|
-
const unusedAt = Effect.fn("unusedAt")(
|
|
135
|
-
function* (rev: string, root: string) {
|
|
136
|
-
const files = yield* pathsAt(rev, [], root);
|
|
137
|
-
if (files.length === 0) return [] satisfies readonly UnusedSymbol[];
|
|
138
|
-
const fs = yield* FileSystem.FileSystem;
|
|
139
|
-
const path = yield* Path.Path;
|
|
140
|
-
const tree = yield* checkoutFiles(rev, files, yield* fs.makeTempDirectoryScoped({ prefix: "checks-exports-" }), root);
|
|
141
|
-
const modules = path.join(root, "node_modules");
|
|
142
|
-
if (yield* fs.exists(modules)) yield* fs.symlink(modules, path.join(tree, "node_modules"));
|
|
143
|
-
const reported = yield* knipReport(tree, INCLUDED).pipe(
|
|
144
|
-
Effect.mapError((cause) => new ExportsError({ message: `at ${rev}: ${cause.message}` })),
|
|
145
|
-
);
|
|
146
|
-
return reported.kind === "unconfigured" ? [] : yield* symbolsOf(reported.stdout);
|
|
147
|
-
},
|
|
148
|
-
Effect.scoped,
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
// Knip scans the checked-out tree, and a pull request's merge checkout holds the base branch tip beside the range head.
|
|
152
|
-
const judgedBase = Effect.fn("judgedBase")(function* (first: string, second: string | undefined, root: string) {
|
|
153
|
-
const { base } = yield* rangeEnds(first, second, root);
|
|
154
|
-
if (second === undefined) return base;
|
|
155
|
-
const head = yield* commitOf(second, root);
|
|
156
|
-
const parents = (yield* git(["rev-parse", "HEAD^@"], root)).split("\n").filter((line) => line !== "");
|
|
157
|
-
const [other, ...rest] = parents.filter((parent) => parent !== head);
|
|
158
|
-
return parents.includes(head) && other !== undefined && rest.length === 0 ? other : base;
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
const check = Effect.fn("check")(function* (first: string, second: string | undefined) {
|
|
162
|
-
const { root, reported } = yield* scan;
|
|
163
|
-
if (reported.kind === "unconfigured") return false;
|
|
164
|
-
const baseRev = yield* judgedBase(first, second, root);
|
|
165
|
-
const base = yield* baselineAt(baseRev, root);
|
|
166
|
-
const head = yield* baselineInTree(root);
|
|
167
|
-
const unusedAtBase = outside(head, base).length === 0 ? [] : yield* unusedAt(baseRev, root);
|
|
168
|
-
const drift = driftOf({ reported: yield* symbolsOf(reported.stdout), base, head, unusedAtBase });
|
|
169
|
-
yield* Console.log(report(head, drift));
|
|
170
|
-
return drift.unlisted.length === 0 && drift.added.length === 0 && drift.stale.length === 0;
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
const seed = Effect.gen(function* () {
|
|
174
|
-
const { root, reported } = yield* scan;
|
|
175
|
-
if (reported.kind === "unconfigured") return false;
|
|
176
|
-
const symbols = yield* symbolsOf(reported.stdout);
|
|
177
|
-
yield* (yield* FileSystem.FileSystem).writeFileString(yield* baselinePath(root), `${JSON.stringify(symbols, null, 2)}\n`);
|
|
178
|
-
yield* Console.log(`${NAME}: wrote ${symbols.length} unused export(s) to ${BASELINE_FILE}`);
|
|
179
|
-
return true;
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
const gate = Effect.gen(function* () {
|
|
183
|
-
const args = process.argv.slice(2);
|
|
184
|
-
if (args.length === 1 && args[0] === WRITE) return yield* seed;
|
|
185
|
-
const { first, second } = yield* refArgs(args, USAGE);
|
|
186
|
-
return yield* check(first, second);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
if (import.meta.main) runMain(NAME, gate);
|
package/src/complexity/unused.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
import { Console, Effect, Schema } from "effect";
|
|
3
|
-
import { LINTED_SOURCE } from "../core/gates.ts";
|
|
4
|
-
import { runMain } from "../core/main.ts";
|
|
5
|
-
import { scanTree } from "./knip.ts";
|
|
6
|
-
|
|
7
|
-
export type Scan = { readonly tracked: number; readonly files: readonly string[] };
|
|
8
|
-
|
|
9
|
-
const NAME = "unused";
|
|
10
|
-
const JUDGED = /[.]tsx?$|[.]astro$/;
|
|
11
|
-
|
|
12
|
-
class UnusedError extends Schema.TaggedError<UnusedError>()("UnusedError", {
|
|
13
|
-
message: Schema.String,
|
|
14
|
-
}) {}
|
|
15
|
-
|
|
16
|
-
const decodeKnipReport = Schema.decodeUnknownEffect(
|
|
17
|
-
Schema.fromJsonString(
|
|
18
|
-
Schema.Struct({
|
|
19
|
-
issues: Schema.Array(Schema.Struct({ files: Schema.optional(Schema.Array(Schema.Struct({ name: Schema.String }))) })),
|
|
20
|
-
}),
|
|
21
|
-
),
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
export const filesOf = Effect.fn("filesOf")(function* (stdout: string) {
|
|
25
|
-
const { issues } = yield* decodeKnipReport(stdout).pipe(
|
|
26
|
-
Effect.mapError((cause) => new UnusedError({ message: `knip's JSON report does not decode: ${cause.message}` })),
|
|
27
|
-
);
|
|
28
|
-
return issues
|
|
29
|
-
.flatMap(({ files = [] }) => files.map(({ name }) => name))
|
|
30
|
-
.filter((file) => JUDGED.test(file))
|
|
31
|
-
.toSorted();
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
export function report({ tracked, files }: Scan): string {
|
|
35
|
-
if (files.length === 0) return `${NAME}: no unreferenced files among ${tracked} tracked .ts/.tsx/.astro file(s)`;
|
|
36
|
-
return [`${NAME}: ${files.length} unreferenced file(s):`, ...files.map((file) => ` ${file}`)].join("\n");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const unused = Effect.gen(function* () {
|
|
40
|
-
const { tracked, reported } = yield* scanTree(NAME, ["--files"], LINTED_SOURCE).pipe(
|
|
41
|
-
Effect.mapError((cause) => new UnusedError({ message: cause.message })),
|
|
42
|
-
);
|
|
43
|
-
if (reported.kind === "unconfigured") return false;
|
|
44
|
-
const files = yield* filesOf(reported.stdout);
|
|
45
|
-
yield* Console.log(report({ tracked, files }));
|
|
46
|
-
return files.length === 0;
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
if (import.meta.main) runMain(NAME, unused);
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
import { Console, Effect, FileSystem, Path, Schema } from "effect";
|
|
3
|
-
import { collect, git } from "../core/git.ts";
|
|
4
|
-
import { runMain, Usage } from "../core/main.ts";
|
|
5
|
-
|
|
6
|
-
export const PROJECT_CONFIG = "dependency-cruiser.config.ts";
|
|
7
|
-
|
|
8
|
-
export const OWN_CONFIGS = [
|
|
9
|
-
".dependency-cruiser.json",
|
|
10
|
-
".dependency-cruiser.js",
|
|
11
|
-
".dependency-cruiser.cjs",
|
|
12
|
-
".dependency-cruiser.mjs",
|
|
13
|
-
".dependency-cruiser.ts",
|
|
14
|
-
".dependency-cruiser.cts",
|
|
15
|
-
".dependency-cruiser.mts",
|
|
16
|
-
] as const;
|
|
17
|
-
|
|
18
|
-
export const CRUISED = ["*.ts", "*.tsx", "*.mts", "*.cts"] as const;
|
|
19
|
-
|
|
20
|
-
const NAME = "imports";
|
|
21
|
-
const USAGE = "usage: imports.ts";
|
|
22
|
-
const KIT_DEFAULTS = "kit-defaults.ts";
|
|
23
|
-
|
|
24
|
-
class ImportsError extends Schema.TaggedError<ImportsError>()("ImportsError", {
|
|
25
|
-
message: Schema.String,
|
|
26
|
-
}) {}
|
|
27
|
-
|
|
28
|
-
const Violation = Schema.Struct({
|
|
29
|
-
from: Schema.String,
|
|
30
|
-
to: Schema.String,
|
|
31
|
-
rule: Schema.Struct({ name: Schema.String, severity: Schema.String }),
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
export type Violation = typeof Violation.Type;
|
|
35
|
-
|
|
36
|
-
const decodeCruise = Schema.decodeUnknownEffect(
|
|
37
|
-
Schema.fromJsonString(
|
|
38
|
-
Schema.Struct({
|
|
39
|
-
summary: Schema.Struct({ violations: Schema.Array(Violation), error: Schema.Int, totalCruised: Schema.Int }),
|
|
40
|
-
}),
|
|
41
|
-
),
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
export function describeViolation({ from, to, rule }: Violation): string {
|
|
45
|
-
return from === to ? ` ${rule.severity} ${rule.name}: ${from}` : ` ${rule.severity} ${rule.name}: ${from} → ${to}`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const configOf = Effect.fn("configOf")(function* (root: string) {
|
|
49
|
-
const fs = yield* FileSystem.FileSystem;
|
|
50
|
-
const path = yield* Path.Path;
|
|
51
|
-
for (const name of [PROJECT_CONFIG, ...OWN_CONFIGS]) {
|
|
52
|
-
if (yield* fs.exists(path.join(root, name))) return { file: name, shown: name };
|
|
53
|
-
}
|
|
54
|
-
return { file: path.join(import.meta.dir, KIT_DEFAULTS), shown: "the kit's defaults" };
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const depcruise = Effect.fn("depcruise")(function* () {
|
|
58
|
-
const path = yield* Path.Path;
|
|
59
|
-
const main = yield* Effect.try({
|
|
60
|
-
try: () => new URL("../../bin/dependency-cruiser.mjs", import.meta.resolve("dependency-cruiser")),
|
|
61
|
-
catch: () => new ImportsError({ message: "cannot resolve dependency-cruiser, a peer dependency of the kit" }),
|
|
62
|
-
});
|
|
63
|
-
return yield* path.fromFileUrl(main);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
const trackedInTree = Effect.fn("trackedInTree")(function* (root: string) {
|
|
67
|
-
const fs = yield* FileSystem.FileSystem;
|
|
68
|
-
const path = yield* Path.Path;
|
|
69
|
-
const listed = yield* git(["ls-files", "-z", "--", ...CRUISED], root);
|
|
70
|
-
return yield* Effect.filter(listed.split("\0").filter(Boolean), (file) => fs.exists(path.join(root, file)), { concurrency: "unbounded" });
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
const imports = Effect.gen(function* () {
|
|
74
|
-
if (process.argv.length > 2) return yield* new Usage({ message: USAGE });
|
|
75
|
-
const root = (yield* git(["rev-parse", "--show-toplevel"])).trim();
|
|
76
|
-
const files = yield* trackedInTree(root);
|
|
77
|
-
if (files.length === 0) return yield* new ImportsError({ message: "no tracked TypeScript to cruise" });
|
|
78
|
-
const config = yield* configOf(root);
|
|
79
|
-
const run = yield* collect(process.execPath, [yield* depcruise(), "--config", config.file, "--output-type", "json", ...files], root).pipe(
|
|
80
|
-
Effect.mapError((cause) => new ImportsError({ message: `cannot run dependency-cruiser: ${cause.message}` })),
|
|
81
|
-
);
|
|
82
|
-
const { summary } = yield* decodeCruise(run.stdout).pipe(
|
|
83
|
-
Effect.mapError(() => new ImportsError({ message: `dependency-cruiser exits ${run.exitCode} with no report: ${(run.stdout + run.stderr).trim()}` })),
|
|
84
|
-
);
|
|
85
|
-
const cruised = `${summary.totalCruised} module(s) cruised against ${config.shown}`;
|
|
86
|
-
if (summary.violations.length === 0) {
|
|
87
|
-
yield* Console.log(`${NAME}: ${cruised}, no violation`);
|
|
88
|
-
return true;
|
|
89
|
-
}
|
|
90
|
-
yield* Console.error([`${NAME}: ${summary.violations.length} violation(s) in ${cruised}`, ...summary.violations.map(describeViolation)].join("\n"));
|
|
91
|
-
return summary.error === 0;
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
if (import.meta.main) runMain(NAME, imports);
|