ai-deslop 0.2.0 → 0.2.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/README.md +7 -4
- package/dist/index.d.ts +18 -0
- package/dist/index.js +82 -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-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 +13 -4
- 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,7 +11,7 @@ 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
|
-
|
|
14
|
+
Ships compiled JavaScript — works with any oxlint JS-plugin runtime (Bun, Deno, or Node.js).
|
|
15
15
|
|
|
16
16
|
## Usage
|
|
17
17
|
|
|
@@ -19,10 +19,10 @@ Register the plugin in `oxlint.config.ts` and enable every rule as an error via
|
|
|
19
19
|
|
|
20
20
|
```ts
|
|
21
21
|
import { defineConfig } from "oxlint";
|
|
22
|
-
import
|
|
22
|
+
import { configs as deslopConfigs } from "ai-deslop";
|
|
23
23
|
|
|
24
24
|
export default defineConfig({
|
|
25
|
-
jsPlugins: [
|
|
25
|
+
jsPlugins: ["ai-deslop"],
|
|
26
26
|
options: {
|
|
27
27
|
...deslopConfigs.recommended.options,
|
|
28
28
|
},
|
|
@@ -108,8 +108,11 @@ The rules use Oxlint's ESTree and lexical-scope APIs rather than a TypeScript ty
|
|
|
108
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
109
|
|
|
110
110
|
```ts
|
|
111
|
+
import { defineConfig } from "oxlint";
|
|
112
|
+
import { configs as deslopConfigs } from "ai-deslop";
|
|
113
|
+
|
|
111
114
|
export default defineConfig({
|
|
112
|
-
jsPlugins: [
|
|
115
|
+
jsPlugins: ["ai-deslop"],
|
|
113
116
|
options: {
|
|
114
117
|
...deslopConfigs.recommended.options,
|
|
115
118
|
},
|
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 `deslop/*` rule as `"error"` (except opt-in rules like `canonical-class-names`), 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,82 @@
|
|
|
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 { canonicalClassNames } from "./tailwind/rule.js";
|
|
22
|
+
/** Every `deslop/*` rule, plus selected standard rules, each set to `"error"`. Spread into the `rules` field of an oxlint config. */
|
|
23
|
+
export const recommendedRules = {
|
|
24
|
+
"deslop/no-array-filter-map": "error",
|
|
25
|
+
"deslop/no-reduce-accumulator-copy": "error",
|
|
26
|
+
"deslop/no-chained-type-assertions": "error",
|
|
27
|
+
"deslop/no-conditional-empty-object-spread": "error",
|
|
28
|
+
"deslop/no-known-value-widening": "error",
|
|
29
|
+
"deslop/no-module-mocking": "error",
|
|
30
|
+
"deslop/no-object-parameters": "error",
|
|
31
|
+
"deslop/no-reflect-apply": "error",
|
|
32
|
+
"deslop/no-reflect-get": "error",
|
|
33
|
+
"deslop/no-runtime-typeof": "error",
|
|
34
|
+
"deslop/no-shape-in-symbol-names": "error",
|
|
35
|
+
"deslop/no-unknown-parameters": "error",
|
|
36
|
+
"deslop/no-unknown-returns": "error",
|
|
37
|
+
"deslop/no-unknown-type-aliases": "error",
|
|
38
|
+
"deslop/no-unsafe-dictionary-type": "error",
|
|
39
|
+
"deslop/no-widen-then-assert": "error",
|
|
40
|
+
"deslop/require-safety-comment-for-type-assertion": "error",
|
|
41
|
+
"deslop/no-call-only-assertions": "error",
|
|
42
|
+
"deslop/no-pass-through-type-alias": "error",
|
|
43
|
+
"typescript/no-deprecated": "error",
|
|
44
|
+
"typescript/no-floating-promises": "error",
|
|
45
|
+
};
|
|
46
|
+
/** ESLint flat-config style configs. `recommended` enables every `deslop/*` rule as `"error"` (except opt-in rules like `canonical-class-names`), plus standard `typescript/*` rules. The `typescript/*` rules require type-aware linting (`options.typeAware`), so `recommended` sets it. */
|
|
47
|
+
export const configs = {
|
|
48
|
+
recommended: {
|
|
49
|
+
name: "deslop/recommended",
|
|
50
|
+
options: { typeAware: true },
|
|
51
|
+
rules: recommendedRules,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
/** Generic Oxlint rules that reject low-evidence and low-signal implementation patterns. */
|
|
55
|
+
const deslopPlugin = eslintCompatPlugin({
|
|
56
|
+
meta: { name: "deslop" },
|
|
57
|
+
rules: {
|
|
58
|
+
"no-array-filter-map": noArrayFilterMapRule,
|
|
59
|
+
"no-reduce-accumulator-copy": noReduceAccumulatorCopyRule,
|
|
60
|
+
"no-chained-type-assertions": noChainedTypeAssertionsRule,
|
|
61
|
+
"no-conditional-empty-object-spread": noConditionalEmptyObjectSpreadRule,
|
|
62
|
+
"no-known-value-widening": noKnownValueWideningRule,
|
|
63
|
+
"no-module-mocking": noModuleMockingRule,
|
|
64
|
+
"no-object-parameters": noObjectParametersRule,
|
|
65
|
+
"no-reflect-apply": noReflectApplyRule,
|
|
66
|
+
"no-reflect-get": noReflectGetRule,
|
|
67
|
+
"no-runtime-typeof": noRuntimeTypeofRule,
|
|
68
|
+
"no-shape-in-symbol-names": noForbiddenTermInSymbolNamesRule,
|
|
69
|
+
"no-unknown-parameters": noUnknownParametersRule,
|
|
70
|
+
"no-unknown-returns": noUnknownReturnsRule,
|
|
71
|
+
"no-unknown-type-aliases": noUnknownTypeAliasesRule,
|
|
72
|
+
"no-unsafe-dictionary-type": noUnsafeDictionaryTypeRule,
|
|
73
|
+
"no-widen-then-assert": noWidenThenAssertRule,
|
|
74
|
+
"require-safety-comment-for-type-assertion": requireSafetyCommentForTypeAssertionRule,
|
|
75
|
+
"no-call-only-assertions": noCallOnlyAssertionsRule,
|
|
76
|
+
"no-pass-through-type-alias": noPassThroughTypeAliasRule,
|
|
77
|
+
// Opt-in: NOT in `recommendedRules`. Requires a `cssPath` option, so users add it manually.
|
|
78
|
+
"canonical-class-names": canonicalClassNames,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
deslopPlugin.configs = configs;
|
|
82
|
+
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
|
+
});
|