slop-police 0.2.0 → 0.3.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/README.md +111 -10
- package/bin/slop-police.js +30 -11
- package/lib/binary.js +18 -0
- package/lib/index.d.ts +119 -0
- package/lib/index.js +35 -0
- package/lib/rule.js +54 -0
- package/lib/runner.js +177 -0
- package/lib/testing.d.ts +21 -0
- package/lib/testing.js +40 -0
- package/lib/typescript/LICENSE +55 -0
- package/lib/typescript/NOTICE.txt +436 -0
- package/lib/typescript/dist/api/compilerOptions.d.ts +108 -0
- package/lib/typescript/dist/api/compilerOptions.js +1 -0
- package/lib/typescript/dist/api/fs.d.ts +22 -0
- package/lib/typescript/dist/api/fs.js +108 -0
- package/lib/typescript/dist/api/node/encoder.d.ts +14 -0
- package/lib/typescript/dist/api/node/encoder.generated.d.ts +4 -0
- package/lib/typescript/dist/api/node/encoder.generated.js +73 -0
- package/lib/typescript/dist/api/node/encoder.js +286 -0
- package/lib/typescript/dist/api/node/msgpack.d.ts +31 -0
- package/lib/typescript/dist/api/node/msgpack.js +212 -0
- package/lib/typescript/dist/api/node/node.d.ts +75 -0
- package/lib/typescript/dist/api/node/node.generated.d.ts +181 -0
- package/lib/typescript/dist/api/node/node.generated.js +834 -0
- package/lib/typescript/dist/api/node/node.infrastructure.d.ts +61 -0
- package/lib/typescript/dist/api/node/node.infrastructure.js +121 -0
- package/lib/typescript/dist/api/node/node.js +297 -0
- package/lib/typescript/dist/api/node/protocol.d.ts +28 -0
- package/lib/typescript/dist/api/node/protocol.generated.d.ts +3 -0
- package/lib/typescript/dist/api/node/protocol.generated.js +240 -0
- package/lib/typescript/dist/api/node/protocol.js +29 -0
- package/lib/typescript/dist/api/node/wtf8.d.ts +5 -0
- package/lib/typescript/dist/api/node/wtf8.js +60 -0
- package/lib/typescript/dist/api/options.d.ts +31 -0
- package/lib/typescript/dist/api/options.js +10 -0
- package/lib/typescript/dist/api/path.d.ts +109 -0
- package/lib/typescript/dist/api/path.js +507 -0
- package/lib/typescript/dist/api/proto.d.ts +238 -0
- package/lib/typescript/dist/api/proto.js +36 -0
- package/lib/typescript/dist/api/sourceFileCache.d.ts +78 -0
- package/lib/typescript/dist/api/sourceFileCache.js +168 -0
- package/lib/typescript/dist/api/sync/api.d.ts +528 -0
- package/lib/typescript/dist/api/sync/api.js +1664 -0
- package/lib/typescript/dist/api/sync/client.d.ts +29 -0
- package/lib/typescript/dist/api/sync/client.js +123 -0
- package/lib/typescript/dist/api/sync/types.d.ts +309 -0
- package/lib/typescript/dist/api/sync/types.js +1 -0
- package/lib/typescript/dist/api/syncChannel.d.ts +113 -0
- package/lib/typescript/dist/api/syncChannel.js +507 -0
- package/lib/typescript/dist/api/timing.d.ts +174 -0
- package/lib/typescript/dist/api/timing.js +148 -0
- package/lib/typescript/dist/ast/ast.d.ts +99 -0
- package/lib/typescript/dist/ast/ast.generated.d.ts +1160 -0
- package/lib/typescript/dist/ast/ast.generated.js +3 -0
- package/lib/typescript/dist/ast/ast.js +6 -0
- package/lib/typescript/dist/ast/astnav.d.ts +18 -0
- package/lib/typescript/dist/ast/astnav.js +595 -0
- package/lib/typescript/dist/ast/clone.d.ts +15 -0
- package/lib/typescript/dist/ast/clone.js +103 -0
- package/lib/typescript/dist/ast/factory.generated.d.ts +519 -0
- package/lib/typescript/dist/ast/factory.generated.js +3056 -0
- package/lib/typescript/dist/ast/index.d.ts +19 -0
- package/lib/typescript/dist/ast/index.js +19 -0
- package/lib/typescript/dist/ast/is.d.ts +34 -0
- package/lib/typescript/dist/ast/is.generated.d.ts +328 -0
- package/lib/typescript/dist/ast/is.generated.js +1102 -0
- package/lib/typescript/dist/ast/is.js +232 -0
- package/lib/typescript/dist/ast/jsdoc.d.ts +10 -0
- package/lib/typescript/dist/ast/jsdoc.js +201 -0
- package/lib/typescript/dist/ast/scanner.d.ts +96 -0
- package/lib/typescript/dist/ast/scanner.js +2229 -0
- package/lib/typescript/dist/ast/utils.d.ts +16 -0
- package/lib/typescript/dist/ast/utils.js +66 -0
- package/lib/typescript/dist/ast/visitor.d.ts +13 -0
- package/lib/typescript/dist/ast/visitor.generated.d.ts +49 -0
- package/lib/typescript/dist/ast/visitor.generated.js +977 -0
- package/lib/typescript/dist/ast/visitor.js +46 -0
- package/lib/typescript/dist/enums/characterCodes.d.ts +1 -0
- package/lib/typescript/dist/enums/characterCodes.enum.d.ts +128 -0
- package/lib/typescript/dist/enums/characterCodes.enum.js +131 -0
- package/lib/typescript/dist/enums/characterCodes.js +129 -0
- package/lib/typescript/dist/enums/commentDirectiveType.d.ts +1 -0
- package/lib/typescript/dist/enums/commentDirectiveType.enum.d.ts +4 -0
- package/lib/typescript/dist/enums/commentDirectiveType.enum.js +5 -0
- package/lib/typescript/dist/enums/commentDirectiveType.js +5 -0
- package/lib/typescript/dist/enums/completionItemKind.d.ts +1 -0
- package/lib/typescript/dist/enums/completionItemKind.enum.d.ts +27 -0
- package/lib/typescript/dist/enums/completionItemKind.enum.js +29 -0
- package/lib/typescript/dist/enums/completionItemKind.js +29 -0
- package/lib/typescript/dist/enums/diagnosticCategory.d.ts +1 -0
- package/lib/typescript/dist/enums/diagnosticCategory.enum.d.ts +6 -0
- package/lib/typescript/dist/enums/diagnosticCategory.enum.js +8 -0
- package/lib/typescript/dist/enums/diagnosticCategory.js +8 -0
- package/lib/typescript/dist/enums/elementFlags.d.ts +1 -0
- package/lib/typescript/dist/enums/elementFlags.enum.d.ts +11 -0
- package/lib/typescript/dist/enums/elementFlags.enum.js +13 -0
- package/lib/typescript/dist/enums/elementFlags.js +13 -0
- package/lib/typescript/dist/enums/internalSymbolName.d.ts +1 -0
- package/lib/typescript/dist/enums/internalSymbolName.enum.d.ts +22 -0
- package/lib/typescript/dist/enums/internalSymbolName.enum.js +24 -0
- package/lib/typescript/dist/enums/internalSymbolName.js +24 -0
- package/lib/typescript/dist/enums/jsxEmit.d.ts +1 -0
- package/lib/typescript/dist/enums/jsxEmit.enum.d.ts +8 -0
- package/lib/typescript/dist/enums/jsxEmit.enum.js +10 -0
- package/lib/typescript/dist/enums/jsxEmit.js +10 -0
- package/lib/typescript/dist/enums/languageVariant.d.ts +1 -0
- package/lib/typescript/dist/enums/languageVariant.enum.d.ts +4 -0
- package/lib/typescript/dist/enums/languageVariant.enum.js +5 -0
- package/lib/typescript/dist/enums/languageVariant.js +5 -0
- package/lib/typescript/dist/enums/modifierFlags.d.ts +1 -0
- package/lib/typescript/dist/enums/modifierFlags.enum.d.ts +41 -0
- package/lib/typescript/dist/enums/modifierFlags.enum.js +43 -0
- package/lib/typescript/dist/enums/modifierFlags.js +43 -0
- package/lib/typescript/dist/enums/moduleDetectionKind.d.ts +1 -0
- package/lib/typescript/dist/enums/moduleDetectionKind.enum.d.ts +6 -0
- package/lib/typescript/dist/enums/moduleDetectionKind.enum.js +8 -0
- package/lib/typescript/dist/enums/moduleDetectionKind.js +8 -0
- package/lib/typescript/dist/enums/moduleKind.d.ts +1 -0
- package/lib/typescript/dist/enums/moduleKind.enum.d.ts +16 -0
- package/lib/typescript/dist/enums/moduleKind.enum.js +18 -0
- package/lib/typescript/dist/enums/moduleKind.js +18 -0
- package/lib/typescript/dist/enums/moduleResolutionKind.d.ts +1 -0
- package/lib/typescript/dist/enums/moduleResolutionKind.enum.d.ts +8 -0
- package/lib/typescript/dist/enums/moduleResolutionKind.enum.js +10 -0
- package/lib/typescript/dist/enums/moduleResolutionKind.js +10 -0
- package/lib/typescript/dist/enums/newLineKind.d.ts +1 -0
- package/lib/typescript/dist/enums/newLineKind.enum.d.ts +5 -0
- package/lib/typescript/dist/enums/newLineKind.enum.js +7 -0
- package/lib/typescript/dist/enums/newLineKind.js +7 -0
- package/lib/typescript/dist/enums/nodeBuilderFlags.d.ts +1 -0
- package/lib/typescript/dist/enums/nodeBuilderFlags.enum.d.ts +35 -0
- package/lib/typescript/dist/enums/nodeBuilderFlags.enum.js +37 -0
- package/lib/typescript/dist/enums/nodeBuilderFlags.js +37 -0
- package/lib/typescript/dist/enums/nodeFlags.d.ts +1 -0
- package/lib/typescript/dist/enums/nodeFlags.enum.d.ts +43 -0
- package/lib/typescript/dist/enums/nodeFlags.enum.js +45 -0
- package/lib/typescript/dist/enums/nodeFlags.js +45 -0
- package/lib/typescript/dist/enums/objectFlags.d.ts +1 -0
- package/lib/typescript/dist/enums/objectFlags.enum.d.ts +49 -0
- package/lib/typescript/dist/enums/objectFlags.enum.js +51 -0
- package/lib/typescript/dist/enums/objectFlags.js +51 -0
- package/lib/typescript/dist/enums/outerExpressionKinds.d.ts +1 -0
- package/lib/typescript/dist/enums/outerExpressionKinds.enum.d.ts +15 -0
- package/lib/typescript/dist/enums/outerExpressionKinds.enum.js +17 -0
- package/lib/typescript/dist/enums/outerExpressionKinds.js +17 -0
- package/lib/typescript/dist/enums/regularExpressionFlags.d.ts +1 -0
- package/lib/typescript/dist/enums/regularExpressionFlags.enum.d.ts +12 -0
- package/lib/typescript/dist/enums/regularExpressionFlags.enum.js +13 -0
- package/lib/typescript/dist/enums/regularExpressionFlags.js +13 -0
- package/lib/typescript/dist/enums/scriptKind.d.ts +1 -0
- package/lib/typescript/dist/enums/scriptKind.enum.d.ts +10 -0
- package/lib/typescript/dist/enums/scriptKind.enum.js +11 -0
- package/lib/typescript/dist/enums/scriptKind.js +11 -0
- package/lib/typescript/dist/enums/scriptTarget.d.ts +1 -0
- package/lib/typescript/dist/enums/scriptTarget.enum.d.ts +16 -0
- package/lib/typescript/dist/enums/scriptTarget.enum.js +17 -0
- package/lib/typescript/dist/enums/scriptTarget.js +17 -0
- package/lib/typescript/dist/enums/signatureFlags.d.ts +1 -0
- package/lib/typescript/dist/enums/signatureFlags.enum.d.ts +14 -0
- package/lib/typescript/dist/enums/signatureFlags.enum.js +16 -0
- package/lib/typescript/dist/enums/signatureFlags.js +16 -0
- package/lib/typescript/dist/enums/signatureKind.d.ts +1 -0
- package/lib/typescript/dist/enums/signatureKind.enum.d.ts +4 -0
- package/lib/typescript/dist/enums/signatureKind.enum.js +6 -0
- package/lib/typescript/dist/enums/signatureKind.js +6 -0
- package/lib/typescript/dist/enums/symbolFlags.d.ts +1 -0
- package/lib/typescript/dist/enums/symbolFlags.enum.d.ts +70 -0
- package/lib/typescript/dist/enums/symbolFlags.enum.js +72 -0
- package/lib/typescript/dist/enums/symbolFlags.js +72 -0
- package/lib/typescript/dist/enums/syntaxKind.d.ts +1 -0
- package/lib/typescript/dist/enums/syntaxKind.enum.d.ts +388 -0
- package/lib/typescript/dist/enums/syntaxKind.enum.js +390 -0
- package/lib/typescript/dist/enums/syntaxKind.js +390 -0
- package/lib/typescript/dist/enums/tokenFlags.d.ts +1 -0
- package/lib/typescript/dist/enums/tokenFlags.enum.d.ts +29 -0
- package/lib/typescript/dist/enums/tokenFlags.enum.js +31 -0
- package/lib/typescript/dist/enums/tokenFlags.js +31 -0
- package/lib/typescript/dist/enums/typeFlags.d.ts +1 -0
- package/lib/typescript/dist/enums/typeFlags.enum.d.ts +75 -0
- package/lib/typescript/dist/enums/typeFlags.enum.js +77 -0
- package/lib/typescript/dist/enums/typeFlags.js +77 -0
- package/lib/typescript/dist/enums/typePredicateKind.d.ts +1 -0
- package/lib/typescript/dist/enums/typePredicateKind.enum.d.ts +6 -0
- package/lib/typescript/dist/enums/typePredicateKind.enum.js +8 -0
- package/lib/typescript/dist/enums/typePredicateKind.js +8 -0
- package/lib/typescript/dist/internal/utils.d.ts +22 -0
- package/lib/typescript/dist/internal/utils.js +24 -0
- package/lib/typescript/lib/getExePath.d.ts +2 -0
- package/lib/typescript/lib/getExePath.js +70 -0
- package/lib/typescript/nodes-by-kind.d.ts +198 -0
- package/lib/typescript/package.json +10 -0
- package/package.json +18 -5
package/README.md
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">slop-police</h1>
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<p align="center"><b>Stop LLM crimes in your codebase</b></p>
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
<p align="center">
|
|
6
|
+
A type-aware TypeScript linter for the dead code and needless indirection AI agents leave behind.
|
|
7
|
+
</p>
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
| | pnpm | npm |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| Add to the project | `pnpm add -D slop-police` | `npm i -D slop-police` |
|
|
12
|
+
| Run | `pnpm exec slop-police` | `npx slop-police` |
|
|
13
|
+
| Run once, no install | `pnpm dlx slop-police` | `npx slop-police` |
|
|
14
|
+
|
|
15
|
+
Example output:
|
|
11
16
|
|
|
12
17
|
```
|
|
13
18
|
packages/frontend/src/store/deck-store.ts:42:3 error unused-methods Method DeckStore.reset is never used
|
|
14
19
|
packages/api/src/shared/links/links.ts:30:3 error unused-object-keys links.privacyPolicy is never read
|
|
15
20
|
|
|
16
|
-
2
|
|
21
|
+
2 errors, 0 warnings
|
|
17
22
|
```
|
|
18
23
|
|
|
19
24
|
## Configuration
|
|
@@ -45,6 +50,7 @@ slop-police reads `slop-police.json` from the current directory:
|
|
|
45
50
|
- `ignore`: glob patterns (relative to the config) for files to skip entirely.
|
|
46
51
|
- `testFiles`: glob patterns for tests. Usages inside tests count, but declarations inside tests are not reported.
|
|
47
52
|
- `rules`: `"error"`, `"warn"` or `"off"`, or `[severity, options]`. Rules that aren't listed are off.
|
|
53
|
+
- `plugins`: modules with [custom rules](#custom-rules), as paths relative to the config or package names.
|
|
48
54
|
|
|
49
55
|
Exit code is `1` when there are errors, `2` for an invalid config, `0` otherwise.
|
|
50
56
|
|
|
@@ -62,7 +68,7 @@ class Store {
|
|
|
62
68
|
new Store().load();
|
|
63
69
|
```
|
|
64
70
|
|
|
65
|
-
A member also counts as used when it implements an interface member that is used, when it's named in `Pick<Class, "name">`, or when an instance is passed to a JSX prop whose type requires it. Options: `includePrivate` and `includeProtected` (both `false` by default).
|
|
71
|
+
A member also counts as used when it implements an interface member that is used or that is declared outside the analyzed code (a library calls it), when it's named in `Pick<Class, "name">`, or when an instance is passed to a JSX prop whose type requires it. Options: `includePrivate` and `includeProtected` (both `false` by default).
|
|
66
72
|
|
|
67
73
|
### unused-object-keys
|
|
68
74
|
|
|
@@ -174,7 +180,7 @@ const enabled = await fetchFlag().catch((error) => { // Promise rejection is log
|
|
|
174
180
|
|
|
175
181
|
### no-stringified-unknown-error
|
|
176
182
|
|
|
177
|
-
`error instanceof Error ? error.message : String(error)` (in either order), which flattens a caught value into a string and drops its type, stack and cause. A plain `instanceof Error` check counts when the variable is named `error`, `err`, `e` or `cause`; an `||` of `instanceof` checks counts for any name.
|
|
183
|
+
`error instanceof Error ? error.message : String(error)` (in either order), which flattens a caught value into a string and drops its type, stack and cause. A plain `instanceof Error` check counts when the variable is named `error`, `err`, `e` or `cause`; an `||` of `instanceof` checks counts for any name. A function that does nothing but this conversion, like `getErrorMessage(error)`, is where it belongs and isn't reported; use it instead of repeating the expression.
|
|
178
184
|
|
|
179
185
|
```ts
|
|
180
186
|
try {
|
|
@@ -184,6 +190,101 @@ try {
|
|
|
184
190
|
}
|
|
185
191
|
```
|
|
186
192
|
|
|
193
|
+
## Custom rules
|
|
194
|
+
|
|
195
|
+
Write rules in TypeScript or JavaScript, list their modules in `plugins` and enable them in `rules` like built-in ones:
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
{
|
|
199
|
+
"projects": ["tsconfig.json"],
|
|
200
|
+
"plugins": ["./slop-rules.ts"],
|
|
201
|
+
"rules": {
|
|
202
|
+
"no-direct-fetch": ["error", { "allowIn": "src/api/" }],
|
|
203
|
+
"no-needless-nullish-fallback": "warn"
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
A plugin exports rules made with `defineRule`, by default or by name. `visit` has a handler per node kind, called for every node of that kind in every linted file:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
// slop-rules.ts
|
|
212
|
+
import { defineRule, isIdentifier, SyntaxKind } from "slop-police";
|
|
213
|
+
|
|
214
|
+
export const noDirectFetch = defineRule({
|
|
215
|
+
name: "no-direct-fetch",
|
|
216
|
+
// Option defaults. The config's options must have the same types.
|
|
217
|
+
options: { allowIn: "" },
|
|
218
|
+
visit: {
|
|
219
|
+
CallExpression(node, ctx) {
|
|
220
|
+
if (ctx.options.allowIn && ctx.file.path.startsWith(ctx.options.allowIn)) return;
|
|
221
|
+
if (isIdentifier(node.expression) && node.expression.text === "fetch") {
|
|
222
|
+
ctx.report(node.expression, "Use the API client instead of fetch()");
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
export const noNeedlessNullishFallback = defineRule({
|
|
229
|
+
name: "no-needless-nullish-fallback",
|
|
230
|
+
visit: {
|
|
231
|
+
BinaryExpression(node, ctx) {
|
|
232
|
+
if (node.operatorToken.kind !== SyntaxKind.QuestionQuestionToken) return;
|
|
233
|
+
const { checker } = ctx;
|
|
234
|
+
const type = checker.getTypeAtLocation(node.left);
|
|
235
|
+
if (!type) return;
|
|
236
|
+
const nullable = [checker.getUndefinedType(), checker.getNullType()].some((t) => checker.isTypeAssignableTo(t, type));
|
|
237
|
+
if (!nullable) ctx.report(node.right, `${node.left.getText()} is never null or undefined`);
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Nodes, `SyntaxKind`, the `is*` guards and the checker come from the TypeScript 7 API that slop-police runs on. The handler's `ctx` has:
|
|
244
|
+
|
|
245
|
+
- `options`: the defaults merged with the config's options.
|
|
246
|
+
- `file`: the file being visited: `path` (relative to the config), `fileName`, `isTest` and `sourceFile`.
|
|
247
|
+
- `checker` and `program`: those of the file's project. Each checker call is a round trip to the linter process, so check the syntax first.
|
|
248
|
+
- `report(node, message)`: reports a finding at the node's start. `// slop-police-ignore` comments work as for built-in rules.
|
|
249
|
+
|
|
250
|
+
For rules that compare files, `create(ctx)` returns the visitor instead, so its state lives in a closure, and `finish` runs after the last file:
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
import { defineRule, type Node } from "slop-police";
|
|
254
|
+
|
|
255
|
+
export const noRepeatedUrl = defineRule({
|
|
256
|
+
name: "no-repeated-url",
|
|
257
|
+
create(ctx) {
|
|
258
|
+
const byUrl = new Map<string, Node[]>();
|
|
259
|
+
return {
|
|
260
|
+
StringLiteral(node) {
|
|
261
|
+
if (node.text.startsWith("https://")) byUrl.set(node.text, [...(byUrl.get(node.text) ?? []), node]);
|
|
262
|
+
},
|
|
263
|
+
finish() {
|
|
264
|
+
for (const [url, nodes] of byUrl) {
|
|
265
|
+
if (nodes.length > 2) for (const node of nodes) ctx.report(node, `${url} is hard-coded in ${nodes.length} places`);
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Test rules with `lint` from `slop-police/testing`, which runs a rule alone on a source string (or on files, given an object of paths to contents) in a strict TypeScript project:
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
import { lint } from "slop-police/testing";
|
|
277
|
+
import { noDirectFetch } from "./slop-rules.ts";
|
|
278
|
+
|
|
279
|
+
test("no-direct-fetch", () => {
|
|
280
|
+
expect(lint(noDirectFetch, `fetch("/users");`)).toEqual([
|
|
281
|
+
{ line: 1, column: 1, message: "Use the API client instead of fetch()" },
|
|
282
|
+
]);
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Plugins need the npm launcher (`npx slop-police` or `pnpm exec slop-police`), which runs them in Node while the linter serves ASTs and types. TypeScript plugins need Node.js 22.18 or later, and published plugin packages must ship JavaScript.
|
|
287
|
+
|
|
187
288
|
## Ignoring findings
|
|
188
289
|
|
|
189
290
|
A `// slop-police-ignore` comment directly above a statement, class member or object property silences findings in it, including everything nested inside. List rule names to silence only those, and add a reason after `--`:
|
package/bin/slop-police.js
CHANGED
|
@@ -1,18 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { findBinary } from "../lib/binary.js";
|
|
5
6
|
|
|
6
7
|
let binary;
|
|
7
8
|
try {
|
|
8
|
-
binary =
|
|
9
|
-
} catch {
|
|
10
|
-
console.error(
|
|
11
|
-
`slop-police: no binary for ${process.platform}-${process.arch}. Supported: macOS and Linux on arm64 and x64 (use WSL on Windows).`,
|
|
12
|
-
);
|
|
9
|
+
binary = findBinary();
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.error(error.message);
|
|
13
12
|
process.exit(2);
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
const root = process.cwd();
|
|
16
|
+
const plugins = configuredPlugins(root);
|
|
17
|
+
if (plugins) {
|
|
18
|
+
// Plugin rules run here, in Node, with the binary serving ASTs and types.
|
|
19
|
+
const { run } = await import("../lib/runner.js");
|
|
20
|
+
process.exitCode = await run(binary, root, plugins);
|
|
21
|
+
} else {
|
|
22
|
+
const result = spawnSync(binary, process.argv.slice(2), { stdio: "inherit" });
|
|
23
|
+
if (result.error) throw result.error;
|
|
24
|
+
process.exitCode = result.status ?? 1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// configuredPlugins returns the config's plugins, if it has any. Any problem
|
|
28
|
+
// with the config is left to the binary to report.
|
|
29
|
+
function configuredPlugins(root) {
|
|
30
|
+
try {
|
|
31
|
+
const { plugins } = JSON.parse(fs.readFileSync(path.join(root, "slop-police.json"), "utf8"));
|
|
32
|
+
if (Array.isArray(plugins) && plugins.length > 0 && plugins.every((plugin) => typeof plugin === "string")) {
|
|
33
|
+
return plugins;
|
|
34
|
+
}
|
|
35
|
+
} catch {}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
package/lib/binary.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
|
|
3
|
+
// findBinary returns the path of the prebuilt binary from the platform package
|
|
4
|
+
// npm installed for this machine. SLOP_POLICE_BINARY overrides it, for
|
|
5
|
+
// developing slop-police itself.
|
|
6
|
+
export function findBinary() {
|
|
7
|
+
if (process.env.SLOP_POLICE_BINARY) {
|
|
8
|
+
return process.env.SLOP_POLICE_BINARY;
|
|
9
|
+
}
|
|
10
|
+
const platformPackage = `slop-police-${process.platform}-${process.arch}`;
|
|
11
|
+
try {
|
|
12
|
+
return createRequire(import.meta.url).resolve(`${platformPackage}/bin/slop-police`);
|
|
13
|
+
} catch {
|
|
14
|
+
throw new Error(
|
|
15
|
+
`slop-police: no binary for ${process.platform}-${process.arch}. Supported: macOS and Linux on arm64 and x64 (use WSL on Windows).`,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// TypeScript's API declarations use Symbol.dispose.
|
|
2
|
+
/// <reference lib="esnext.disposable" />
|
|
3
|
+
import type { Checker, Program } from "./typescript/dist/api/sync/api.js";
|
|
4
|
+
import type { Node, SourceFile, SyntaxKind } from "./typescript/dist/ast/index.js";
|
|
5
|
+
import type { NodesByKind } from "./typescript/nodes-by-kind.js";
|
|
6
|
+
|
|
7
|
+
export * from "./typescript/dist/ast/index.js";
|
|
8
|
+
export {
|
|
9
|
+
ElementFlags,
|
|
10
|
+
isBigIntLiteralType,
|
|
11
|
+
isBooleanLiteralType,
|
|
12
|
+
isClassOrInterfaceType,
|
|
13
|
+
isConditionalType,
|
|
14
|
+
isErrorType,
|
|
15
|
+
isIndexedAccessType,
|
|
16
|
+
isIndexType,
|
|
17
|
+
isIntersectionType,
|
|
18
|
+
isIntrinsicType,
|
|
19
|
+
isLiteralType,
|
|
20
|
+
isNumberLiteralType,
|
|
21
|
+
isObjectType,
|
|
22
|
+
isStringLiteralType,
|
|
23
|
+
isStringMappingType,
|
|
24
|
+
isSubstitutionType,
|
|
25
|
+
isTemplateLiteralType,
|
|
26
|
+
isTupleType,
|
|
27
|
+
isTypeParameter,
|
|
28
|
+
isTypeReference,
|
|
29
|
+
isUnionType,
|
|
30
|
+
NodeBuilderFlags,
|
|
31
|
+
ObjectFlags,
|
|
32
|
+
SignatureFlags,
|
|
33
|
+
SignatureKind,
|
|
34
|
+
SymbolFlags,
|
|
35
|
+
TypeFlags,
|
|
36
|
+
TypePredicateKind,
|
|
37
|
+
} from "./typescript/dist/api/sync/api.js";
|
|
38
|
+
export type {
|
|
39
|
+
Checker,
|
|
40
|
+
ConditionalType,
|
|
41
|
+
IndexedAccessType,
|
|
42
|
+
IndexType,
|
|
43
|
+
InterfaceType,
|
|
44
|
+
IntersectionType,
|
|
45
|
+
IntrinsicType,
|
|
46
|
+
LiteralType,
|
|
47
|
+
ObjectType,
|
|
48
|
+
Program,
|
|
49
|
+
Signature,
|
|
50
|
+
StringLiteralType,
|
|
51
|
+
Symbol,
|
|
52
|
+
TupleType,
|
|
53
|
+
Type,
|
|
54
|
+
TypeParameter,
|
|
55
|
+
TypeReference,
|
|
56
|
+
UnionType,
|
|
57
|
+
} from "./typescript/dist/api/sync/api.js";
|
|
58
|
+
|
|
59
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
|
60
|
+
|
|
61
|
+
/** Option defaults. The config's values must have the same JSON types. */
|
|
62
|
+
export type RuleOptions = Record<string, JsonValue>;
|
|
63
|
+
|
|
64
|
+
export interface LintedFile {
|
|
65
|
+
/** The path relative to slop-police.json, with forward slashes. */
|
|
66
|
+
readonly path: string;
|
|
67
|
+
/** The absolute path. */
|
|
68
|
+
readonly fileName: string;
|
|
69
|
+
/** Whether the file matches the config's testFiles. */
|
|
70
|
+
readonly isTest: boolean;
|
|
71
|
+
readonly sourceFile: SourceFile;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface RuleContext<Options extends RuleOptions = {}> {
|
|
75
|
+
/** The rule's option defaults, merged with the options from the config. */
|
|
76
|
+
readonly options: Options;
|
|
77
|
+
/** The file being visited. Undefined in finish. */
|
|
78
|
+
readonly file: LintedFile;
|
|
79
|
+
/** The type checker of the file's project. Undefined in finish. */
|
|
80
|
+
readonly checker: Checker;
|
|
81
|
+
/** The program of the file's project. Undefined in finish. */
|
|
82
|
+
readonly program: Program;
|
|
83
|
+
/**
|
|
84
|
+
* Reports a finding at the start of node, which can be in any linted file.
|
|
85
|
+
* `// slop-police-ignore` comments apply as for built-in rules.
|
|
86
|
+
*/
|
|
87
|
+
report(node: Node, message: string): void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
type NodeOfKind<Kind extends string> = Kind extends keyof NodesByKind ? NodesByKind[Kind] : Node;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Handlers by SyntaxKind name, called for every node of that kind in every
|
|
94
|
+
* linted file, plus finish, called once after the last file.
|
|
95
|
+
*/
|
|
96
|
+
export type Visitor<Options extends RuleOptions = {}> = {
|
|
97
|
+
[Kind in keyof typeof SyntaxKind]?: (node: NodeOfKind<Kind>, ctx: RuleContext<Options>) => void;
|
|
98
|
+
} & {
|
|
99
|
+
finish?: (ctx: RuleContext<Options>) => void;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
interface RuleBase<Options extends RuleOptions> {
|
|
103
|
+
/** The name to enable the rule with in slop-police.json and to list in ignore comments. */
|
|
104
|
+
name: string;
|
|
105
|
+
/** Option defaults, which also set the types of the options the config can pass. */
|
|
106
|
+
options?: Options;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type Rule<Options extends RuleOptions = {}> = RuleBase<Options> &
|
|
110
|
+
(
|
|
111
|
+
| { visit: Visitor<Options>; create?: never }
|
|
112
|
+
| {
|
|
113
|
+
/** Called once per run, for rules that keep state across files. Returns the visitor. */
|
|
114
|
+
create(ctx: RuleContext<Options>): Visitor<Options>;
|
|
115
|
+
visit?: never;
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
export declare function defineRule<Options extends RuleOptions = {}>(rule: Rule<Options>): Rule<Options>;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// The API for plugin rules: defineRule, plus TypeScript 7's AST (node types,
|
|
2
|
+
// SyntaxKind, is* guards and utilities) and the checker's enums and type
|
|
3
|
+
// guards.
|
|
4
|
+
export * from "./typescript/dist/ast/index.js";
|
|
5
|
+
export {
|
|
6
|
+
ElementFlags,
|
|
7
|
+
isBigIntLiteralType,
|
|
8
|
+
isBooleanLiteralType,
|
|
9
|
+
isClassOrInterfaceType,
|
|
10
|
+
isConditionalType,
|
|
11
|
+
isErrorType,
|
|
12
|
+
isIndexedAccessType,
|
|
13
|
+
isIndexType,
|
|
14
|
+
isIntersectionType,
|
|
15
|
+
isIntrinsicType,
|
|
16
|
+
isLiteralType,
|
|
17
|
+
isNumberLiteralType,
|
|
18
|
+
isObjectType,
|
|
19
|
+
isStringLiteralType,
|
|
20
|
+
isStringMappingType,
|
|
21
|
+
isSubstitutionType,
|
|
22
|
+
isTemplateLiteralType,
|
|
23
|
+
isTupleType,
|
|
24
|
+
isTypeParameter,
|
|
25
|
+
isTypeReference,
|
|
26
|
+
isUnionType,
|
|
27
|
+
NodeBuilderFlags,
|
|
28
|
+
ObjectFlags,
|
|
29
|
+
SignatureFlags,
|
|
30
|
+
SignatureKind,
|
|
31
|
+
SymbolFlags,
|
|
32
|
+
TypeFlags,
|
|
33
|
+
TypePredicateKind,
|
|
34
|
+
} from "./typescript/dist/api/sync/api.js";
|
|
35
|
+
export { defineRule } from "./rule.js";
|
package/lib/rule.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { SyntaxKind } from "./typescript/dist/ast/index.js";
|
|
3
|
+
|
|
4
|
+
// Marks rules made by defineRule, so plugins can export helpers next to them.
|
|
5
|
+
// Symbol.for, so that rules from another copy of slop-police count too.
|
|
6
|
+
const ruleBrand = Symbol.for("slop-police.rule");
|
|
7
|
+
|
|
8
|
+
export function defineRule(rule) {
|
|
9
|
+
const name = rule?.name;
|
|
10
|
+
if (typeof name !== "string" || !/^[^\s,]+$/.test(name)) {
|
|
11
|
+
throw new TypeError(`Rule name must be a non-empty string without spaces or commas, got ${JSON.stringify(name)}`);
|
|
12
|
+
}
|
|
13
|
+
if ((rule.visit === undefined) === (rule.create === undefined)) {
|
|
14
|
+
throw new TypeError(`Rule ${name} must have either visit or create`);
|
|
15
|
+
}
|
|
16
|
+
if (rule.create !== undefined && typeof rule.create !== "function") {
|
|
17
|
+
throw new TypeError(`Rule ${name}: create must be a function`);
|
|
18
|
+
}
|
|
19
|
+
// The defaults go through the binary, which merges the config's options
|
|
20
|
+
// into them, as JSON.
|
|
21
|
+
const { options } = rule;
|
|
22
|
+
if (
|
|
23
|
+
options !== undefined &&
|
|
24
|
+
(typeof options !== "object" || options === null || Array.isArray(options) || !isDeepStrictEqual(JSON.parse(JSON.stringify(options)), options))
|
|
25
|
+
) {
|
|
26
|
+
throw new TypeError(`Rule ${name}: options must be an object of JSON default values`);
|
|
27
|
+
}
|
|
28
|
+
if (rule.visit !== undefined) {
|
|
29
|
+
visitorHandlers(name, rule.visit);
|
|
30
|
+
}
|
|
31
|
+
return Object.freeze({ ...rule, [ruleBrand]: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isRule(value) {
|
|
35
|
+
return value?.[ruleBrand] === true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// visitorHandlers checks a visitor and returns its handlers as [kind, handler]
|
|
39
|
+
// pairs, where kind is a SyntaxKind or "finish".
|
|
40
|
+
export function visitorHandlers(ruleName, visitor) {
|
|
41
|
+
if (typeof visitor !== "object" || visitor === null) {
|
|
42
|
+
throw new TypeError(`Rule ${ruleName}: the visitor must be an object`);
|
|
43
|
+
}
|
|
44
|
+
return Object.entries(visitor).map(([key, handler]) => {
|
|
45
|
+
const kind = key === "finish" ? key : SyntaxKind[key];
|
|
46
|
+
if (kind !== "finish" && typeof kind !== "number") {
|
|
47
|
+
throw new TypeError(`Rule ${ruleName}: unknown node kind ${key} in the visitor`);
|
|
48
|
+
}
|
|
49
|
+
if (typeof handler !== "function") {
|
|
50
|
+
throw new TypeError(`Rule ${ruleName}: visitor ${key} must be a function`);
|
|
51
|
+
}
|
|
52
|
+
return [kind, handler];
|
|
53
|
+
});
|
|
54
|
+
}
|
package/lib/runner.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { isRule, visitorHandlers } from "./rule.js";
|
|
5
|
+
import { API } from "./typescript/dist/api/sync/api.js";
|
|
6
|
+
|
|
7
|
+
// LintError is a failed run, with the exit code to report it with: 2 for
|
|
8
|
+
// config errors, 1 for others.
|
|
9
|
+
class LintError extends Error {
|
|
10
|
+
constructor(message, exitCode) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.exitCode = exitCode;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// run lints root with its plugins, prints the report and returns the exit
|
|
17
|
+
// code, like the binary does without plugins.
|
|
18
|
+
export async function run(binary, root, plugins) {
|
|
19
|
+
try {
|
|
20
|
+
const result = lint(binary, root, await loadPlugins(root, plugins));
|
|
21
|
+
process.stdout.write(`${result.output}\n`);
|
|
22
|
+
return result.exitCode;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (error instanceof LintError) {
|
|
25
|
+
console.error(error.message);
|
|
26
|
+
return error.exitCode;
|
|
27
|
+
}
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// loadPlugins imports the plugins (paths relative to root, or package names)
|
|
33
|
+
// and returns the rules they export, as a rule or an array of rules.
|
|
34
|
+
async function loadPlugins(root, plugins) {
|
|
35
|
+
const rules = new Map();
|
|
36
|
+
for (const plugin of plugins) {
|
|
37
|
+
let exports;
|
|
38
|
+
try {
|
|
39
|
+
exports = await import(resolvePlugin(root, plugin));
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw new LintError(`Unable to load plugin ${plugin}: ${error.message}${loadHint(error, plugin)}`, 2);
|
|
42
|
+
}
|
|
43
|
+
const found = Object.values(exports).flat().filter(isRule);
|
|
44
|
+
if (found.length === 0) {
|
|
45
|
+
throw new LintError(`Plugin ${plugin} exports no rules (create them with defineRule)`, 2);
|
|
46
|
+
}
|
|
47
|
+
for (const rule of found) {
|
|
48
|
+
const existing = rules.get(rule.name);
|
|
49
|
+
if (existing && existing.rule !== rule) {
|
|
50
|
+
throw new LintError(`Rule ${rule.name} is defined in both ${existing.plugin} and ${plugin}`, 2);
|
|
51
|
+
}
|
|
52
|
+
rules.set(rule.name, { rule, plugin });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return [...rules.values()].map(({ rule }) => rule);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolvePlugin(root, plugin) {
|
|
59
|
+
if (plugin.startsWith(".") || path.isAbsolute(plugin)) {
|
|
60
|
+
return pathToFileURL(path.resolve(root, plugin)).href;
|
|
61
|
+
}
|
|
62
|
+
return pathToFileURL(createRequire(path.join(root, "package.json")).resolve(plugin)).href;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function loadHint(error, plugin) {
|
|
66
|
+
if (error.code === "ERR_UNKNOWN_FILE_EXTENSION" && /\.[cm]?tsx?$/.test(plugin)) {
|
|
67
|
+
return " (TypeScript plugins need Node.js 22.18 or later)";
|
|
68
|
+
}
|
|
69
|
+
return "";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// lint runs the built-in rules enabled in root's config in the binary and the
|
|
73
|
+
// enabled ones among rules here, and returns the binary's report: {results,
|
|
74
|
+
// output, exitCode}. It throws a LintError when the run fails.
|
|
75
|
+
export function lint(binary, root, rules) {
|
|
76
|
+
const api = new API({ tsserverPath: binary, cwd: root });
|
|
77
|
+
try {
|
|
78
|
+
// The API has no public way to send requests of its own.
|
|
79
|
+
const { client } = api;
|
|
80
|
+
const start = client.apiRequest("slopPolice/start", {
|
|
81
|
+
rules: rules.map((rule) => ({ name: rule.name, options: rule.options ?? {} })),
|
|
82
|
+
});
|
|
83
|
+
if (start.error) {
|
|
84
|
+
throw new LintError(start.error, start.exitCode);
|
|
85
|
+
}
|
|
86
|
+
const findings = start.rules.length > 0 ? runRules(api, start, rules) : [];
|
|
87
|
+
return client.apiRequest("slopPolice/finish", { findings });
|
|
88
|
+
} finally {
|
|
89
|
+
api.close();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// runRules visits every analyzed file once, calling the enabled rules'
|
|
94
|
+
// handlers for each node, and returns their findings.
|
|
95
|
+
function runRules(api, start, rules) {
|
|
96
|
+
const snapshot = api.updateSnapshot({ openProjects: [...new Set(start.files.map((file) => file.project))] });
|
|
97
|
+
const byName = new Map(rules.map((rule) => [rule.name, rule]));
|
|
98
|
+
const findings = [];
|
|
99
|
+
const handlers = []; // [rule name, ctx, handler] by SyntaxKind
|
|
100
|
+
const finishers = [];
|
|
101
|
+
let current; // the file being visited, and its project
|
|
102
|
+
|
|
103
|
+
for (const { name, options } of start.rules) {
|
|
104
|
+
const rule = byName.get(name);
|
|
105
|
+
const ctx = {
|
|
106
|
+
options,
|
|
107
|
+
get file() {
|
|
108
|
+
return current?.file;
|
|
109
|
+
},
|
|
110
|
+
get checker() {
|
|
111
|
+
return current?.project.checker;
|
|
112
|
+
},
|
|
113
|
+
get program() {
|
|
114
|
+
return current?.project.program;
|
|
115
|
+
},
|
|
116
|
+
report(node, message) {
|
|
117
|
+
if (typeof node?.id !== "string") {
|
|
118
|
+
throw new TypeError("report() takes a node of a linted file");
|
|
119
|
+
}
|
|
120
|
+
findings.push({ rule: name, node: node.id, message: String(message) });
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
let visitor = rule.visit;
|
|
124
|
+
if (rule.create) {
|
|
125
|
+
try {
|
|
126
|
+
visitor = rule.create(ctx);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
throw ruleError(name, "in create", error);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const [kind, handler] of visitorHandlers(name, visitor)) {
|
|
132
|
+
if (kind === "finish") {
|
|
133
|
+
finishers.push([name, ctx, handler]);
|
|
134
|
+
} else {
|
|
135
|
+
(handlers[kind] ??= []).push([name, ctx, handler]);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const visit = (node) => {
|
|
141
|
+
const list = handlers[node.kind];
|
|
142
|
+
if (list) {
|
|
143
|
+
for (const [name, ctx, handler] of list) {
|
|
144
|
+
try {
|
|
145
|
+
handler(node, ctx);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
throw ruleError(name, `on ${location(current.file, node)}`, error);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
node.forEachChild(visit);
|
|
152
|
+
};
|
|
153
|
+
for (const file of start.files) {
|
|
154
|
+
const project = snapshot.getProject(file.project);
|
|
155
|
+
const sourceFile = project.program.getSourceFile(file.fileName);
|
|
156
|
+
current = { file: { path: file.path, fileName: file.fileName, isTest: file.isTest, sourceFile }, project };
|
|
157
|
+
visit(sourceFile);
|
|
158
|
+
}
|
|
159
|
+
current = undefined;
|
|
160
|
+
for (const [name, ctx, finish] of finishers) {
|
|
161
|
+
try {
|
|
162
|
+
finish(ctx);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
throw ruleError(name, "in finish", error);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return findings;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function ruleError(name, where, error) {
|
|
171
|
+
return new LintError(`Rule ${name} failed ${where}: ${error?.stack ?? error}`, 1);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function location(file, node) {
|
|
175
|
+
const { line, character } = file.sourceFile.getLineAndCharacterOfPosition(node.getStart(file.sourceFile));
|
|
176
|
+
return `${file.path}:${line + 1}:${character + 1}`;
|
|
177
|
+
}
|
package/lib/testing.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Rule, RuleOptions } from "./index.js";
|
|
2
|
+
|
|
3
|
+
export interface Finding {
|
|
4
|
+
/** 1-based. */
|
|
5
|
+
line: number;
|
|
6
|
+
/** 1-based, in UTF-16 code units. */
|
|
7
|
+
column: number;
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Runs rule alone on source, as index.ts in a strict TypeScript project. */
|
|
12
|
+
export declare function lint(rule: Rule<any>, source: string, options?: RuleOptions): Finding[];
|
|
13
|
+
/**
|
|
14
|
+
* Runs rule alone on files (paths to contents), in a strict TypeScript project
|
|
15
|
+
* unless files has a tsconfig.json.
|
|
16
|
+
*/
|
|
17
|
+
export declare function lint(
|
|
18
|
+
rule: Rule<any>,
|
|
19
|
+
files: Record<string, string>,
|
|
20
|
+
options?: RuleOptions,
|
|
21
|
+
): (Finding & { file: string })[];
|
package/lib/testing.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { findBinary } from "./binary.js";
|
|
5
|
+
import { lint as lintWithRules } from "./runner.js";
|
|
6
|
+
|
|
7
|
+
const tsconfig = {
|
|
8
|
+
compilerOptions: {
|
|
9
|
+
strict: true,
|
|
10
|
+
target: "ES2022",
|
|
11
|
+
module: "ESNext",
|
|
12
|
+
moduleResolution: "Bundler",
|
|
13
|
+
jsx: "preserve",
|
|
14
|
+
allowImportingTsExtensions: true,
|
|
15
|
+
noEmit: true,
|
|
16
|
+
},
|
|
17
|
+
include: ["**/*.ts", "**/*.tsx"],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// lint runs rule alone on source (index.ts) or on files (paths to contents,
|
|
21
|
+
// with a default strict tsconfig.json unless one is given) and returns the
|
|
22
|
+
// findings, with the file of each when given files.
|
|
23
|
+
export function lint(rule, source, options) {
|
|
24
|
+
const files = typeof source === "string" ? { "index.ts": source } : source;
|
|
25
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "slop-police-test-"));
|
|
26
|
+
try {
|
|
27
|
+
const config = { projects: ["tsconfig.json"], rules: { [rule.name]: options ? ["error", options] : "error" } };
|
|
28
|
+
const all = { "tsconfig.json": JSON.stringify(tsconfig), ...files, "slop-police.json": JSON.stringify(config) };
|
|
29
|
+
for (const [name, content] of Object.entries(all)) {
|
|
30
|
+
const file = path.join(root, name);
|
|
31
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
32
|
+
fs.writeFileSync(file, content);
|
|
33
|
+
}
|
|
34
|
+
return lintWithRules(findBinary(), root, [rule]).results.map(({ file, line, column, message }) =>
|
|
35
|
+
typeof source === "string" ? { line, column, message } : { file, line, column, message },
|
|
36
|
+
);
|
|
37
|
+
} finally {
|
|
38
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
}
|