ai-deslop 0.2.0 → 0.2.2
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 +35 -106
- package/dist/index.d.ts +18 -0
- package/dist/index.js +84 -0
- package/dist/rules/no-array-filter-map.d.ts +2 -0
- package/dist/rules/no-array-filter-map.js +30 -0
- package/dist/rules/no-call-only-assertions.d.ts +2 -0
- package/dist/rules/no-call-only-assertions.js +87 -0
- package/dist/rules/no-chained-type-assertions.d.ts +2 -0
- package/dist/rules/no-chained-type-assertions.js +60 -0
- package/dist/rules/no-conditional-empty-object-spread.d.ts +2 -0
- package/dist/rules/no-conditional-empty-object-spread.js +40 -0
- package/dist/rules/no-duplicate-types.d.ts +2 -0
- package/dist/rules/no-duplicate-types.js +143 -0
- package/dist/rules/no-known-value-widening.d.ts +2 -0
- package/dist/rules/no-known-value-widening.js +300 -0
- package/dist/rules/no-module-mocking.d.ts +2 -0
- package/dist/rules/no-module-mocking.js +69 -0
- package/dist/rules/no-object-parameters.d.ts +2 -0
- package/dist/rules/no-object-parameters.js +56 -0
- package/dist/rules/no-pass-through-type-alias.d.ts +2 -0
- package/dist/rules/no-pass-through-type-alias.js +41 -0
- package/dist/rules/no-reduce-accumulator-copy.d.ts +2 -0
- package/dist/rules/no-reduce-accumulator-copy.js +99 -0
- package/dist/rules/no-reflect-apply.d.ts +2 -0
- package/dist/rules/no-reflect-apply.js +25 -0
- package/dist/rules/no-reflect-get.d.ts +2 -0
- package/dist/rules/no-reflect-get.js +25 -0
- package/dist/rules/no-runtime-typeof.d.ts +2 -0
- package/dist/rules/no-runtime-typeof.js +64 -0
- package/dist/rules/no-shape-in-symbol-names.d.ts +2 -0
- package/dist/rules/no-shape-in-symbol-names.js +40 -0
- package/dist/rules/no-unknown-parameters.d.ts +2 -0
- package/dist/rules/no-unknown-parameters.js +51 -0
- package/dist/rules/no-unknown-returns.d.ts +2 -0
- package/dist/rules/no-unknown-returns.js +58 -0
- package/dist/rules/no-unknown-type-aliases.d.ts +2 -0
- package/dist/rules/no-unknown-type-aliases.js +40 -0
- package/dist/rules/no-unsafe-dictionary-type.d.ts +2 -0
- package/dist/rules/no-unsafe-dictionary-type.js +134 -0
- package/dist/rules/no-widen-then-assert.d.ts +2 -0
- package/dist/rules/no-widen-then-assert.js +270 -0
- package/dist/rules/require-safety-comment-for-type-assertion.d.ts +2 -0
- package/dist/rules/require-safety-comment-for-type-assertion.js +100 -0
- package/dist/shared/array-method.d.ts +12 -0
- package/dist/shared/array-method.js +84 -0
- package/dist/shared/dictionary-types.d.ts +20 -0
- package/dist/shared/dictionary-types.js +377 -0
- package/dist/shared/function-parameters.d.ts +8 -0
- package/dist/shared/function-parameters.js +40 -0
- package/dist/shared/lexical-type-parameters.d.ts +5 -0
- package/dist/shared/lexical-type-parameters.js +47 -0
- package/dist/shared/reflect-method.d.ts +3 -0
- package/dist/shared/reflect-method.js +20 -0
- package/dist/shared/scope.d.ts +3 -0
- package/dist/shared/scope.js +11 -0
- package/dist/shared/type-alias-resolution.d.ts +23 -0
- package/dist/shared/type-alias-resolution.js +162 -0
- package/dist/tailwind/candidates.d.ts +14 -0
- package/dist/tailwind/candidates.js +24 -0
- package/dist/tailwind/design-system.d.ts +3 -0
- package/dist/tailwind/design-system.js +41 -0
- package/dist/tailwind/rule.d.ts +3 -0
- package/dist/tailwind/rule.js +133 -0
- package/dist/tailwind/worker.d.ts +1 -0
- package/dist/tailwind/worker.js +13 -0
- package/package.json +16 -5
- package/src/index.ts +0 -87
- package/src/rules/no-array-filter-map.ts +0 -28
- package/src/rules/no-call-only-assertions.ts +0 -94
- package/src/rules/no-chained-type-assertions.ts +0 -77
- package/src/rules/no-conditional-empty-object-spread.ts +0 -49
- package/src/rules/no-known-value-widening.ts +0 -415
- package/src/rules/no-module-mocking.ts +0 -80
- package/src/rules/no-object-parameters.ts +0 -83
- package/src/rules/no-pass-through-type-alias.ts +0 -52
- package/src/rules/no-reduce-accumulator-copy.ts +0 -109
- package/src/rules/no-reflect-apply.ts +0 -28
- package/src/rules/no-reflect-get.ts +0 -28
- package/src/rules/no-runtime-typeof.ts +0 -77
- package/src/rules/no-shape-in-symbol-names.ts +0 -46
- package/src/rules/no-unknown-parameters.ts +0 -69
- package/src/rules/no-unknown-returns.ts +0 -82
- package/src/rules/no-unknown-type-aliases.ts +0 -54
- package/src/rules/no-unsafe-dictionary-type.ts +0 -154
- package/src/rules/no-widen-then-assert.ts +0 -366
- package/src/rules/require-safety-comment-for-type-assertion.ts +0 -131
- package/src/shared/array-method.ts +0 -94
- package/src/shared/dictionary-types.ts +0 -515
- package/src/shared/function-parameters.ts +0 -49
- package/src/shared/lexical-type-parameters.ts +0 -61
- package/src/shared/reflect-method.ts +0 -24
- package/src/shared/scope.ts +0 -15
- package/src/shared/type-alias-resolution.ts +0 -250
- package/src/tailwind/candidates.ts +0 -38
- package/src/tailwind/design-system.ts +0 -59
- package/src/tailwind/rule.ts +0 -126
- package/src/tailwind/worker.ts +0 -38
package/README.md
CHANGED
|
@@ -11,116 +11,45 @@ npm install --save-dev ai-deslop
|
|
|
11
11
|
# @oxlint/plugins comes along as a runtime dependency — keep it in sync with your oxlint version
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
## Usage
|
|
17
|
-
|
|
18
|
-
Register the plugin in `oxlint.config.ts` and enable every rule as an error via the exported `recommended` config:
|
|
19
|
-
|
|
20
|
-
```ts
|
|
21
|
-
import { defineConfig } from "oxlint";
|
|
22
|
-
import deslopPlugin, { configs as deslopConfigs } from "ai-deslop";
|
|
23
|
-
|
|
24
|
-
export default defineConfig({
|
|
25
|
-
jsPlugins: [deslopPlugin],
|
|
26
|
-
options: {
|
|
27
|
-
...deslopConfigs.recommended.options,
|
|
28
|
-
},
|
|
29
|
-
rules: {
|
|
30
|
-
...deslopConfigs.recommended.rules,
|
|
31
|
-
},
|
|
32
|
-
});
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
The `recommended` config also enables standard type-aware rules (`typescript/no-deprecated`, `typescript/no-floating-promises`), so it sets `options.typeAware`. Type-aware linting requires the `oxlint-tsgolint` package to be installed.
|
|
36
|
-
|
|
37
|
-
Or with `.oxlintrc.json` (list the rules explicitly):
|
|
38
|
-
|
|
39
|
-
```json
|
|
40
|
-
{
|
|
41
|
-
"jsPlugins": ["ai-deslop"],
|
|
42
|
-
"options": {
|
|
43
|
-
"typeAware": true
|
|
44
|
-
},
|
|
45
|
-
"rules": {
|
|
46
|
-
"deslop/no-array-filter-map": "error",
|
|
47
|
-
"deslop/no-reduce-accumulator-copy": "error",
|
|
48
|
-
"deslop/no-chained-type-assertions": "error",
|
|
49
|
-
"deslop/no-conditional-empty-object-spread": "error",
|
|
50
|
-
"deslop/no-known-value-widening": "error",
|
|
51
|
-
"deslop/no-module-mocking": "error",
|
|
52
|
-
"deslop/no-object-parameters": "error",
|
|
53
|
-
"deslop/no-reflect-apply": "error",
|
|
54
|
-
"deslop/no-reflect-get": "error",
|
|
55
|
-
"deslop/no-runtime-typeof": "error",
|
|
56
|
-
"deslop/no-shape-in-symbol-names": "error",
|
|
57
|
-
"deslop/no-unknown-parameters": "error",
|
|
58
|
-
"deslop/no-unknown-returns": "error",
|
|
59
|
-
"deslop/no-unknown-type-aliases": "error",
|
|
60
|
-
"deslop/no-unsafe-dictionary-type": "error",
|
|
61
|
-
"deslop/no-widen-then-assert": "error",
|
|
62
|
-
"deslop/require-safety-comment-for-type-assertion": "error",
|
|
63
|
-
"deslop/no-call-only-assertions": "error",
|
|
64
|
-
"deslop/no-pass-through-type-alias": "error",
|
|
65
|
-
"typescript/no-deprecated": "error",
|
|
66
|
-
"typescript/no-floating-promises": "error"
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
```
|
|
14
|
+
Ships compiled JavaScript — works with any oxlint JS-plugin runtime (Bun, Deno, or Node.js).
|
|
70
15
|
|
|
71
16
|
## Rules
|
|
72
17
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
|
18
|
+
| Rule | Status | Description |
|
|
19
|
+
| --- | --- | --- |
|
|
20
|
+
| [`no-array-filter-map`](docs/deslop.md) | Recommended | Rejects adjacent eager array filter/map passes; allows lazy iterator pipelines. |
|
|
21
|
+
| [`no-reduce-accumulator-copy`](docs/deslop.md) | Recommended | Rejects non-spread accumulator copies inside reducers. Enable native `oxc/no-accumulating-spread` alongside it. |
|
|
22
|
+
| [`no-chained-type-assertions`](docs/deslop.md) | Recommended | Rejects nested `as` / angle-bracket assertions that fabricate evidence. |
|
|
23
|
+
| [`no-conditional-empty-object-spread`](docs/deslop.md) | Recommended | Reports object spreads using a conditional `{}` branch to omit fields. |
|
|
24
|
+
| [`no-known-value-widening`](docs/deslop.md) | Recommended | Rejects known expressions flowing into explicit `unknown`, `object`, or open dictionaries. |
|
|
25
|
+
| [`no-module-mocking`](docs/deslop.md) | Recommended | Rejects Vitest and Jest module mocks. |
|
|
26
|
+
| [`no-object-parameters`](docs/deslop.md) | Recommended | Rejects `object` on function inputs. |
|
|
27
|
+
| [`no-reflect-apply`](docs/deslop.md) | Recommended | Rejects global `Reflect.apply`. |
|
|
28
|
+
| [`no-reflect-get`](docs/deslop.md) | Recommended | Rejects global `Reflect.get`. |
|
|
29
|
+
| [`no-runtime-typeof`](docs/deslop.md) | Recommended | Requires boundary parsing instead of ad hoc `typeof` narrowing. |
|
|
30
|
+
| [`no-shape-in-symbol-names`](docs/deslop.md) | Recommended | Rejects `shape` in locally owned symbol names. |
|
|
31
|
+
| [`no-unknown-parameters`](docs/deslop.md) | Recommended | Rejects `unknown` on function inputs. |
|
|
32
|
+
| [`no-unknown-returns`](docs/deslop.md) | Recommended | Rejects return contracts resolving to `unknown`. |
|
|
33
|
+
| [`no-unknown-type-aliases`](docs/deslop.md) | Recommended | Rejects aliases whose resolved type is `unknown`. |
|
|
34
|
+
| [`no-unsafe-dictionary-type`](docs/deslop.md) | Recommended | Rejects dictionary values based on unsafe escape hatches. |
|
|
35
|
+
| [`no-widen-then-assert`](docs/deslop.md) | Recommended | Rejects widening known evidence before narrowing assertions. |
|
|
36
|
+
| [`require-safety-comment-for-type-assertion`](docs/deslop.md) | Recommended | Requires an invariant justification for non-const assertions. |
|
|
37
|
+
| [`no-call-only-assertions`](docs/deslop.md) | Recommended | Flags tests whose assertions only check mock calls. |
|
|
38
|
+
| [`no-pass-through-type-alias`](docs/deslop.md) | Recommended | Disallows aliases that only rename another type. |
|
|
39
|
+
| [`no-duplicate-types`](docs/deslop.md#no-duplicate-types) | Opt-in | Reports exact duplicate object contracts. No fixer. |
|
|
40
|
+
| [`canonical-class-names`](docs/tailwind.md) | Opt-in | Fixes non-canonical Tailwind CSS v4 class spellings. |
|
|
41
|
+
|
|
42
|
+
| Standard rule | Status | Description |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| `typescript/no-deprecated` | Recommended | Disallows using code marked `@deprecated`. Requires type-aware linting. |
|
|
45
|
+
| `typescript/no-floating-promises` | Recommended | Disallows floating promises. Requires type-aware linting. |
|
|
46
|
+
|
|
47
|
+
## Guides
|
|
48
|
+
|
|
49
|
+
| Guide | Contents |
|
|
76
50
|
| --- | --- |
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
| `no-chained-type-assertions` | Rejects nested `as` / angle-bracket assertions that fabricate evidence (`as const` chains stay valid). |
|
|
80
|
-
| `no-conditional-empty-object-spread` | Reports object spreads using a conditional `{}` branch to omit fields. |
|
|
81
|
-
| `no-known-value-widening` | Rejects known expressions flowing into explicit `unknown` / `object` / open-dictionary targets. |
|
|
82
|
-
| `no-module-mocking` | Rejects Vitest and Jest `mock` / `doMock` / `unstable_mockModule` calls. |
|
|
83
|
-
| `no-object-parameters` | Rejects `object` (and aliases resolving to it) on function inputs. |
|
|
84
|
-
| `no-reflect-apply` | Rejects global `Reflect.apply` in favor of typed function calls. |
|
|
85
|
-
| `no-reflect-get` | Rejects global `Reflect.get` in favor of typed property access or boundary parsing. |
|
|
86
|
-
| `no-runtime-typeof` | Requires boundary parsing instead of ad hoc `typeof` narrowing (`typeof x === "undefined"` probes stay allowed). |
|
|
87
|
-
| `no-shape-in-symbol-names` | Rejects the case-insensitive substring `shape` in locally owned symbol names. |
|
|
88
|
-
| `no-unknown-parameters` | Rejects `unknown` on function inputs (except the `cause` convention and type-predicate subjects). |
|
|
89
|
-
| `no-unknown-returns` | Rejects function contracts resolving to `unknown` / `Promise<unknown>`. |
|
|
90
|
-
| `no-unknown-type-aliases` | Rejects aliases whose resolved type is `unknown`. |
|
|
91
|
-
| `no-unsafe-dictionary-type` | Rejects dictionary value contracts based on `unknown`, `any`, `object`, `{}`. |
|
|
92
|
-
| `no-widen-then-assert` | Rejects flows that widen known evidence then assert it back to a narrower type. |
|
|
93
|
-
| `require-safety-comment-for-type-assertion` | Requires a nearby invariant justification comment (default marker `SAFETY:`) for each non-const assertion. |
|
|
94
|
-
| `no-call-only-assertions` | Flags tests whose every assertion only checks a mock was called (`toHaveBeenCalledWith` stays allowed). |
|
|
95
|
-
| `no-pass-through-type-alias` | Disallows type aliases that only rename another type. |
|
|
96
|
-
|
|
97
|
-
The rules use Oxlint's ESTree and lexical-scope APIs rather than a TypeScript type checker. They resolve same-file aliases but do not infer imported type definitions or cross-file call signatures.
|
|
98
|
-
|
|
99
|
-
### Standard (`typescript/*`, in `recommended`)
|
|
100
|
-
|
|
101
|
-
| Rule | Description |
|
|
102
|
-
| --- | --- |
|
|
103
|
-
| `typescript/no-deprecated` | Disallows using code marked `@deprecated`. Requires type-aware linting. |
|
|
104
|
-
| `typescript/no-floating-promises` | Disallows floating Promises without handling. Requires type-aware linting. |
|
|
105
|
-
|
|
106
|
-
### Opt-in (`deslop/canonical-class-names`, default off)
|
|
107
|
-
|
|
108
|
-
Enforces canonical Tailwind CSS class spellings with `--fix` support (`mt-[16px]` → `mt-4`). Same source of truth as the Tailwind language server. Excluded from `recommended` because it requires a `cssPath` option — add it manually:
|
|
109
|
-
|
|
110
|
-
```ts
|
|
111
|
-
export default defineConfig({
|
|
112
|
-
jsPlugins: [deslopPlugin],
|
|
113
|
-
options: {
|
|
114
|
-
...deslopConfigs.recommended.options,
|
|
115
|
-
},
|
|
116
|
-
rules: {
|
|
117
|
-
...deslopConfigs.recommended.rules,
|
|
118
|
-
"deslop/canonical-class-names": ["warn", { cssPath: "./src/styles.css" }],
|
|
119
|
-
},
|
|
120
|
-
});
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
Requires Tailwind CSS v4 (`tailwindcss` is an optional peer). Options: `cssPath` (required, absolute or relative to cwd), `rootFontSize` (default `16`), `attributes` (default `["class", "className"]`), `calleeFunctions` (default `["cn", "clsx", "cva", "twMerge", "tw", "classNames", "cx"]`). Only rewrites individual spellings — ordering/dedup belongs to the formatter. Strings with `${}` interpolations are skipped.
|
|
51
|
+
| [Deslop rules](docs/deslop.md) | Recommended and opt-in deslop configuration, complete TypeScript/JSON examples, and `no-duplicate-types` options. |
|
|
52
|
+
| [Tailwind canonical class names](docs/tailwind.md) | Tailwind v4 setup, complete TypeScript/JSON examples, and every supported option. |
|
|
124
53
|
|
|
125
54
|
## Credits
|
|
126
55
|
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { eslintCompatPlugin } from "@oxlint/plugins";
|
|
2
|
+
/** Every `deslop/*` rule, plus selected standard rules, each set to `"error"`. Spread into the `rules` field of an oxlint config. */
|
|
3
|
+
export declare const recommendedRules: Record<string, "error">;
|
|
4
|
+
/** ESLint flat-config style configs. `recommended` enables every recommended `deslop/*` rule as `"error"` (excluding opt-in rules), plus standard `typescript/*` rules. The `typescript/*` rules require type-aware linting (`options.typeAware`), so `recommended` sets it. */
|
|
5
|
+
export declare const configs: {
|
|
6
|
+
recommended: {
|
|
7
|
+
name: string;
|
|
8
|
+
options: {
|
|
9
|
+
typeAware: boolean;
|
|
10
|
+
};
|
|
11
|
+
rules: Record<string, "error">;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
/** Generic Oxlint rules that reject low-evidence and low-signal implementation patterns. */
|
|
15
|
+
declare const deslopPlugin: ReturnType<typeof eslintCompatPlugin> & {
|
|
16
|
+
configs: typeof configs;
|
|
17
|
+
};
|
|
18
|
+
export default deslopPlugin;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { eslintCompatPlugin } from "@oxlint/plugins";
|
|
2
|
+
import { noArrayFilterMapRule } from "./rules/no-array-filter-map.js";
|
|
3
|
+
import { noReduceAccumulatorCopyRule } from "./rules/no-reduce-accumulator-copy.js";
|
|
4
|
+
import { noChainedTypeAssertionsRule } from "./rules/no-chained-type-assertions.js";
|
|
5
|
+
import { noConditionalEmptyObjectSpreadRule } from "./rules/no-conditional-empty-object-spread.js";
|
|
6
|
+
import { noKnownValueWideningRule } from "./rules/no-known-value-widening.js";
|
|
7
|
+
import { noModuleMockingRule } from "./rules/no-module-mocking.js";
|
|
8
|
+
import { noObjectParametersRule } from "./rules/no-object-parameters.js";
|
|
9
|
+
import { noReflectApplyRule } from "./rules/no-reflect-apply.js";
|
|
10
|
+
import { noReflectGetRule } from "./rules/no-reflect-get.js";
|
|
11
|
+
import { noRuntimeTypeofRule } from "./rules/no-runtime-typeof.js";
|
|
12
|
+
import { noForbiddenTermInSymbolNamesRule } from "./rules/no-shape-in-symbol-names.js";
|
|
13
|
+
import { noUnknownParametersRule } from "./rules/no-unknown-parameters.js";
|
|
14
|
+
import { noUnknownReturnsRule } from "./rules/no-unknown-returns.js";
|
|
15
|
+
import { noUnknownTypeAliasesRule } from "./rules/no-unknown-type-aliases.js";
|
|
16
|
+
import { noUnsafeDictionaryTypeRule } from "./rules/no-unsafe-dictionary-type.js";
|
|
17
|
+
import { noWidenThenAssertRule } from "./rules/no-widen-then-assert.js";
|
|
18
|
+
import { requireSafetyCommentForTypeAssertionRule } from "./rules/require-safety-comment-for-type-assertion.js";
|
|
19
|
+
import { noCallOnlyAssertionsRule } from "./rules/no-call-only-assertions.js";
|
|
20
|
+
import { noPassThroughTypeAliasRule } from "./rules/no-pass-through-type-alias.js";
|
|
21
|
+
import { noDuplicateTypesRule } from "./rules/no-duplicate-types.js";
|
|
22
|
+
import { canonicalClassNames } from "./tailwind/rule.js";
|
|
23
|
+
/** Every `deslop/*` rule, plus selected standard rules, each set to `"error"`. Spread into the `rules` field of an oxlint config. */
|
|
24
|
+
export const recommendedRules = {
|
|
25
|
+
"deslop/no-array-filter-map": "error",
|
|
26
|
+
"deslop/no-reduce-accumulator-copy": "error",
|
|
27
|
+
"deslop/no-chained-type-assertions": "error",
|
|
28
|
+
"deslop/no-conditional-empty-object-spread": "error",
|
|
29
|
+
"deslop/no-known-value-widening": "error",
|
|
30
|
+
"deslop/no-module-mocking": "error",
|
|
31
|
+
"deslop/no-object-parameters": "error",
|
|
32
|
+
"deslop/no-reflect-apply": "error",
|
|
33
|
+
"deslop/no-reflect-get": "error",
|
|
34
|
+
"deslop/no-runtime-typeof": "error",
|
|
35
|
+
"deslop/no-shape-in-symbol-names": "error",
|
|
36
|
+
"deslop/no-unknown-parameters": "error",
|
|
37
|
+
"deslop/no-unknown-returns": "error",
|
|
38
|
+
"deslop/no-unknown-type-aliases": "error",
|
|
39
|
+
"deslop/no-unsafe-dictionary-type": "error",
|
|
40
|
+
"deslop/no-widen-then-assert": "error",
|
|
41
|
+
"deslop/require-safety-comment-for-type-assertion": "error",
|
|
42
|
+
"deslop/no-call-only-assertions": "error",
|
|
43
|
+
"deslop/no-pass-through-type-alias": "error",
|
|
44
|
+
"typescript/no-deprecated": "error",
|
|
45
|
+
"typescript/no-floating-promises": "error",
|
|
46
|
+
};
|
|
47
|
+
/** ESLint flat-config style configs. `recommended` enables every recommended `deslop/*` rule as `"error"` (excluding opt-in rules), plus standard `typescript/*` rules. The `typescript/*` rules require type-aware linting (`options.typeAware`), so `recommended` sets it. */
|
|
48
|
+
export const configs = {
|
|
49
|
+
recommended: {
|
|
50
|
+
name: "deslop/recommended",
|
|
51
|
+
options: { typeAware: true },
|
|
52
|
+
rules: recommendedRules,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
/** Generic Oxlint rules that reject low-evidence and low-signal implementation patterns. */
|
|
56
|
+
const deslopPlugin = eslintCompatPlugin({
|
|
57
|
+
meta: { name: "deslop" },
|
|
58
|
+
rules: {
|
|
59
|
+
"no-array-filter-map": noArrayFilterMapRule,
|
|
60
|
+
"no-reduce-accumulator-copy": noReduceAccumulatorCopyRule,
|
|
61
|
+
"no-chained-type-assertions": noChainedTypeAssertionsRule,
|
|
62
|
+
"no-conditional-empty-object-spread": noConditionalEmptyObjectSpreadRule,
|
|
63
|
+
"no-known-value-widening": noKnownValueWideningRule,
|
|
64
|
+
"no-module-mocking": noModuleMockingRule,
|
|
65
|
+
"no-object-parameters": noObjectParametersRule,
|
|
66
|
+
"no-reflect-apply": noReflectApplyRule,
|
|
67
|
+
"no-reflect-get": noReflectGetRule,
|
|
68
|
+
"no-runtime-typeof": noRuntimeTypeofRule,
|
|
69
|
+
"no-shape-in-symbol-names": noForbiddenTermInSymbolNamesRule,
|
|
70
|
+
"no-unknown-parameters": noUnknownParametersRule,
|
|
71
|
+
"no-unknown-returns": noUnknownReturnsRule,
|
|
72
|
+
"no-unknown-type-aliases": noUnknownTypeAliasesRule,
|
|
73
|
+
"no-unsafe-dictionary-type": noUnsafeDictionaryTypeRule,
|
|
74
|
+
"no-widen-then-assert": noWidenThenAssertRule,
|
|
75
|
+
"require-safety-comment-for-type-assertion": requireSafetyCommentForTypeAssertionRule,
|
|
76
|
+
"no-call-only-assertions": noCallOnlyAssertionsRule,
|
|
77
|
+
"no-pass-through-type-alias": noPassThroughTypeAliasRule,
|
|
78
|
+
"no-duplicate-types": noDuplicateTypesRule,
|
|
79
|
+
// Opt-in: NOT in `recommendedRules`. Requires a `cssPath` option, so users add it manually.
|
|
80
|
+
"canonical-class-names": canonicalClassNames,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
deslopPlugin.configs = configs;
|
|
84
|
+
export default deslopPlugin;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { arrayMethodTarget, isKnownArrayExpression, unwrapArrayExpression } from "../shared/array-method.js";
|
|
3
|
+
/** Reject eager array filter/map pipelines; lazy iterator helpers remain allowed. */
|
|
4
|
+
export const noArrayFilterMapRule = defineRule({
|
|
5
|
+
meta: {
|
|
6
|
+
type: "suggestion",
|
|
7
|
+
docs: { description: "Disallow adjacent array filter/map passes in favor of lazy iterator helpers or a single transformation." },
|
|
8
|
+
messages: {
|
|
9
|
+
arrayFilterMap: "Avoid consecutive array `{{first}}` and `{{second}}` passes. Prefer `.values().{{first}}(...).{{second}}(...).toArray()` where iterator helpers are supported, or a single `flatMap`/mutating reducer. Preserve callback ordering, indexes, and filtering semantics.",
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
createOnce(context) {
|
|
13
|
+
return {
|
|
14
|
+
CallExpression(node) {
|
|
15
|
+
const outer = arrayMethodTarget(node.callee);
|
|
16
|
+
if (outer === null || (outer.name !== "map" && outer.name !== "filter"))
|
|
17
|
+
return;
|
|
18
|
+
const innerCall = unwrapArrayExpression(outer.object);
|
|
19
|
+
if (innerCall.type !== "CallExpression")
|
|
20
|
+
return;
|
|
21
|
+
const inner = arrayMethodTarget(innerCall.callee);
|
|
22
|
+
if (inner === null || inner.name !== (outer.name === "map" ? "filter" : "map"))
|
|
23
|
+
return;
|
|
24
|
+
if (!isKnownArrayExpression(context.sourceCode, inner.object))
|
|
25
|
+
return;
|
|
26
|
+
context.report({ node, messageId: "arrayFilterMap", data: { first: inner.name, second: outer.name } });
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
// Tests whose every assertion is a "was it called" mock check verify orchestration, not behavior.
|
|
3
|
+
// `toHaveBeenCalledWith` is allowed because it asserts on real data.
|
|
4
|
+
const CALL_ONLY_MATCHERS = new Set(["toHaveBeenCalled", "toHaveBeenCalledTimes", "toHaveBeenCalledOnce"]);
|
|
5
|
+
const TEST_NAMES = new Set(["it", "test"]);
|
|
6
|
+
const MODIFIERS = new Set(["not", "resolves", "rejects"]);
|
|
7
|
+
function rootIdentifierName(node) {
|
|
8
|
+
let current = node;
|
|
9
|
+
while (true) {
|
|
10
|
+
if (current.type === "Identifier")
|
|
11
|
+
return current.name;
|
|
12
|
+
if (current.type === "MemberExpression") {
|
|
13
|
+
current = current.object;
|
|
14
|
+
}
|
|
15
|
+
else if (current.type === "CallExpression" || current.type === "TaggedTemplateExpression") {
|
|
16
|
+
current = current.type === "CallExpression" ? current.callee : current.tag;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function isTestCall(node) {
|
|
24
|
+
const name = rootIdentifierName(node.callee);
|
|
25
|
+
if (name === undefined || !TEST_NAMES.has(name))
|
|
26
|
+
return false;
|
|
27
|
+
const callback = node.arguments.at(-1);
|
|
28
|
+
return callback?.type === "FunctionExpression" || callback?.type === "ArrowFunctionExpression";
|
|
29
|
+
}
|
|
30
|
+
function matcherName(node) {
|
|
31
|
+
if (node.callee.type !== "MemberExpression" || node.callee.computed)
|
|
32
|
+
return undefined;
|
|
33
|
+
if (node.callee.property.type !== "Identifier")
|
|
34
|
+
return undefined;
|
|
35
|
+
let object = node.callee.object;
|
|
36
|
+
while (object.type === "MemberExpression" &&
|
|
37
|
+
!object.computed &&
|
|
38
|
+
object.property.type === "Identifier" &&
|
|
39
|
+
MODIFIERS.has(object.property.name)) {
|
|
40
|
+
object = object.object;
|
|
41
|
+
}
|
|
42
|
+
const isExpectCall = object.type === "CallExpression" && rootIdentifierName(object.callee) === "expect";
|
|
43
|
+
return isExpectCall ? node.callee.property.name : undefined;
|
|
44
|
+
}
|
|
45
|
+
/** Tests must assert on behavior, not only that a mock was called. */
|
|
46
|
+
export const noCallOnlyAssertionsRule = defineRule({
|
|
47
|
+
meta: {
|
|
48
|
+
type: "suggestion",
|
|
49
|
+
docs: { description: "Tests must assert on behavior, not only that a mock was called" },
|
|
50
|
+
messages: {
|
|
51
|
+
callOnlyAssertions: "Every assertion in this test only checks that a mock was called. First check whether this is a valuable test; remove it if not. Otherwise, assert on returned values, state, or call arguments instead.",
|
|
52
|
+
},
|
|
53
|
+
schema: [],
|
|
54
|
+
},
|
|
55
|
+
createOnce(context) {
|
|
56
|
+
let tests = [];
|
|
57
|
+
return {
|
|
58
|
+
before() {
|
|
59
|
+
tests = [];
|
|
60
|
+
},
|
|
61
|
+
CallExpression(node) {
|
|
62
|
+
if (isTestCall(node)) {
|
|
63
|
+
tests.push({ node, total: 0, callOnly: 0 });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const test = tests.at(-1);
|
|
67
|
+
if (!test)
|
|
68
|
+
return;
|
|
69
|
+
const matcher = matcherName(node);
|
|
70
|
+
if (!matcher)
|
|
71
|
+
return;
|
|
72
|
+
test.total += 1;
|
|
73
|
+
if (CALL_ONLY_MATCHERS.has(matcher))
|
|
74
|
+
test.callOnly += 1;
|
|
75
|
+
},
|
|
76
|
+
"CallExpression:exit"(node) {
|
|
77
|
+
const test = tests.at(-1);
|
|
78
|
+
if (test?.node !== node)
|
|
79
|
+
return;
|
|
80
|
+
tests.pop();
|
|
81
|
+
if (test.total > 0 && test.total === test.callOnly) {
|
|
82
|
+
context.report({ messageId: "callOnlyAssertions", node: node.callee });
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
function isTypeAssertionExpression(node) {
|
|
3
|
+
return node.type === "TSAsExpression" || node.type === "TSTypeAssertion";
|
|
4
|
+
}
|
|
5
|
+
function unwrapParenthesizedExpression(expression) {
|
|
6
|
+
let current = expression;
|
|
7
|
+
while (current.type === "ParenthesizedExpression") {
|
|
8
|
+
current = current.expression;
|
|
9
|
+
}
|
|
10
|
+
return current;
|
|
11
|
+
}
|
|
12
|
+
function isConstAssertion(node) {
|
|
13
|
+
const { typeAnnotation } = node;
|
|
14
|
+
return (typeAnnotation.type === "TSTypeReference" &&
|
|
15
|
+
typeAnnotation.typeName.type === "Identifier" &&
|
|
16
|
+
typeAnnotation.typeName.name === "const");
|
|
17
|
+
}
|
|
18
|
+
function isOutermostAssertionInChain(node) {
|
|
19
|
+
let current = node;
|
|
20
|
+
let parent = node.parent;
|
|
21
|
+
while (parent.type === "ParenthesizedExpression" && parent.expression === current) {
|
|
22
|
+
current = parent;
|
|
23
|
+
parent = parent.parent;
|
|
24
|
+
}
|
|
25
|
+
return !isTypeAssertionExpression(parent) || parent.expression !== current;
|
|
26
|
+
}
|
|
27
|
+
function isForbiddenAssertionChain(node) {
|
|
28
|
+
let assertionCount = 0;
|
|
29
|
+
let hasNonConstAssertion = false;
|
|
30
|
+
let current = node;
|
|
31
|
+
while (isTypeAssertionExpression(current)) {
|
|
32
|
+
assertionCount += 1;
|
|
33
|
+
hasNonConstAssertion ||= !isConstAssertion(current);
|
|
34
|
+
current = unwrapParenthesizedExpression(current.expression);
|
|
35
|
+
}
|
|
36
|
+
return assertionCount > 1 && hasNonConstAssertion;
|
|
37
|
+
}
|
|
38
|
+
/** Disallow nested TypeScript type assertions, while permitting chains made only of const assertions. */
|
|
39
|
+
export const noChainedTypeAssertionsRule = defineRule({
|
|
40
|
+
meta: {
|
|
41
|
+
type: "problem",
|
|
42
|
+
docs: {
|
|
43
|
+
description: "Disallow chained TypeScript as and angle-bracket assertions, including parenthesized chains.",
|
|
44
|
+
},
|
|
45
|
+
messages: {
|
|
46
|
+
chained: "This assertion chain discards type evidence. Keep the original precise type, or parse untrusted input at its boundary before narrowing it.",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
createOnce(context) {
|
|
50
|
+
const checkTypeAssertion = (node) => {
|
|
51
|
+
if (!isOutermostAssertionInChain(node) || !isForbiddenAssertionChain(node))
|
|
52
|
+
return;
|
|
53
|
+
context.report({ node, messageId: "chained" });
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
TSAsExpression: checkTypeAssertion,
|
|
57
|
+
TSTypeAssertion: checkTypeAssertion,
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
function unwrapParentheses(node) {
|
|
3
|
+
let current = node;
|
|
4
|
+
while (current.type === "ParenthesizedExpression") {
|
|
5
|
+
current = current.expression;
|
|
6
|
+
}
|
|
7
|
+
return current;
|
|
8
|
+
}
|
|
9
|
+
function isEmptyObjectExpression(node) {
|
|
10
|
+
return node.type === "ObjectExpression" && node.properties.length === 0;
|
|
11
|
+
}
|
|
12
|
+
function isConditionalEmptyObjectSpread(node) {
|
|
13
|
+
const conditional = unwrapParentheses(node);
|
|
14
|
+
return (conditional.type === "ConditionalExpression" &&
|
|
15
|
+
(isEmptyObjectExpression(conditional.consequent) ||
|
|
16
|
+
isEmptyObjectExpression(conditional.alternate)));
|
|
17
|
+
}
|
|
18
|
+
/** Ban conditional empty-object spreads without changing their omission semantics. */
|
|
19
|
+
export const noConditionalEmptyObjectSpreadRule = defineRule({
|
|
20
|
+
meta: {
|
|
21
|
+
type: "suggestion",
|
|
22
|
+
docs: {
|
|
23
|
+
description: "Disallow object spreads that conditionally spread an empty object to omit fields.",
|
|
24
|
+
},
|
|
25
|
+
messages: {
|
|
26
|
+
avoid: "This conditional spread hides property omission behind an empty object. Build the object in separate statements and add the property only when present.",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
createOnce(context) {
|
|
30
|
+
return {
|
|
31
|
+
SpreadElement(node) {
|
|
32
|
+
if (node.parent.type !== "ObjectExpression")
|
|
33
|
+
return;
|
|
34
|
+
if (isConditionalEmptyObjectSpread(node.argument)) {
|
|
35
|
+
context.report({ node, messageId: "avoid" });
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
function propertyKey(node) {
|
|
3
|
+
const value = node;
|
|
4
|
+
if (value.type === "Identifier" && typeof value.name === "string")
|
|
5
|
+
return value.name;
|
|
6
|
+
if (typeof value.value === "string")
|
|
7
|
+
return JSON.stringify(value.value);
|
|
8
|
+
if (typeof value.value === "number")
|
|
9
|
+
return String(value.value);
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
function literalValue(node) {
|
|
13
|
+
const literal = node.literal;
|
|
14
|
+
if (typeof literal.value === "string")
|
|
15
|
+
return JSON.stringify(literal.value);
|
|
16
|
+
if (typeof literal.value === "number" || typeof literal.value === "boolean")
|
|
17
|
+
return String(literal.value);
|
|
18
|
+
if (typeof literal.value === "bigint")
|
|
19
|
+
return `${literal.value}n`;
|
|
20
|
+
if (literal.type === "UnaryExpression" && literal.operator === "-") {
|
|
21
|
+
if (typeof literal.argument?.value === "number")
|
|
22
|
+
return `-${literal.argument.value}`;
|
|
23
|
+
if (typeof literal.argument?.value === "bigint")
|
|
24
|
+
return `-${literal.argument.value}n`;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
function canonicalTypeName(node) {
|
|
29
|
+
if (node.type === "Identifier")
|
|
30
|
+
return node.name;
|
|
31
|
+
if (node.type !== "TSQualifiedName")
|
|
32
|
+
return null;
|
|
33
|
+
const left = canonicalTypeName(node.left);
|
|
34
|
+
return left === null ? null : `${left}.${node.right.name}`;
|
|
35
|
+
}
|
|
36
|
+
function canonicalMembers(members, parameters) {
|
|
37
|
+
const result = [];
|
|
38
|
+
for (const member of members) {
|
|
39
|
+
if (member.type !== "TSPropertySignature" || member.computed || member.typeAnnotation === null)
|
|
40
|
+
return null;
|
|
41
|
+
const key = propertyKey(member.key);
|
|
42
|
+
const type = canonicalType(member.typeAnnotation.typeAnnotation, parameters);
|
|
43
|
+
if (key === null || type === null)
|
|
44
|
+
return null;
|
|
45
|
+
result.push(`prop(${key},${member.optional ? "?" : "!"},${member.readonly ? "ro" : "rw"},${type})`);
|
|
46
|
+
}
|
|
47
|
+
result.sort();
|
|
48
|
+
return `{${result.join(",")}}`;
|
|
49
|
+
}
|
|
50
|
+
function canonicalType(node, parameters) {
|
|
51
|
+
switch (node.type) {
|
|
52
|
+
case "TSAnyKeyword":
|
|
53
|
+
case "TSBigIntKeyword":
|
|
54
|
+
case "TSBooleanKeyword":
|
|
55
|
+
case "TSNeverKeyword":
|
|
56
|
+
case "TSNullKeyword":
|
|
57
|
+
case "TSNumberKeyword":
|
|
58
|
+
case "TSObjectKeyword":
|
|
59
|
+
case "TSStringKeyword":
|
|
60
|
+
case "TSSymbolKeyword":
|
|
61
|
+
case "TSUndefinedKeyword":
|
|
62
|
+
case "TSUnknownKeyword":
|
|
63
|
+
case "TSVoidKeyword":
|
|
64
|
+
return node.type;
|
|
65
|
+
case "TSLiteralType": {
|
|
66
|
+
const value = literalValue(node);
|
|
67
|
+
return value === null ? null : `literal(${value})`;
|
|
68
|
+
}
|
|
69
|
+
case "TSArrayType": {
|
|
70
|
+
const element = canonicalType(node.elementType, parameters);
|
|
71
|
+
return element === null ? null : `array(${element})`;
|
|
72
|
+
}
|
|
73
|
+
case "TSParenthesizedType": return canonicalType(node.typeAnnotation, parameters);
|
|
74
|
+
case "TSTypeOperator": {
|
|
75
|
+
const type = canonicalType(node.typeAnnotation, parameters);
|
|
76
|
+
return type === null ? null : `${node.operator}(${type})`;
|
|
77
|
+
}
|
|
78
|
+
case "TSUnionType":
|
|
79
|
+
case "TSIntersectionType": {
|
|
80
|
+
const types = node.types.map((type) => canonicalType(type, parameters));
|
|
81
|
+
if (types.some((type) => type === null))
|
|
82
|
+
return null;
|
|
83
|
+
return `${node.type === "TSUnionType" ? "union" : "intersection"}(${[...new Set(types)].sort().join(",")})`;
|
|
84
|
+
}
|
|
85
|
+
case "TSTypeLiteral": return canonicalMembers(node.members, parameters);
|
|
86
|
+
case "TSTypeReference": {
|
|
87
|
+
const name = canonicalTypeName(node.typeName);
|
|
88
|
+
if (name === null)
|
|
89
|
+
return null;
|
|
90
|
+
const arguments_ = node.typeArguments?.params.map((type) => canonicalType(type, parameters)) ?? [];
|
|
91
|
+
if (arguments_.some((type) => type === null))
|
|
92
|
+
return null;
|
|
93
|
+
return `ref(${parameters.get(name) ?? name}<${arguments_.join(",")}>)`;
|
|
94
|
+
}
|
|
95
|
+
default: return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function canonicalDeclaration(node) {
|
|
99
|
+
const parameters = new Map();
|
|
100
|
+
for (const [index, parameter] of (node.typeParameters?.params ?? []).entries()) {
|
|
101
|
+
parameters.set(parameter.name.name, `$${index}`);
|
|
102
|
+
}
|
|
103
|
+
if (node.type === "TSTypeAliasDeclaration")
|
|
104
|
+
return canonicalType(node.typeAnnotation, parameters);
|
|
105
|
+
if (node.extends.length > 0)
|
|
106
|
+
return null;
|
|
107
|
+
return canonicalMembers(node.body.body, parameters);
|
|
108
|
+
}
|
|
109
|
+
/** Report exact structural duplicates without TypeScript assignability or fuzzy matching. */
|
|
110
|
+
export const noDuplicateTypesRule = defineRule({
|
|
111
|
+
meta: {
|
|
112
|
+
type: "suggestion",
|
|
113
|
+
docs: { description: "Report structurally identical TypeScript declarations" },
|
|
114
|
+
messages: { duplicate: "{{message}} First seen as {{firstName}} in {{firstFile}}." },
|
|
115
|
+
schema: [{ type: "object", properties: { message: { type: "string" }, minProperties: { type: "integer", minimum: 0 } }, additionalProperties: false }],
|
|
116
|
+
},
|
|
117
|
+
createOnce(context) {
|
|
118
|
+
const seen = new Map();
|
|
119
|
+
let options = {};
|
|
120
|
+
const report = (node, fingerprint) => {
|
|
121
|
+
const first = seen.get(fingerprint);
|
|
122
|
+
if (first === undefined) {
|
|
123
|
+
seen.set(fingerprint, { name: node.id.name, filename: context.filename });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
context.report({ node, messageId: "duplicate", data: { message: options.message ?? "This type is structurally identical to an existing declaration.", firstName: first.name, firstFile: first.filename } });
|
|
127
|
+
};
|
|
128
|
+
const inspect = (node) => {
|
|
129
|
+
const fingerprint = canonicalDeclaration(node);
|
|
130
|
+
if (fingerprint === null)
|
|
131
|
+
return;
|
|
132
|
+
const properties = node.type === "TSTypeAliasDeclaration" && node.typeAnnotation.type === "TSTypeLiteral" ? node.typeAnnotation.members.length : node.type === "TSInterfaceDeclaration" ? node.body.body.length : Infinity;
|
|
133
|
+
if (properties < (options.minProperties ?? 2))
|
|
134
|
+
return;
|
|
135
|
+
report(node, fingerprint);
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
Program() { options = context.options[0] ?? {}; },
|
|
139
|
+
TSTypeAliasDeclaration: inspect,
|
|
140
|
+
TSInterfaceDeclaration: inspect,
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
});
|