@timmo001/oxlint-rules 0.4.0 → 0.6.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 +59 -5
- package/dist/cli.js +498 -55
- package/dist/configs/recommended-effect.js +539 -99
- package/dist/configs/recommended.js +336 -16
- package/dist/effect/index.js +162 -13
- package/dist/generic/index.js +331 -15
- package/package.json +12 -9
- package/skills/install-timmo-oxlint-rules/SKILL.md +18 -5
- package/src/effect/index.ts +4 -0
- package/src/effect/rules/no-raw-gh.ts +126 -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 +9 -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 +6 -5
- 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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { staticMemberName } from "./ast.ts";
|
|
2
|
+
import { jsonObject } from "./json.ts";
|
|
3
|
+
|
|
4
|
+
import type { ESTree, Settings } from "@oxlint/plugins";
|
|
5
|
+
|
|
6
|
+
type AssignedMethod = "assignedElements" | "assignedNodes";
|
|
7
|
+
|
|
8
|
+
type QueryMethod = "querySelector" | "querySelectorAll";
|
|
9
|
+
|
|
10
|
+
export type LitQueryMethod = AssignedMethod | QueryMethod;
|
|
11
|
+
|
|
12
|
+
type WrapperExpression =
|
|
13
|
+
| ESTree.ChainExpression
|
|
14
|
+
| ESTree.ParenthesizedExpression
|
|
15
|
+
| ESTree.TSAsExpression
|
|
16
|
+
| ESTree.TSNonNullExpression
|
|
17
|
+
| ESTree.TSSatisfiesExpression
|
|
18
|
+
| ESTree.TSTypeAssertion;
|
|
19
|
+
|
|
20
|
+
const renderRoots = new Set(["renderRoot", "shadowRoot"]);
|
|
21
|
+
|
|
22
|
+
/** Read `settings.lit.elementBaseClasses`, as eslint-plugin-lit does. */
|
|
23
|
+
export function litElementBaseClasses(
|
|
24
|
+
settings: Readonly<Settings>,
|
|
25
|
+
): ReadonlySet<string> {
|
|
26
|
+
const configured = jsonObject(settings.lit)?.elementBaseClasses;
|
|
27
|
+
|
|
28
|
+
return new Set([
|
|
29
|
+
"LitElement",
|
|
30
|
+
...(Array.isArray(configured) ? configured.map(String) : []),
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isWrapperExpression(
|
|
35
|
+
node: ESTree.Node | null,
|
|
36
|
+
): node is WrapperExpression {
|
|
37
|
+
return (
|
|
38
|
+
node?.type === "ChainExpression" ||
|
|
39
|
+
node?.type === "ParenthesizedExpression" ||
|
|
40
|
+
node?.type === "TSAsExpression" ||
|
|
41
|
+
node?.type === "TSNonNullExpression" ||
|
|
42
|
+
node?.type === "TSSatisfiesExpression" ||
|
|
43
|
+
node?.type === "TSTypeAssertion"
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function unwrapExpression(node: ESTree.Node): ESTree.Node {
|
|
48
|
+
let current = node;
|
|
49
|
+
|
|
50
|
+
while (isWrapperExpression(current)) current = current.expression;
|
|
51
|
+
|
|
52
|
+
return current;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function extendsLitBase(
|
|
56
|
+
node: ESTree.Node,
|
|
57
|
+
bases: ReadonlySet<string>,
|
|
58
|
+
): boolean {
|
|
59
|
+
if (node.type === "Identifier") return bases.has(node.name);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
node.type === "CallExpression" &&
|
|
63
|
+
node.arguments.some((argument) => extendsLitBase(argument, bases))
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isLitClass(node: ESTree.Class, bases: ReadonlySet<string>): boolean {
|
|
68
|
+
const hasCustomElementDecorator = node.decorators.some(
|
|
69
|
+
(decorator) =>
|
|
70
|
+
decorator.expression.type === "CallExpression" &&
|
|
71
|
+
decorator.expression.callee.type === "Identifier" &&
|
|
72
|
+
decorator.expression.callee.name === "customElement",
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
hasCustomElementDecorator ||
|
|
77
|
+
(node.superClass !== null && extendsLitBase(node.superClass, bases))
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function classOfBody(body: ESTree.Node): ESTree.Class | null {
|
|
82
|
+
const owner = body.parent;
|
|
83
|
+
|
|
84
|
+
return owner?.type === "ClassDeclaration" || owner?.type === "ClassExpression"
|
|
85
|
+
? owner
|
|
86
|
+
: null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The class whose instance `this` refers to at `node`, if any. */
|
|
90
|
+
function thisClass(node: ESTree.Node): ESTree.Class | null {
|
|
91
|
+
let current = node;
|
|
92
|
+
|
|
93
|
+
while (current.parent) {
|
|
94
|
+
const parent: ESTree.Node = current.parent;
|
|
95
|
+
|
|
96
|
+
if (
|
|
97
|
+
parent.type === "FunctionDeclaration" ||
|
|
98
|
+
parent.type === "FunctionExpression"
|
|
99
|
+
) {
|
|
100
|
+
const owner = parent.parent;
|
|
101
|
+
|
|
102
|
+
return (owner?.type === "MethodDefinition" ||
|
|
103
|
+
owner?.type === "TSAbstractMethodDefinition") &&
|
|
104
|
+
!owner.static &&
|
|
105
|
+
owner.value === parent
|
|
106
|
+
? classOfBody(owner.parent)
|
|
107
|
+
: null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (
|
|
111
|
+
(parent.type === "PropertyDefinition" ||
|
|
112
|
+
parent.type === "AccessorProperty") &&
|
|
113
|
+
parent.value === current
|
|
114
|
+
) {
|
|
115
|
+
return parent.static ? null : classOfBody(parent.parent);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (
|
|
119
|
+
parent.type === "StaticBlock" ||
|
|
120
|
+
parent.type === "ClassBody" ||
|
|
121
|
+
parent.type === "Program"
|
|
122
|
+
) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
current = parent;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isStaticSelector(node: ESTree.Node | undefined): boolean {
|
|
133
|
+
return (
|
|
134
|
+
node?.type === "Literal" ||
|
|
135
|
+
(node?.type === "TemplateLiteral" && node.expressions.length === 0)
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function renderRootQuery(
|
|
140
|
+
node: ESTree.CallExpression,
|
|
141
|
+
bases: ReadonlySet<string>,
|
|
142
|
+
): { readonly method: QueryMethod; readonly root: string } | null {
|
|
143
|
+
const method = staticMemberName(node.callee);
|
|
144
|
+
|
|
145
|
+
if (
|
|
146
|
+
(method !== "querySelector" && method !== "querySelectorAll") ||
|
|
147
|
+
node.callee.type !== "MemberExpression" ||
|
|
148
|
+
!isStaticSelector(node.arguments[0])
|
|
149
|
+
) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const receiver = unwrapExpression(node.callee.object);
|
|
154
|
+
const root = staticMemberName(receiver);
|
|
155
|
+
|
|
156
|
+
if (
|
|
157
|
+
!root ||
|
|
158
|
+
!renderRoots.has(root) ||
|
|
159
|
+
receiver.type !== "MemberExpression" ||
|
|
160
|
+
receiver.object.type !== "ThisExpression"
|
|
161
|
+
) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const owner = thisClass(receiver.object);
|
|
166
|
+
|
|
167
|
+
return owner && isLitClass(owner, bases) ? { method, root } : null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isAssignedMethod(name: string | null): name is AssignedMethod {
|
|
171
|
+
return name === "assignedElements" || name === "assignedNodes";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function feedsAssignedCall(node: ESTree.CallExpression): boolean {
|
|
175
|
+
let current: ESTree.Node = node;
|
|
176
|
+
|
|
177
|
+
while (isWrapperExpression(current.parent)) current = current.parent;
|
|
178
|
+
|
|
179
|
+
const member = current.parent;
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
member?.type === "MemberExpression" &&
|
|
183
|
+
member.object === current &&
|
|
184
|
+
isAssignedMethod(staticMemberName(member))
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Match a static render-root query in a Lit class that a query decorator can
|
|
190
|
+
* replace, following eslint-plugin-lit's `prefer-query-decorators`.
|
|
191
|
+
*/
|
|
192
|
+
export function litQueryDecoratorCall(
|
|
193
|
+
node: ESTree.CallExpression,
|
|
194
|
+
bases: ReadonlySet<string>,
|
|
195
|
+
): { readonly method: LitQueryMethod; readonly root: string } | null {
|
|
196
|
+
const method = staticMemberName(node.callee);
|
|
197
|
+
|
|
198
|
+
if (!isAssignedMethod(method)) {
|
|
199
|
+
return feedsAssignedCall(node) ? null : renderRootQuery(node, bases);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (node.callee.type !== "MemberExpression") return null;
|
|
203
|
+
const inner = unwrapExpression(node.callee.object);
|
|
204
|
+
|
|
205
|
+
if (inner.type !== "CallExpression") return null;
|
|
206
|
+
const query = renderRootQuery(inner, bases);
|
|
207
|
+
|
|
208
|
+
return query?.method === "querySelector"
|
|
209
|
+
? { method, root: query.root }
|
|
210
|
+
: null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Whether a call is a static render-root query in a Lit class. */
|
|
214
|
+
export function isLitRenderRootQuery(
|
|
215
|
+
node: ESTree.CallExpression,
|
|
216
|
+
bases: ReadonlySet<string>,
|
|
217
|
+
): boolean {
|
|
218
|
+
return renderRootQuery(node, bases) !== null;
|
|
219
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { resolveVariable } from "../../vendor/anti-slop/src/shared/scope.ts";
|