@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.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +264 -0
  3. package/package.json +34 -0
  4. package/src/effect/index.ts +13 -0
  5. package/src/effect/rules/no-service-constructor-imports.test.ts +56 -0
  6. package/src/effect/rules/no-service-constructor-imports.ts +52 -0
  7. package/src/index.ts +41 -0
  8. package/src/rules/no-chained-type-assertions.ts +77 -0
  9. package/src/rules/no-conditional-empty-object-spread.test.ts +32 -0
  10. package/src/rules/no-conditional-empty-object-spread.ts +49 -0
  11. package/src/rules/no-known-value-widening.test.ts +114 -0
  12. package/src/rules/no-known-value-widening.ts +247 -0
  13. package/src/rules/no-module-mocking.test.ts +28 -0
  14. package/src/rules/no-module-mocking.ts +91 -0
  15. package/src/rules/no-object-parameters.test.ts +32 -0
  16. package/src/rules/no-object-parameters.ts +126 -0
  17. package/src/rules/no-reflect-apply.test.ts +19 -0
  18. package/src/rules/no-reflect-apply.ts +28 -0
  19. package/src/rules/no-reflect-get.test.ts +20 -0
  20. package/src/rules/no-reflect-get.ts +28 -0
  21. package/src/rules/no-runtime-typeof.test.ts +42 -0
  22. package/src/rules/no-runtime-typeof.ts +67 -0
  23. package/src/rules/no-shape-in-symbol-names.ts +39 -0
  24. package/src/rules/no-unknown-parameters.ts +83 -0
  25. package/src/rules/no-unknown-returns.test.ts +33 -0
  26. package/src/rules/no-unknown-returns.ts +115 -0
  27. package/src/rules/no-unknown-type-aliases.test.ts +18 -0
  28. package/src/rules/no-unknown-type-aliases.ts +70 -0
  29. package/src/rules/no-unsafe-dictionary-type.test.ts +103 -0
  30. package/src/rules/no-unsafe-dictionary-type.ts +134 -0
  31. package/src/rules/no-widen-then-assert.test.ts +19 -0
  32. package/src/rules/no-widen-then-assert.ts +366 -0
  33. package/src/rules/require-safety-comment-for-type-assertion.test.ts +29 -0
  34. package/src/rules/require-safety-comment-for-type-assertion.ts +62 -0
  35. package/src/shared/dictionary-types.ts +502 -0
  36. package/src/shared/lexical-type-parameters.ts +61 -0
  37. 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
+ [![skills.sh](https://skills.sh/b/dmmulroy/anti-slop)](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
+ });