@avi2dg/checks 0.25.0 → 0.27.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 +26 -0
- package/README.md +9 -10
- package/dist/data-shape/index.js +428 -0
- package/{templates → dist/templates}/reference.md +1 -1
- package/docs/configs/commit-messages.md +1 -1
- package/docs/configs/dependency-rules.md +7 -1
- package/docs/configs/effect-rules.md +12 -2
- package/docs/configs/native-settings.md +10 -31
- package/docs/configs/typescript-rules.md +43 -1
- package/docs/design.md +210 -104
- package/docs/gates/checks-changelog.md +8 -5
- package/docs/gates/checks-ci-wiring.md +2 -2
- package/docs/gates/checks-comment-gate.md +3 -4
- package/docs/gates/checks-commit-identity.md +3 -3
- package/docs/gates/checks-docs.md +67 -45
- package/docs/gates/checks-exports.md +104 -0
- package/docs/gates/checks-flake.md +3 -3
- package/docs/gates/checks-lint-coverage.md +17 -9
- package/docs/gates/checks-lint.md +8 -4
- package/docs/gates/checks-mutation-compare.md +3 -3
- package/docs/gates/checks-quarantine-clock.md +3 -5
- package/docs/gates/checks-release-notes.md +4 -4
- package/docs/gates/checks-release-report.md +3 -3
- package/docs/gates/checks-repetition.md +2 -1
- package/docs/gates/checks-subsumed-tests.md +3 -3
- package/docs/gates/checks-suppressions-ratchet.md +3 -4
- package/docs/gates/checks-test-layout.md +4 -4
- package/docs/gates/checks-test.md +3 -2
- package/docs/gates/checks-unused.md +4 -4
- package/docs/gates/checks-vendor.md +3 -3
- package/oxlintrc.json +22 -2
- package/package.json +17 -9
- package/src/complexity/exports.ts +188 -0
- package/src/complexity/knip.ts +70 -0
- package/src/complexity/unused.ts +7 -56
- package/src/core/gates.ts +2 -1
- package/src/core/swc.ts +1 -1
- package/src/delivery/ci-wiring.ts +1 -1
- package/src/dependencies/vendor.ts +4 -5
- package/src/docs/doc-names.ts +128 -0
- package/src/docs/doc-references.ts +1 -1
- package/src/docs/doc-snapshot.ts +1 -1
- package/src/docs/doc-templates.ts +4 -4
- package/src/docs/docs.ts +17 -14
- package/src/docs/prose-matchers.ts +11 -1
- package/src/quality/comments.ts +2 -2
- package/src/quality/lint-coverage.sh +35 -2
- package/{presets → src/quality/presets}/effect.language-service.json +3 -1
- package/src/testing/mutation-compare.ts +51 -45
- package/src/testing/test-layout.ts +1 -1
- package/src/testing/test-report.ts +1 -1
- package/ts-reset.d.ts +2 -0
- package/tsconfig.effect.json +3 -0
- /package/{templates → dist/templates}/adr.md +0 -0
- /package/{templates → dist/templates}/agents.md +0 -0
- /package/{templates → dist/templates}/changelog.md +0 -0
- /package/{templates → dist/templates}/claude.md +0 -0
- /package/{templates → dist/templates}/explanation.md +0 -0
- /package/{templates → dist/templates}/how-to.md +0 -0
- /package/{templates → dist/templates}/readme.md +0 -0
- /package/{templates → dist/templates}/tutorial.md +0 -0
- /package/{presets → src/quality/presets}/effect.oxlint.json +0 -0
|
@@ -4,7 +4,7 @@ audience: consumers
|
|
|
4
4
|
---
|
|
5
5
|
# checks-unused
|
|
6
6
|
|
|
7
|
-
`checks-unused` is the gate that refuses a TypeScript file no entry point reaches
|
|
7
|
+
`checks-unused` is the gate that refuses a TypeScript file no entry point reaches.
|
|
8
8
|
|
|
9
9
|
## What it checks
|
|
10
10
|
|
|
@@ -54,10 +54,10 @@ A passing run counts the files it judged:
|
|
|
54
54
|
unused: no unreferenced files among 110 tracked .ts/.tsx file(s)
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
##
|
|
57
|
+
## When it runs
|
|
58
58
|
|
|
59
|
-
`checks-lint` runs
|
|
60
|
-
|
|
59
|
+
`checks-lint` runs it when the repository tracks a `.ts` or `.tsx` file.
|
|
60
|
+
Such a repository names its entries in a Knip configuration.
|
|
61
61
|
|
|
62
62
|
## Related topics
|
|
63
63
|
|
|
@@ -102,10 +102,10 @@ A consumer whose `tsconfig.json` has no explicit `include` keeps the trees out w
|
|
|
102
102
|
{ "exclude": ["node_modules", "repos"] }
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
##
|
|
105
|
+
## When it runs
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
The repository's `prepare` script runs it, as [Wiring](#wiring) shows.
|
|
108
|
+
It pins only the libraries its arguments name, and a run with no arguments changes nothing.
|
|
109
109
|
A repository that pins no library needs no `prepare` entry for it.
|
|
110
110
|
|
|
111
111
|
## Related topics
|
package/oxlintrc.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"plugins": ["typescript", "oxc", "eslint", "import"],
|
|
3
|
-
"jsPlugins": ["./dist/effect-channel/index.js", "./dist/readability/index.js"],
|
|
3
|
+
"jsPlugins": ["./dist/effect-channel/index.js", "./dist/readability/index.js", "./dist/data-shape/index.js"],
|
|
4
4
|
"categories": { "correctness": "error", "suspicious": "error" },
|
|
5
5
|
"rules": {
|
|
6
6
|
"effect-channel/no-error-channel-escape": "error",
|
|
@@ -18,5 +18,25 @@
|
|
|
18
18
|
"typescript/no-non-null-assertion": "error",
|
|
19
19
|
"typescript/no-deprecated": "error",
|
|
20
20
|
"typescript/consistent-return": "off"
|
|
21
|
-
}
|
|
21
|
+
},
|
|
22
|
+
"overrides": [
|
|
23
|
+
{
|
|
24
|
+
"files": ["**/*.ts", "**/*.tsx"],
|
|
25
|
+
"rules": {
|
|
26
|
+
"data-shape/readonly-collection-param": "error"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"files": ["**/*.ts", "**/*.tsx"],
|
|
31
|
+
"excludeFiles": ["tests/**"],
|
|
32
|
+
"rules": {
|
|
33
|
+
"data-shape/schema-twin": "error",
|
|
34
|
+
"typescript/no-unsafe-assignment": "error",
|
|
35
|
+
"typescript/no-unsafe-member-access": "error",
|
|
36
|
+
"typescript/no-unsafe-argument": "error",
|
|
37
|
+
"typescript/no-unsafe-return": "error",
|
|
38
|
+
"typescript/no-unsafe-call": "error"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
]
|
|
22
42
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avi2dg/checks",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Deterministic checks shared across
|
|
3
|
+
"version": "0.27.0",
|
|
4
|
+
"description": "Deterministic checks shared across a set of TypeScript repositories",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -47,6 +47,8 @@
|
|
|
47
47
|
"src/complexity/suppressions-ratchet.ts",
|
|
48
48
|
"src/complexity/repetition.ts",
|
|
49
49
|
"src/complexity/unused.ts",
|
|
50
|
+
"src/complexity/exports.ts",
|
|
51
|
+
"src/complexity/knip.ts",
|
|
50
52
|
"src/testing/quarantine-clock.ts",
|
|
51
53
|
"src/docs/docs.ts",
|
|
52
54
|
"src/dependencies/vendor.ts",
|
|
@@ -56,14 +58,17 @@
|
|
|
56
58
|
"src/docs/doc-references.ts",
|
|
57
59
|
"src/docs/doc-snapshot.ts",
|
|
58
60
|
"src/docs/doc-templates.ts",
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"presets/effect.
|
|
61
|
+
"src/docs/doc-names.ts",
|
|
62
|
+
"dist/templates/",
|
|
63
|
+
"src/quality/presets/effect.oxlint.json",
|
|
64
|
+
"src/quality/presets/effect.language-service.json",
|
|
62
65
|
"oxlintrc.json",
|
|
63
66
|
"stryker.preset.js",
|
|
64
67
|
"tsconfig.effect.json",
|
|
68
|
+
"ts-reset.d.ts",
|
|
65
69
|
"dist/effect-channel/index.js",
|
|
66
|
-
"dist/readability/index.js"
|
|
70
|
+
"dist/readability/index.js",
|
|
71
|
+
"dist/data-shape/index.js"
|
|
67
72
|
],
|
|
68
73
|
"exports": {
|
|
69
74
|
"./dependency-cruiser.config.js": "./dependency-cruiser.config.js",
|
|
@@ -72,6 +77,7 @@
|
|
|
72
77
|
"./scripts/comment-matchers.ts": "./src/quality/comment-matchers.ts",
|
|
73
78
|
"./scripts/prose-matchers.ts": "./src/docs/prose-matchers.ts",
|
|
74
79
|
"./stryker.preset.js": "./stryker.preset.js",
|
|
80
|
+
"./templates/*": "./dist/templates/*",
|
|
75
81
|
"./tsconfig.effect.json": "./tsconfig.effect.json"
|
|
76
82
|
},
|
|
77
83
|
"bin": {
|
|
@@ -91,13 +97,14 @@
|
|
|
91
97
|
"checks-suppressions-ratchet": "src/complexity/suppressions-ratchet.ts",
|
|
92
98
|
"checks-repetition": "src/complexity/repetition.ts",
|
|
93
99
|
"checks-unused": "src/complexity/unused.ts",
|
|
100
|
+
"checks-exports": "src/complexity/exports.ts",
|
|
94
101
|
"checks-quarantine-clock": "src/testing/quarantine-clock.ts",
|
|
95
102
|
"checks-docs": "src/docs/docs.ts",
|
|
96
103
|
"checks-vendor": "src/dependencies/vendor.ts"
|
|
97
104
|
},
|
|
98
105
|
"scripts": {
|
|
99
106
|
"prepare": "bun src/dependencies/vendor.ts --library effect --package effect --repository https://github.com/Effect-TS/effect.git --tag 'effect@{version}' --path packages/effect/package.json",
|
|
100
|
-
"build": "bun build src/quality/effect-channel/index.ts --outdir dist/effect-channel --target node --format esm && bun build src/complexity/readability/index.ts --outdir dist/readability --target node --format esm && bun scripts/doc-templates-write.ts && bun src/delivery/changelog-write.ts && bun scripts/doc-blocks-write.ts",
|
|
107
|
+
"build": "bun build src/quality/effect-channel/index.ts --outdir dist/effect-channel --target node --format esm && bun build src/complexity/readability/index.ts --outdir dist/readability --target node --format esm && bun build src/quality/data-shape/index.ts --outdir dist/data-shape --target node --format esm && bun scripts/doc-templates-write.ts && bun src/delivery/changelog-write.ts && bun scripts/doc-blocks-write.ts",
|
|
101
108
|
"lint": "oxlint --type-aware && bun src/core/lint.ts && depcruise --config .dependency-cruiser.cjs .",
|
|
102
109
|
"typecheck": "tsc --noEmit && effect-tsgo diagnostics --project tsconfig.json --format text --strict",
|
|
103
110
|
"test": "bun src/testing/test.ts"
|
|
@@ -109,14 +116,14 @@
|
|
|
109
116
|
"effect": "4.0.0-rc.115",
|
|
110
117
|
"jscpd": "5.3.2",
|
|
111
118
|
"oxlint": "1.83.0",
|
|
112
|
-
"oxlint-tsgolint": "7.0.2002"
|
|
119
|
+
"oxlint-tsgolint": "7.0.2002",
|
|
120
|
+
"typescript": "7.0.2"
|
|
113
121
|
},
|
|
114
122
|
"devDependencies": {
|
|
115
123
|
"@effect/tsgo": "0.45.0",
|
|
116
124
|
"@oxlint/plugins": "1.83.0",
|
|
117
125
|
"@swc/core": "1.16.2",
|
|
118
126
|
"@types/bun": "^1.3.0",
|
|
119
|
-
"ajv": "8.20.0",
|
|
120
127
|
"dependency-cruiser": "18.4.0",
|
|
121
128
|
"effect": "4.0.0-rc.115",
|
|
122
129
|
"jscpd": "5.3.2",
|
|
@@ -129,6 +136,7 @@
|
|
|
129
136
|
"@commitlint/config-conventional": "21.2.3",
|
|
130
137
|
"@effect/platform-bun": "4.0.0-rc.115",
|
|
131
138
|
"@effect/platform-node-shared": "4.0.0-rc.115",
|
|
139
|
+
"@total-typescript/ts-reset": "0.6.1",
|
|
132
140
|
"knip": "6.38.0"
|
|
133
141
|
}
|
|
134
142
|
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Console, Effect, FileSystem, Path, Schema } from "effect";
|
|
3
|
+
import { checkoutFiles, commitOf, git, pathsAt, rangeEnds, refArgs } from "../core/git.ts";
|
|
4
|
+
import { runMain } from "../core/main.ts";
|
|
5
|
+
import { knipReport, scanTree } from "./knip.ts";
|
|
6
|
+
|
|
7
|
+
const UnusedSymbolSchema = Schema.Struct({
|
|
8
|
+
file: Schema.String,
|
|
9
|
+
kind: Schema.Literals(["export", "type"]),
|
|
10
|
+
name: Schema.String,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export type UnusedSymbol = typeof UnusedSymbolSchema.Type;
|
|
14
|
+
|
|
15
|
+
export type Baseline = readonly UnusedSymbol[];
|
|
16
|
+
|
|
17
|
+
export type Held = {
|
|
18
|
+
readonly reported: readonly UnusedSymbol[];
|
|
19
|
+
readonly base: Baseline;
|
|
20
|
+
readonly head: Baseline;
|
|
21
|
+
readonly unusedAtBase: readonly UnusedSymbol[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type Drift = {
|
|
25
|
+
readonly unlisted: readonly UnusedSymbol[];
|
|
26
|
+
readonly added: readonly UnusedSymbol[];
|
|
27
|
+
readonly stale: readonly UnusedSymbol[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const NAME = "exports";
|
|
31
|
+
export const BASELINE_FILE = "exports-baseline.json";
|
|
32
|
+
const INCLUDED = ["--include", "exports,types"];
|
|
33
|
+
const WRITE = "--write";
|
|
34
|
+
const USAGE = `usage: exports.ts <ref> | <base-ref> <head-ref> | ${WRITE}`;
|
|
35
|
+
|
|
36
|
+
class ExportsError extends Schema.TaggedError<ExportsError>()("ExportsError", {
|
|
37
|
+
message: Schema.String,
|
|
38
|
+
}) {}
|
|
39
|
+
|
|
40
|
+
const KnipSymbol = Schema.Struct({ name: Schema.String });
|
|
41
|
+
|
|
42
|
+
const decodeKnipReport = Schema.decodeUnknownEffect(
|
|
43
|
+
Schema.fromJsonString(
|
|
44
|
+
Schema.Struct({
|
|
45
|
+
issues: Schema.Array(
|
|
46
|
+
Schema.Struct({
|
|
47
|
+
file: Schema.String,
|
|
48
|
+
exports: Schema.optional(Schema.Array(KnipSymbol)),
|
|
49
|
+
types: Schema.optional(Schema.Array(KnipSymbol)),
|
|
50
|
+
}),
|
|
51
|
+
),
|
|
52
|
+
}),
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const decodeBaseline = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Array(UnusedSymbolSchema)));
|
|
57
|
+
|
|
58
|
+
function byPosition(a: UnusedSymbol, b: UnusedSymbol): number {
|
|
59
|
+
return a.file.localeCompare(b.file) || a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const symbolsOf = Effect.fn("symbolsOf")(function* (stdout: string) {
|
|
63
|
+
const { issues } = yield* decodeKnipReport(stdout).pipe(
|
|
64
|
+
Effect.mapError((cause) => new ExportsError({ message: `knip's JSON report does not decode: ${cause.message}` })),
|
|
65
|
+
);
|
|
66
|
+
return issues
|
|
67
|
+
.flatMap(({ file, exports = [], types = [] }): readonly UnusedSymbol[] => [
|
|
68
|
+
...exports.map(({ name }): UnusedSymbol => ({ file, kind: "export", name })),
|
|
69
|
+
...types.map(({ name }): UnusedSymbol => ({ file, kind: "type", name })),
|
|
70
|
+
])
|
|
71
|
+
.toSorted(byPosition);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export const baselineOf = Effect.fn("baselineOf")(function* (text: string, where: string) {
|
|
75
|
+
return yield* decodeBaseline(text).pipe(
|
|
76
|
+
Effect.mapError((cause) => new ExportsError({ message: `${where} does not decode: ${cause.message}` })),
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const baselinePath = Effect.fn("baselinePath")(function* (root: string) {
|
|
81
|
+
return (yield* Path.Path).join(root, BASELINE_FILE);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const baselineInTree = Effect.fn("baselineInTree")(function* (root: string) {
|
|
85
|
+
const fs = yield* FileSystem.FileSystem;
|
|
86
|
+
const at = yield* baselinePath(root);
|
|
87
|
+
if (!(yield* fs.exists(at))) return [] satisfies Baseline;
|
|
88
|
+
return yield* fs.readFileString(at).pipe(Effect.flatMap((text) => baselineOf(text, BASELINE_FILE)));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const baselineAt = Effect.fn("baselineAt")(function* (treeish: string, root: string) {
|
|
92
|
+
const blob = (yield* git(["ls-tree", "--object-only", treeish, "--", BASELINE_FILE], root)).trim();
|
|
93
|
+
if (blob === "") return [] satisfies Baseline;
|
|
94
|
+
return yield* baselineOf(yield* git(["cat-file", "blob", blob], root), `${BASELINE_FILE} at ${treeish}`);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const keyOf = ({ file, kind, name }: UnusedSymbol): string => `${kind}\0${file}\0${name}`;
|
|
98
|
+
|
|
99
|
+
function outside(symbols: readonly UnusedSymbol[], others: readonly UnusedSymbol[]): readonly UnusedSymbol[] {
|
|
100
|
+
const known = new Set(others.map(keyOf));
|
|
101
|
+
return symbols.filter((symbol) => !known.has(keyOf(symbol))).toSorted(byPosition);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function driftOf({ reported, base, head, unusedAtBase }: Held): Drift {
|
|
105
|
+
return {
|
|
106
|
+
unlisted: outside(reported, head),
|
|
107
|
+
added: outside(head, [...base, ...unusedAtBase]),
|
|
108
|
+
stale: outside(head, reported),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function describe(symbol: UnusedSymbol): string {
|
|
113
|
+
return ` ${symbol.file}: ${symbol.name} (${symbol.kind})`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function report(head: Baseline, { unlisted, added, stale }: Drift): string {
|
|
117
|
+
const sections = [
|
|
118
|
+
{ symbols: unlisted, what: `unused export(s) not in ${BASELINE_FILE}:` },
|
|
119
|
+
{ symbols: added, what: `${BASELINE_FILE} export(s) the range adds that its base did not leave unused, remove the export instead:` },
|
|
120
|
+
{ symbols: stale, what: `${BASELINE_FILE} export(s) no longer reported, remove them:` },
|
|
121
|
+
].filter(({ symbols }) => symbols.length > 0);
|
|
122
|
+
if (sections.length > 0) {
|
|
123
|
+
return sections.flatMap(({ symbols, what }) => [`${NAME}: ${symbols.length} ${what}`, ...symbols.map(describe)]).join("\n");
|
|
124
|
+
}
|
|
125
|
+
return head.length === 0
|
|
126
|
+
? `${NAME}: no unused exports or types`
|
|
127
|
+
: `${NAME}: ${head.length} unused export(s) in ${BASELINE_FILE}, and no new ones`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const scan = scanTree(NAME, INCLUDED).pipe(Effect.mapError((cause) => new ExportsError({ message: cause.message })));
|
|
131
|
+
|
|
132
|
+
// Knip loads the configuration's imports, such as the kit's knip-base.json, from a node_modules the checkout lacks.
|
|
133
|
+
const unusedAt = Effect.fn("unusedAt")(
|
|
134
|
+
function* (rev: string, root: string) {
|
|
135
|
+
const files = yield* pathsAt(rev, [], root);
|
|
136
|
+
if (files.length === 0) return [] satisfies readonly UnusedSymbol[];
|
|
137
|
+
const fs = yield* FileSystem.FileSystem;
|
|
138
|
+
const path = yield* Path.Path;
|
|
139
|
+
const tree = yield* checkoutFiles(rev, files, yield* fs.makeTempDirectoryScoped({ prefix: "checks-exports-" }), root);
|
|
140
|
+
const modules = path.join(root, "node_modules");
|
|
141
|
+
if (yield* fs.exists(modules)) yield* fs.symlink(modules, path.join(tree, "node_modules"));
|
|
142
|
+
const reported = yield* knipReport(tree, INCLUDED).pipe(
|
|
143
|
+
Effect.mapError((cause) => new ExportsError({ message: `at ${rev}: ${cause.message}` })),
|
|
144
|
+
);
|
|
145
|
+
return reported.kind === "unconfigured" ? [] : yield* symbolsOf(reported.stdout);
|
|
146
|
+
},
|
|
147
|
+
Effect.scoped,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// Knip scans the checked-out tree, and a pull request's merge checkout holds the base branch tip beside the range head.
|
|
151
|
+
const judgedBase = Effect.fn("judgedBase")(function* (first: string, second: string | undefined, root: string) {
|
|
152
|
+
const { base } = yield* rangeEnds(first, second, root);
|
|
153
|
+
if (second === undefined) return base;
|
|
154
|
+
const head = yield* commitOf(second, root);
|
|
155
|
+
const parents = (yield* git(["rev-parse", "HEAD^@"], root)).split("\n").filter((line) => line !== "");
|
|
156
|
+
const [other, ...rest] = parents.filter((parent) => parent !== head);
|
|
157
|
+
return parents.includes(head) && other !== undefined && rest.length === 0 ? other : base;
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const check = Effect.fn("check")(function* (first: string, second: string | undefined) {
|
|
161
|
+
const { root, reported } = yield* scan;
|
|
162
|
+
if (reported.kind === "unconfigured") return false;
|
|
163
|
+
const baseRev = yield* judgedBase(first, second, root);
|
|
164
|
+
const base = yield* baselineAt(baseRev, root);
|
|
165
|
+
const head = yield* baselineInTree(root);
|
|
166
|
+
const unusedAtBase = outside(head, base).length === 0 ? [] : yield* unusedAt(baseRev, root);
|
|
167
|
+
const drift = driftOf({ reported: yield* symbolsOf(reported.stdout), base, head, unusedAtBase });
|
|
168
|
+
yield* Console.log(report(head, drift));
|
|
169
|
+
return drift.unlisted.length === 0 && drift.added.length === 0 && drift.stale.length === 0;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const seed = Effect.gen(function* () {
|
|
173
|
+
const { root, reported } = yield* scan;
|
|
174
|
+
if (reported.kind === "unconfigured") return false;
|
|
175
|
+
const symbols = yield* symbolsOf(reported.stdout);
|
|
176
|
+
yield* (yield* FileSystem.FileSystem).writeFileString(yield* baselinePath(root), `${JSON.stringify(symbols, null, 2)}\n`);
|
|
177
|
+
yield* Console.log(`${NAME}: wrote ${symbols.length} unused export(s) to ${BASELINE_FILE}`);
|
|
178
|
+
return true;
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const gate = Effect.gen(function* () {
|
|
182
|
+
const args = process.argv.slice(2);
|
|
183
|
+
if (args.length === 1 && args[0] === WRITE) return yield* seed;
|
|
184
|
+
const { first, second } = yield* refArgs(args, USAGE);
|
|
185
|
+
return yield* check(first, second);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (import.meta.main) runMain(NAME, gate);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Console, Effect, FileSystem, Path, Schema } from "effect";
|
|
2
|
+
import { collect, git } from "../core/git.ts";
|
|
3
|
+
|
|
4
|
+
class KnipError extends Schema.TaggedError<KnipError>()("KnipError", {
|
|
5
|
+
message: Schema.String,
|
|
6
|
+
}) {}
|
|
7
|
+
|
|
8
|
+
const CONFIGS = [
|
|
9
|
+
".knip.json",
|
|
10
|
+
".knip.jsonc",
|
|
11
|
+
"knip.json",
|
|
12
|
+
"knip.jsonc",
|
|
13
|
+
"knip.js",
|
|
14
|
+
"knip.ts",
|
|
15
|
+
"knip.config.js",
|
|
16
|
+
"knip.config.ts",
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
const TYPESCRIPT = ["*.ts", "*.tsx"];
|
|
20
|
+
|
|
21
|
+
const hasKnipConfig = Effect.fn("hasKnipConfig")(function* (root: string) {
|
|
22
|
+
const fs = yield* FileSystem.FileSystem;
|
|
23
|
+
const path = yield* Path.Path;
|
|
24
|
+
for (const name of CONFIGS) {
|
|
25
|
+
if (yield* fs.exists(path.join(root, name))) return true;
|
|
26
|
+
}
|
|
27
|
+
const manifest: unknown = yield* fs.readFileString(path.join(root, "package.json")).pipe(
|
|
28
|
+
Effect.flatMap((text) =>
|
|
29
|
+
Effect.try({
|
|
30
|
+
try: () => JSON.parse(text),
|
|
31
|
+
catch: () => new KnipError({ message: "package.json does not parse as JSON" }),
|
|
32
|
+
})
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
return typeof manifest === "object" && manifest !== null && "knip" in manifest;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const knipBinary = Effect.fn("knipBinary")(function* () {
|
|
39
|
+
const path = yield* Path.Path;
|
|
40
|
+
const main = yield* Effect.try({
|
|
41
|
+
try: () => new URL("../bin/knip.js", import.meta.resolve("knip")),
|
|
42
|
+
catch: () => new KnipError({ message: "cannot resolve knip from the installed kit" }),
|
|
43
|
+
});
|
|
44
|
+
return yield* path.fromFileUrl(main);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export const knipReport = Effect.fn("knipReport")(function* (root: string, args: readonly string[]) {
|
|
48
|
+
if (!(yield* hasKnipConfig(root))) return { kind: "unconfigured" } as const;
|
|
49
|
+
const run = yield* collect(process.execPath, [yield* knipBinary(), ...args, "--reporter", "json"], root).pipe(
|
|
50
|
+
Effect.mapError((cause) => new KnipError({ message: `cannot run knip: ${cause.message}` })),
|
|
51
|
+
);
|
|
52
|
+
if (run.exitCode !== 0 && run.exitCode !== 1) {
|
|
53
|
+
return yield* new KnipError({ message: `knip exits ${run.exitCode}: ${(run.stdout + run.stderr).trim()}` });
|
|
54
|
+
}
|
|
55
|
+
return { kind: "reported", stdout: run.stdout } as const;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export const scanTree = Effect.fn("scanTree")(function* (name: string, args: readonly string[]) {
|
|
59
|
+
const root = (yield* git(["rev-parse", "--show-toplevel"])).trim();
|
|
60
|
+
const tracked = (yield* git(["ls-files", "--", ...TYPESCRIPT], root))
|
|
61
|
+
.split("\n")
|
|
62
|
+
.map((line) => line.trim())
|
|
63
|
+
.filter((line) => line !== "");
|
|
64
|
+
if (tracked.length === 0) return yield* new KnipError({ message: "no tracked .ts or .tsx files to scan" });
|
|
65
|
+
const reported = yield* knipReport(root, args);
|
|
66
|
+
if (reported.kind === "unconfigured") {
|
|
67
|
+
yield* Console.log(`${name}: no knip configuration names entry files, so add one extending the kit's knip-base.json`);
|
|
68
|
+
}
|
|
69
|
+
return { root, tracked: tracked.length, reported };
|
|
70
|
+
});
|
package/src/complexity/unused.ts
CHANGED
|
@@ -1,22 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { Console, Effect,
|
|
3
|
-
import { collect, git } from "../core/git.ts";
|
|
2
|
+
import { Console, Effect, Schema } from "effect";
|
|
4
3
|
import { runMain } from "../core/main.ts";
|
|
4
|
+
import { scanTree } from "./knip.ts";
|
|
5
5
|
|
|
6
6
|
export type Scan = { readonly tracked: number; readonly files: readonly string[] };
|
|
7
7
|
|
|
8
8
|
const NAME = "unused";
|
|
9
|
-
const CONFIGS = [
|
|
10
|
-
".knip.json",
|
|
11
|
-
".knip.jsonc",
|
|
12
|
-
"knip.json",
|
|
13
|
-
"knip.jsonc",
|
|
14
|
-
"knip.js",
|
|
15
|
-
"knip.ts",
|
|
16
|
-
"knip.config.js",
|
|
17
|
-
"knip.config.ts",
|
|
18
|
-
] as const;
|
|
19
|
-
const TYPESCRIPT = ["*.ts", "*.tsx"];
|
|
20
9
|
const JUDGED = /\.tsx?$/;
|
|
21
10
|
|
|
22
11
|
class UnusedError extends Schema.TaggedError<UnusedError>()("UnusedError", {
|
|
@@ -46,51 +35,13 @@ export function report({ tracked, files }: Scan): string {
|
|
|
46
35
|
return [`${NAME}: ${files.length} unreferenced file(s):`, ...files.map((file) => ` ${file}`)].join("\n");
|
|
47
36
|
}
|
|
48
37
|
|
|
49
|
-
const hasConfig = Effect.fn("hasConfig")(function* (root: string) {
|
|
50
|
-
const fs = yield* FileSystem.FileSystem;
|
|
51
|
-
const path = yield* Path.Path;
|
|
52
|
-
for (const name of CONFIGS) {
|
|
53
|
-
if (yield* fs.exists(path.join(root, name))) return true;
|
|
54
|
-
}
|
|
55
|
-
const manifest: unknown = yield* fs.readFileString(path.join(root, "package.json")).pipe(
|
|
56
|
-
Effect.flatMap((text) =>
|
|
57
|
-
Effect.try({
|
|
58
|
-
try: () => JSON.parse(text) as unknown,
|
|
59
|
-
catch: () => new UnusedError({ message: "package.json does not parse as JSON" }),
|
|
60
|
-
})
|
|
61
|
-
),
|
|
62
|
-
);
|
|
63
|
-
return typeof manifest === "object" && manifest !== null && "knip" in manifest;
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
const knip = Effect.fn("knip")(function* () {
|
|
67
|
-
const path = yield* Path.Path;
|
|
68
|
-
const main = yield* Effect.try({
|
|
69
|
-
try: () => new URL("../bin/knip.js", import.meta.resolve("knip")),
|
|
70
|
-
catch: () => new UnusedError({ message: "cannot resolve knip from the installed kit" }),
|
|
71
|
-
});
|
|
72
|
-
return yield* path.fromFileUrl(main);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
38
|
const unused = Effect.gen(function* () {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
.split("\n")
|
|
79
|
-
.map((line) => line.trim())
|
|
80
|
-
.filter((line) => line !== "");
|
|
81
|
-
if (tracked.length === 0) return yield* new UnusedError({ message: "no tracked .ts or .tsx files to scan" });
|
|
82
|
-
if (!(yield* hasConfig(root))) {
|
|
83
|
-
yield* Console.log(`${NAME}: no knip configuration names entry files, so add one extending the kit's knip-base.json`);
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
const run = yield* collect(process.execPath, [yield* knip(), "--files", "--reporter", "json"], root).pipe(
|
|
87
|
-
Effect.mapError((cause) => new UnusedError({ message: `cannot run knip: ${cause.message}` })),
|
|
39
|
+
const { tracked, reported } = yield* scanTree(NAME, ["--files"]).pipe(
|
|
40
|
+
Effect.mapError((cause) => new UnusedError({ message: cause.message })),
|
|
88
41
|
);
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
const files = yield* filesOf(run.stdout);
|
|
93
|
-
yield* Console.log(report({ tracked: tracked.length, files }));
|
|
42
|
+
if (reported.kind === "unconfigured") return false;
|
|
43
|
+
const files = yield* filesOf(reported.stdout);
|
|
44
|
+
yield* Console.log(report({ tracked, files }));
|
|
94
45
|
return files.length === 0;
|
|
95
46
|
});
|
|
96
47
|
|
package/src/core/gates.ts
CHANGED
|
@@ -12,7 +12,7 @@ export const EVERY_REPOSITORY = "every repository";
|
|
|
12
12
|
|
|
13
13
|
export const VECTORS = ["complexity", "quality", "testing", "docs", "delivery", "dependencies"] as const;
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
type Vector = (typeof VECTORS)[number];
|
|
16
16
|
|
|
17
17
|
export type KitGate = {
|
|
18
18
|
readonly bin: string;
|
|
@@ -41,5 +41,6 @@ export const KIT_GATES = [
|
|
|
41
41
|
{ bin: "checks-docs", vector: "docs", file: "docs.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
42
42
|
{ bin: "checks-repetition", vector: "complexity", file: "repetition.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE },
|
|
43
43
|
{ bin: "checks-unused", vector: "complexity", file: "unused.ts", reads: "tree", appliesTo: TYPESCRIPT_SOURCE },
|
|
44
|
+
{ bin: "checks-exports", vector: "complexity", file: "exports.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE },
|
|
44
45
|
{ bin: "checks-quarantine-clock", vector: "testing", file: "quarantine-clock.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
45
46
|
] as const satisfies readonly KitGate[];
|
package/src/core/swc.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parse } from "@swc/core";
|
|
2
2
|
import { Effect, Schema } from "effect";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
class TypeScriptParseError extends Schema.TaggedError<TypeScriptParseError>()("TypeScriptParseError", {
|
|
5
5
|
message: Schema.String,
|
|
6
6
|
}) {}
|
|
7
7
|
|
|
@@ -200,7 +200,7 @@ function gapsAmong(gates: readonly Gate[], steps: readonly RunStep[], lintGates:
|
|
|
200
200
|
const blocked = invoking.flatMap(({ location, blocker }) =>
|
|
201
201
|
blocker === undefined ? [] : [{ location, blocker }],
|
|
202
202
|
);
|
|
203
|
-
return [{ gate: gate.command, entryPoint: entryPoint
|
|
203
|
+
return [{ gate: gate.command, ...(entryPoint === undefined ? {} : { entryPoint: entryPoint.join(" ") }), blocked }];
|
|
204
204
|
});
|
|
205
205
|
}
|
|
206
206
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { Console, Effect, FileSystem, Option, Path, Schema } from "effect";
|
|
2
|
+
import { Config, Console, Effect, FileSystem, Option, Path, Schema } from "effect";
|
|
3
3
|
import { git } from "../core/git.ts";
|
|
4
4
|
import { runMain } from "../core/main.ts";
|
|
5
5
|
import { librariesFrom, NAME, OPENER, type Library } from "./vendor-args.ts";
|
|
@@ -305,10 +305,9 @@ const vend = Effect.fn("vend")(function* (root: string, cache: string, library:
|
|
|
305
305
|
|
|
306
306
|
const cacheRoot = Effect.fn("cacheRoot")(function* () {
|
|
307
307
|
const path = yield* Path.Path;
|
|
308
|
-
const home =
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
308
|
+
const home = yield* Config.NonEmptyString("HOME").pipe(
|
|
309
|
+
Effect.mapError(() => new VendorError({ message: "HOME is missing, so the shared cache has no root" })),
|
|
310
|
+
);
|
|
312
311
|
return path.join(home, CACHE_HOME);
|
|
313
312
|
});
|
|
314
313
|
|