@avi2dg/checks 0.24.2 → 0.26.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 +22 -0
- package/README.md +29 -22
- package/dist/effect-channel/index.js +4 -4
- package/dist/readability/index.js +5 -5
- package/docs/configs/commit-messages.md +1 -1
- package/docs/configs/effect-rules.md +2 -2
- package/docs/configs/native-settings.md +4 -2
- package/docs/design.md +10 -4
- package/docs/gates/checks-comment-gate.md +3 -4
- package/docs/gates/checks-docs.md +16 -14
- package/docs/gates/checks-exports.md +104 -0
- package/docs/gates/checks-flake.md +1 -1
- package/docs/gates/checks-lint.md +1 -1
- package/docs/gates/checks-quarantine-clock.md +1 -1
- package/docs/gates/checks-test-layout.md +14 -9
- package/docs/gates/checks-test.md +1 -1
- package/docs/gates/checks-unused.md +65 -0
- package/knip-base.json +4 -0
- package/package.json +75 -108
- package/src/complexity/exports.ts +196 -0
- package/src/complexity/knip.ts +70 -0
- package/{scripts → src/complexity}/repetition.ts +2 -2
- package/{scripts → src/complexity}/suppressions-ratchet.ts +2 -2
- package/src/complexity/unused.ts +48 -0
- package/src/core/gates.ts +46 -0
- package/{scripts → src/core}/lint.ts +2 -2
- package/{scripts → src/core}/swc.ts +1 -1
- package/{scripts → src/delivery}/changelog-write.ts +2 -2
- package/{scripts → src/delivery}/changelog.ts +3 -3
- package/{scripts → src/delivery}/ci-wiring.ts +3 -3
- package/{scripts → src/delivery}/commit-identity.ts +2 -2
- package/{scripts → src/delivery}/release-notes.ts +1 -1
- package/{scripts → src/delivery}/release-report.ts +2 -2
- package/{scripts → src/dependencies}/vendor-args.ts +1 -1
- package/{scripts → src/dependencies}/vendor.ts +2 -2
- package/{scripts → src/docs}/doc-references.ts +1 -1
- package/{scripts → src/docs}/doc-snapshot.ts +2 -2
- package/{scripts → src/docs}/doc-templates.ts +3 -3
- package/{scripts → src/docs}/docs.ts +2 -2
- package/{scripts → src/quality}/comment-gate.ts +2 -2
- package/{scripts → src/quality}/comment-matchers.ts +1 -1
- package/{scripts → src/quality}/comments.ts +2 -2
- package/{scripts → src/testing}/flake.ts +1 -1
- package/{scripts → src/testing}/mutation-compare.ts +1 -1
- package/{scripts → src/testing}/quarantine-clock.ts +2 -2
- package/{scripts → src/testing}/subsumed-tests.ts +2 -2
- package/{scripts → src/testing}/test-layout.ts +47 -14
- package/{scripts → src/testing}/test-report.ts +1 -1
- package/{scripts → src/testing}/test-skips.ts +1 -1
- package/{scripts → src/testing}/test.ts +2 -2
- package/CONTRIBUTING.md +0 -94
- package/docs/gates/checks-backtest.md +0 -54
- package/scripts/backtest.ts +0 -122
- package/scripts/gates.ts +0 -37
- package/scripts/range-gate.ts +0 -8
- /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}/reference.md +0 -0
- /package/{templates → dist/templates}/tutorial.md +0 -0
- /package/{scripts → src/core}/git.ts +0 -0
- /package/{scripts → src/core}/main.ts +0 -0
- /package/{scripts → src/delivery}/shell-command.ts +0 -0
- /package/{scripts → src/docs}/doc-outline.ts +0 -0
- /package/{scripts → src/docs}/doc-rules.ts +0 -0
- /package/{scripts → src/docs}/prose-matchers.ts +0 -0
- /package/{scripts → src/quality}/lint-coverage.sh +0 -0
- /package/{presets → src/quality/presets}/effect.language-service.json +0 -0
- /package/{presets → src/quality/presets}/effect.oxlint.json +0 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: reference
|
|
3
|
+
audience: consumers
|
|
4
|
+
---
|
|
5
|
+
# checks-unused
|
|
6
|
+
|
|
7
|
+
`checks-unused` is the gate that refuses a TypeScript file no entry point reaches, and a reader looks it up when a file seems dead but every other check passes.
|
|
8
|
+
|
|
9
|
+
## What it checks
|
|
10
|
+
|
|
11
|
+
It runs Knip with the repository's own configuration and names each `.ts` or `.tsx` file no entry reaches.
|
|
12
|
+
It reads only the files issue type, so an unused export or dependency never fails it.
|
|
13
|
+
It asks Knip for that issue type itself, so a configuration that narrows `include`, excludes files or turns the files rule off still has its files judged.
|
|
14
|
+
It fails when the repository tracks no TypeScript source, since an empty scan would pass without judging anything.
|
|
15
|
+
It fails when the repository holds no Knip configuration, since Knip's default entries cannot tell a dead file from an entry point.
|
|
16
|
+
|
|
17
|
+
## What it reads
|
|
18
|
+
|
|
19
|
+
It reads the working tree, so an uncommitted file is judged like a committed one.
|
|
20
|
+
It reads the repository's Knip configuration, which names the entries.
|
|
21
|
+
Knip configurations do not extend a package file, so the consumer configuration imports the kit's base and spreads it:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import base from "@avi2dg/checks/knip-base.json";
|
|
25
|
+
|
|
26
|
+
export default { ...base, entry: ["src/index.ts", "tests/**/*.test.ts"] };
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The base in `knip-base.json` reports only unreferenced files.
|
|
30
|
+
The gate resolves the Knip binary from the installed kit, so a consumer installs nothing beyond the kit.
|
|
31
|
+
|
|
32
|
+
## Arguments
|
|
33
|
+
|
|
34
|
+
It takes none.
|
|
35
|
+
|
|
36
|
+
## Exit codes
|
|
37
|
+
|
|
38
|
+
| Code | When |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| 0 | no tracked file is unreferenced |
|
|
41
|
+
| 1 | a tracked file is unreferenced or the repository holds no Knip configuration |
|
|
42
|
+
| 2 | the repository tracks no TypeScript source, Knip cannot run, or its configuration does not parse |
|
|
43
|
+
|
|
44
|
+
## Sample output
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
unused: 1 unreferenced file(s):
|
|
48
|
+
src/planted-dead.ts
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A passing run counts the files it judged:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
unused: no unreferenced files among 110 tracked .ts/.tsx file(s)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Opting out
|
|
58
|
+
|
|
59
|
+
`checks-lint` runs this gate only when the repository tracks `.ts` or `.tsx` files.
|
|
60
|
+
A repository that tracks one names its entries in a Knip configuration.
|
|
61
|
+
|
|
62
|
+
## Related topics
|
|
63
|
+
|
|
64
|
+
- [checks-lint](checks-lint.md)
|
|
65
|
+
- [checks-repetition](checks-repetition.md)
|
package/knip-base.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avi2dg/checks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Deterministic checks shared across the captain's TypeScript repos",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -13,53 +13,54 @@
|
|
|
13
13
|
"type": "module",
|
|
14
14
|
"files": [
|
|
15
15
|
"CHANGELOG.md",
|
|
16
|
-
"CONTRIBUTING.md",
|
|
17
16
|
"docs/configs/",
|
|
18
17
|
"docs/design.md",
|
|
19
18
|
"docs/gates/",
|
|
20
19
|
"bunfig.toml",
|
|
21
20
|
"commitlint.config.js",
|
|
22
21
|
"dependency-cruiser.config.js",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
22
|
+
"knip-base.json",
|
|
23
|
+
"src/quality/lint-coverage.sh",
|
|
24
|
+
"src/core/main.ts",
|
|
25
|
+
"src/core/git.ts",
|
|
26
|
+
"src/core/gates.ts",
|
|
27
|
+
"src/core/lint.ts",
|
|
28
|
+
"src/delivery/changelog.ts",
|
|
29
|
+
"src/delivery/changelog-write.ts",
|
|
30
|
+
"src/delivery/release-notes.ts",
|
|
31
|
+
"src/delivery/release-report.ts",
|
|
32
|
+
"src/testing/test-layout.ts",
|
|
33
|
+
"src/testing/test.ts",
|
|
34
|
+
"src/testing/test-skips.ts",
|
|
35
|
+
"src/core/swc.ts",
|
|
36
|
+
"src/testing/test-report.ts",
|
|
37
|
+
"src/testing/flake.ts",
|
|
38
|
+
"src/delivery/commit-identity.ts",
|
|
39
|
+
"src/testing/mutation-compare.ts",
|
|
40
|
+
"src/testing/subsumed-tests.ts",
|
|
41
|
+
"src/delivery/ci-wiring.ts",
|
|
42
|
+
"src/delivery/shell-command.ts",
|
|
43
|
+
"src/quality/comment-matchers.ts",
|
|
44
|
+
"src/docs/prose-matchers.ts",
|
|
45
|
+
"src/quality/comments.ts",
|
|
46
|
+
"src/quality/comment-gate.ts",
|
|
47
|
+
"src/complexity/suppressions-ratchet.ts",
|
|
48
|
+
"src/complexity/repetition.ts",
|
|
49
|
+
"src/complexity/unused.ts",
|
|
50
|
+
"src/complexity/exports.ts",
|
|
51
|
+
"src/complexity/knip.ts",
|
|
52
|
+
"src/testing/quarantine-clock.ts",
|
|
53
|
+
"src/docs/docs.ts",
|
|
54
|
+
"src/dependencies/vendor.ts",
|
|
55
|
+
"src/dependencies/vendor-args.ts",
|
|
56
|
+
"src/docs/doc-outline.ts",
|
|
57
|
+
"src/docs/doc-rules.ts",
|
|
58
|
+
"src/docs/doc-references.ts",
|
|
59
|
+
"src/docs/doc-snapshot.ts",
|
|
60
|
+
"src/docs/doc-templates.ts",
|
|
61
|
+
"dist/templates/",
|
|
62
|
+
"src/quality/presets/effect.oxlint.json",
|
|
63
|
+
"src/quality/presets/effect.language-service.json",
|
|
63
64
|
"oxlintrc.json",
|
|
64
65
|
"stryker.preset.js",
|
|
65
66
|
"tsconfig.effect.json",
|
|
@@ -67,77 +68,43 @@
|
|
|
67
68
|
"dist/readability/index.js"
|
|
68
69
|
],
|
|
69
70
|
"exports": {
|
|
70
|
-
"./bunfig.toml": "./bunfig.toml",
|
|
71
|
-
"./commitlint.config.js": "./commitlint.config.js",
|
|
72
71
|
"./dependency-cruiser.config.js": "./dependency-cruiser.config.js",
|
|
73
|
-
"./
|
|
74
|
-
"./scripts/
|
|
75
|
-
"./scripts/
|
|
76
|
-
"./scripts/
|
|
77
|
-
"./scripts/release-notes.ts": "./scripts/release-notes.ts",
|
|
78
|
-
"./scripts/release-report.ts": "./scripts/release-report.ts",
|
|
79
|
-
"./scripts/test-layout.ts": "./scripts/test-layout.ts",
|
|
80
|
-
"./scripts/test.ts": "./scripts/test.ts",
|
|
81
|
-
"./scripts/test-skips.ts": "./scripts/test-skips.ts",
|
|
82
|
-
"./scripts/flake.ts": "./scripts/flake.ts",
|
|
83
|
-
"./scripts/commit-identity.ts": "./scripts/commit-identity.ts",
|
|
84
|
-
"./scripts/mutation-compare.ts": "./scripts/mutation-compare.ts",
|
|
85
|
-
"./scripts/subsumed-tests.ts": "./scripts/subsumed-tests.ts",
|
|
86
|
-
"./scripts/ci-wiring.ts": "./scripts/ci-wiring.ts",
|
|
87
|
-
"./scripts/comment-matchers.ts": "./scripts/comment-matchers.ts",
|
|
88
|
-
"./scripts/prose-matchers.ts": "./scripts/prose-matchers.ts",
|
|
89
|
-
"./scripts/comments.ts": "./scripts/comments.ts",
|
|
90
|
-
"./scripts/comment-gate.ts": "./scripts/comment-gate.ts",
|
|
91
|
-
"./scripts/suppressions-ratchet.ts": "./scripts/suppressions-ratchet.ts",
|
|
92
|
-
"./scripts/backtest.ts": "./scripts/backtest.ts",
|
|
93
|
-
"./scripts/repetition.ts": "./scripts/repetition.ts",
|
|
94
|
-
"./scripts/quarantine-clock.ts": "./scripts/quarantine-clock.ts",
|
|
95
|
-
"./scripts/docs.ts": "./scripts/docs.ts",
|
|
96
|
-
"./scripts/vendor.ts": "./scripts/vendor.ts",
|
|
97
|
-
"./templates/readme.md": "./templates/readme.md",
|
|
98
|
-
"./templates/changelog.md": "./templates/changelog.md",
|
|
99
|
-
"./templates/adr.md": "./templates/adr.md",
|
|
100
|
-
"./templates/agents.md": "./templates/agents.md",
|
|
101
|
-
"./templates/claude.md": "./templates/claude.md",
|
|
102
|
-
"./templates/tutorial.md": "./templates/tutorial.md",
|
|
103
|
-
"./templates/how-to.md": "./templates/how-to.md",
|
|
104
|
-
"./templates/reference.md": "./templates/reference.md",
|
|
105
|
-
"./templates/explanation.md": "./templates/explanation.md",
|
|
106
|
-
"./presets/effect.oxlint.json": "./presets/effect.oxlint.json",
|
|
107
|
-
"./presets/effect.language-service.json": "./presets/effect.language-service.json",
|
|
108
|
-
"./oxlintrc.json": "./oxlintrc.json",
|
|
72
|
+
"./knip-base.json": "./knip-base.json",
|
|
73
|
+
"./scripts/test-skips.ts": "./src/testing/test-skips.ts",
|
|
74
|
+
"./scripts/comment-matchers.ts": "./src/quality/comment-matchers.ts",
|
|
75
|
+
"./scripts/prose-matchers.ts": "./src/docs/prose-matchers.ts",
|
|
109
76
|
"./stryker.preset.js": "./stryker.preset.js",
|
|
110
|
-
"./
|
|
111
|
-
"./
|
|
112
|
-
"./dist/readability/index.js": "./dist/readability/index.js"
|
|
77
|
+
"./templates/*": "./dist/templates/*",
|
|
78
|
+
"./tsconfig.effect.json": "./tsconfig.effect.json"
|
|
113
79
|
},
|
|
114
80
|
"bin": {
|
|
115
|
-
"checks-lint": "
|
|
116
|
-
"checks-changelog": "
|
|
117
|
-
"checks-release-notes": "
|
|
118
|
-
"checks-release-report": "
|
|
119
|
-
"checks-lint-coverage": "
|
|
120
|
-
"checks-test-layout": "
|
|
121
|
-
"checks-test": "
|
|
122
|
-
"checks-flake": "
|
|
123
|
-
"checks-commit-identity": "
|
|
124
|
-
"checks-mutation-compare": "
|
|
125
|
-
"checks-subsumed-tests": "
|
|
126
|
-
"checks-ci-wiring": "
|
|
127
|
-
"checks-comment-gate": "
|
|
128
|
-
"checks-suppressions-ratchet": "
|
|
129
|
-
"checks-
|
|
130
|
-
"checks-
|
|
131
|
-
"checks-
|
|
132
|
-
"checks-
|
|
133
|
-
"checks-
|
|
81
|
+
"checks-lint": "src/core/lint.ts",
|
|
82
|
+
"checks-changelog": "src/delivery/changelog-write.ts",
|
|
83
|
+
"checks-release-notes": "src/delivery/release-notes.ts",
|
|
84
|
+
"checks-release-report": "src/delivery/release-report.ts",
|
|
85
|
+
"checks-lint-coverage": "src/quality/lint-coverage.sh",
|
|
86
|
+
"checks-test-layout": "src/testing/test-layout.ts",
|
|
87
|
+
"checks-test": "src/testing/test.ts",
|
|
88
|
+
"checks-flake": "src/testing/flake.ts",
|
|
89
|
+
"checks-commit-identity": "src/delivery/commit-identity.ts",
|
|
90
|
+
"checks-mutation-compare": "src/testing/mutation-compare.ts",
|
|
91
|
+
"checks-subsumed-tests": "src/testing/subsumed-tests.ts",
|
|
92
|
+
"checks-ci-wiring": "src/delivery/ci-wiring.ts",
|
|
93
|
+
"checks-comment-gate": "src/quality/comment-gate.ts",
|
|
94
|
+
"checks-suppressions-ratchet": "src/complexity/suppressions-ratchet.ts",
|
|
95
|
+
"checks-repetition": "src/complexity/repetition.ts",
|
|
96
|
+
"checks-unused": "src/complexity/unused.ts",
|
|
97
|
+
"checks-exports": "src/complexity/exports.ts",
|
|
98
|
+
"checks-quarantine-clock": "src/testing/quarantine-clock.ts",
|
|
99
|
+
"checks-docs": "src/docs/docs.ts",
|
|
100
|
+
"checks-vendor": "src/dependencies/vendor.ts"
|
|
134
101
|
},
|
|
135
102
|
"scripts": {
|
|
136
|
-
"prepare": "bun
|
|
137
|
-
"build": "bun build effect-channel/index.ts --outdir dist/effect-channel --target node --format esm && bun build readability/index.ts --outdir dist/readability --target node --format esm && bun scripts/doc-templates-write.ts && bun
|
|
138
|
-
"lint": "oxlint --type-aware && bun
|
|
103
|
+
"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",
|
|
104
|
+
"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",
|
|
105
|
+
"lint": "oxlint --type-aware && bun src/core/lint.ts && depcruise --config .dependency-cruiser.cjs .",
|
|
139
106
|
"typecheck": "tsc --noEmit && effect-tsgo diagnostics --project tsconfig.json --format text --strict",
|
|
140
|
-
"test": "bun
|
|
107
|
+
"test": "bun src/testing/test.ts"
|
|
141
108
|
},
|
|
142
109
|
"peerDependencies": {
|
|
143
110
|
"@swc/core": "1.16.2",
|
|
@@ -153,7 +120,6 @@
|
|
|
153
120
|
"@oxlint/plugins": "1.83.0",
|
|
154
121
|
"@swc/core": "1.16.2",
|
|
155
122
|
"@types/bun": "^1.3.0",
|
|
156
|
-
"ajv": "8.20.0",
|
|
157
123
|
"dependency-cruiser": "18.4.0",
|
|
158
124
|
"effect": "4.0.0-rc.115",
|
|
159
125
|
"jscpd": "5.3.2",
|
|
@@ -165,6 +131,7 @@
|
|
|
165
131
|
"@commitlint/cli": "21.2.3",
|
|
166
132
|
"@commitlint/config-conventional": "21.2.3",
|
|
167
133
|
"@effect/platform-bun": "4.0.0-rc.115",
|
|
168
|
-
"@effect/platform-node-shared": "4.0.0-rc.115"
|
|
134
|
+
"@effect/platform-node-shared": "4.0.0-rc.115",
|
|
135
|
+
"knip": "6.38.0"
|
|
169
136
|
}
|
|
170
137
|
}
|
|
@@ -0,0 +1,196 @@
|
|
|
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
|
+
export type UnusedSymbol = {
|
|
8
|
+
readonly file: string;
|
|
9
|
+
readonly kind: "export" | "type";
|
|
10
|
+
readonly name: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type Baseline = readonly UnusedSymbol[];
|
|
14
|
+
|
|
15
|
+
export type Held = {
|
|
16
|
+
readonly reported: readonly UnusedSymbol[];
|
|
17
|
+
readonly base: Baseline;
|
|
18
|
+
readonly head: Baseline;
|
|
19
|
+
readonly unusedAtBase: readonly UnusedSymbol[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type Drift = {
|
|
23
|
+
readonly unlisted: readonly UnusedSymbol[];
|
|
24
|
+
readonly added: readonly UnusedSymbol[];
|
|
25
|
+
readonly stale: readonly UnusedSymbol[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const NAME = "exports";
|
|
29
|
+
export const BASELINE_FILE = "exports-baseline.json";
|
|
30
|
+
const INCLUDED = ["--include", "exports,types"];
|
|
31
|
+
const WRITE = "--write";
|
|
32
|
+
const USAGE = `usage: exports.ts <ref> | <base-ref> <head-ref> | ${WRITE}`;
|
|
33
|
+
|
|
34
|
+
class ExportsError extends Schema.TaggedError<ExportsError>()("ExportsError", {
|
|
35
|
+
message: Schema.String,
|
|
36
|
+
}) {}
|
|
37
|
+
|
|
38
|
+
const KnipSymbol = Schema.Struct({ name: Schema.String });
|
|
39
|
+
|
|
40
|
+
const decodeKnipReport = Schema.decodeUnknownEffect(
|
|
41
|
+
Schema.fromJsonString(
|
|
42
|
+
Schema.Struct({
|
|
43
|
+
issues: Schema.Array(
|
|
44
|
+
Schema.Struct({
|
|
45
|
+
file: Schema.String,
|
|
46
|
+
exports: Schema.optional(Schema.Array(KnipSymbol)),
|
|
47
|
+
types: Schema.optional(Schema.Array(KnipSymbol)),
|
|
48
|
+
}),
|
|
49
|
+
),
|
|
50
|
+
}),
|
|
51
|
+
),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const decodeBaseline = Schema.decodeUnknownEffect(
|
|
55
|
+
Schema.fromJsonString(
|
|
56
|
+
Schema.Array(
|
|
57
|
+
Schema.Struct({
|
|
58
|
+
file: Schema.String,
|
|
59
|
+
kind: Schema.Literals(["export", "type"]),
|
|
60
|
+
name: Schema.String,
|
|
61
|
+
}),
|
|
62
|
+
),
|
|
63
|
+
),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
function byPosition(a: UnusedSymbol, b: UnusedSymbol): number {
|
|
67
|
+
return a.file.localeCompare(b.file) || a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const symbolsOf = Effect.fn("symbolsOf")(function* (stdout: string) {
|
|
71
|
+
const { issues } = yield* decodeKnipReport(stdout).pipe(
|
|
72
|
+
Effect.mapError((cause) => new ExportsError({ message: `knip's JSON report does not decode: ${cause.message}` })),
|
|
73
|
+
);
|
|
74
|
+
return issues
|
|
75
|
+
.flatMap(({ file, exports = [], types = [] }): readonly UnusedSymbol[] => [
|
|
76
|
+
...exports.map(({ name }): UnusedSymbol => ({ file, kind: "export", name })),
|
|
77
|
+
...types.map(({ name }): UnusedSymbol => ({ file, kind: "type", name })),
|
|
78
|
+
])
|
|
79
|
+
.toSorted(byPosition);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export const baselineOf = Effect.fn("baselineOf")(function* (text: string, where: string) {
|
|
83
|
+
return yield* decodeBaseline(text).pipe(
|
|
84
|
+
Effect.mapError((cause) => new ExportsError({ message: `${where} does not decode: ${cause.message}` })),
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const baselinePath = Effect.fn("baselinePath")(function* (root: string) {
|
|
89
|
+
return (yield* Path.Path).join(root, BASELINE_FILE);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const baselineInTree = Effect.fn("baselineInTree")(function* (root: string) {
|
|
93
|
+
const fs = yield* FileSystem.FileSystem;
|
|
94
|
+
const at = yield* baselinePath(root);
|
|
95
|
+
if (!(yield* fs.exists(at))) return [] satisfies Baseline;
|
|
96
|
+
return yield* fs.readFileString(at).pipe(Effect.flatMap((text) => baselineOf(text, BASELINE_FILE)));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const baselineAt = Effect.fn("baselineAt")(function* (treeish: string, root: string) {
|
|
100
|
+
const blob = (yield* git(["ls-tree", "--object-only", treeish, "--", BASELINE_FILE], root)).trim();
|
|
101
|
+
if (blob === "") return [] satisfies Baseline;
|
|
102
|
+
return yield* baselineOf(yield* git(["cat-file", "blob", blob], root), `${BASELINE_FILE} at ${treeish}`);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const keyOf = ({ file, kind, name }: UnusedSymbol): string => `${kind}\0${file}\0${name}`;
|
|
106
|
+
|
|
107
|
+
function outside(symbols: readonly UnusedSymbol[], others: readonly UnusedSymbol[]): readonly UnusedSymbol[] {
|
|
108
|
+
const known = new Set(others.map(keyOf));
|
|
109
|
+
return symbols.filter((symbol) => !known.has(keyOf(symbol))).toSorted(byPosition);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function driftOf({ reported, base, head, unusedAtBase }: Held): Drift {
|
|
113
|
+
return {
|
|
114
|
+
unlisted: outside(reported, head),
|
|
115
|
+
added: outside(head, [...base, ...unusedAtBase]),
|
|
116
|
+
stale: outside(head, reported),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function describe(symbol: UnusedSymbol): string {
|
|
121
|
+
return ` ${symbol.file}: ${symbol.name} (${symbol.kind})`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function report(head: Baseline, { unlisted, added, stale }: Drift): string {
|
|
125
|
+
const sections = [
|
|
126
|
+
{ symbols: unlisted, what: `unused export(s) not in ${BASELINE_FILE}:` },
|
|
127
|
+
{ symbols: added, what: `${BASELINE_FILE} export(s) the range adds that its base did not leave unused, remove the export instead:` },
|
|
128
|
+
{ symbols: stale, what: `${BASELINE_FILE} export(s) no longer reported, remove them:` },
|
|
129
|
+
].filter(({ symbols }) => symbols.length > 0);
|
|
130
|
+
if (sections.length > 0) {
|
|
131
|
+
return sections.flatMap(({ symbols, what }) => [`${NAME}: ${symbols.length} ${what}`, ...symbols.map(describe)]).join("\n");
|
|
132
|
+
}
|
|
133
|
+
return head.length === 0
|
|
134
|
+
? `${NAME}: no unused exports or types`
|
|
135
|
+
: `${NAME}: ${head.length} unused export(s) in ${BASELINE_FILE}, and no new ones`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const scan = scanTree(NAME, INCLUDED).pipe(Effect.mapError((cause) => new ExportsError({ message: cause.message })));
|
|
139
|
+
|
|
140
|
+
// Knip loads the configuration's imports, such as the kit's knip-base.json, from a node_modules the checkout lacks.
|
|
141
|
+
const unusedAt = Effect.fn("unusedAt")(
|
|
142
|
+
function* (rev: string, root: string) {
|
|
143
|
+
const files = yield* pathsAt(rev, [], root);
|
|
144
|
+
if (files.length === 0) return [] satisfies readonly UnusedSymbol[];
|
|
145
|
+
const fs = yield* FileSystem.FileSystem;
|
|
146
|
+
const path = yield* Path.Path;
|
|
147
|
+
const tree = yield* checkoutFiles(rev, files, yield* fs.makeTempDirectoryScoped({ prefix: "checks-exports-" }), root);
|
|
148
|
+
const modules = path.join(root, "node_modules");
|
|
149
|
+
if (yield* fs.exists(modules)) yield* fs.symlink(modules, path.join(tree, "node_modules"));
|
|
150
|
+
const reported = yield* knipReport(tree, INCLUDED).pipe(
|
|
151
|
+
Effect.mapError((cause) => new ExportsError({ message: `at ${rev}: ${cause.message}` })),
|
|
152
|
+
);
|
|
153
|
+
return reported.kind === "unconfigured" ? [] : yield* symbolsOf(reported.stdout);
|
|
154
|
+
},
|
|
155
|
+
Effect.scoped,
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Knip scans the checked-out tree, and a pull request's merge checkout holds the base branch tip beside the range head.
|
|
159
|
+
const judgedBase = Effect.fn("judgedBase")(function* (first: string, second: string | undefined, root: string) {
|
|
160
|
+
const { base } = yield* rangeEnds(first, second, root);
|
|
161
|
+
if (second === undefined) return base;
|
|
162
|
+
const head = yield* commitOf(second, root);
|
|
163
|
+
const parents = (yield* git(["rev-parse", "HEAD^@"], root)).split("\n").filter((line) => line !== "");
|
|
164
|
+
const [other, ...rest] = parents.filter((parent) => parent !== head);
|
|
165
|
+
return parents.includes(head) && other !== undefined && rest.length === 0 ? other : base;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const check = Effect.fn("check")(function* (first: string, second: string | undefined) {
|
|
169
|
+
const { root, reported } = yield* scan;
|
|
170
|
+
if (reported.kind === "unconfigured") return false;
|
|
171
|
+
const baseRev = yield* judgedBase(first, second, root);
|
|
172
|
+
const base = yield* baselineAt(baseRev, root);
|
|
173
|
+
const head = yield* baselineInTree(root);
|
|
174
|
+
const unusedAtBase = outside(head, base).length === 0 ? [] : yield* unusedAt(baseRev, root);
|
|
175
|
+
const drift = driftOf({ reported: yield* symbolsOf(reported.stdout), base, head, unusedAtBase });
|
|
176
|
+
yield* Console.log(report(head, drift));
|
|
177
|
+
return drift.unlisted.length === 0 && drift.added.length === 0 && drift.stale.length === 0;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const seed = Effect.gen(function* () {
|
|
181
|
+
const { root, reported } = yield* scan;
|
|
182
|
+
if (reported.kind === "unconfigured") return false;
|
|
183
|
+
const symbols = yield* symbolsOf(reported.stdout);
|
|
184
|
+
yield* (yield* FileSystem.FileSystem).writeFileString(yield* baselinePath(root), `${JSON.stringify(symbols, null, 2)}\n`);
|
|
185
|
+
yield* Console.log(`${NAME}: wrote ${symbols.length} unused export(s) to ${BASELINE_FILE}`);
|
|
186
|
+
return true;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const gate = Effect.gen(function* () {
|
|
190
|
+
const args = process.argv.slice(2);
|
|
191
|
+
if (args.length === 1 && args[0] === WRITE) return yield* seed;
|
|
192
|
+
const { first, second } = yield* refArgs(args, USAGE);
|
|
193
|
+
return yield* check(first, second);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
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) as unknown,
|
|
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
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Console, Effect, FileSystem, Path, Schema } from "effect";
|
|
3
|
-
import { changedPaths, checkoutFiles, collect, git, pathsAt, rangeFromArgs } from "
|
|
4
|
-
import { runMain } from "
|
|
3
|
+
import { changedPaths, checkoutFiles, collect, git, pathsAt, rangeFromArgs } from "../core/git.ts";
|
|
4
|
+
import { runMain } from "../core/main.ts";
|
|
5
5
|
|
|
6
6
|
export type Fragment = {
|
|
7
7
|
readonly file: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Console, Effect, Schema } from "effect";
|
|
3
|
-
import { git, commitOf, parentOrEmptyTree, refArgs } from "
|
|
4
|
-
import { runMain } from "
|
|
3
|
+
import { git, commitOf, parentOrEmptyTree, refArgs } from "../core/git.ts";
|
|
4
|
+
import { runMain } from "../core/main.ts";
|
|
5
5
|
|
|
6
6
|
export const SUPPRESSIONS = "oxlint-suppressions.json";
|
|
7
7
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Console, Effect, Schema } from "effect";
|
|
3
|
+
import { runMain } from "../core/main.ts";
|
|
4
|
+
import { scanTree } from "./knip.ts";
|
|
5
|
+
|
|
6
|
+
export type Scan = { readonly tracked: number; readonly files: readonly string[] };
|
|
7
|
+
|
|
8
|
+
const NAME = "unused";
|
|
9
|
+
const JUDGED = /\.tsx?$/;
|
|
10
|
+
|
|
11
|
+
class UnusedError extends Schema.TaggedError<UnusedError>()("UnusedError", {
|
|
12
|
+
message: Schema.String,
|
|
13
|
+
}) {}
|
|
14
|
+
|
|
15
|
+
const decodeKnipReport = Schema.decodeUnknownEffect(
|
|
16
|
+
Schema.fromJsonString(
|
|
17
|
+
Schema.Struct({
|
|
18
|
+
issues: Schema.Array(Schema.Struct({ files: Schema.optional(Schema.Array(Schema.Struct({ name: Schema.String }))) })),
|
|
19
|
+
}),
|
|
20
|
+
),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
export const filesOf = Effect.fn("filesOf")(function* (stdout: string) {
|
|
24
|
+
const { issues } = yield* decodeKnipReport(stdout).pipe(
|
|
25
|
+
Effect.mapError((cause) => new UnusedError({ message: `knip's JSON report does not decode: ${cause.message}` })),
|
|
26
|
+
);
|
|
27
|
+
return issues
|
|
28
|
+
.flatMap(({ files = [] }) => files.map(({ name }) => name))
|
|
29
|
+
.filter((file) => JUDGED.test(file))
|
|
30
|
+
.toSorted();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export function report({ tracked, files }: Scan): string {
|
|
34
|
+
if (files.length === 0) return `${NAME}: no unreferenced files among ${tracked} tracked .ts/.tsx file(s)`;
|
|
35
|
+
return [`${NAME}: ${files.length} unreferenced file(s):`, ...files.map((file) => ` ${file}`)].join("\n");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const unused = Effect.gen(function* () {
|
|
39
|
+
const { tracked, reported } = yield* scanTree(NAME, ["--files"]).pipe(
|
|
40
|
+
Effect.mapError((cause) => new UnusedError({ message: cause.message })),
|
|
41
|
+
);
|
|
42
|
+
if (reported.kind === "unconfigured") return false;
|
|
43
|
+
const files = yield* filesOf(reported.stdout);
|
|
44
|
+
yield* Console.log(report({ tracked, files }));
|
|
45
|
+
return files.length === 0;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (import.meta.main) runMain(NAME, unused);
|