@kamaal111/oxlint-plugin-anti-slop 0.0.1
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/LICENSE +21 -0
- package/README.md +264 -0
- package/package.json +34 -0
- package/src/effect/index.ts +13 -0
- package/src/effect/rules/no-service-constructor-imports.test.ts +56 -0
- package/src/effect/rules/no-service-constructor-imports.ts +52 -0
- package/src/index.ts +41 -0
- package/src/rules/no-chained-type-assertions.ts +77 -0
- package/src/rules/no-conditional-empty-object-spread.test.ts +32 -0
- package/src/rules/no-conditional-empty-object-spread.ts +49 -0
- package/src/rules/no-known-value-widening.test.ts +114 -0
- package/src/rules/no-known-value-widening.ts +247 -0
- package/src/rules/no-module-mocking.test.ts +28 -0
- package/src/rules/no-module-mocking.ts +91 -0
- package/src/rules/no-object-parameters.test.ts +32 -0
- package/src/rules/no-object-parameters.ts +126 -0
- package/src/rules/no-reflect-apply.test.ts +19 -0
- package/src/rules/no-reflect-apply.ts +28 -0
- package/src/rules/no-reflect-get.test.ts +20 -0
- package/src/rules/no-reflect-get.ts +28 -0
- package/src/rules/no-runtime-typeof.test.ts +42 -0
- package/src/rules/no-runtime-typeof.ts +67 -0
- package/src/rules/no-shape-in-symbol-names.ts +39 -0
- package/src/rules/no-unknown-parameters.ts +83 -0
- package/src/rules/no-unknown-returns.test.ts +33 -0
- package/src/rules/no-unknown-returns.ts +115 -0
- package/src/rules/no-unknown-type-aliases.test.ts +18 -0
- package/src/rules/no-unknown-type-aliases.ts +70 -0
- package/src/rules/no-unsafe-dictionary-type.test.ts +103 -0
- package/src/rules/no-unsafe-dictionary-type.ts +134 -0
- package/src/rules/no-widen-then-assert.test.ts +19 -0
- package/src/rules/no-widen-then-assert.ts +366 -0
- package/src/rules/require-safety-comment-for-type-assertion.test.ts +29 -0
- package/src/rules/require-safety-comment-for-type-assertion.ts +62 -0
- package/src/shared/dictionary-types.ts +502 -0
- package/src/shared/lexical-type-parameters.ts +61 -0
- package/src/shared/reflect-method.ts +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dillon Mulroy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# anti-slop
|
|
2
|
+
|
|
3
|
+
[](https://skills.sh/dmmulroy/anti-slop)
|
|
4
|
+
|
|
5
|
+
Opinionated Oxlint rules that reject low-evidence and low-signal TypeScript and JavaScript patterns.
|
|
6
|
+
|
|
7
|
+
This project is meant to be vendored, not treated as a fixed npm dependency. Copy the rules into your repository, read them, and change them to match your team's standards. The bundled agent skill handles the initial copy and configuration; after that, the vendored files are yours to maintain and make your own.
|
|
8
|
+
|
|
9
|
+
## Install with an agent skill
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx skills add dmmulroy/anti-slop --skill install-anti-slop
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then ask your coding agent to install or configure anti-slop in the current repository. The skill copies the plugin, installs current Oxlint dependencies, merges the plugin into the existing lint configuration, enables every generic rule, and validates the result. In repositories that depend on Effect, it also enables the opt-in Effect rule group.
|
|
16
|
+
|
|
17
|
+
To inspect available skills first:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx skills add dmmulroy/anti-slop --list
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Manual local installation
|
|
24
|
+
|
|
25
|
+
Copy `src/` into the target repository, for example at `tools/oxlint/anti-slop/`, and install matching current versions of `oxlint` and `@oxlint/plugins`.
|
|
26
|
+
|
|
27
|
+
Register the copied entry point in `oxlint.config.ts`:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { defineConfig } from "oxlint";
|
|
31
|
+
|
|
32
|
+
export default defineConfig({
|
|
33
|
+
ignorePatterns: [
|
|
34
|
+
".agent/**",
|
|
35
|
+
".agents/**",
|
|
36
|
+
".claude/**",
|
|
37
|
+
".codex/**",
|
|
38
|
+
".continue/**",
|
|
39
|
+
".cursor/**",
|
|
40
|
+
".gemini/**",
|
|
41
|
+
".opencode/**",
|
|
42
|
+
".pi/**",
|
|
43
|
+
".roo/**",
|
|
44
|
+
".windsurf/**",
|
|
45
|
+
"tools/oxlint/anti-slop/**",
|
|
46
|
+
],
|
|
47
|
+
jsPlugins: [
|
|
48
|
+
{ name: "anti-slop", specifier: "./tools/oxlint/anti-slop/index.ts" },
|
|
49
|
+
],
|
|
50
|
+
rules: {
|
|
51
|
+
"anti-slop/no-chained-type-assertions": "error",
|
|
52
|
+
"anti-slop/no-conditional-empty-object-spread": "error",
|
|
53
|
+
"anti-slop/no-known-value-widening": "error",
|
|
54
|
+
"anti-slop/no-module-mocking": "error",
|
|
55
|
+
"anti-slop/no-object-parameters": "error",
|
|
56
|
+
"anti-slop/no-reflect-apply": "error",
|
|
57
|
+
"anti-slop/no-reflect-get": "error",
|
|
58
|
+
"anti-slop/no-runtime-typeof": "error",
|
|
59
|
+
"anti-slop/no-shape-in-symbol-names": "error",
|
|
60
|
+
"anti-slop/no-unknown-parameters": "error",
|
|
61
|
+
"anti-slop/no-unknown-returns": "error",
|
|
62
|
+
"anti-slop/no-unknown-type-aliases": "error",
|
|
63
|
+
"anti-slop/no-unsafe-dictionary-type": "error",
|
|
64
|
+
"anti-slop/no-widen-then-assert": "error",
|
|
65
|
+
"anti-slop/require-safety-comment-for-type-assertion": "error"
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The same `ignorePatterns`, `jsPlugins`, and rules work under `lint` in a Vite+ config. Merge the ignore patterns into Vite+'s `fmt.ignorePatterns` as well so `vp check` does not reformat installed agent assets or the vendored plugin. Preserve existing ignores and add any other project-local agent tooling directories detected in the repository; do not broadly ignore every dot-directory.
|
|
71
|
+
|
|
72
|
+
### Optional Effect rules
|
|
73
|
+
|
|
74
|
+
Effect-specific rules live in a separate plugin so projects that do not use Effect do not inherit Effect architecture policy. Register the Effect entry point only in repositories that use Effect:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
export default defineConfig({
|
|
78
|
+
jsPlugins: [
|
|
79
|
+
{ name: "anti-slop", specifier: "./tools/oxlint/anti-slop/index.ts" },
|
|
80
|
+
{
|
|
81
|
+
name: "anti-slop-effect",
|
|
82
|
+
specifier: "./tools/oxlint/anti-slop/effect/index.ts"
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
rules: {
|
|
86
|
+
"anti-slop-effect/no-service-constructor-imports": "error"
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Rules
|
|
92
|
+
|
|
93
|
+
### Generic rules
|
|
94
|
+
|
|
95
|
+
- `no-chained-type-assertions` — rejects nested type assertions that fabricate evidence.
|
|
96
|
+
- `no-conditional-empty-object-spread` — rejects conditional spreads that use `{}` to omit fields.
|
|
97
|
+
- `no-known-value-widening` — rejects explicit broad target types that discard known value evidence.
|
|
98
|
+
- `no-module-mocking` — rejects Vitest and Jest module mocks in favor of real dependency seams.
|
|
99
|
+
- `no-object-parameters` — rejects the broad `object` type on function inputs.
|
|
100
|
+
- `no-reflect-apply` — rejects `Reflect.apply` in favor of typed function calls.
|
|
101
|
+
- `no-reflect-get` — rejects `Reflect.get` in favor of typed property access or boundary parsing.
|
|
102
|
+
- `no-runtime-typeof` — requires boundary parsing instead of ad hoc `typeof` narrowing.
|
|
103
|
+
- `no-shape-in-symbol-names` — rejects `shape` in symbol names.
|
|
104
|
+
- `no-unknown-parameters` — rejects `unknown` inputs except the explicit `cause` convention.
|
|
105
|
+
- `no-unknown-returns` — rejects function contracts that return `unknown` or `Promise<unknown>`.
|
|
106
|
+
- `no-unknown-type-aliases` — rejects aliases that merely conceal `unknown`.
|
|
107
|
+
- `no-unsafe-dictionary-type` — rejects dictionary value contracts based on `unknown`, `any`, `object`, `{}`, and semantic equivalents.
|
|
108
|
+
- `no-widen-then-assert` — rejects local flows that widen known values and later assert them back.
|
|
109
|
+
- `require-safety-comment-for-type-assertion` — requires each non-const assertion to document its checked invariant.
|
|
110
|
+
|
|
111
|
+
### Effect rules
|
|
112
|
+
|
|
113
|
+
- `no-service-constructor-imports` — rejects relative project imports of exported `make<CapabilityName>` constructors outside `*.test.*` and `*.spec.*` files. Runtime callers should import the owning Layer and yield the contextual service instead. Package imports and static constructors such as `WorkspaceName.make` are outside the rule.
|
|
114
|
+
|
|
115
|
+
## Violation examples
|
|
116
|
+
|
|
117
|
+
Each snippet below is rejected by the named rule.
|
|
118
|
+
|
|
119
|
+
### `no-chained-type-assertions`
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
const user = input as object as User;
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### `no-conditional-empty-object-spread`
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const options = {
|
|
129
|
+
...(timeout !== undefined ? { timeout } : {}),
|
|
130
|
+
};
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### `no-known-value-widening`
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
const handlers: Record<string, Handler> = {
|
|
137
|
+
start: startHandler,
|
|
138
|
+
};
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
This discards the known `start` key. Preserve inference or use `satisfies Record<string, Handler>` instead.
|
|
142
|
+
|
|
143
|
+
### `no-module-mocking`
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
vi.mock("./user-store");
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### `no-object-parameters`
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
function save(value: object) {}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### `no-reflect-apply`
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
const value = Reflect.apply(operation, owner, args);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### `no-reflect-get`
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
const value = Reflect.get(owner, key);
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### `no-runtime-typeof`
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
if (typeof input === "string") {
|
|
171
|
+
useName(input);
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Schema-free projects can permit `typeof` checks directly inside type predicate and
|
|
176
|
+
assertion functions while continuing to reject ad hoc checks elsewhere:
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{
|
|
180
|
+
"anti-slop/no-runtime-typeof": [
|
|
181
|
+
"error",
|
|
182
|
+
{ "allowInTypeGuards": true }
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The option defaults to `false`.
|
|
188
|
+
|
|
189
|
+
### `no-shape-in-symbol-names`
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
interface UserShape {
|
|
193
|
+
id: string;
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Effect: `no-service-constructor-imports`
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
import { makeIssueService } from "./issue-service.ts";
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Import the owning Layer and yield `IssueService` instead. Focused `*.test.*` and `*.spec.*` files may import the constructor directly.
|
|
204
|
+
|
|
205
|
+
### `no-unknown-parameters`
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
function handle(input: unknown) {}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### `no-unknown-returns`
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
function loadUser(): unknown {
|
|
215
|
+
return input;
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### `no-unknown-type-aliases`
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
type ExternalValue = unknown;
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### `no-unsafe-dictionary-type`
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
type Metadata = Record<string, unknown>;
|
|
229
|
+
type OtherMetadata = { [key: string]: object };
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### `no-widen-then-assert`
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
const loaded: User = loadUser();
|
|
236
|
+
const stored: unknown = loaded;
|
|
237
|
+
const user = stored as User;
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### `require-safety-comment-for-type-assertion`
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
const userId = value as UserId;
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Add a specific justification immediately before a necessary assertion:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
// SAFETY: parseUserId validated the identifier before branding it.
|
|
250
|
+
const userId = value as UserId;
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Development
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
pnpm install
|
|
257
|
+
pnpm check
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
`src/` is canonical. After changing production source, run `pnpm sync:skill-assets`; CI checks that the skill's bundled copy remains identical.
|
|
261
|
+
|
|
262
|
+
## License
|
|
263
|
+
|
|
264
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kamaal111/oxlint-plugin-anti-slop",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Opinionated Oxlint rules that reject low-evidence TypeScript and JavaScript patterns.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/kamaal111/anti-slop.git"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"exports": "./src/index.ts",
|
|
11
|
+
"files": [
|
|
12
|
+
"src"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"check": "pnpm lint && pnpm test && pnpm typecheck && pnpm check:skill-assets",
|
|
16
|
+
"lint": "oxlint src",
|
|
17
|
+
"test": "tsx src/rules/no-conditional-empty-object-spread.test.ts && tsx src/rules/no-unsafe-dictionary-type.test.ts && tsx src/rules/no-known-value-widening.test.ts && tsx src/rules/no-object-parameters.test.ts && tsx src/rules/no-runtime-typeof.test.ts && tsx src/rules/no-unknown-returns.test.ts && tsx src/rules/no-unknown-type-aliases.test.ts && tsx src/rules/no-widen-then-assert.test.ts && tsx src/rules/no-reflect-get.test.ts && tsx src/rules/no-reflect-apply.test.ts && tsx src/rules/no-module-mocking.test.ts && tsx src/rules/require-safety-comment-for-type-assertion.test.ts && tsx src/effect/rules/no-service-constructor-imports.test.ts",
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"release": "sh scripts/publish.sh",
|
|
20
|
+
"sync:skill-assets": "node scripts/sync-skill-assets.mjs",
|
|
21
|
+
"check:skill-assets": "node scripts/sync-skill-assets.mjs --check"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@oxlint/plugins": "1.78.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "26.2.0",
|
|
28
|
+
"oxlint": "1.78.0",
|
|
29
|
+
"tsx": "4.23.12",
|
|
30
|
+
"typescript": "7.0.2"
|
|
31
|
+
},
|
|
32
|
+
"packageManager": "pnpm@10.33.0",
|
|
33
|
+
"license": "MIT"
|
|
34
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { eslintCompatPlugin } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
import { noServiceConstructorImportsRule } from "./rules/no-service-constructor-imports.ts";
|
|
4
|
+
|
|
5
|
+
/** Opt-in Oxlint rules for Effect service and Layer architecture. */
|
|
6
|
+
const antiSlopEffectPlugin = eslintCompatPlugin({
|
|
7
|
+
meta: { name: "anti-slop-effect" },
|
|
8
|
+
rules: {
|
|
9
|
+
"no-service-constructor-imports": noServiceConstructorImportsRule,
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export default antiSlopEffectPlugin;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { RuleTester } from "oxlint/plugins-dev";
|
|
2
|
+
|
|
3
|
+
import { noServiceConstructorImportsRule } from "./no-service-constructor-imports.ts";
|
|
4
|
+
|
|
5
|
+
new RuleTester().run(
|
|
6
|
+
"no-service-constructor-imports",
|
|
7
|
+
noServiceConstructorImportsRule,
|
|
8
|
+
{
|
|
9
|
+
valid: [
|
|
10
|
+
{
|
|
11
|
+
filename: "src/issue-service.test.ts",
|
|
12
|
+
code: 'import { makeIssueService } from "./issue-service.ts";',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
filename: "src/issue-service.spec.tsx",
|
|
16
|
+
code: 'import { makeIssueService } from "../issue-service.ts";',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
filename: "src/runtime.ts",
|
|
20
|
+
code: 'import { makeExecutionMemo } from "alchemy/Runtime/ExecutionMemo";',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
filename: "src/runtime.ts",
|
|
24
|
+
code: 'import { issueServiceLayer } from "./issue-service.ts";\nWorkspaceName.make("name");',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
filename: "src/runtime.ts",
|
|
28
|
+
code: 'import { makeissueService } from "./issue-service.ts";',
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
invalid: [
|
|
32
|
+
{
|
|
33
|
+
filename: "src/runtime.ts",
|
|
34
|
+
code: 'import { makeIssueService } from "./issue-service.ts";',
|
|
35
|
+
errors: [
|
|
36
|
+
{
|
|
37
|
+
messageId: "serviceConstructorImport",
|
|
38
|
+
data: { name: "makeIssueService" },
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
output: null,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
filename: "src/runtime.ts",
|
|
45
|
+
code: 'import { makeIssueService as createIssueService } from "../issue-service.ts";',
|
|
46
|
+
errors: [
|
|
47
|
+
{
|
|
48
|
+
messageId: "serviceConstructorImport",
|
|
49
|
+
data: { name: "makeIssueService" },
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
output: null,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
import type { ESTree } from "@oxlint/plugins";
|
|
4
|
+
|
|
5
|
+
const SERVICE_CONSTRUCTOR_NAME = /^make[A-Z]/u;
|
|
6
|
+
const TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/u;
|
|
7
|
+
|
|
8
|
+
function isProjectLocalImport(source: string): boolean {
|
|
9
|
+
return source.startsWith("./") || source.startsWith("../");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getImportedName(specifier: ESTree.ImportSpecifier): string {
|
|
13
|
+
if (specifier.imported.type === "Identifier") return specifier.imported.name;
|
|
14
|
+
return specifier.imported.value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Keep dependency-bearing Effect service constructors local to their owning capability modules. */
|
|
18
|
+
export const noServiceConstructorImportsRule = defineRule({
|
|
19
|
+
meta: {
|
|
20
|
+
type: "problem",
|
|
21
|
+
docs: {
|
|
22
|
+
description:
|
|
23
|
+
"Disallow project-local make<CapabilityName> imports outside test and spec files.",
|
|
24
|
+
},
|
|
25
|
+
messages: {
|
|
26
|
+
serviceConstructorImport:
|
|
27
|
+
'Do not import Effect service constructor "{{name}}" into runtime code. Import the owning Layer, yield the contextual service, and allow its requirements to propagate to the composition root.',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
create(context) {
|
|
31
|
+
const isTestFile = TEST_FILE.test(context.filename.replaceAll("\\", "/"));
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
ImportDeclaration(node) {
|
|
35
|
+
if (isTestFile || !isProjectLocalImport(node.source.value)) return;
|
|
36
|
+
|
|
37
|
+
for (const specifier of node.specifiers) {
|
|
38
|
+
if (specifier.type !== "ImportSpecifier") continue;
|
|
39
|
+
|
|
40
|
+
const importedName = getImportedName(specifier);
|
|
41
|
+
if (!SERVICE_CONSTRUCTOR_NAME.test(importedName)) continue;
|
|
42
|
+
|
|
43
|
+
context.report({
|
|
44
|
+
node: specifier,
|
|
45
|
+
messageId: "serviceConstructorImport",
|
|
46
|
+
data: { name: importedName },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { eslintCompatPlugin } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
import { noChainedTypeAssertionsRule } from "./rules/no-chained-type-assertions.ts";
|
|
4
|
+
import { noConditionalEmptyObjectSpreadRule } from "./rules/no-conditional-empty-object-spread.ts";
|
|
5
|
+
import { noKnownValueWideningRule } from "./rules/no-known-value-widening.ts";
|
|
6
|
+
import { noModuleMockingRule } from "./rules/no-module-mocking.ts";
|
|
7
|
+
import { noObjectParametersRule } from "./rules/no-object-parameters.ts";
|
|
8
|
+
import { noReflectApplyRule } from "./rules/no-reflect-apply.ts";
|
|
9
|
+
import { noReflectGetRule } from "./rules/no-reflect-get.ts";
|
|
10
|
+
import { noRuntimeTypeofRule } from "./rules/no-runtime-typeof.ts";
|
|
11
|
+
import { noForbiddenTermInSymbolNamesRule } from "./rules/no-shape-in-symbol-names.ts";
|
|
12
|
+
import { noUnknownParametersRule } from "./rules/no-unknown-parameters.ts";
|
|
13
|
+
import { noUnknownReturnsRule } from "./rules/no-unknown-returns.ts";
|
|
14
|
+
import { noUnknownTypeAliasesRule } from "./rules/no-unknown-type-aliases.ts";
|
|
15
|
+
import { noUnsafeDictionaryTypeRule } from "./rules/no-unsafe-dictionary-type.ts";
|
|
16
|
+
import { noWidenThenAssertRule } from "./rules/no-widen-then-assert.ts";
|
|
17
|
+
import { requireSafetyCommentForTypeAssertionRule } from "./rules/require-safety-comment-for-type-assertion.ts";
|
|
18
|
+
|
|
19
|
+
/** Generic Oxlint rules that reject low-evidence and low-signal implementation patterns. */
|
|
20
|
+
const antiSlopPlugin = eslintCompatPlugin({
|
|
21
|
+
meta: { name: "anti-slop" },
|
|
22
|
+
rules: {
|
|
23
|
+
"no-chained-type-assertions": noChainedTypeAssertionsRule,
|
|
24
|
+
"no-conditional-empty-object-spread": noConditionalEmptyObjectSpreadRule,
|
|
25
|
+
"no-known-value-widening": noKnownValueWideningRule,
|
|
26
|
+
"no-module-mocking": noModuleMockingRule,
|
|
27
|
+
"no-object-parameters": noObjectParametersRule,
|
|
28
|
+
"no-reflect-apply": noReflectApplyRule,
|
|
29
|
+
"no-reflect-get": noReflectGetRule,
|
|
30
|
+
"no-runtime-typeof": noRuntimeTypeofRule,
|
|
31
|
+
"no-unsafe-dictionary-type": noUnsafeDictionaryTypeRule,
|
|
32
|
+
"no-shape-in-symbol-names": noForbiddenTermInSymbolNamesRule,
|
|
33
|
+
"no-unknown-parameters": noUnknownParametersRule,
|
|
34
|
+
"no-unknown-returns": noUnknownReturnsRule,
|
|
35
|
+
"no-unknown-type-aliases": noUnknownTypeAliasesRule,
|
|
36
|
+
"no-widen-then-assert": noWidenThenAssertRule,
|
|
37
|
+
"require-safety-comment-for-type-assertion": requireSafetyCommentForTypeAssertionRule,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export default antiSlopPlugin;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import type { ESTree } from "@oxlint/plugins";
|
|
3
|
+
|
|
4
|
+
type TypeAssertionExpression = ESTree.TSAsExpression | ESTree.TSTypeAssertion;
|
|
5
|
+
|
|
6
|
+
function isTypeAssertionExpression(node: ESTree.Node): node is TypeAssertionExpression {
|
|
7
|
+
return node.type === "TSAsExpression" || node.type === "TSTypeAssertion";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function unwrapParenthesizedExpression(expression: ESTree.Expression): ESTree.Expression {
|
|
11
|
+
let current = expression;
|
|
12
|
+
while (current.type === "ParenthesizedExpression") {
|
|
13
|
+
current = current.expression;
|
|
14
|
+
}
|
|
15
|
+
return current;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isConstAssertion(node: TypeAssertionExpression): boolean {
|
|
19
|
+
const { typeAnnotation } = node;
|
|
20
|
+
return (
|
|
21
|
+
typeAnnotation.type === "TSTypeReference" &&
|
|
22
|
+
typeAnnotation.typeName.type === "Identifier" &&
|
|
23
|
+
typeAnnotation.typeName.name === "const"
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isOutermostAssertionInChain(node: TypeAssertionExpression): boolean {
|
|
28
|
+
let current: ESTree.Expression = node;
|
|
29
|
+
let parent = node.parent;
|
|
30
|
+
|
|
31
|
+
while (parent.type === "ParenthesizedExpression" && parent.expression === current) {
|
|
32
|
+
current = parent;
|
|
33
|
+
parent = parent.parent;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return !isTypeAssertionExpression(parent) || parent.expression !== current;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isForbiddenAssertionChain(node: TypeAssertionExpression): boolean {
|
|
40
|
+
let assertionCount = 0;
|
|
41
|
+
let hasNonConstAssertion = false;
|
|
42
|
+
let current: ESTree.Expression = node;
|
|
43
|
+
|
|
44
|
+
while (isTypeAssertionExpression(current)) {
|
|
45
|
+
assertionCount += 1;
|
|
46
|
+
hasNonConstAssertion ||= !isConstAssertion(current);
|
|
47
|
+
current = unwrapParenthesizedExpression(current.expression);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return assertionCount > 1 && hasNonConstAssertion;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Disallow nested TypeScript type assertions, while permitting chains made only of const assertions. */
|
|
54
|
+
export const noChainedTypeAssertionsRule = defineRule({
|
|
55
|
+
meta: {
|
|
56
|
+
type: "problem",
|
|
57
|
+
docs: {
|
|
58
|
+
description:
|
|
59
|
+
"Disallow chained TypeScript as and angle-bracket assertions, including parenthesized chains.",
|
|
60
|
+
},
|
|
61
|
+
messages: {
|
|
62
|
+
chained:
|
|
63
|
+
"This assertion chain discards type evidence. Keep the original precise type, or parse untrusted input at its boundary before narrowing it.",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
createOnce(context) {
|
|
67
|
+
const checkTypeAssertion = (node: TypeAssertionExpression) => {
|
|
68
|
+
if (!isOutermostAssertionInChain(node) || !isForbiddenAssertionChain(node)) return;
|
|
69
|
+
context.report({ node, messageId: "chained" });
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
TSAsExpression: checkTypeAssertion,
|
|
74
|
+
TSTypeAssertion: checkTypeAssertion,
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { RuleTester } from "oxlint/plugins-dev";
|
|
2
|
+
|
|
3
|
+
import { noConditionalEmptyObjectSpreadRule } from "./no-conditional-empty-object-spread.ts";
|
|
4
|
+
|
|
5
|
+
const tester = new RuleTester({ languageOptions: { parserOptions: { lang: "ts" } } });
|
|
6
|
+
const error = { messageId: "avoid" };
|
|
7
|
+
|
|
8
|
+
if (noConditionalEmptyObjectSpreadRule.meta?.fixable !== undefined) {
|
|
9
|
+
throw new Error("The rule must not offer an unsafe semantics-changing fix.");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
tester.run(
|
|
13
|
+
"anti-slop/no-conditional-empty-object-spread",
|
|
14
|
+
noConditionalEmptyObjectSpreadRule,
|
|
15
|
+
{
|
|
16
|
+
valid: [
|
|
17
|
+
"const result = { value };",
|
|
18
|
+
"const result = { ...values };",
|
|
19
|
+
"const result = condition ? { value } : {};",
|
|
20
|
+
],
|
|
21
|
+
invalid: [
|
|
22
|
+
{
|
|
23
|
+
code: "const result = { ...(value !== undefined ? { value } : {}) };",
|
|
24
|
+
errors: [error],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
code: "const result = { ...(condition ? {} : { value }) };",
|
|
28
|
+
errors: [error],
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import type { ESTree } from "@oxlint/plugins";
|
|
3
|
+
|
|
4
|
+
function unwrapParentheses(node: ESTree.Expression): ESTree.Expression {
|
|
5
|
+
let current = node;
|
|
6
|
+
while (current.type === "ParenthesizedExpression") {
|
|
7
|
+
current = current.expression;
|
|
8
|
+
}
|
|
9
|
+
return current;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isEmptyObjectExpression(node: ESTree.Expression): boolean {
|
|
13
|
+
return node.type === "ObjectExpression" && node.properties.length === 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isConditionalEmptyObjectSpread(node: ESTree.Expression): boolean {
|
|
17
|
+
const conditional = unwrapParentheses(node);
|
|
18
|
+
return (
|
|
19
|
+
conditional.type === "ConditionalExpression" &&
|
|
20
|
+
(isEmptyObjectExpression(conditional.consequent) ||
|
|
21
|
+
isEmptyObjectExpression(conditional.alternate))
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Ban conditional empty-object spreads without changing their omission semantics. */
|
|
26
|
+
export const noConditionalEmptyObjectSpreadRule = defineRule({
|
|
27
|
+
meta: {
|
|
28
|
+
type: "suggestion",
|
|
29
|
+
docs: {
|
|
30
|
+
description:
|
|
31
|
+
"Disallow object spreads that conditionally spread an empty object to omit fields.",
|
|
32
|
+
},
|
|
33
|
+
messages: {
|
|
34
|
+
avoid:
|
|
35
|
+
"This conditional spread hides property omission behind an empty object. Build the object in separate statements and add the property only when present.",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
createOnce(context) {
|
|
39
|
+
return {
|
|
40
|
+
SpreadElement(node) {
|
|
41
|
+
if (node.parent.type !== "ObjectExpression") return;
|
|
42
|
+
|
|
43
|
+
if (isConditionalEmptyObjectSpread(node.argument)) {
|
|
44
|
+
context.report({ node, messageId: "avoid" });
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
});
|