@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
package/dist/generic/index.js
CHANGED
|
@@ -1,8 +1,86 @@
|
|
|
1
1
|
// src/generic/index.ts
|
|
2
2
|
import { eslintCompatPlugin } from "@oxlint/plugins";
|
|
3
3
|
|
|
4
|
-
// src/generic/rules/
|
|
4
|
+
// src/generic/rules/no-explicit-inferable-type-predicate.ts
|
|
5
5
|
import { defineRule } from "@oxlint/plugins";
|
|
6
|
+
function isNamed(node, name) {
|
|
7
|
+
return node.type === "Identifier" && node.name === name;
|
|
8
|
+
}
|
|
9
|
+
function isNullish(node) {
|
|
10
|
+
return node.type === "Literal" && node.value === null || isNamed(node, "undefined");
|
|
11
|
+
}
|
|
12
|
+
function isNullishExclusion(node, name) {
|
|
13
|
+
return node.type === "BinaryExpression" && (node.operator === "!==" || node.operator === "!=") && (isNamed(node.left, name) && isNullish(node.right) || isNullish(node.left) && isNamed(node.right, name));
|
|
14
|
+
}
|
|
15
|
+
function isTypeofCheck(node, name) {
|
|
16
|
+
if (node.type !== "BinaryExpression" || node.operator !== "===" && node.operator !== "==") {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const isTypeof = (side) => side.type === "UnaryExpression" && side.operator === "typeof" && isNamed(side.argument, name);
|
|
20
|
+
const isLiteral = (side) => side.type === "Literal";
|
|
21
|
+
return isTypeof(node.left) && isLiteral(node.right) || isLiteral(node.left) && isTypeof(node.right);
|
|
22
|
+
}
|
|
23
|
+
function isInferableCheck(node, name) {
|
|
24
|
+
if (node.type === "LogicalExpression") {
|
|
25
|
+
return node.operator === "&&" && isNullishExclusion(node.left, name) && isNullishExclusion(node.right, name);
|
|
26
|
+
}
|
|
27
|
+
return isNullishExclusion(node, name) || isTypeofCheck(node, name) || node.type === "BinaryExpression" && node.operator === "instanceof" && isNamed(node.left, name);
|
|
28
|
+
}
|
|
29
|
+
function returnedExpression(owner) {
|
|
30
|
+
const body = owner.body;
|
|
31
|
+
if (!body)
|
|
32
|
+
return null;
|
|
33
|
+
if (body.type !== "BlockStatement")
|
|
34
|
+
return body;
|
|
35
|
+
const [statement] = body.body;
|
|
36
|
+
return body.body.length === 1 && statement?.type === "ReturnStatement" ? statement.argument : null;
|
|
37
|
+
}
|
|
38
|
+
function isPublicSignature(owner) {
|
|
39
|
+
const parent = owner.parent;
|
|
40
|
+
if (parent.type === "MethodDefinition" || parent.type === "ExportNamedDeclaration" || parent.type === "ExportDefaultDeclaration") {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return parent.type === "VariableDeclarator" && parent.parent.parent?.type === "ExportNamedDeclaration";
|
|
44
|
+
}
|
|
45
|
+
var noExplicitInferableTypePredicateRule = defineRule({
|
|
46
|
+
meta: {
|
|
47
|
+
type: "suggestion",
|
|
48
|
+
docs: {
|
|
49
|
+
description: "Disallow explicit type predicates on simple null, typeof, or instanceof checks that TypeScript 5.5+ infers."
|
|
50
|
+
},
|
|
51
|
+
messages: {
|
|
52
|
+
removeTypePredicate: "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."
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
create(context) {
|
|
56
|
+
const check = (owner) => {
|
|
57
|
+
const annotation = owner.returnType?.typeAnnotation;
|
|
58
|
+
if (annotation?.type !== "TSTypePredicate" || annotation.asserts || annotation.parameterName.type !== "Identifier" || isPublicSignature(owner)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const parameter = annotation.parameterName.name;
|
|
62
|
+
const expression = returnedExpression(owner);
|
|
63
|
+
if (!owner.params.some((param) => isNamed(param, parameter)) || !expression || !isInferableCheck(expression, parameter)) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
context.report({
|
|
67
|
+
node: annotation,
|
|
68
|
+
messageId: "removeTypePredicate",
|
|
69
|
+
data: { parameter }
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
ArrowFunctionExpression: check,
|
|
74
|
+
FunctionDeclaration: check,
|
|
75
|
+
FunctionExpression: check
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// src/generic/rules/no-unchecked-query-selector-type.ts
|
|
81
|
+
import { defineRule as defineRule2 } from "@oxlint/plugins";
|
|
82
|
+
|
|
83
|
+
// src/shared/ast.ts
|
|
6
84
|
function nearestEnclosingFunction(node) {
|
|
7
85
|
let current = node.parent;
|
|
8
86
|
while (current) {
|
|
@@ -13,30 +91,204 @@ function nearestEnclosingFunction(node) {
|
|
|
13
91
|
}
|
|
14
92
|
return null;
|
|
15
93
|
}
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
|
|
94
|
+
function staticMemberName(node) {
|
|
95
|
+
if (node.type !== "MemberExpression" || node.computed)
|
|
96
|
+
return null;
|
|
97
|
+
return node.property.type === "Identifier" ? node.property.name : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/shared/json.ts
|
|
101
|
+
function jsonObject(value) {
|
|
102
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/shared/lit.ts
|
|
106
|
+
var renderRoots = new Set(["renderRoot", "shadowRoot"]);
|
|
107
|
+
function litElementBaseClasses(settings) {
|
|
108
|
+
const configured = jsonObject(settings.lit)?.elementBaseClasses;
|
|
109
|
+
return new Set([
|
|
110
|
+
"LitElement",
|
|
111
|
+
...Array.isArray(configured) ? configured.map(String) : []
|
|
112
|
+
]);
|
|
113
|
+
}
|
|
114
|
+
function isWrapperExpression(node) {
|
|
115
|
+
return node?.type === "ChainExpression" || node?.type === "ParenthesizedExpression" || node?.type === "TSAsExpression" || node?.type === "TSNonNullExpression" || node?.type === "TSSatisfiesExpression" || node?.type === "TSTypeAssertion";
|
|
116
|
+
}
|
|
117
|
+
function unwrapExpression(node) {
|
|
118
|
+
let current = node;
|
|
119
|
+
while (isWrapperExpression(current))
|
|
120
|
+
current = current.expression;
|
|
121
|
+
return current;
|
|
122
|
+
}
|
|
123
|
+
function extendsLitBase(node, bases) {
|
|
124
|
+
if (node.type === "Identifier")
|
|
125
|
+
return bases.has(node.name);
|
|
126
|
+
return node.type === "CallExpression" && node.arguments.some((argument) => extendsLitBase(argument, bases));
|
|
127
|
+
}
|
|
128
|
+
function isLitClass(node, bases) {
|
|
129
|
+
const hasCustomElementDecorator = node.decorators.some((decorator) => decorator.expression.type === "CallExpression" && decorator.expression.callee.type === "Identifier" && decorator.expression.callee.name === "customElement");
|
|
130
|
+
return hasCustomElementDecorator || node.superClass !== null && extendsLitBase(node.superClass, bases);
|
|
131
|
+
}
|
|
132
|
+
function classOfBody(body) {
|
|
133
|
+
const owner = body.parent;
|
|
134
|
+
return owner?.type === "ClassDeclaration" || owner?.type === "ClassExpression" ? owner : null;
|
|
135
|
+
}
|
|
136
|
+
function thisClass(node) {
|
|
137
|
+
let current = node;
|
|
138
|
+
while (current.parent) {
|
|
139
|
+
const parent = current.parent;
|
|
140
|
+
if (parent.type === "FunctionDeclaration" || parent.type === "FunctionExpression") {
|
|
141
|
+
const owner = parent.parent;
|
|
142
|
+
return (owner?.type === "MethodDefinition" || owner?.type === "TSAbstractMethodDefinition") && !owner.static && owner.value === parent ? classOfBody(owner.parent) : null;
|
|
143
|
+
}
|
|
144
|
+
if ((parent.type === "PropertyDefinition" || parent.type === "AccessorProperty") && parent.value === current) {
|
|
145
|
+
return parent.static ? null : classOfBody(parent.parent);
|
|
146
|
+
}
|
|
147
|
+
if (parent.type === "StaticBlock" || parent.type === "ClassBody" || parent.type === "Program") {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
current = parent;
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
function isStaticSelector(node) {
|
|
155
|
+
return node?.type === "Literal" || node?.type === "TemplateLiteral" && node.expressions.length === 0;
|
|
156
|
+
}
|
|
157
|
+
function renderRootQuery(node, bases) {
|
|
158
|
+
const method = staticMemberName(node.callee);
|
|
159
|
+
if (method !== "querySelector" && method !== "querySelectorAll" || node.callee.type !== "MemberExpression" || !isStaticSelector(node.arguments[0])) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
const receiver = unwrapExpression(node.callee.object);
|
|
163
|
+
const root = staticMemberName(receiver);
|
|
164
|
+
if (!root || !renderRoots.has(root) || receiver.type !== "MemberExpression" || receiver.object.type !== "ThisExpression") {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
const owner = thisClass(receiver.object);
|
|
168
|
+
return owner && isLitClass(owner, bases) ? { method, root } : null;
|
|
169
|
+
}
|
|
170
|
+
function isAssignedMethod(name) {
|
|
171
|
+
return name === "assignedElements" || name === "assignedNodes";
|
|
172
|
+
}
|
|
173
|
+
function feedsAssignedCall(node) {
|
|
174
|
+
let current = node;
|
|
175
|
+
while (isWrapperExpression(current.parent))
|
|
176
|
+
current = current.parent;
|
|
177
|
+
const member = current.parent;
|
|
178
|
+
return member?.type === "MemberExpression" && member.object === current && isAssignedMethod(staticMemberName(member));
|
|
179
|
+
}
|
|
180
|
+
function litQueryDecoratorCall(node, bases) {
|
|
181
|
+
const method = staticMemberName(node.callee);
|
|
182
|
+
if (!isAssignedMethod(method)) {
|
|
183
|
+
return feedsAssignedCall(node) ? null : renderRootQuery(node, bases);
|
|
184
|
+
}
|
|
185
|
+
if (node.callee.type !== "MemberExpression")
|
|
186
|
+
return null;
|
|
187
|
+
const inner = unwrapExpression(node.callee.object);
|
|
188
|
+
if (inner.type !== "CallExpression")
|
|
19
189
|
return null;
|
|
190
|
+
const query = renderRootQuery(inner, bases);
|
|
191
|
+
return query?.method === "querySelector" ? { method, root: query.root } : null;
|
|
192
|
+
}
|
|
193
|
+
function isLitRenderRootQuery(node, bases) {
|
|
194
|
+
return renderRootQuery(node, bases) !== null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/generic/rules/no-unchecked-query-selector-type.ts
|
|
198
|
+
var queryMethods = new Set(["closest", "querySelector", "querySelectorAll"]);
|
|
199
|
+
var noUncheckedQuerySelectorTypeRule = defineRule2({
|
|
200
|
+
meta: {
|
|
201
|
+
type: "problem",
|
|
202
|
+
docs: {
|
|
203
|
+
description: "Disallow type arguments on `querySelector`, `querySelectorAll`, and `closest`, which cast the result without checking it."
|
|
204
|
+
},
|
|
205
|
+
messages: {
|
|
206
|
+
checkQueryResult: "`{{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."
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
create(context) {
|
|
210
|
+
const bases = litElementBaseClasses(context.settings);
|
|
211
|
+
return {
|
|
212
|
+
CallExpression(node) {
|
|
213
|
+
const method = staticMemberName(node.callee);
|
|
214
|
+
if (!node.typeArguments || !method || !queryMethods.has(method) || isLitRenderRootQuery(node, bases)) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
context.report({
|
|
218
|
+
node: node.typeArguments,
|
|
219
|
+
messageId: "checkQueryResult",
|
|
220
|
+
data: { method }
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
};
|
|
20
224
|
}
|
|
21
|
-
|
|
22
|
-
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// src/generic/rules/no-untyped-custom-event.ts
|
|
228
|
+
import { defineRule as defineRule3 } from "@oxlint/plugins";
|
|
229
|
+
var noUntypedCustomEventRule = defineRule3({
|
|
230
|
+
meta: {
|
|
231
|
+
type: "problem",
|
|
232
|
+
docs: {
|
|
233
|
+
description: "Disallow `CustomEvent` type references without a detail type, which leave `detail` as `any`."
|
|
234
|
+
},
|
|
235
|
+
messages: {
|
|
236
|
+
typeCustomEventDetail: "`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`."
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
create(context) {
|
|
240
|
+
return {
|
|
241
|
+
TSTypeReference(node) {
|
|
242
|
+
if (node.typeName.type !== "Identifier" || node.typeName.name !== "CustomEvent" || node.typeArguments) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
context.report({ node, messageId: "typeCustomEventDetail" });
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// src/generic/rules/prefer-event-parameter-type.ts
|
|
252
|
+
import { defineRule as defineRule4 } from "@oxlint/plugins";
|
|
253
|
+
function staticMemberPath(node) {
|
|
254
|
+
const path = [];
|
|
255
|
+
let current = node.type === "ChainExpression" ? node.expression : node;
|
|
256
|
+
while (current.type === "MemberExpression") {
|
|
257
|
+
if (current.computed || current.property.type !== "Identifier") {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
path.unshift(current.property.name);
|
|
261
|
+
current = current.object;
|
|
262
|
+
}
|
|
263
|
+
return current.type === "Identifier" && path.length > 0 ? { root: current.name, path } : null;
|
|
264
|
+
}
|
|
265
|
+
function isEventMemberPath(path) {
|
|
266
|
+
const [first] = path;
|
|
267
|
+
if (first === "detail")
|
|
268
|
+
return true;
|
|
269
|
+
return path.length === 1 && (first === "currentTarget" || first === "target");
|
|
270
|
+
}
|
|
271
|
+
function assertedEventParameter(node) {
|
|
272
|
+
const member = staticMemberPath(node.expression);
|
|
273
|
+
if (!member || !isEventMemberPath(member.path))
|
|
274
|
+
return null;
|
|
23
275
|
const owner = nearestEnclosingFunction(node);
|
|
24
|
-
if (!owner?.params.some((parameter) => parameter.type === "Identifier" && parameter.name ===
|
|
276
|
+
if (!owner?.params.some((parameter) => parameter.type === "Identifier" && parameter.name === member.root)) {
|
|
25
277
|
return null;
|
|
26
278
|
}
|
|
27
279
|
return {
|
|
28
|
-
parameter:
|
|
29
|
-
property
|
|
280
|
+
parameter: member.root,
|
|
281
|
+
property: member.path.join(".")
|
|
30
282
|
};
|
|
31
283
|
}
|
|
32
|
-
var preferEventParameterTypeRule =
|
|
284
|
+
var preferEventParameterTypeRule = defineRule4({
|
|
33
285
|
meta: {
|
|
34
286
|
type: "suggestion",
|
|
35
287
|
docs: {
|
|
36
|
-
description: "Prefer typing event target properties in the handler parameter instead of asserting them at use sites."
|
|
288
|
+
description: "Prefer typing event target and detail properties in the handler parameter instead of asserting them at use sites."
|
|
37
289
|
},
|
|
38
290
|
messages: {
|
|
39
|
-
typeEventParameter: "Type `{{parameter}}.{{property}}` in the function signature instead of asserting it at the use site."
|
|
291
|
+
typeEventParameter: "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."
|
|
40
292
|
}
|
|
41
293
|
},
|
|
42
294
|
create(context) {
|
|
@@ -57,9 +309,69 @@ var preferEventParameterTypeRule = defineRule({
|
|
|
57
309
|
}
|
|
58
310
|
});
|
|
59
311
|
|
|
312
|
+
// src/generic/rules/prefer-lit-query-decorators.ts
|
|
313
|
+
import { defineRule as defineRule5 } from "@oxlint/plugins";
|
|
314
|
+
var messageIds = {
|
|
315
|
+
assignedElements: "preferQueryAssignedElements",
|
|
316
|
+
assignedNodes: "preferQueryAssignedNodes",
|
|
317
|
+
querySelector: "preferQuery",
|
|
318
|
+
querySelectorAll: "preferQueryAll"
|
|
319
|
+
};
|
|
320
|
+
var preferLitQueryDecoratorsRule = defineRule5({
|
|
321
|
+
meta: {
|
|
322
|
+
type: "suggestion",
|
|
323
|
+
docs: {
|
|
324
|
+
description: "Require Lit query decorators instead of static render-root queries in Lit elements."
|
|
325
|
+
},
|
|
326
|
+
schema: [
|
|
327
|
+
{
|
|
328
|
+
type: "object",
|
|
329
|
+
properties: {
|
|
330
|
+
querySelector: { type: "boolean" },
|
|
331
|
+
querySelectorAll: { type: "boolean" },
|
|
332
|
+
assignedElements: { type: "boolean" },
|
|
333
|
+
assignedNodes: { type: "boolean" }
|
|
334
|
+
},
|
|
335
|
+
additionalProperties: false
|
|
336
|
+
}
|
|
337
|
+
],
|
|
338
|
+
defaultOptions: [
|
|
339
|
+
{
|
|
340
|
+
querySelector: true,
|
|
341
|
+
querySelectorAll: true,
|
|
342
|
+
assignedElements: true,
|
|
343
|
+
assignedNodes: true
|
|
344
|
+
}
|
|
345
|
+
],
|
|
346
|
+
messages: {
|
|
347
|
+
preferQuery: "Use the @query decorator instead of this.{{root}}.querySelector().",
|
|
348
|
+
preferQueryAll: "Use the @queryAll decorator instead of this.{{root}}.querySelectorAll().",
|
|
349
|
+
preferQueryAssignedElements: "Use the @queryAssignedElements decorator instead of this.{{root}}.querySelector().assignedElements().",
|
|
350
|
+
preferQueryAssignedNodes: "Use the @queryAssignedNodes decorator instead of this.{{root}}.querySelector().assignedNodes()."
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
create(context) {
|
|
354
|
+
const bases = litElementBaseClasses(context.settings);
|
|
355
|
+
const options = jsonObject(context.options[0]);
|
|
356
|
+
const disabled = new Set(options ? Object.keys(options).filter((method) => options[method] === false) : []);
|
|
357
|
+
return {
|
|
358
|
+
CallExpression(node) {
|
|
359
|
+
const query = litQueryDecoratorCall(node, bases);
|
|
360
|
+
if (!query || disabled.has(query.method))
|
|
361
|
+
return;
|
|
362
|
+
context.report({
|
|
363
|
+
node,
|
|
364
|
+
messageId: messageIds[query.method],
|
|
365
|
+
data: { root: query.root }
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
60
372
|
// src/generic/rules/prefer-types-over-assertions.ts
|
|
61
|
-
import { defineRule as
|
|
62
|
-
var preferTypesOverAssertionsRule =
|
|
373
|
+
import { defineRule as defineRule6 } from "@oxlint/plugins";
|
|
374
|
+
var preferTypesOverAssertionsRule = defineRule6({
|
|
63
375
|
meta: {
|
|
64
376
|
type: "problem",
|
|
65
377
|
docs: {
|
|
@@ -71,7 +383,7 @@ var preferTypesOverAssertionsRule = defineRule2({
|
|
|
71
383
|
},
|
|
72
384
|
create(context) {
|
|
73
385
|
const checkAssertion = (node) => {
|
|
74
|
-
if (node.typeAnnotation.type === "TSTypeReference" && node.typeAnnotation.typeName.type === "Identifier" && node.typeAnnotation.typeName.name === "const") {
|
|
386
|
+
if (node.typeAnnotation.type === "TSTypeReference" && node.typeAnnotation.typeName.type === "Identifier" && node.typeAnnotation.typeName.name === "const" || assertedEventParameter(node)) {
|
|
75
387
|
return;
|
|
76
388
|
}
|
|
77
389
|
context.report({ node, messageId: "avoidAssertion" });
|
|
@@ -87,7 +399,11 @@ var preferTypesOverAssertionsRule = defineRule2({
|
|
|
87
399
|
var timmoPlugin = eslintCompatPlugin({
|
|
88
400
|
meta: { name: "timmo" },
|
|
89
401
|
rules: {
|
|
402
|
+
"no-explicit-inferable-type-predicate": noExplicitInferableTypePredicateRule,
|
|
403
|
+
"no-unchecked-query-selector-type": noUncheckedQuerySelectorTypeRule,
|
|
404
|
+
"no-untyped-custom-event": noUntypedCustomEventRule,
|
|
90
405
|
"prefer-event-parameter-type": preferEventParameterTypeRule,
|
|
406
|
+
"prefer-lit-query-decorators": preferLitQueryDecoratorsRule,
|
|
91
407
|
"prefer-types-over-assertions": preferTypesOverAssertionsRule
|
|
92
408
|
}
|
|
93
409
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timmo001/oxlint-rules",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Shared Oxlint plugins and configs with optional Effect rules.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
"src/generic",
|
|
26
26
|
"!src/generic/**/*.test.ts",
|
|
27
27
|
"!src/effect/**/*.test.ts",
|
|
28
|
+
"src/shared",
|
|
29
|
+
"!src/shared/**/*.test.ts",
|
|
28
30
|
"vendor/anti-slop/src",
|
|
29
31
|
"vendor/anti-slop/LICENSE",
|
|
30
32
|
"!vendor/anti-slop/src/**/*.test.ts",
|
|
@@ -36,24 +38,25 @@
|
|
|
36
38
|
],
|
|
37
39
|
"scripts": {
|
|
38
40
|
"build": "bun run scripts/build.ts",
|
|
39
|
-
"check": "bun run
|
|
41
|
+
"check": "bun run --parallel --no-exit-on-error lint test typecheck format:check skills:validate",
|
|
40
42
|
"format": "prettier --write \"{src,scripts}/**/*.ts\" \"skills/**/*.{md,mjs}\" \"*.ts\" \"*.md\" \"*.json\"",
|
|
41
43
|
"format:check": "prettier --check \"{src,scripts}/**/*.ts\" \"skills/**/*.{md,mjs}\" \"*.ts\" \"*.md\" \"*.json\"",
|
|
42
|
-
"lint": "bun run build &&
|
|
43
|
-
"
|
|
44
|
+
"lint": "bun run build && bun run lint:oxlint",
|
|
45
|
+
"lint:oxlint": "oxlint --disable-nested-config .",
|
|
46
|
+
"skills:validate": "printf '%s\\n' skills/*/ | xargs -P 0 -n 1 skills-ref validate",
|
|
44
47
|
"test": "node --experimental-strip-types scripts/test.ts",
|
|
45
48
|
"test:package": "node --experimental-strip-types scripts/test-package.ts",
|
|
46
49
|
"typecheck": "tsc --noEmit"
|
|
47
50
|
},
|
|
48
51
|
"peerDependencies": {
|
|
49
|
-
"@oxlint/plugins": "1.
|
|
50
|
-
"oxlint": "1.
|
|
52
|
+
"@oxlint/plugins": "1.87.0",
|
|
53
|
+
"oxlint": "1.87.0"
|
|
51
54
|
},
|
|
52
55
|
"devDependencies": {
|
|
53
|
-
"@oxlint/plugins": "1.
|
|
56
|
+
"@oxlint/plugins": "1.87.0",
|
|
54
57
|
"@types/bun": "^1.3.14",
|
|
55
|
-
"oxlint": "1.
|
|
56
|
-
"oxlint-tsgolint": "7.0.
|
|
58
|
+
"oxlint": "1.87.0",
|
|
59
|
+
"oxlint-tsgolint": "7.0.2004",
|
|
57
60
|
"prettier": "^3.9.5",
|
|
58
61
|
"skills-ref": "0.1.5",
|
|
59
62
|
"typescript": "^7.0.0"
|
|
@@ -32,10 +32,8 @@ description: >-
|
|
|
32
32
|
instead only when `effect` is a direct dependency or the user explicitly
|
|
33
33
|
requests it.
|
|
34
34
|
4. Keep dependency and config edits visible. Do not delegate them to a script.
|
|
35
|
-
5.
|
|
36
|
-
|
|
37
|
-
Enable `options.typeAware` in the target config and verify type-aware linting
|
|
38
|
-
alongside the shared rules.
|
|
35
|
+
5. Explicitly pin the latest `oxlint-tsgolint` that satisfies Oxlint's peer
|
|
36
|
+
requirement and was validated with the rules release.
|
|
39
37
|
|
|
40
38
|
## Copy rules
|
|
41
39
|
|
|
@@ -51,8 +49,23 @@ description: >-
|
|
|
51
49
|
output as authoritative rather than maintaining plugin or rule inventories
|
|
52
50
|
in this skill.
|
|
53
51
|
|
|
52
|
+
## Strictness
|
|
53
|
+
|
|
54
|
+
1. Start with every strictness option in the target config's `options`:
|
|
55
|
+
`typeAware: true`, `typeCheck: true`, and `maxWarnings: 0`. Keep them in the
|
|
56
|
+
config rather than as lint script flags, and remove any existing
|
|
57
|
+
`--type-aware`, `--type-check`, `--deny-warnings`, or `--max-warnings` flags
|
|
58
|
+
so the config is the single source.
|
|
59
|
+
2. Run the target's lint command. If it passes, keep every option.
|
|
60
|
+
3. If it fails, group the failures by cause and the option that raises them,
|
|
61
|
+
then ask one structured question: fix the failures, or drop the smallest set
|
|
62
|
+
of options that makes lint pass (`typeCheck` first, then `maxWarnings`, then
|
|
63
|
+
`typeAware`). Recommend the reduction that keeps the most strictness, and name
|
|
64
|
+
any shared cause, such as a missing declaration in an upstream package, that
|
|
65
|
+
a follow-up could fix for every consumer.
|
|
66
|
+
|
|
54
67
|
Run the target repository's lint command to verify the configuration loads, plus
|
|
55
68
|
any checks explicitly required by that repository. Run typechecks, tests, or a
|
|
56
69
|
build only when code or dependencies changed in a way those checks cover. Report
|
|
57
70
|
package-manager changes, preserved local configuration, enabled rule groups,
|
|
58
|
-
and checks.
|
|
71
|
+
strictness options kept or dropped, and checks.
|
package/src/effect/index.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { eslintCompatPlugin } from "@oxlint/plugins";
|
|
2
2
|
|
|
3
|
+
import { noRawGhRule } from "./rules/no-raw-gh.ts";
|
|
3
4
|
import { noTryCatchInEffectGeneratorsRule } from "./rules/no-try-catch-in-effect-generators.ts";
|
|
5
|
+
import { preferPlatformServicesRule } from "./rules/prefer-platform-services.ts";
|
|
4
6
|
|
|
5
7
|
const timmoEffectPlugin = eslintCompatPlugin({
|
|
6
8
|
meta: { name: "timmo-effect" },
|
|
7
9
|
rules: {
|
|
10
|
+
"no-raw-gh": noRawGhRule,
|
|
8
11
|
"no-try-catch-in-effect-generators": noTryCatchInEffectGeneratorsRule,
|
|
12
|
+
"prefer-platform-services": preferPlatformServicesRule,
|
|
9
13
|
},
|
|
10
14
|
});
|
|
11
15
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
import { staticMemberName } from "../../shared/ast.ts";
|
|
4
|
+
import { resolveVariable } from "../../shared/scope.ts";
|
|
5
|
+
|
|
6
|
+
import type { ESTree, SourceCode } from "@oxlint/plugins";
|
|
7
|
+
|
|
8
|
+
const PACKAGE = "@timmo001/effect-gh";
|
|
9
|
+
|
|
10
|
+
const rawMethods = new Set(["execute", "json", "stream"]);
|
|
11
|
+
|
|
12
|
+
/** Whether an identifier is bound by a named import of `name` from effect-gh. */
|
|
13
|
+
function isEffectGhImport(
|
|
14
|
+
sourceCode: SourceCode,
|
|
15
|
+
identifier: ESTree.IdentifierReference,
|
|
16
|
+
name: string,
|
|
17
|
+
): boolean {
|
|
18
|
+
return (
|
|
19
|
+
resolveVariable(sourceCode, identifier)?.defs.some((definition) => {
|
|
20
|
+
if (
|
|
21
|
+
definition.type !== "ImportBinding" ||
|
|
22
|
+
definition.parent?.type !== "ImportDeclaration" ||
|
|
23
|
+
definition.parent.source.value !== PACKAGE ||
|
|
24
|
+
definition.node.type !== "ImportSpecifier"
|
|
25
|
+
) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const imported = definition.node.imported;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
(imported.type === "Identifier" ? imported.name : imported.value) ===
|
|
33
|
+
name
|
|
34
|
+
);
|
|
35
|
+
}) ?? false
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Whether an expression is `yield* Gh` with Gh from effect-gh. */
|
|
40
|
+
function isYieldGh(sourceCode: SourceCode, node: ESTree.Node | null): boolean {
|
|
41
|
+
return (
|
|
42
|
+
node?.type === "YieldExpression" &&
|
|
43
|
+
node.delegate &&
|
|
44
|
+
node.argument?.type === "Identifier" &&
|
|
45
|
+
isEffectGhImport(sourceCode, node.argument, "Gh")
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Whether an identifier is a variable initialised with `yield* Gh`. */
|
|
50
|
+
function isGhBinding(
|
|
51
|
+
sourceCode: SourceCode,
|
|
52
|
+
identifier: ESTree.IdentifierReference,
|
|
53
|
+
): boolean {
|
|
54
|
+
return (
|
|
55
|
+
resolveVariable(sourceCode, identifier)?.defs.some(
|
|
56
|
+
(definition) =>
|
|
57
|
+
definition.type === "Variable" &&
|
|
58
|
+
definition.node.type === "VariableDeclarator" &&
|
|
59
|
+
isYieldGh(sourceCode, definition.node.init),
|
|
60
|
+
) ?? false
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isGhArgument(node: ESTree.Node | undefined): boolean {
|
|
65
|
+
if (node?.type === "Literal") return node.value === "gh";
|
|
66
|
+
|
|
67
|
+
if (node?.type !== "ArrayExpression") return false;
|
|
68
|
+
const first = node.elements[0];
|
|
69
|
+
|
|
70
|
+
return first?.type === "Literal" && first.value === "gh";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Route gh through effect-gh's typed operations instead of raw argv or direct spawns. */
|
|
74
|
+
export const noRawGhRule = defineRule({
|
|
75
|
+
meta: {
|
|
76
|
+
type: "problem",
|
|
77
|
+
docs: {
|
|
78
|
+
description:
|
|
79
|
+
"Use @timmo001/effect-gh typed operations instead of the raw Gh methods, Api.raw, or spawning gh directly.",
|
|
80
|
+
},
|
|
81
|
+
messages: {
|
|
82
|
+
rawGh:
|
|
83
|
+
"Use a typed effect-gh operation instead of `Gh.{{method}}` with raw argv. Add one to effect-gh if the command has none.",
|
|
84
|
+
rawApi:
|
|
85
|
+
"Use `Api.json`, `Api.pages`, `Api.empty`, `Api.text` or `Api.headers` instead of `Api.raw`, so the response is typed.",
|
|
86
|
+
spawnGh:
|
|
87
|
+
"Run gh through a typed @timmo001/effect-gh operation instead of spawning it directly.",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
create(context) {
|
|
91
|
+
return {
|
|
92
|
+
CallExpression(node) {
|
|
93
|
+
const callee = node.callee;
|
|
94
|
+
const method = staticMemberName(callee);
|
|
95
|
+
|
|
96
|
+
if (callee.type === "MemberExpression" && method !== null) {
|
|
97
|
+
const object = callee.object;
|
|
98
|
+
|
|
99
|
+
if (
|
|
100
|
+
rawMethods.has(method) &&
|
|
101
|
+
(isYieldGh(context.sourceCode, object) ||
|
|
102
|
+
(object.type === "Identifier" &&
|
|
103
|
+
isGhBinding(context.sourceCode, object)))
|
|
104
|
+
) {
|
|
105
|
+
context.report({ node, messageId: "rawGh", data: { method } });
|
|
106
|
+
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (
|
|
111
|
+
method === "raw" &&
|
|
112
|
+
object.type === "Identifier" &&
|
|
113
|
+
isEffectGhImport(context.sourceCode, object, "Api")
|
|
114
|
+
) {
|
|
115
|
+
context.report({ node, messageId: "rawApi" });
|
|
116
|
+
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (isGhArgument(node.arguments[0]))
|
|
122
|
+
context.report({ node, messageId: "spawnGh" });
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
});
|