@timmo001/oxlint-rules 0.3.6 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -4
- package/dist/cli.js +462 -55
- package/dist/configs/recommended-effect.js +498 -95
- package/dist/configs/recommended.js +362 -14
- package/dist/effect/index.js +95 -11
- package/dist/generic/index.js +356 -13
- package/package.json +8 -6
- package/src/effect/index.ts +2 -0
- package/src/effect/rules/no-try-catch-in-effect-generators.ts +4 -98
- package/src/effect/rules/prefer-platform-services.ts +105 -0
- package/src/generic/index.ts +11 -0
- package/src/generic/rules/no-explicit-inferable-type-predicate.ts +146 -0
- package/src/generic/rules/no-unchecked-query-selector-type.ts +48 -0
- package/src/generic/rules/no-untyped-custom-event.ts +31 -0
- package/src/generic/rules/prefer-event-parameter-type.ts +36 -35
- package/src/generic/rules/prefer-lit-query-decorators.ts +84 -0
- package/src/generic/rules/prefer-types-over-assertions.ts +39 -0
- package/src/shared/ast.ts +31 -0
- package/src/shared/effect.ts +72 -0
- package/src/shared/json.ts +11 -0
- package/src/shared/lit.ts +219 -0
- package/src/shared/scope.ts +1 -0
package/src/generic/index.ts
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import { eslintCompatPlugin } from "@oxlint/plugins";
|
|
2
2
|
|
|
3
|
+
import { noExplicitInferableTypePredicateRule } from "./rules/no-explicit-inferable-type-predicate.ts";
|
|
4
|
+
import { noUncheckedQuerySelectorTypeRule } from "./rules/no-unchecked-query-selector-type.ts";
|
|
5
|
+
import { noUntypedCustomEventRule } from "./rules/no-untyped-custom-event.ts";
|
|
3
6
|
import { preferEventParameterTypeRule } from "./rules/prefer-event-parameter-type.ts";
|
|
7
|
+
import { preferLitQueryDecoratorsRule } from "./rules/prefer-lit-query-decorators.ts";
|
|
8
|
+
import { preferTypesOverAssertionsRule } from "./rules/prefer-types-over-assertions.ts";
|
|
4
9
|
|
|
5
10
|
const timmoPlugin = eslintCompatPlugin({
|
|
6
11
|
meta: { name: "timmo" },
|
|
7
12
|
rules: {
|
|
13
|
+
"no-explicit-inferable-type-predicate":
|
|
14
|
+
noExplicitInferableTypePredicateRule,
|
|
15
|
+
"no-unchecked-query-selector-type": noUncheckedQuerySelectorTypeRule,
|
|
16
|
+
"no-untyped-custom-event": noUntypedCustomEventRule,
|
|
8
17
|
"prefer-event-parameter-type": preferEventParameterTypeRule,
|
|
18
|
+
"prefer-lit-query-decorators": preferLitQueryDecoratorsRule,
|
|
19
|
+
"prefer-types-over-assertions": preferTypesOverAssertionsRule,
|
|
9
20
|
},
|
|
10
21
|
});
|
|
11
22
|
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
import type { FunctionNode } from "../../shared/ast.ts";
|
|
4
|
+
import type { ESTree } from "@oxlint/plugins";
|
|
5
|
+
|
|
6
|
+
function isNamed(node: ESTree.Node, name: string): boolean {
|
|
7
|
+
return node.type === "Identifier" && node.name === name;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isNullish(node: ESTree.Node): boolean {
|
|
11
|
+
return (
|
|
12
|
+
(node.type === "Literal" && node.value === null) ||
|
|
13
|
+
isNamed(node, "undefined")
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isNullishExclusion(node: ESTree.Node, name: string): boolean {
|
|
18
|
+
return (
|
|
19
|
+
node.type === "BinaryExpression" &&
|
|
20
|
+
(node.operator === "!==" || node.operator === "!=") &&
|
|
21
|
+
((isNamed(node.left, name) && isNullish(node.right)) ||
|
|
22
|
+
(isNullish(node.left) && isNamed(node.right, name)))
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isTypeofCheck(node: ESTree.Node, name: string): boolean {
|
|
27
|
+
if (
|
|
28
|
+
node.type !== "BinaryExpression" ||
|
|
29
|
+
(node.operator !== "===" && node.operator !== "==")
|
|
30
|
+
) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const isTypeof = (side: ESTree.Node) =>
|
|
35
|
+
side.type === "UnaryExpression" &&
|
|
36
|
+
side.operator === "typeof" &&
|
|
37
|
+
isNamed(side.argument, name);
|
|
38
|
+
|
|
39
|
+
const isLiteral = (side: ESTree.Node) => side.type === "Literal";
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
(isTypeof(node.left) && isLiteral(node.right)) ||
|
|
43
|
+
(isLiteral(node.left) && isTypeof(node.right))
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isInferableCheck(node: ESTree.Node, name: string): boolean {
|
|
48
|
+
if (node.type === "LogicalExpression") {
|
|
49
|
+
return (
|
|
50
|
+
node.operator === "&&" &&
|
|
51
|
+
isNullishExclusion(node.left, name) &&
|
|
52
|
+
isNullishExclusion(node.right, name)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
isNullishExclusion(node, name) ||
|
|
58
|
+
isTypeofCheck(node, name) ||
|
|
59
|
+
(node.type === "BinaryExpression" &&
|
|
60
|
+
node.operator === "instanceof" &&
|
|
61
|
+
isNamed(node.left, name))
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function returnedExpression(owner: FunctionNode): ESTree.Node | null {
|
|
66
|
+
const body = owner.body;
|
|
67
|
+
|
|
68
|
+
if (!body) return null;
|
|
69
|
+
|
|
70
|
+
if (body.type !== "BlockStatement") return body;
|
|
71
|
+
|
|
72
|
+
const [statement] = body.body;
|
|
73
|
+
|
|
74
|
+
return body.body.length === 1 && statement?.type === "ReturnStatement"
|
|
75
|
+
? statement.argument
|
|
76
|
+
: null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isPublicSignature(owner: FunctionNode): boolean {
|
|
80
|
+
const parent = owner.parent;
|
|
81
|
+
|
|
82
|
+
if (
|
|
83
|
+
parent.type === "MethodDefinition" ||
|
|
84
|
+
parent.type === "ExportNamedDeclaration" ||
|
|
85
|
+
parent.type === "ExportDefaultDeclaration"
|
|
86
|
+
) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
parent.type === "VariableDeclarator" &&
|
|
92
|
+
parent.parent.parent?.type === "ExportNamedDeclaration"
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Flag explicit type predicates that TypeScript would infer from a simple check. */
|
|
97
|
+
export const noExplicitInferableTypePredicateRule = defineRule({
|
|
98
|
+
meta: {
|
|
99
|
+
type: "suggestion",
|
|
100
|
+
docs: {
|
|
101
|
+
description:
|
|
102
|
+
"Disallow explicit type predicates on simple null, typeof, or instanceof checks that TypeScript 5.5+ infers.",
|
|
103
|
+
},
|
|
104
|
+
messages: {
|
|
105
|
+
removeTypePredicate:
|
|
106
|
+
"TypeScript infers the type predicate for `{{parameter}}` from this check, while an explicit predicate is never checked against the body. Remove the annotation and rely on the inferred predicate.",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
create(context) {
|
|
110
|
+
const check = (owner: FunctionNode) => {
|
|
111
|
+
const annotation = owner.returnType?.typeAnnotation;
|
|
112
|
+
|
|
113
|
+
if (
|
|
114
|
+
annotation?.type !== "TSTypePredicate" ||
|
|
115
|
+
annotation.asserts ||
|
|
116
|
+
annotation.parameterName.type !== "Identifier" ||
|
|
117
|
+
isPublicSignature(owner)
|
|
118
|
+
) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const parameter = annotation.parameterName.name;
|
|
123
|
+
const expression = returnedExpression(owner);
|
|
124
|
+
|
|
125
|
+
if (
|
|
126
|
+
!owner.params.some((param) => isNamed(param, parameter)) ||
|
|
127
|
+
!expression ||
|
|
128
|
+
!isInferableCheck(expression, parameter)
|
|
129
|
+
) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
context.report({
|
|
134
|
+
node: annotation,
|
|
135
|
+
messageId: "removeTypePredicate",
|
|
136
|
+
data: { parameter },
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
ArrowFunctionExpression: check,
|
|
142
|
+
FunctionDeclaration: check,
|
|
143
|
+
FunctionExpression: check,
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
import { staticMemberName } from "../../shared/ast.ts";
|
|
4
|
+
import {
|
|
5
|
+
isLitRenderRootQuery,
|
|
6
|
+
litElementBaseClasses,
|
|
7
|
+
} from "../../shared/lit.ts";
|
|
8
|
+
|
|
9
|
+
const queryMethods = new Set(["closest", "querySelector", "querySelectorAll"]);
|
|
10
|
+
|
|
11
|
+
/** Disallow type arguments that silently cast DOM query results. */
|
|
12
|
+
export const noUncheckedQuerySelectorTypeRule = defineRule({
|
|
13
|
+
meta: {
|
|
14
|
+
type: "problem",
|
|
15
|
+
docs: {
|
|
16
|
+
description:
|
|
17
|
+
"Disallow type arguments on `querySelector`, `querySelectorAll`, and `closest`, which cast the result without checking it.",
|
|
18
|
+
},
|
|
19
|
+
messages: {
|
|
20
|
+
checkQueryResult:
|
|
21
|
+
"`{{method}}<T>` casts the result without checking it. Check it with `instanceof`, use a checked lookup helper, or query a tag name so TypeScript infers the element type.",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
create(context) {
|
|
25
|
+
const bases = litElementBaseClasses(context.settings);
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
CallExpression(node) {
|
|
29
|
+
const method = staticMemberName(node.callee);
|
|
30
|
+
|
|
31
|
+
if (
|
|
32
|
+
!node.typeArguments ||
|
|
33
|
+
!method ||
|
|
34
|
+
!queryMethods.has(method) ||
|
|
35
|
+
isLitRenderRootQuery(node, bases)
|
|
36
|
+
) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
context.report({
|
|
41
|
+
node: node.typeArguments,
|
|
42
|
+
messageId: "checkQueryResult",
|
|
43
|
+
data: { method },
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
/** Require a detail type wherever `CustomEvent` is used as a type. */
|
|
4
|
+
export const noUntypedCustomEventRule = defineRule({
|
|
5
|
+
meta: {
|
|
6
|
+
type: "problem",
|
|
7
|
+
docs: {
|
|
8
|
+
description:
|
|
9
|
+
"Disallow `CustomEvent` type references without a detail type, which leave `detail` as `any`.",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
typeCustomEventDetail:
|
|
13
|
+
"`CustomEvent` without a type argument leaves `detail` as `any`. Reuse the project's typed event helper if one fits, or add one, instead of a bare `CustomEvent`.",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
return {
|
|
18
|
+
TSTypeReference(node) {
|
|
19
|
+
if (
|
|
20
|
+
node.typeName.type !== "Identifier" ||
|
|
21
|
+
node.typeName.name !== "CustomEvent" ||
|
|
22
|
+
node.typeArguments
|
|
23
|
+
) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
context.report({ node, messageId: "typeCustomEventDetail" });
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
});
|
|
@@ -1,76 +1,77 @@
|
|
|
1
1
|
import { defineRule } from "@oxlint/plugins";
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import { nearestEnclosingFunction } from "../../shared/ast.ts";
|
|
4
4
|
|
|
5
|
-
type
|
|
5
|
+
import type { TypeAssertion } from "../../shared/ast.ts";
|
|
6
|
+
import type { ESTree } from "@oxlint/plugins";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
function staticMemberPath(
|
|
9
|
+
node: ESTree.Expression,
|
|
10
|
+
): { readonly root: string; readonly path: readonly string[] } | null {
|
|
11
|
+
const path: string[] = [];
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
let current: ESTree.Node =
|
|
14
|
+
node.type === "ChainExpression" ? node.expression : node;
|
|
11
15
|
|
|
12
|
-
while (current) {
|
|
13
|
-
if (
|
|
14
|
-
|
|
15
|
-
current.type === "FunctionExpression" ||
|
|
16
|
-
current.type === "ArrowFunctionExpression"
|
|
17
|
-
) {
|
|
18
|
-
return current;
|
|
16
|
+
while (current.type === "MemberExpression") {
|
|
17
|
+
if (current.computed || current.property.type !== "Identifier") {
|
|
18
|
+
return null;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
current
|
|
21
|
+
path.unshift(current.property.name);
|
|
22
|
+
current = current.object;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
return
|
|
25
|
+
return current.type === "Identifier" && path.length > 0
|
|
26
|
+
? { root: current.name, path }
|
|
27
|
+
: null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isEventMemberPath(path: readonly string[]): boolean {
|
|
31
|
+
const [first] = path;
|
|
32
|
+
|
|
33
|
+
if (first === "detail") return true;
|
|
34
|
+
|
|
35
|
+
return path.length === 1 && (first === "currentTarget" || first === "target");
|
|
25
36
|
}
|
|
26
37
|
|
|
27
|
-
|
|
38
|
+
/** Find an assertion of an event parameter's target, current target, or detail. */
|
|
39
|
+
export function assertedEventParameter(node: TypeAssertion): {
|
|
28
40
|
readonly parameter: string;
|
|
29
|
-
readonly property:
|
|
41
|
+
readonly property: string;
|
|
30
42
|
} | null {
|
|
31
|
-
const
|
|
43
|
+
const member = staticMemberPath(node.expression);
|
|
32
44
|
|
|
33
|
-
if (
|
|
34
|
-
expression.type !== "MemberExpression" ||
|
|
35
|
-
expression.computed ||
|
|
36
|
-
expression.object.type !== "Identifier" ||
|
|
37
|
-
expression.property.type !== "Identifier" ||
|
|
38
|
-
(expression.property.name !== "currentTarget" &&
|
|
39
|
-
expression.property.name !== "target")
|
|
40
|
-
) {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
45
|
+
if (!member || !isEventMemberPath(member.path)) return null;
|
|
43
46
|
|
|
44
|
-
const parameterName = expression.object.name;
|
|
45
|
-
const property = expression.property.name;
|
|
46
47
|
const owner = nearestEnclosingFunction(node);
|
|
47
48
|
|
|
48
49
|
if (
|
|
49
50
|
!owner?.params.some(
|
|
50
51
|
(parameter) =>
|
|
51
|
-
parameter.type === "Identifier" && parameter.name ===
|
|
52
|
+
parameter.type === "Identifier" && parameter.name === member.root,
|
|
52
53
|
)
|
|
53
54
|
) {
|
|
54
55
|
return null;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
return {
|
|
58
|
-
parameter:
|
|
59
|
-
property,
|
|
59
|
+
parameter: member.root,
|
|
60
|
+
property: member.path.join("."),
|
|
60
61
|
};
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
/** Prefer expressing an event target type in its handler parameter signature. */
|
|
64
|
+
/** Prefer expressing an event's target or detail type in its handler parameter signature. */
|
|
64
65
|
export const preferEventParameterTypeRule = defineRule({
|
|
65
66
|
meta: {
|
|
66
67
|
type: "suggestion",
|
|
67
68
|
docs: {
|
|
68
69
|
description:
|
|
69
|
-
"Prefer typing event target properties in the handler parameter instead of asserting them at use sites.",
|
|
70
|
+
"Prefer typing event target and detail properties in the handler parameter instead of asserting them at use sites.",
|
|
70
71
|
},
|
|
71
72
|
messages: {
|
|
72
73
|
typeEventParameter:
|
|
73
|
-
"Type `{{parameter}}.{{property}}` in the function signature instead of asserting it at the use site.",
|
|
74
|
+
"Type `{{parameter}}.{{property}}` in the function signature instead of asserting it at the use site. Reuse the project's typed event helper if one fits, or add one.",
|
|
74
75
|
},
|
|
75
76
|
},
|
|
76
77
|
create(context) {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
import { jsonObject } from "../../shared/json.ts";
|
|
4
|
+
import {
|
|
5
|
+
litElementBaseClasses,
|
|
6
|
+
litQueryDecoratorCall,
|
|
7
|
+
} from "../../shared/lit.ts";
|
|
8
|
+
|
|
9
|
+
import type { LitQueryMethod } from "../../shared/lit.ts";
|
|
10
|
+
|
|
11
|
+
const messageIds = {
|
|
12
|
+
assignedElements: "preferQueryAssignedElements",
|
|
13
|
+
assignedNodes: "preferQueryAssignedNodes",
|
|
14
|
+
querySelector: "preferQuery",
|
|
15
|
+
querySelectorAll: "preferQueryAll",
|
|
16
|
+
} as const satisfies Record<LitQueryMethod, string>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Prefer Lit query decorators over manual render-root queries. Follows
|
|
20
|
+
* eslint-plugin-lit's `prefer-query-decorators` and also matches non-null,
|
|
21
|
+
* optional, and asserted render roots.
|
|
22
|
+
*/
|
|
23
|
+
export const preferLitQueryDecoratorsRule = defineRule({
|
|
24
|
+
meta: {
|
|
25
|
+
type: "suggestion",
|
|
26
|
+
docs: {
|
|
27
|
+
description:
|
|
28
|
+
"Require Lit query decorators instead of static render-root queries in Lit elements.",
|
|
29
|
+
},
|
|
30
|
+
schema: [
|
|
31
|
+
{
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
querySelector: { type: "boolean" },
|
|
35
|
+
querySelectorAll: { type: "boolean" },
|
|
36
|
+
assignedElements: { type: "boolean" },
|
|
37
|
+
assignedNodes: { type: "boolean" },
|
|
38
|
+
},
|
|
39
|
+
additionalProperties: false,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
defaultOptions: [
|
|
43
|
+
{
|
|
44
|
+
querySelector: true,
|
|
45
|
+
querySelectorAll: true,
|
|
46
|
+
assignedElements: true,
|
|
47
|
+
assignedNodes: true,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
messages: {
|
|
51
|
+
preferQuery:
|
|
52
|
+
"Use the @query decorator instead of this.{{root}}.querySelector().",
|
|
53
|
+
preferQueryAll:
|
|
54
|
+
"Use the @queryAll decorator instead of this.{{root}}.querySelectorAll().",
|
|
55
|
+
preferQueryAssignedElements:
|
|
56
|
+
"Use the @queryAssignedElements decorator instead of this.{{root}}.querySelector().assignedElements().",
|
|
57
|
+
preferQueryAssignedNodes:
|
|
58
|
+
"Use the @queryAssignedNodes decorator instead of this.{{root}}.querySelector().assignedNodes().",
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
create(context) {
|
|
62
|
+
const bases = litElementBaseClasses(context.settings);
|
|
63
|
+
const options = jsonObject(context.options[0]);
|
|
64
|
+
|
|
65
|
+
const disabled = new Set(
|
|
66
|
+
options
|
|
67
|
+
? Object.keys(options).filter((method) => options[method] === false)
|
|
68
|
+
: [],
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
CallExpression(node) {
|
|
73
|
+
const query = litQueryDecoratorCall(node, bases);
|
|
74
|
+
|
|
75
|
+
if (!query || disabled.has(query.method)) return;
|
|
76
|
+
context.report({
|
|
77
|
+
node,
|
|
78
|
+
messageId: messageIds[query.method],
|
|
79
|
+
data: { root: query.root },
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
import { assertedEventParameter } from "./prefer-event-parameter-type.ts";
|
|
4
|
+
|
|
5
|
+
import type { TypeAssertion } from "../../shared/ast.ts";
|
|
6
|
+
|
|
7
|
+
/** Prefer typing values at their source over asserting them at use sites. */
|
|
8
|
+
export const preferTypesOverAssertionsRule = defineRule({
|
|
9
|
+
meta: {
|
|
10
|
+
type: "problem",
|
|
11
|
+
docs: {
|
|
12
|
+
description:
|
|
13
|
+
"Prefer typing a value at its source over a TypeScript type assertion.",
|
|
14
|
+
},
|
|
15
|
+
messages: {
|
|
16
|
+
avoidAssertion:
|
|
17
|
+
"Type this value at its source, such as a function parameter or DOM lookup, instead of asserting it here. A SAFETY comment is not a fix.",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
create(context) {
|
|
21
|
+
const checkAssertion = (node: TypeAssertion) => {
|
|
22
|
+
if (
|
|
23
|
+
(node.typeAnnotation.type === "TSTypeReference" &&
|
|
24
|
+
node.typeAnnotation.typeName.type === "Identifier" &&
|
|
25
|
+
node.typeAnnotation.typeName.name === "const") ||
|
|
26
|
+
assertedEventParameter(node)
|
|
27
|
+
) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
context.report({ node, messageId: "avoidAssertion" });
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
TSAsExpression: checkAssertion,
|
|
36
|
+
TSTypeAssertion: checkAssertion,
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ESTree } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
export type FunctionNode = ESTree.Function | ESTree.ArrowFunctionExpression;
|
|
4
|
+
|
|
5
|
+
export type TypeAssertion = ESTree.TSAsExpression | ESTree.TSTypeAssertion;
|
|
6
|
+
|
|
7
|
+
export function nearestEnclosingFunction(
|
|
8
|
+
node: ESTree.Node,
|
|
9
|
+
): FunctionNode | null {
|
|
10
|
+
let current: ESTree.Node | null = node.parent;
|
|
11
|
+
|
|
12
|
+
while (current) {
|
|
13
|
+
if (
|
|
14
|
+
current.type === "FunctionDeclaration" ||
|
|
15
|
+
current.type === "FunctionExpression" ||
|
|
16
|
+
current.type === "ArrowFunctionExpression"
|
|
17
|
+
) {
|
|
18
|
+
return current;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
current = current.parent;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function staticMemberName(node: ESTree.Node): string | null {
|
|
28
|
+
if (node.type !== "MemberExpression" || node.computed) return null;
|
|
29
|
+
|
|
30
|
+
return node.property.type === "Identifier" ? node.property.name : null;
|
|
31
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { staticMemberName } from "./ast.ts";
|
|
2
|
+
import { resolveVariable } from "./scope.ts";
|
|
3
|
+
|
|
4
|
+
import type { ESTree, SourceCode } from "@oxlint/plugins";
|
|
5
|
+
|
|
6
|
+
export function isEffectImport(
|
|
7
|
+
sourceCode: SourceCode,
|
|
8
|
+
identifier: ESTree.IdentifierReference,
|
|
9
|
+
namespace: boolean,
|
|
10
|
+
): boolean {
|
|
11
|
+
const variable = resolveVariable(sourceCode, identifier);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
variable?.defs.some((definition) => {
|
|
15
|
+
if (
|
|
16
|
+
definition.type !== "ImportBinding" ||
|
|
17
|
+
definition.parent?.type !== "ImportDeclaration" ||
|
|
18
|
+
definition.parent.source.value !== "effect"
|
|
19
|
+
) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (namespace) return definition.node.type === "ImportNamespaceSpecifier";
|
|
24
|
+
|
|
25
|
+
if (definition.node.type !== "ImportSpecifier") return false;
|
|
26
|
+
const imported = definition.node.imported;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
(imported.type === "Identifier" ? imported.name : imported.value) ===
|
|
30
|
+
"Effect"
|
|
31
|
+
);
|
|
32
|
+
}) ?? false
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isEffectMethod(
|
|
37
|
+
sourceCode: SourceCode,
|
|
38
|
+
node: ESTree.Expression,
|
|
39
|
+
method: "fn" | "gen",
|
|
40
|
+
): boolean {
|
|
41
|
+
if (staticMemberName(node) !== method || node.type !== "MemberExpression") {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const object = node.object;
|
|
46
|
+
|
|
47
|
+
if (object.type === "Identifier") {
|
|
48
|
+
return isEffectImport(sourceCode, object, false);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
staticMemberName(object) !== "Effect" ||
|
|
53
|
+
object.type !== "MemberExpression" ||
|
|
54
|
+
object.object.type !== "Identifier"
|
|
55
|
+
) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return isEffectImport(sourceCode, object.object, true);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Whether a module imports Effect or one of its platform packages. */
|
|
63
|
+
export function importsEffect(program: ESTree.Program): boolean {
|
|
64
|
+
return program.body.some(
|
|
65
|
+
(statement) =>
|
|
66
|
+
statement.type === "ImportDeclaration" &&
|
|
67
|
+
statement.importKind !== "type" &&
|
|
68
|
+
(statement.source.value === "effect" ||
|
|
69
|
+
statement.source.value.startsWith("effect/") ||
|
|
70
|
+
statement.source.value.startsWith("@effect/")),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Settings } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
/** Narrow a rule option or setting, which Oxlint passes as untyped JSON. */
|
|
4
|
+
export function jsonObject(
|
|
5
|
+
value: Settings[string] | undefined,
|
|
6
|
+
): Readonly<Settings> | null {
|
|
7
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- Oxlint settings and options are the JSON input boundary.
|
|
8
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
9
|
+
? value
|
|
10
|
+
: null;
|
|
11
|
+
}
|