@thomaflette/eslint-plugin-solid-2 0.1.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/LICENSE +21 -0
- package/README.md +87 -0
- package/dist/index.cjs +1920 -0
- package/dist/index.d.cts +314 -0
- package/dist/index.d.mts +315 -0
- package/dist/index.mjs +1897 -0
- package/package.json +67 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1897 @@
|
|
|
1
|
+
import { ASTUtils, ESLintUtils } from "@typescript-eslint/utils";
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
//#region src/utils.ts
|
|
4
|
+
const domElementRegex = /^[a-z]/;
|
|
5
|
+
const isDOMElementName = (name) => domElementRegex.test(name);
|
|
6
|
+
/**
|
|
7
|
+
* Whether a JSX opening element is a host (DOM) element rather than a custom component. DOM-only
|
|
8
|
+
* DOM-only rules must skip components, whose attributes are author-defined props with no DOM
|
|
9
|
+
* semantics. A lowercase tag (`<div>`) or a namespaced tag (`<svg:rect>`) is a
|
|
10
|
+
* host element; a capitalized tag (`<Card>`) or a member tag (`<Foo.Bar>`) is a component.
|
|
11
|
+
*/
|
|
12
|
+
const isHostElement = (opening) => {
|
|
13
|
+
const tag = opening.name;
|
|
14
|
+
if (tag.type === "JSXIdentifier") return isDOMElementName(tag.name);
|
|
15
|
+
return tag.type === "JSXNamespacedName";
|
|
16
|
+
};
|
|
17
|
+
const getFunctionName = (node) => {
|
|
18
|
+
if ((node.type === "FunctionDeclaration" || node.type === "FunctionExpression") && node.id != null) return node.id.name;
|
|
19
|
+
if (node.parent?.type === "VariableDeclarator" && node.parent.id.type === "Identifier") return node.parent.id.name;
|
|
20
|
+
return null;
|
|
21
|
+
};
|
|
22
|
+
const isFunctionNode = (node) => node?.type === "FunctionDeclaration" || node?.type === "FunctionExpression" || node?.type === "ArrowFunctionExpression";
|
|
23
|
+
const isJSXElementOrFragment = (node) => node?.type === "JSXElement" || node?.type === "JSXFragment";
|
|
24
|
+
const trace = (node, context) => {
|
|
25
|
+
if (node.type !== "Identifier") return node;
|
|
26
|
+
const variable = ASTUtils.findVariable(context.sourceCode.getScope(node), node);
|
|
27
|
+
const def = variable?.defs[0];
|
|
28
|
+
if (!variable || !def) return node;
|
|
29
|
+
switch (def.type) {
|
|
30
|
+
case "FunctionName":
|
|
31
|
+
case "ClassName":
|
|
32
|
+
case "ImportBinding": return def.node;
|
|
33
|
+
case "Variable": {
|
|
34
|
+
const declaration = def.node.parent;
|
|
35
|
+
if (declaration?.type === "VariableDeclaration" && declaration.kind === "const" && def.node.id.type === "Identifier" && def.node.init) return trace(def.node.init, context);
|
|
36
|
+
return node;
|
|
37
|
+
}
|
|
38
|
+
default: return node;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
function jsxPropName(prop) {
|
|
42
|
+
if (prop.name.type === "JSXNamespacedName") return `${prop.name.namespace.name}:${prop.name.name.name}`;
|
|
43
|
+
return prop.name.name;
|
|
44
|
+
}
|
|
45
|
+
function* jsxGetAllProps(props) {
|
|
46
|
+
for (const attr of props) {
|
|
47
|
+
if (attr.type === "JSXSpreadAttribute" && attr.argument.type === "ObjectExpression") {
|
|
48
|
+
for (const property of attr.argument.properties) if (property.type === "Property") {
|
|
49
|
+
if (property.key.type === "Identifier") yield [property.key.name, property.key];
|
|
50
|
+
else if (property.key.type === "Literal") yield [String(property.key.value), property.key];
|
|
51
|
+
}
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (attr.type === "JSXAttribute") yield [jsxPropName(attr), attr.name];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/rules/create-rule.ts
|
|
59
|
+
const DOCS_BASE = "https://github.com/yumemi-thomas/eslint-plugin-solid-2/blob/main/docs";
|
|
60
|
+
const createRule = ESLintUtils.RuleCreator((name) => `${DOCS_BASE}/${name}.md`);
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/rules/solid-rule-utils.ts
|
|
63
|
+
const SOLID_COMPONENT_TYPES = new Set([
|
|
64
|
+
"Component",
|
|
65
|
+
"VoidComponent",
|
|
66
|
+
"ParentComponent",
|
|
67
|
+
"FlowComponent"
|
|
68
|
+
]);
|
|
69
|
+
/** Whether a node is an `import ... from "solid-js"` declaration. The single source of truth for
|
|
70
|
+
* "this binding comes from solid-js", shared by the callee, type-annotation, and alias resolvers. */
|
|
71
|
+
function isSolidJsImportDeclaration(node) {
|
|
72
|
+
return node?.type === "ImportDeclaration" && node.source.type === "Literal" && node.source.value === "solid-js";
|
|
73
|
+
}
|
|
74
|
+
function typeReferenceName(node) {
|
|
75
|
+
if (node?.type !== "TSTypeReference") return null;
|
|
76
|
+
if (node.typeName.type === "Identifier") return node.typeName.name;
|
|
77
|
+
if (node.typeName.type === "TSQualifiedName" && node.typeName.right.type === "Identifier") return node.typeName.right.name;
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The identifier that carries the binding for a type reference: the type name itself for a plain
|
|
82
|
+
* `Component`, or the leftmost segment for a qualified `Solid.Component` (i.e. the namespace root).
|
|
83
|
+
* `qualified` distinguishes the two so the binding check can demand an explicit solid-js import for
|
|
84
|
+
* the qualified form (an unresolved `Solid.Component` is not a Solid auto-import).
|
|
85
|
+
*/
|
|
86
|
+
function typeReferenceRootIdentifier(node) {
|
|
87
|
+
if (node?.type !== "TSTypeReference") return null;
|
|
88
|
+
let name = node.typeName;
|
|
89
|
+
let qualified = false;
|
|
90
|
+
while (name.type === "TSQualifiedName") {
|
|
91
|
+
qualified = true;
|
|
92
|
+
name = name.left;
|
|
93
|
+
}
|
|
94
|
+
return name.type === "Identifier" ? {
|
|
95
|
+
id: name,
|
|
96
|
+
qualified
|
|
97
|
+
} : null;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Whether a component type-name identifier actually refers to Solid's type — the type-annotation
|
|
101
|
+
* analogue of {@link resolveSolidCallee} (see ADR-0003). It counts as Solid when the name binds to a
|
|
102
|
+
* `solid-js` import, or (for a bare name only) is unresolved — an ambient/auto-import global. A name
|
|
103
|
+
* that binds to a local `type`/`interface` declaration or an import from another package is *not*
|
|
104
|
+
* Solid, which is what keeps `hasSolidComponentTypeAnnotation` from firing on same-named non-Solid
|
|
105
|
+
* types (a UI kit's `Component`, a local `VoidComponent`, a foreign `Ns.Component`).
|
|
106
|
+
*/
|
|
107
|
+
function typeNameBindsToSolid(id, qualified, context) {
|
|
108
|
+
let scope = context.sourceCode.getScope(id);
|
|
109
|
+
while (scope) {
|
|
110
|
+
for (const variable of scope.variables) {
|
|
111
|
+
if (variable.name !== id.name) continue;
|
|
112
|
+
for (const def of variable.defs) {
|
|
113
|
+
if (def.type === "ImportBinding") return isSolidJsImportDeclaration(def.node.parent);
|
|
114
|
+
if (def.type === "Type" || def.type === "ClassName" || def.type === "TSEnumName") return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
scope = scope.upper;
|
|
118
|
+
}
|
|
119
|
+
return !qualified;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Whether the function is declared with an explicit Solid component type annotation,
|
|
123
|
+
* e.g. `const C: Component<P> = (props) => ...`. This is a *sound* signal of component-hood:
|
|
124
|
+
* it is never true for a plain JSX-returning helper, and holds across file boundaries. The type
|
|
125
|
+
* must actually resolve to Solid's `Component`/`VoidComponent`/… (not a same-named local or
|
|
126
|
+
* third-party type) — see {@link typeNameBindsToSolid} and ADR-0002/0003.
|
|
127
|
+
*/
|
|
128
|
+
function hasSolidComponentTypeAnnotation(node, context) {
|
|
129
|
+
const parent = node.parent;
|
|
130
|
+
if (parent?.type === "VariableDeclarator" && parent.id.type === "Identifier") {
|
|
131
|
+
const annotation = parent.id.typeAnnotation?.typeAnnotation;
|
|
132
|
+
const name = typeReferenceName(annotation);
|
|
133
|
+
if (name == null || !SOLID_COMPONENT_TYPES.has(name)) return false;
|
|
134
|
+
const root = typeReferenceRootIdentifier(annotation);
|
|
135
|
+
return root != null && typeNameBindsToSolid(root.id, root.qualified, context);
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
const inFileComponentVarsCache = /* @__PURE__ */ new WeakMap();
|
|
140
|
+
function getInFileComponentVariables(sourceCode) {
|
|
141
|
+
const cached = inFileComponentVarsCache.get(sourceCode);
|
|
142
|
+
if (cached) return cached;
|
|
143
|
+
const vars = /* @__PURE__ */ new Set();
|
|
144
|
+
const stack = [sourceCode.ast];
|
|
145
|
+
while (stack.length > 0) {
|
|
146
|
+
const node = stack.pop();
|
|
147
|
+
if (node.type === "JSXOpeningElement" && node.name.type === "JSXIdentifier" && !isDOMElementName(node.name.name)) {
|
|
148
|
+
const variable = ASTUtils.findVariable(sourceCode.getScope(node.name), node.name.name);
|
|
149
|
+
if (variable != null) vars.add(variable);
|
|
150
|
+
}
|
|
151
|
+
for (const key in node) {
|
|
152
|
+
if (key === "parent" || key === "tokens" || key === "comments") continue;
|
|
153
|
+
const value = node[key];
|
|
154
|
+
if (Array.isArray(value)) {
|
|
155
|
+
for (const item of value) if (item != null && typeof item === "object" && typeof item.type === "string") stack.push(item);
|
|
156
|
+
} else if (value != null && typeof value === "object" && typeof value.type === "string") stack.push(value);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
inFileComponentVarsCache.set(sourceCode, vars);
|
|
160
|
+
return vars;
|
|
161
|
+
}
|
|
162
|
+
function expressionCanYieldJsx(node) {
|
|
163
|
+
switch (node?.type) {
|
|
164
|
+
case "JSXElement":
|
|
165
|
+
case "JSXFragment": return true;
|
|
166
|
+
case "ConditionalExpression": return expressionCanYieldJsx(node.consequent) || expressionCanYieldJsx(node.alternate);
|
|
167
|
+
case "LogicalExpression": return expressionCanYieldJsx(node.left) || expressionCanYieldJsx(node.right);
|
|
168
|
+
case "SequenceExpression": return expressionCanYieldJsx(node.expressions.at(-1));
|
|
169
|
+
default: return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** Whether any `return` in the function (or an arrow's expression body) yields JSX. */
|
|
173
|
+
function functionReturnsJsx(node) {
|
|
174
|
+
if (node.body.type !== "BlockStatement") return expressionCanYieldJsx(node.body);
|
|
175
|
+
const statements = [...node.body.body];
|
|
176
|
+
while (statements.length > 0) {
|
|
177
|
+
const statement = statements.pop();
|
|
178
|
+
switch (statement.type) {
|
|
179
|
+
case "BlockStatement":
|
|
180
|
+
statements.push(...statement.body);
|
|
181
|
+
break;
|
|
182
|
+
case "IfStatement":
|
|
183
|
+
statements.push(statement.consequent);
|
|
184
|
+
if (statement.alternate) statements.push(statement.alternate);
|
|
185
|
+
break;
|
|
186
|
+
case "LabeledStatement":
|
|
187
|
+
case "WithStatement":
|
|
188
|
+
statements.push(statement.body);
|
|
189
|
+
break;
|
|
190
|
+
case "SwitchStatement":
|
|
191
|
+
for (const switchCase of statement.cases) statements.push(...switchCase.consequent);
|
|
192
|
+
break;
|
|
193
|
+
case "TryStatement":
|
|
194
|
+
statements.push(statement.block);
|
|
195
|
+
if (statement.handler) statements.push(statement.handler.body);
|
|
196
|
+
if (statement.finalizer) statements.push(statement.finalizer);
|
|
197
|
+
break;
|
|
198
|
+
case "ReturnStatement":
|
|
199
|
+
if (expressionCanYieldJsx(statement.argument)) return true;
|
|
200
|
+
break;
|
|
201
|
+
default: break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
function getTypeAwareServices(context) {
|
|
207
|
+
const services = context.sourceCode.parserServices;
|
|
208
|
+
if (services?.program != null && services.esTreeNodeToTSNodeMap != null) return services;
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
const jsxTagSymbolsCache = /* @__PURE__ */ new WeakMap();
|
|
212
|
+
function resolveAlias(symbol, checker) {
|
|
213
|
+
return symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
|
|
214
|
+
}
|
|
215
|
+
function getJsxTagSymbols(services) {
|
|
216
|
+
const program = services.program;
|
|
217
|
+
const cached = jsxTagSymbolsCache.get(program);
|
|
218
|
+
if (cached) return cached;
|
|
219
|
+
const checker = program.getTypeChecker();
|
|
220
|
+
const symbols = /* @__PURE__ */ new Set();
|
|
221
|
+
const visit = (node) => {
|
|
222
|
+
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
223
|
+
const symbol = checker.getSymbolAtLocation(node.tagName);
|
|
224
|
+
if (symbol) symbols.add(resolveAlias(symbol, checker));
|
|
225
|
+
}
|
|
226
|
+
ts.forEachChild(node, visit);
|
|
227
|
+
};
|
|
228
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
229
|
+
if (sourceFile.isDeclarationFile || sourceFile.fileName.includes("/node_modules/")) continue;
|
|
230
|
+
visit(sourceFile);
|
|
231
|
+
}
|
|
232
|
+
jsxTagSymbolsCache.set(program, symbols);
|
|
233
|
+
return symbols;
|
|
234
|
+
}
|
|
235
|
+
function getFunctionSymbol(node, services) {
|
|
236
|
+
let nameNode;
|
|
237
|
+
if ((node.type === "FunctionDeclaration" || node.type === "FunctionExpression") && node.id != null) nameNode = node.id;
|
|
238
|
+
else if (node.parent?.type === "VariableDeclarator" && node.parent.id.type === "Identifier") nameNode = node.parent.id;
|
|
239
|
+
if (!nameNode) return;
|
|
240
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(nameNode);
|
|
241
|
+
return tsNode ? services.program.getTypeChecker().getSymbolAtLocation(tsNode) : void 0;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Verdict on whether `node`'s type is array-like (an array or tuple — i.e. has a numeric index
|
|
245
|
+
* signature), considering every union member:
|
|
246
|
+
*
|
|
247
|
+
* - `"array"` — provably array-like; safe to rewrite `.map` to `<For each>`.
|
|
248
|
+
* - `"not-array"` — provably not (a `Map`/`Set`/observable); `prefer-for` skips the report.
|
|
249
|
+
* - `"unknown"` — the type can't be resolved, is `any`/`unknown`, or mixes array and non-array
|
|
250
|
+
* members. `prefer-for` stays silent because a rewrite may be semantically wrong.
|
|
251
|
+
*/
|
|
252
|
+
function getArrayReceiverVerdict(node, services) {
|
|
253
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
|
|
254
|
+
if (!tsNode) return "unknown";
|
|
255
|
+
const checker = services.program.getTypeChecker();
|
|
256
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
257
|
+
const members = type.isUnion() ? type.types : [type];
|
|
258
|
+
if (members.some((member) => member.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown))) return "unknown";
|
|
259
|
+
const arrayMembers = members.filter((member) => checker.getIndexTypeOfType(member, ts.IndexKind.Number) != null);
|
|
260
|
+
if (arrayMembers.length === members.length) return "array";
|
|
261
|
+
return arrayMembers.length === 0 ? "not-array" : "unknown";
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Sound component detection (see docs/adr/0002). A function is treated as a Solid component only by
|
|
265
|
+
* signals that are never true for a plain helper — never the leaky "capitalized JSX-returning
|
|
266
|
+
* function" guess:
|
|
267
|
+
*
|
|
268
|
+
* - **Always:** it is annotated `Component`/`VoidComponent`/… or used as `<C/>` in the same file.
|
|
269
|
+
* - **Only when `typescriptEnabled` and type info is present:** additionally, its symbol is used as
|
|
270
|
+
* a JSX tag anywhere in the program (catches exported/cross-file components).
|
|
271
|
+
*
|
|
272
|
+
* Both paths are sound (zero false positives); type information is purely additive — it finds more
|
|
273
|
+
* real components, never flips a verdict on correct code. The trade-off is a tolerated false
|
|
274
|
+
* negative: an unannotated component used only in another file is not seen without type info.
|
|
275
|
+
*
|
|
276
|
+
* The module owns its own whole-file index and reads `typescriptEnabled` from `context`, so a
|
|
277
|
+
* caller needs nothing but the node — no `jsxComponentNames` set to build, thread, or defer. The
|
|
278
|
+
* index is complete on first query (see {@link getInFileComponentVariables}), so this is correct
|
|
279
|
+
* when called inline during traversal.
|
|
280
|
+
*/
|
|
281
|
+
function isComponent$1(node, context) {
|
|
282
|
+
if (hasSolidComponentTypeAnnotation(node, context)) return true;
|
|
283
|
+
const name = getFunctionName(node);
|
|
284
|
+
if (name != null) {
|
|
285
|
+
const fnVariable = ASTUtils.findVariable(context.sourceCode.getScope(node), name);
|
|
286
|
+
if (fnVariable != null && getInFileComponentVariables(context.sourceCode).has(fnVariable)) return true;
|
|
287
|
+
}
|
|
288
|
+
if (context.options[0]?.typescriptEnabled) {
|
|
289
|
+
const services = getTypeAwareServices(context);
|
|
290
|
+
if (services) {
|
|
291
|
+
const symbol = getFunctionSymbol(node, services);
|
|
292
|
+
if (symbol && getJsxTagSymbols(services).has(resolveAlias(symbol, services.program.getTypeChecker()))) return true;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Whether `identifier` actually refers to the Solid API for one of `canonicalNames` — a name
|
|
299
|
+
* imported from "solid-js" (directly or aliased), a `const` alias of one (`const c = onCleanup`),
|
|
300
|
+
* or an unresolved global (auto-import). Returns false when it binds to the user's own declaration
|
|
301
|
+
* or an import from another package, which prevents false positives on same-named non-Solid
|
|
302
|
+
* functions (a stream library's `flush`, a state library's `createStore`, a local `onCleanup`, ...).
|
|
303
|
+
*
|
|
304
|
+
* Delegates to {@link resolveSolidCallee}, whose `trace` step resolves both aliased imports and
|
|
305
|
+
* `const` aliases — so no caller needs to pre-collect import aliases (see ADR-0003).
|
|
306
|
+
*/
|
|
307
|
+
function bindsToSolid(identifier, context, canonicalNames) {
|
|
308
|
+
return resolveSolidCallee(identifier, context, canonicalNames) != null;
|
|
309
|
+
}
|
|
310
|
+
/** Whether `node` is the `argumentIndex`th argument of a call whose callee binds to the Solid API. */
|
|
311
|
+
function isSolidApiCallbackArgument(node, argumentIndex, context, canonicalNames) {
|
|
312
|
+
return node.parent?.type === "CallExpression" && node.parent.arguments[argumentIndex] === node && node.parent.callee.type === "Identifier" && bindsToSolid(node.parent.callee, context, canonicalNames);
|
|
313
|
+
}
|
|
314
|
+
function getReturnedExpressions(node) {
|
|
315
|
+
if (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement") return [node.body];
|
|
316
|
+
if (node.body.type !== "BlockStatement") return [];
|
|
317
|
+
const returned = [];
|
|
318
|
+
const statements = [...node.body.body];
|
|
319
|
+
while (statements.length > 0) {
|
|
320
|
+
const statement = statements.pop();
|
|
321
|
+
switch (statement.type) {
|
|
322
|
+
case "BlockStatement":
|
|
323
|
+
statements.push(...statement.body);
|
|
324
|
+
break;
|
|
325
|
+
case "DoWhileStatement":
|
|
326
|
+
case "ForInStatement":
|
|
327
|
+
case "ForOfStatement":
|
|
328
|
+
case "ForStatement":
|
|
329
|
+
case "WhileStatement":
|
|
330
|
+
case "LabeledStatement":
|
|
331
|
+
case "WithStatement":
|
|
332
|
+
statements.push(statement.body);
|
|
333
|
+
break;
|
|
334
|
+
case "IfStatement":
|
|
335
|
+
statements.push(statement.consequent);
|
|
336
|
+
if (statement.alternate) statements.push(statement.alternate);
|
|
337
|
+
break;
|
|
338
|
+
case "SwitchStatement":
|
|
339
|
+
for (const switchCase of statement.cases) statements.push(...switchCase.consequent);
|
|
340
|
+
break;
|
|
341
|
+
case "TryStatement":
|
|
342
|
+
statements.push(statement.block);
|
|
343
|
+
if (statement.handler) statements.push(statement.handler.body);
|
|
344
|
+
if (statement.finalizer) statements.push(statement.finalizer);
|
|
345
|
+
break;
|
|
346
|
+
case "ReturnStatement":
|
|
347
|
+
returned.push(statement.argument ?? null);
|
|
348
|
+
break;
|
|
349
|
+
default: break;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return returned;
|
|
353
|
+
}
|
|
354
|
+
function getPropertyName$2(node) {
|
|
355
|
+
if (!node.computed && node.key.type === "Identifier") return node.key.name;
|
|
356
|
+
if (node.key.type === "Literal" && typeof node.key.value === "string") return node.key.value;
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
function getNearestFunctionAncestor(node) {
|
|
360
|
+
let current = node.parent;
|
|
361
|
+
while (current != null) {
|
|
362
|
+
if (isFunctionNode(current)) return current;
|
|
363
|
+
current = current.parent;
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Resolves an identifier to the canonical Solid API name it refers to, or null. The `trace` step
|
|
369
|
+
* follows both aliased imports (`import { createEffect as fx }`) and `const` aliases to their
|
|
370
|
+
* origin, so an aliased Solid API resolves to its **canonical** name (`"createEffect"`), not the
|
|
371
|
+
* local alias. A bare canonical name is trusted only when unresolved (an auto-import/global); a
|
|
372
|
+
* name bound to a local declaration or a non-solid-js import resolves to null.
|
|
373
|
+
*/
|
|
374
|
+
function resolveSolidCallee(node, context, canonicalNames) {
|
|
375
|
+
if (node.type !== "Identifier") return null;
|
|
376
|
+
const traced = trace(node, context);
|
|
377
|
+
if (traced.type === "Identifier" && canonicalNames.has(traced.name)) {
|
|
378
|
+
const variable = ASTUtils.findVariable(context.sourceCode.getScope(traced), traced);
|
|
379
|
+
return variable == null || variable.defs.length === 0 ? traced.name : null;
|
|
380
|
+
}
|
|
381
|
+
if (traced.type === "ImportSpecifier" && isSolidJsImportDeclaration(traced.parent)) {
|
|
382
|
+
const importedName = traced.imported.type === "Identifier" ? traced.imported.name : traced.imported.value;
|
|
383
|
+
if (canonicalNames.has(importedName)) return importedName;
|
|
384
|
+
}
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
/** Whether any scope in the file already declares `name`. */
|
|
388
|
+
function isNameTaken(sourceCode, name) {
|
|
389
|
+
return sourceCode.scopeManager?.scopes.some((scope) => scope.set.has(name)) ?? false;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Fixes that make `names` importable from solid-js at the fix site, so an autofix that emits
|
|
393
|
+
* `<Show>`/`<For>`/... never produces non-compiling code. Returns `[]` when every name is already
|
|
394
|
+
* imported from solid-js under its own name, a fix extending (or creating) the solid-js import for
|
|
395
|
+
* the missing ones, or `null` when a missing name is already bound to something else in the file —
|
|
396
|
+
* callers must then skip their autofix rather than emit a reference that resolves to the wrong
|
|
397
|
+
* binding.
|
|
398
|
+
*/
|
|
399
|
+
function getSolidImportFixes(context, fixer, names) {
|
|
400
|
+
const sourceCode = context.sourceCode;
|
|
401
|
+
const importNode = sourceCode.ast.body.find((node) => isSolidJsImportDeclaration(node) && node.importKind !== "type");
|
|
402
|
+
const available = /* @__PURE__ */ new Set();
|
|
403
|
+
for (const specifier of importNode?.specifiers ?? []) if (specifier.type === "ImportSpecifier" && specifier.imported.type === "Identifier" && specifier.imported.name === specifier.local.name) available.add(specifier.local.name);
|
|
404
|
+
const missing = names.filter((name) => !available.has(name));
|
|
405
|
+
if (missing.length === 0) return [];
|
|
406
|
+
if (missing.some((name) => isNameTaken(sourceCode, name))) return null;
|
|
407
|
+
const lastNamed = importNode?.specifiers.findLast((specifier) => specifier.type === "ImportSpecifier");
|
|
408
|
+
if (lastNamed != null) return [fixer.insertTextAfter(lastNamed, `, ${missing.join(", ")}`)];
|
|
409
|
+
const firstStatement = sourceCode.ast.body[0];
|
|
410
|
+
const importText = `import { ${missing.join(", ")} } from "solid-js";\n`;
|
|
411
|
+
return [firstStatement != null ? fixer.insertTextBefore(firstStatement, importText) : fixer.insertTextAfterRange([0, 0], importText)];
|
|
412
|
+
}
|
|
413
|
+
//#endregion
|
|
414
|
+
//#region src/rules/components-return-once.ts
|
|
415
|
+
const ACCESSOR_FACTORIES$2 = new Set(["createMemo"]);
|
|
416
|
+
const PAIR_ACCESSOR_FACTORIES$2 = new Set(["createSignal", "createOptimistic"]);
|
|
417
|
+
const STORE_FACTORIES$1 = new Set(["createOptimisticStore", "createStore"]);
|
|
418
|
+
function getVariable(id, context) {
|
|
419
|
+
return ASTUtils.findVariable(context.sourceCode.getScope(id), id);
|
|
420
|
+
}
|
|
421
|
+
/** The `VariableDeclarator` that declared `variable`, when it was declared by one. */
|
|
422
|
+
function getDeclarator(variable) {
|
|
423
|
+
const def = variable.defs[0];
|
|
424
|
+
return def?.type === "Variable" ? def.node : null;
|
|
425
|
+
}
|
|
426
|
+
/** Whether `variable` is `elements[0]` of an array-pattern declaration whose init calls a factory. */
|
|
427
|
+
function isPairElementZero(variable, factories, context) {
|
|
428
|
+
const declarator = getDeclarator(variable);
|
|
429
|
+
if (declarator?.id.type !== "ArrayPattern" || declarator.init?.type !== "CallExpression" || declarator.init.callee.type !== "Identifier") return false;
|
|
430
|
+
const first = declarator.id.elements[0];
|
|
431
|
+
return first?.type === "Identifier" && first.name === variable.name && bindsToSolid(declarator.init.callee, context, factories);
|
|
432
|
+
}
|
|
433
|
+
/** Whether calling `variable` reads a signal/memo accessor (follows `const c = count` aliases). */
|
|
434
|
+
function variableIsAccessor(variable, context, seen) {
|
|
435
|
+
if (seen.has(variable)) return false;
|
|
436
|
+
seen.add(variable);
|
|
437
|
+
if (isPairElementZero(variable, PAIR_ACCESSOR_FACTORIES$2, context)) return true;
|
|
438
|
+
const declarator = getDeclarator(variable);
|
|
439
|
+
if (declarator?.id.type !== "Identifier" || declarator.init == null) return false;
|
|
440
|
+
if (declarator.init.type === "CallExpression" && declarator.init.callee.type === "Identifier" && bindsToSolid(declarator.init.callee, context, ACCESSOR_FACTORIES$2)) return true;
|
|
441
|
+
if (declarator.init.type === "Identifier") {
|
|
442
|
+
const source = getVariable(declarator.init, context);
|
|
443
|
+
return source != null && variableIsAccessor(source, context, seen);
|
|
444
|
+
}
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Whether evaluating `node` provably performs a reactive read from `componentFn`'s perspective.
|
|
449
|
+
* Skips nested functions (not evaluated by the guard) and JSX (reads there compile to tracked
|
|
450
|
+
* scopes). Follows one-level-and-deeper `const` derivations (`const loading = props.loading`).
|
|
451
|
+
*/
|
|
452
|
+
function containsReactiveRead(node, componentFn, context, seen = /* @__PURE__ */ new Set()) {
|
|
453
|
+
const isPropsParameter = (variable) => variable.defs.some((def) => def.type === "Parameter" && def.node === componentFn);
|
|
454
|
+
const memberRootIsReactive = (member) => {
|
|
455
|
+
let root = member;
|
|
456
|
+
while (root.type === "MemberExpression") root = root.object;
|
|
457
|
+
if (root.type !== "Identifier") return false;
|
|
458
|
+
const variable = getVariable(root, context);
|
|
459
|
+
if (variable == null) return false;
|
|
460
|
+
if (isPropsParameter(variable) || isPairElementZero(variable, STORE_FACTORIES$1, context)) return true;
|
|
461
|
+
return derivedConstIsReactive(variable);
|
|
462
|
+
};
|
|
463
|
+
const derivedConstIsReactive = (variable) => {
|
|
464
|
+
if (seen.has(variable)) return false;
|
|
465
|
+
seen.add(variable);
|
|
466
|
+
const declarator = getDeclarator(variable);
|
|
467
|
+
return declarator?.id.type === "Identifier" && declarator.init != null && containsReactiveRead(declarator.init, componentFn, context, seen);
|
|
468
|
+
};
|
|
469
|
+
const stack = [node];
|
|
470
|
+
while (stack.length > 0) {
|
|
471
|
+
const current = stack.pop();
|
|
472
|
+
if (isFunctionNode(current) || current.type === "JSXElement" || current.type === "JSXFragment") continue;
|
|
473
|
+
if (current.type === "CallExpression" && current.callee.type === "Identifier") {
|
|
474
|
+
const callee = getVariable(current.callee, context);
|
|
475
|
+
if (callee != null && variableIsAccessor(callee, context, /* @__PURE__ */ new Set())) return true;
|
|
476
|
+
}
|
|
477
|
+
if (current.type === "MemberExpression" && memberRootIsReactive(current)) return true;
|
|
478
|
+
if (current.type === "Identifier") {
|
|
479
|
+
const variable = getVariable(current, context);
|
|
480
|
+
if (variable != null && !isPropsParameter(variable) && derivedConstIsReactive(variable)) return true;
|
|
481
|
+
}
|
|
482
|
+
for (const key in current) {
|
|
483
|
+
if (key === "parent" || key === "tokens" || key === "comments") continue;
|
|
484
|
+
const value = current[key];
|
|
485
|
+
if (Array.isArray(value)) {
|
|
486
|
+
for (const item of value) if (item != null && typeof item === "object" && typeof item.type === "string") stack.push(item);
|
|
487
|
+
} else if (value != null && typeof value === "object" && typeof value.type === "string") stack.push(value);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* The guard expressions that decide whether execution reaches `ret`: every enclosing `if` test,
|
|
494
|
+
* switch discriminant/case test, and loop condition between the return and the component function.
|
|
495
|
+
*/
|
|
496
|
+
function collectGuardExpressions(ret, fn) {
|
|
497
|
+
const guards = [];
|
|
498
|
+
let current = ret;
|
|
499
|
+
while (current !== fn && current.parent != null) {
|
|
500
|
+
const parent = current.parent;
|
|
501
|
+
switch (parent.type) {
|
|
502
|
+
case "IfStatement":
|
|
503
|
+
if (parent.consequent === current || parent.alternate === current) guards.push(parent.test);
|
|
504
|
+
break;
|
|
505
|
+
case "SwitchCase":
|
|
506
|
+
if (parent.test != null) guards.push(parent.test);
|
|
507
|
+
break;
|
|
508
|
+
case "SwitchStatement":
|
|
509
|
+
guards.push(parent.discriminant);
|
|
510
|
+
break;
|
|
511
|
+
case "WhileStatement":
|
|
512
|
+
case "DoWhileStatement":
|
|
513
|
+
if (parent.body === current) guards.push(parent.test);
|
|
514
|
+
break;
|
|
515
|
+
case "ForStatement":
|
|
516
|
+
if (parent.body === current && parent.test != null) guards.push(parent.test);
|
|
517
|
+
break;
|
|
518
|
+
case "ForInStatement":
|
|
519
|
+
case "ForOfStatement":
|
|
520
|
+
if (parent.body === current) guards.push(parent.right);
|
|
521
|
+
break;
|
|
522
|
+
default: break;
|
|
523
|
+
}
|
|
524
|
+
current = parent;
|
|
525
|
+
}
|
|
526
|
+
return guards;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* The decision expressions of a conditional last return: the tests of a (possibly nested) ternary
|
|
530
|
+
* chain, or the left operands along a logical chain (`a && b && <X/>` → `a && b`). Branch values
|
|
531
|
+
* are excluded — reads inside returned JSX are tracked and not this rule's concern.
|
|
532
|
+
*/
|
|
533
|
+
function conditionalDecisionExpressions(argument) {
|
|
534
|
+
const decisions = [];
|
|
535
|
+
if (argument.type === "ConditionalExpression") {
|
|
536
|
+
let current = argument;
|
|
537
|
+
while (current.type === "ConditionalExpression") {
|
|
538
|
+
decisions.push(current.test);
|
|
539
|
+
current = current.alternate;
|
|
540
|
+
}
|
|
541
|
+
} else if (argument.type === "LogicalExpression") {
|
|
542
|
+
let current = argument;
|
|
543
|
+
while (current.type === "LogicalExpression") {
|
|
544
|
+
decisions.push(current.left);
|
|
545
|
+
current = current.right;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return decisions;
|
|
549
|
+
}
|
|
550
|
+
const isNothing = (node) => {
|
|
551
|
+
if (!node) return true;
|
|
552
|
+
switch (node.type) {
|
|
553
|
+
case "Literal": return [
|
|
554
|
+
null,
|
|
555
|
+
void 0,
|
|
556
|
+
false,
|
|
557
|
+
""
|
|
558
|
+
].includes(node.value);
|
|
559
|
+
case "JSXFragment": return node.children.every((child) => isNothing(child));
|
|
560
|
+
default: return false;
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
const getLineLength = (loc) => loc == null ? 0 : loc.end.line - loc.start.line + 1;
|
|
564
|
+
var components_return_once_default = createRule({
|
|
565
|
+
name: "components-return-once",
|
|
566
|
+
meta: {
|
|
567
|
+
type: "problem",
|
|
568
|
+
docs: { description: "Disallow early returns in components. Solid components only run once, so conditionals should stay inside JSX." },
|
|
569
|
+
fixable: "code",
|
|
570
|
+
schema: [{
|
|
571
|
+
type: "object",
|
|
572
|
+
properties: { typescriptEnabled: { type: "boolean" } },
|
|
573
|
+
additionalProperties: false
|
|
574
|
+
}],
|
|
575
|
+
messages: {
|
|
576
|
+
noConditionalReturn: "Solid components run once, so a conditional return breaks reactivity. Move the condition inside JSX, such as a fragment or `<Show />`.",
|
|
577
|
+
noEarlyReturn: "Solid components run once, so an early return breaks reactivity. Move the condition inside JSX, such as a fragment or `<Show />`."
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
defaultOptions: [{}],
|
|
581
|
+
create(context) {
|
|
582
|
+
const sourceCode = context.sourceCode;
|
|
583
|
+
const functionStack = [];
|
|
584
|
+
const putIntoJSX = (node) => {
|
|
585
|
+
const text = sourceCode.getText(node);
|
|
586
|
+
return node.type === "JSXElement" || node.type === "JSXFragment" ? text : `{${text}}`;
|
|
587
|
+
};
|
|
588
|
+
const currentFunction = () => functionStack[functionStack.length - 1];
|
|
589
|
+
const onFunctionEnter = (node) => {
|
|
590
|
+
let lastReturn;
|
|
591
|
+
if (node.body.type === "BlockStatement") {
|
|
592
|
+
const last = [...node.body.body].reverse().find((statement) => !statement.type.endsWith("Declaration"));
|
|
593
|
+
if (last?.type === "ReturnStatement") lastReturn = last;
|
|
594
|
+
}
|
|
595
|
+
functionStack.push({
|
|
596
|
+
node,
|
|
597
|
+
lastReturn,
|
|
598
|
+
earlyReturns: []
|
|
599
|
+
});
|
|
600
|
+
};
|
|
601
|
+
const onFunctionExit = () => {
|
|
602
|
+
const frame = functionStack.pop();
|
|
603
|
+
if (frame && isComponent$1(frame.node, context)) reportComponent(frame);
|
|
604
|
+
};
|
|
605
|
+
const reportComponent = (frame) => {
|
|
606
|
+
const isReactiveGuard = (expression) => containsReactiveRead(expression, frame.node, context);
|
|
607
|
+
for (const earlyReturn of frame.earlyReturns) {
|
|
608
|
+
if (!collectGuardExpressions(earlyReturn, frame.node).some(isReactiveGuard)) continue;
|
|
609
|
+
context.report({
|
|
610
|
+
node: earlyReturn,
|
|
611
|
+
messageId: "noEarlyReturn"
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
const argument = frame.lastReturn?.argument;
|
|
615
|
+
if ((argument?.type === "ConditionalExpression" || argument?.type === "LogicalExpression") && !conditionalDecisionExpressions(argument).some(isReactiveGuard)) return;
|
|
616
|
+
if (argument?.type === "ConditionalExpression") context.report({
|
|
617
|
+
node: argument.parent ?? argument,
|
|
618
|
+
messageId: "noConditionalReturn",
|
|
619
|
+
fix: (fixer) => {
|
|
620
|
+
const conditions = [{
|
|
621
|
+
test: argument.test,
|
|
622
|
+
consequent: argument.consequent
|
|
623
|
+
}];
|
|
624
|
+
let fallback = argument.alternate;
|
|
625
|
+
while (fallback.type === "ConditionalExpression") {
|
|
626
|
+
conditions.push({
|
|
627
|
+
test: fallback.test,
|
|
628
|
+
consequent: fallback.consequent
|
|
629
|
+
});
|
|
630
|
+
fallback = fallback.alternate;
|
|
631
|
+
}
|
|
632
|
+
const withImports = (names, replacement) => {
|
|
633
|
+
const importFixes = getSolidImportFixes(context, fixer, names);
|
|
634
|
+
return importFixes == null ? null : [...importFixes, replacement];
|
|
635
|
+
};
|
|
636
|
+
if (conditions.length >= 2) {
|
|
637
|
+
const fallbackText = !isNothing(fallback) ? ` fallback={${sourceCode.getText(fallback)}}` : "";
|
|
638
|
+
return withImports(["Switch", "Match"], fixer.replaceText(argument, `<Switch${fallbackText}>\n${conditions.map(({ test, consequent }) => `<Match when={${sourceCode.getText(test)}}>${putIntoJSX(consequent)}</Match>`).join("\n")}\n</Switch>`));
|
|
639
|
+
}
|
|
640
|
+
if (isNothing(argument.consequent)) return withImports(["Show"], fixer.replaceText(argument, `<Show when={!(${sourceCode.getText(argument.test)})}>${putIntoJSX(argument.alternate)}</Show>`));
|
|
641
|
+
if (isNothing(fallback) || getLineLength(argument.consequent.loc) >= getLineLength(fallback.loc) * 1.5) {
|
|
642
|
+
const fallbackText = !isNothing(fallback) ? ` fallback={${sourceCode.getText(fallback)}}` : "";
|
|
643
|
+
return withImports(["Show"], fixer.replaceText(argument, `<Show when={${sourceCode.getText(argument.test)}}${fallbackText}>${putIntoJSX(argument.consequent)}</Show>`));
|
|
644
|
+
}
|
|
645
|
+
return fixer.replaceText(argument, `<>${putIntoJSX(argument)}</>`);
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
else if (argument?.type === "LogicalExpression") context.report({
|
|
649
|
+
node: argument,
|
|
650
|
+
messageId: "noConditionalReturn"
|
|
651
|
+
});
|
|
652
|
+
};
|
|
653
|
+
return {
|
|
654
|
+
FunctionDeclaration: onFunctionEnter,
|
|
655
|
+
FunctionExpression: onFunctionEnter,
|
|
656
|
+
ArrowFunctionExpression: onFunctionEnter,
|
|
657
|
+
"FunctionDeclaration:exit": onFunctionExit,
|
|
658
|
+
"FunctionExpression:exit": onFunctionExit,
|
|
659
|
+
"ArrowFunctionExpression:exit": onFunctionExit,
|
|
660
|
+
ReturnStatement(node) {
|
|
661
|
+
if (functionStack.length > 0 && node !== currentFunction().lastReturn) currentFunction().earlyReturns.push(node);
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
//#endregion
|
|
667
|
+
//#region src/rules/jsx-no-duplicate-props.ts
|
|
668
|
+
function hasMeaningfulChildren(element) {
|
|
669
|
+
return element.children.some((child) => {
|
|
670
|
+
if (child.type === "JSXText") return child.value.trim().length > 0;
|
|
671
|
+
if (child.type === "JSXExpressionContainer") return child.expression.type !== "JSXEmptyExpression";
|
|
672
|
+
return true;
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
var jsx_no_duplicate_props_default = createRule({
|
|
676
|
+
name: "jsx-no-duplicate-props",
|
|
677
|
+
meta: {
|
|
678
|
+
type: "problem",
|
|
679
|
+
docs: { description: "Disallow competing JSX content sources such as children, innerHTML, and textContent." },
|
|
680
|
+
schema: [],
|
|
681
|
+
messages: { noDuplicateChildren: "Using {{used}} at the same time is not allowed." }
|
|
682
|
+
},
|
|
683
|
+
defaultOptions: [],
|
|
684
|
+
create(context) {
|
|
685
|
+
return { JSXOpeningElement(node) {
|
|
686
|
+
const props = /* @__PURE__ */ new Set();
|
|
687
|
+
for (const [name] of jsxGetAllProps(node.attributes)) props.add(name.toLowerCase());
|
|
688
|
+
const hasChildrenProp = props.has("children");
|
|
689
|
+
const element = node.parent;
|
|
690
|
+
const hasChildren = hasMeaningfulChildren(element);
|
|
691
|
+
const isHost = isHostElement(node);
|
|
692
|
+
const hasInnerHTML = isHost && props.has("innerhtml");
|
|
693
|
+
const hasTextContent = isHost && props.has("textcontent");
|
|
694
|
+
const used = [
|
|
695
|
+
hasChildrenProp && "`props.children`",
|
|
696
|
+
hasChildren && "JSX children",
|
|
697
|
+
hasInnerHTML && "`props.innerHTML`",
|
|
698
|
+
hasTextContent && "`props.textContent`"
|
|
699
|
+
].filter(Boolean);
|
|
700
|
+
if (used.length > 1) context.report({
|
|
701
|
+
node,
|
|
702
|
+
messageId: "noDuplicateChildren",
|
|
703
|
+
data: { used: used.join(", ") }
|
|
704
|
+
});
|
|
705
|
+
} };
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
//#endregion
|
|
709
|
+
//#region src/rules/no-destructure.ts
|
|
710
|
+
const getName = (node) => {
|
|
711
|
+
switch (node.type) {
|
|
712
|
+
case "Literal": return typeof node.value === "string" ? node.value : null;
|
|
713
|
+
case "Identifier": return node.name;
|
|
714
|
+
case "AssignmentPattern": return getName(node.left);
|
|
715
|
+
default: return ASTUtils.getStringIfConstant(node);
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
function getAvailableName(sourceCode, preferred) {
|
|
719
|
+
if (!isNameTaken(sourceCode, preferred)) return preferred;
|
|
720
|
+
let index = 2;
|
|
721
|
+
while (isNameTaken(sourceCode, `${preferred}${index}`)) index += 1;
|
|
722
|
+
return `${preferred}${index}`;
|
|
723
|
+
}
|
|
724
|
+
const getPropertyInfo = (property) => {
|
|
725
|
+
const variableName = getName(property.value);
|
|
726
|
+
if (variableName === null) return null;
|
|
727
|
+
return {
|
|
728
|
+
init: property.value.type === "AssignmentPattern" ? property.value.right : void 0,
|
|
729
|
+
computed: property.computed,
|
|
730
|
+
real: property.key,
|
|
731
|
+
variableName
|
|
732
|
+
};
|
|
733
|
+
};
|
|
734
|
+
var no_destructure_default = createRule({
|
|
735
|
+
name: "no-destructure",
|
|
736
|
+
meta: {
|
|
737
|
+
type: "problem",
|
|
738
|
+
docs: { description: "Disallow destructuring component props. In Solid 2, destructuring props triggers top-level untracked reads." },
|
|
739
|
+
fixable: "code",
|
|
740
|
+
schema: [{
|
|
741
|
+
type: "object",
|
|
742
|
+
properties: { typescriptEnabled: { type: "boolean" } },
|
|
743
|
+
additionalProperties: false
|
|
744
|
+
}],
|
|
745
|
+
messages: { noDestructure: "Destructuring component props breaks Solid 2 reactivity; keep the `props` object and read properties from it." }
|
|
746
|
+
},
|
|
747
|
+
defaultOptions: [{}],
|
|
748
|
+
create(context) {
|
|
749
|
+
const sourceCode = context.sourceCode;
|
|
750
|
+
const onFunctionExit = (node) => {
|
|
751
|
+
const props = node.params[0];
|
|
752
|
+
if (node.params.length === 1 && props?.type === "ObjectPattern" && node.parent?.type !== "JSXExpressionContainer" && isComponent$1(node, context)) context.report({
|
|
753
|
+
node: props,
|
|
754
|
+
messageId: "noDestructure",
|
|
755
|
+
fix: (fixer) => fixDestructure(node, props, fixer)
|
|
756
|
+
});
|
|
757
|
+
};
|
|
758
|
+
function* fixDestructure(func, props, fixer) {
|
|
759
|
+
const sourceCode = context.sourceCode;
|
|
760
|
+
const importNode = sourceCode.ast.body.find((node) => node.type === "ImportDeclaration" && node.importKind !== "type" && node.source.type === "Literal" && node.source.value === "solid-js");
|
|
761
|
+
const properties = props.properties;
|
|
762
|
+
const propEntries = [];
|
|
763
|
+
let rest = null;
|
|
764
|
+
for (const property of properties) {
|
|
765
|
+
if (property.type === "RestElement") {
|
|
766
|
+
rest = property;
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
const info = getPropertyInfo(property);
|
|
770
|
+
if (info) propEntries.push(info);
|
|
771
|
+
}
|
|
772
|
+
const ordinaryPropertyCount = properties.filter((property) => property.type === "Property").length;
|
|
773
|
+
if (propEntries.length !== ordinaryPropertyCount || rest != null && rest.argument.type !== "Identifier") return;
|
|
774
|
+
const hasDefaults = propEntries.some((entry) => entry.init);
|
|
775
|
+
const propsName = getAvailableName(sourceCode, "props");
|
|
776
|
+
const originalPropsName = hasDefaults ? getAvailableName(sourceCode, "_props") : propsName;
|
|
777
|
+
const helperNames = /* @__PURE__ */ new Map();
|
|
778
|
+
if (importNode) for (const specifier of importNode.specifiers) {
|
|
779
|
+
if (specifier.type !== "ImportSpecifier") continue;
|
|
780
|
+
const importedName = specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value;
|
|
781
|
+
if (importedName === "merge" || importedName === "omit") helperNames.set(importedName, specifier.local.name);
|
|
782
|
+
}
|
|
783
|
+
const resolveHelper = (importedName) => {
|
|
784
|
+
const existing = helperNames.get(importedName);
|
|
785
|
+
if (existing) return existing;
|
|
786
|
+
return isNameTaken(sourceCode, importedName) ? null : importedName;
|
|
787
|
+
};
|
|
788
|
+
const mergeName = hasDefaults ? resolveHelper("merge") : null;
|
|
789
|
+
const omitName = rest ? resolveHelper("omit") : null;
|
|
790
|
+
const defaultPairs = propEntries.filter((entry) => entry.init).map((entry) => {
|
|
791
|
+
return `${entry.computed ? `[${sourceCode.getText(entry.real)}]` : sourceCode.getText(entry.real)}: ${sourceCode.getText(entry.init)}`;
|
|
792
|
+
});
|
|
793
|
+
const omittedKeys = propEntries.map((entry) => entry.real.type === "Identifier" ? JSON.stringify(entry.real.name) : sourceCode.getText(entry.real));
|
|
794
|
+
const setupLines = [];
|
|
795
|
+
if (hasDefaults && mergeName == null || rest && omitName == null) return;
|
|
796
|
+
if (hasDefaults) setupLines.push(`const ${propsName} = ${mergeName}({ ${defaultPairs.join(", ")} }, ${originalPropsName});`);
|
|
797
|
+
if (rest) {
|
|
798
|
+
const restName = rest.argument.type === "Identifier" ? rest.argument.name : "rest";
|
|
799
|
+
const omitArgs = omittedKeys.length > 0 ? `, ${omittedKeys.join(", ")}` : "";
|
|
800
|
+
setupLines.push(`const ${restName} = ${omitName}(${propsName}${omitArgs});`);
|
|
801
|
+
}
|
|
802
|
+
if (setupLines.length > 0 && func.body.type !== "BlockStatement") return;
|
|
803
|
+
const missingHelpers = [];
|
|
804
|
+
if (hasDefaults && !helperNames.has("merge")) missingHelpers.push("merge");
|
|
805
|
+
if (rest && !helperNames.has("omit")) missingHelpers.push("omit");
|
|
806
|
+
if (missingHelpers.length > 0) {
|
|
807
|
+
const importFixes = getSolidImportFixes(context, fixer, missingHelpers);
|
|
808
|
+
if (importFixes == null) return;
|
|
809
|
+
yield* importFixes;
|
|
810
|
+
}
|
|
811
|
+
if (props.typeAnnotation) yield fixer.replaceTextRange([props.range[0], props.typeAnnotation.range[0]], originalPropsName);
|
|
812
|
+
else yield fixer.replaceText(props, originalPropsName);
|
|
813
|
+
if (setupLines.length > 0) {
|
|
814
|
+
if (func.body.type === "BlockStatement") {
|
|
815
|
+
const indent = " ".repeat(func.body.body[0]?.loc?.start.column ?? 2);
|
|
816
|
+
if (func.body.body.length > 0) yield fixer.insertTextBefore(func.body.body[0], `${setupLines.join(`\n${indent}`)}\n${indent}`);
|
|
817
|
+
else yield fixer.insertTextAfterRange([func.body.range[0], func.body.range[0] + 1], `\n${indent}${setupLines.join(`\n${indent}`)}\n`);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
const scope = sourceCode.scopeManager?.acquire(func);
|
|
821
|
+
if (!scope) return;
|
|
822
|
+
for (const entry of propEntries) {
|
|
823
|
+
const variable = scope.set.get(entry.variableName);
|
|
824
|
+
if (!variable) continue;
|
|
825
|
+
const access = entry.real.type === "Identifier" && !entry.computed ? `.${entry.real.name}` : `[${sourceCode.getText(entry.real)}]`;
|
|
826
|
+
for (const reference of variable.references) if (reference.isReadOnly()) yield fixer.replaceText(reference.identifier, `${propsName}${access}`);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return {
|
|
830
|
+
"FunctionDeclaration:exit": onFunctionExit,
|
|
831
|
+
"FunctionExpression:exit": onFunctionExit,
|
|
832
|
+
"ArrowFunctionExpression:exit": onFunctionExit,
|
|
833
|
+
VariableDeclarator(node) {
|
|
834
|
+
if (node.id.type !== "ObjectPattern" || node.init?.type !== "Identifier") return;
|
|
835
|
+
const def = ASTUtils.findVariable(sourceCode.getScope(node.init), node.init)?.defs[0];
|
|
836
|
+
if (def?.type !== "Parameter" || !isFunctionNode(def.node)) return;
|
|
837
|
+
const firstParam = def.node.params[0];
|
|
838
|
+
if ((firstParam?.type === "AssignmentPattern" ? firstParam.left : firstParam) === def.name && isComponent$1(def.node, context)) context.report({
|
|
839
|
+
node: node.id,
|
|
840
|
+
messageId: "noDestructure"
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
//#endregion
|
|
847
|
+
//#region src/rules/no-leaf-owner-operations.ts
|
|
848
|
+
const TRACKED_EFFECT_NAMES = new Set(["createTrackedEffect"]);
|
|
849
|
+
const ON_SETTLED_NAMES = new Set(["onSettled"]);
|
|
850
|
+
const OWNER_CALLBACK_NAMES = new Set([
|
|
851
|
+
"createEffect",
|
|
852
|
+
"createMemo",
|
|
853
|
+
"createRenderEffect",
|
|
854
|
+
"createRoot"
|
|
855
|
+
]);
|
|
856
|
+
const ON_CLEANUP_NAMES = new Set(["onCleanup"]);
|
|
857
|
+
const FLUSH_NAMES = new Set(["flush"]);
|
|
858
|
+
const PRIMITIVE_NAMES = new Set([
|
|
859
|
+
"createEffect",
|
|
860
|
+
"createErrorBoundary",
|
|
861
|
+
"createLoadingBoundary",
|
|
862
|
+
"createMemo",
|
|
863
|
+
"createOwner",
|
|
864
|
+
"createProjection",
|
|
865
|
+
"createRenderEffect",
|
|
866
|
+
"createRevealOrder",
|
|
867
|
+
"createRoot",
|
|
868
|
+
"createTrackedEffect",
|
|
869
|
+
"mapArray",
|
|
870
|
+
"repeat"
|
|
871
|
+
]);
|
|
872
|
+
const FUNCTION_FORM_PRIMITIVE_NAMES = new Set([
|
|
873
|
+
"createOptimistic",
|
|
874
|
+
"createOptimisticStore",
|
|
875
|
+
"createSignal",
|
|
876
|
+
"createStore"
|
|
877
|
+
]);
|
|
878
|
+
function hasProvablyFunctionFirstArgument(node, context) {
|
|
879
|
+
const first = node.arguments[0];
|
|
880
|
+
if (first == null || first.type === "SpreadElement") return false;
|
|
881
|
+
return isFunctionNode(first) || isFunctionNode(trace(first, context));
|
|
882
|
+
}
|
|
883
|
+
var no_leaf_owner_operations_default = createRule({
|
|
884
|
+
name: "no-leaf-owner-operations",
|
|
885
|
+
meta: {
|
|
886
|
+
type: "problem",
|
|
887
|
+
docs: { description: "Disallow onCleanup, reactive-primitive creation, and flush() inside the leaf owners createTrackedEffect and onSettled." },
|
|
888
|
+
schema: [],
|
|
889
|
+
messages: {
|
|
890
|
+
noCleanup: "Cannot use `onCleanup` inside `createTrackedEffect` or `onSettled`; return a cleanup function instead.",
|
|
891
|
+
noFlush: "Cannot call `flush()` from inside `createTrackedEffect` or `onSettled`; schedule work outside instead.",
|
|
892
|
+
noPrimitives: "Cannot create reactive primitives inside `createTrackedEffect` or `onSettled`; move them to the component body or another owner."
|
|
893
|
+
}
|
|
894
|
+
},
|
|
895
|
+
defaultOptions: [],
|
|
896
|
+
create(context) {
|
|
897
|
+
const forbiddenStack = [];
|
|
898
|
+
const onFunctionEnter = (node) => {
|
|
899
|
+
if (node.type !== "FunctionDeclaration" && node.type !== "FunctionExpression" && node.type !== "ArrowFunctionExpression") return;
|
|
900
|
+
const trackedEffect = isSolidApiCallbackArgument(node, 0, context, TRACKED_EFFECT_NAMES);
|
|
901
|
+
let ownerBackedSettled = false;
|
|
902
|
+
if (isSolidApiCallbackArgument(node, 0, context, ON_SETTLED_NAMES)) {
|
|
903
|
+
const settledCall = node.parent;
|
|
904
|
+
const enclosing = getNearestFunctionAncestor(settledCall);
|
|
905
|
+
ownerBackedSettled = enclosing != null && (isComponent$1(enclosing, context) || isSolidApiCallbackArgument(enclosing, 0, context, OWNER_CALLBACK_NAMES) || isSolidApiCallbackArgument(enclosing, 1, context, OWNER_CALLBACK_NAMES));
|
|
906
|
+
}
|
|
907
|
+
if (trackedEffect || ownerBackedSettled) forbiddenStack.push(node);
|
|
908
|
+
};
|
|
909
|
+
const onFunctionExit = (node) => {
|
|
910
|
+
if (forbiddenStack[forbiddenStack.length - 1] === node) forbiddenStack.pop();
|
|
911
|
+
};
|
|
912
|
+
return {
|
|
913
|
+
FunctionDeclaration: onFunctionEnter,
|
|
914
|
+
FunctionExpression: onFunctionEnter,
|
|
915
|
+
ArrowFunctionExpression: onFunctionEnter,
|
|
916
|
+
"FunctionDeclaration:exit": onFunctionExit,
|
|
917
|
+
"FunctionExpression:exit": onFunctionExit,
|
|
918
|
+
"ArrowFunctionExpression:exit": onFunctionExit,
|
|
919
|
+
CallExpression(node) {
|
|
920
|
+
const currentForbidden = forbiddenStack[forbiddenStack.length - 1];
|
|
921
|
+
if (!currentForbidden || getNearestFunctionAncestor(node) !== currentForbidden || node.callee.type !== "Identifier") return;
|
|
922
|
+
const callee = node.callee;
|
|
923
|
+
if (bindsToSolid(callee, context, ON_CLEANUP_NAMES)) context.report({
|
|
924
|
+
node: callee,
|
|
925
|
+
messageId: "noCleanup"
|
|
926
|
+
});
|
|
927
|
+
else if (bindsToSolid(callee, context, FLUSH_NAMES)) context.report({
|
|
928
|
+
node: callee,
|
|
929
|
+
messageId: "noFlush"
|
|
930
|
+
});
|
|
931
|
+
else if (bindsToSolid(callee, context, PRIMITIVE_NAMES)) context.report({
|
|
932
|
+
node: callee,
|
|
933
|
+
messageId: "noPrimitives"
|
|
934
|
+
});
|
|
935
|
+
else if (bindsToSolid(callee, context, FUNCTION_FORM_PRIMITIVE_NAMES) && hasProvablyFunctionFirstArgument(node, context)) context.report({
|
|
936
|
+
node: callee,
|
|
937
|
+
messageId: "noPrimitives"
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
//#endregion
|
|
944
|
+
//#region src/rules/no-owned-scope-writes.ts
|
|
945
|
+
const SETTER_FACTORIES = new Set([
|
|
946
|
+
"createOptimistic",
|
|
947
|
+
"createOptimisticStore",
|
|
948
|
+
"createSignal",
|
|
949
|
+
"createStore"
|
|
950
|
+
]);
|
|
951
|
+
const OWNED_WRITE_FACTORIES = new Set(["createSignal", "createOptimistic"]);
|
|
952
|
+
const EFFECT_FACTORIES = new Set(["createEffect", "createRenderEffect"]);
|
|
953
|
+
const COMPUTE_FACTORIES$1 = new Set([
|
|
954
|
+
"createEffect",
|
|
955
|
+
"createMemo",
|
|
956
|
+
"createRenderEffect"
|
|
957
|
+
]);
|
|
958
|
+
const ACTION_FACTORIES = new Set(["action"]);
|
|
959
|
+
function getPropertyName$1(node) {
|
|
960
|
+
if (!node.computed && node.key.type === "Identifier") return node.key.name;
|
|
961
|
+
if (node.key.type === "Literal" && typeof node.key.value === "string") return node.key.value;
|
|
962
|
+
return null;
|
|
963
|
+
}
|
|
964
|
+
function hasOwnedWriteOption(node) {
|
|
965
|
+
const options = node.arguments[1];
|
|
966
|
+
if (options?.type !== "ObjectExpression") return false;
|
|
967
|
+
return options.properties.some((property) => property.type === "Property" && getPropertyName$1(property) === "ownedWrite" && property.value.type === "Literal" && property.value.value === true);
|
|
968
|
+
}
|
|
969
|
+
function isOwnedScopeFunction(node, context) {
|
|
970
|
+
if (isComponent$1(node, context)) return true;
|
|
971
|
+
if (node.parent?.type !== "CallExpression" || node.parent.arguments[0] !== node || node.parent.callee.type !== "Identifier") return false;
|
|
972
|
+
const callee = node.parent.callee;
|
|
973
|
+
if (!bindsToSolid(callee, context, COMPUTE_FACTORIES$1)) return false;
|
|
974
|
+
if (bindsToSolid(callee, context, EFFECT_FACTORIES)) return node.parent.arguments.length >= 2;
|
|
975
|
+
return true;
|
|
976
|
+
}
|
|
977
|
+
var no_owned_scope_writes_default = createRule({
|
|
978
|
+
name: "no-owned-scope-writes",
|
|
979
|
+
meta: {
|
|
980
|
+
type: "problem",
|
|
981
|
+
docs: { description: "Disallow signal/store writes inside component bodies and reactive compute scopes in Solid 2." },
|
|
982
|
+
schema: [{
|
|
983
|
+
type: "object",
|
|
984
|
+
properties: { typescriptEnabled: { type: "boolean" } },
|
|
985
|
+
additionalProperties: false
|
|
986
|
+
}],
|
|
987
|
+
messages: {
|
|
988
|
+
noActionInOwnedScope: "Calling an action inside a component or reactive compute scope is not allowed in Solid 2. Call it from an event handler or another imperative scope.",
|
|
989
|
+
noOwnedScopeWrite: "Writing to state inside a component or reactive compute scope is not allowed in Solid 2. Derive values instead, move the write to an event handler or apply phase, or use `ownedWrite: true` for internal `createSignal` state."
|
|
990
|
+
}
|
|
991
|
+
},
|
|
992
|
+
defaultOptions: [{}],
|
|
993
|
+
create(context) {
|
|
994
|
+
const setterVariables = /* @__PURE__ */ new Map();
|
|
995
|
+
const actionVariables = /* @__PURE__ */ new Set();
|
|
996
|
+
const functionStack = [];
|
|
997
|
+
const pendingCalls = [];
|
|
998
|
+
const pendingDirectActions = [];
|
|
999
|
+
const sourceCode = context.sourceCode;
|
|
1000
|
+
const onFunctionEnter = (node) => {
|
|
1001
|
+
functionStack.push(node);
|
|
1002
|
+
};
|
|
1003
|
+
const onFunctionExit = () => {
|
|
1004
|
+
functionStack.pop();
|
|
1005
|
+
};
|
|
1006
|
+
return {
|
|
1007
|
+
VariableDeclarator(node) {
|
|
1008
|
+
if (node.id.type === "Identifier" && node.init?.type === "CallExpression" && resolveSolidCallee(node.init.callee, context, ACTION_FACTORIES) != null) {
|
|
1009
|
+
const actionId = node.id;
|
|
1010
|
+
const variable = sourceCode.scopeManager?.getDeclaredVariables(node).find((declared) => declared.name === actionId.name);
|
|
1011
|
+
if (variable) actionVariables.add(variable);
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
if (node.id.type === "Identifier" && node.init?.type === "Identifier") {
|
|
1015
|
+
const source = ASTUtils.findVariable(sourceCode.getScope(node.init), node.init);
|
|
1016
|
+
if (source && actionVariables.has(source)) {
|
|
1017
|
+
const aliasId = node.id;
|
|
1018
|
+
const variable = sourceCode.scopeManager?.getDeclaredVariables(node).find((declared) => declared.name === aliasId.name);
|
|
1019
|
+
if (variable) actionVariables.add(variable);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
if (node.id.type !== "ArrayPattern" || node.init?.type !== "CallExpression" || node.init.callee.type !== "Identifier") return;
|
|
1023
|
+
const canonical = resolveSolidCallee(node.init.callee, context, SETTER_FACTORIES);
|
|
1024
|
+
if (canonical == null) return;
|
|
1025
|
+
const setterElement = node.id.elements[1];
|
|
1026
|
+
if (setterElement?.type !== "Identifier") return;
|
|
1027
|
+
const variable = sourceCode.scopeManager?.getDeclaredVariables(node).find((declared) => declared.name === setterElement.name);
|
|
1028
|
+
if (!variable) return;
|
|
1029
|
+
setterVariables.set(variable, { allowOwnedWrite: OWNED_WRITE_FACTORIES.has(canonical) && hasOwnedWriteOption(node.init) });
|
|
1030
|
+
},
|
|
1031
|
+
FunctionDeclaration: onFunctionEnter,
|
|
1032
|
+
FunctionExpression: onFunctionEnter,
|
|
1033
|
+
ArrowFunctionExpression: onFunctionEnter,
|
|
1034
|
+
"FunctionDeclaration:exit": onFunctionExit,
|
|
1035
|
+
"FunctionExpression:exit": onFunctionExit,
|
|
1036
|
+
"ArrowFunctionExpression:exit": onFunctionExit,
|
|
1037
|
+
CallExpression(node) {
|
|
1038
|
+
const enclosingFn = functionStack[functionStack.length - 1];
|
|
1039
|
+
if (!enclosingFn) return;
|
|
1040
|
+
if (node.callee.type === "CallExpression" && resolveSolidCallee(node.callee.callee, context, ACTION_FACTORIES) != null) {
|
|
1041
|
+
pendingDirectActions.push({
|
|
1042
|
+
call: node,
|
|
1043
|
+
enclosingFn
|
|
1044
|
+
});
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
if (node.callee.type !== "Identifier") return;
|
|
1048
|
+
pendingCalls.push({
|
|
1049
|
+
callee: node.callee,
|
|
1050
|
+
enclosingFn
|
|
1051
|
+
});
|
|
1052
|
+
},
|
|
1053
|
+
"Program:exit"() {
|
|
1054
|
+
const ownedScopeCache = /* @__PURE__ */ new Map();
|
|
1055
|
+
const isOwnedScope = (fn) => {
|
|
1056
|
+
let verdict = ownedScopeCache.get(fn);
|
|
1057
|
+
if (verdict === void 0) {
|
|
1058
|
+
verdict = isOwnedScopeFunction(fn, context);
|
|
1059
|
+
ownedScopeCache.set(fn, verdict);
|
|
1060
|
+
}
|
|
1061
|
+
return verdict;
|
|
1062
|
+
};
|
|
1063
|
+
for (const { callee, enclosingFn } of pendingCalls) {
|
|
1064
|
+
const variable = ASTUtils.findVariable(sourceCode.getScope(callee), callee);
|
|
1065
|
+
if (variable && actionVariables.has(variable) && isOwnedScope(enclosingFn)) {
|
|
1066
|
+
context.report({
|
|
1067
|
+
node: callee,
|
|
1068
|
+
messageId: "noActionInOwnedScope"
|
|
1069
|
+
});
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
const setter = variable && setterVariables.get(variable);
|
|
1073
|
+
if (!setter || setter.allowOwnedWrite) continue;
|
|
1074
|
+
if (isOwnedScope(enclosingFn)) context.report({
|
|
1075
|
+
node: callee,
|
|
1076
|
+
messageId: "noOwnedScopeWrite"
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
for (const { call, enclosingFn } of pendingDirectActions) if (isOwnedScope(enclosingFn)) context.report({
|
|
1080
|
+
node: call,
|
|
1081
|
+
messageId: "noActionInOwnedScope"
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
//#endregion
|
|
1088
|
+
//#region src/rules/no-reactive-read-after-await.ts
|
|
1089
|
+
const COMPUTE_FACTORIES = new Set([
|
|
1090
|
+
"createMemo",
|
|
1091
|
+
"createEffect",
|
|
1092
|
+
"createRenderEffect",
|
|
1093
|
+
"createProjection"
|
|
1094
|
+
]);
|
|
1095
|
+
const ACCESSOR_FACTORIES$1 = new Set(["createMemo"]);
|
|
1096
|
+
const PAIR_ACCESSOR_FACTORIES$1 = new Set(["createSignal", "createOptimistic"]);
|
|
1097
|
+
const SOLID_ACCESSOR_TYPE_NAMES = new Set(["Accessor", "SourceAccessor"]);
|
|
1098
|
+
function hasGuaranteedAwait(node) {
|
|
1099
|
+
if (node == null) return false;
|
|
1100
|
+
switch (node.type) {
|
|
1101
|
+
case "AwaitExpression": return true;
|
|
1102
|
+
case "SequenceExpression": return node.expressions.some(hasGuaranteedAwait);
|
|
1103
|
+
case "ArrayExpression": return node.elements.some(hasGuaranteedAwait);
|
|
1104
|
+
case "ObjectExpression": return node.properties.some((property) => {
|
|
1105
|
+
if (property.type === "SpreadElement") return hasGuaranteedAwait(property.argument);
|
|
1106
|
+
return property.computed && hasGuaranteedAwait(property.key) || hasGuaranteedAwait(property.value);
|
|
1107
|
+
});
|
|
1108
|
+
case "TemplateLiteral": return node.expressions.some(hasGuaranteedAwait);
|
|
1109
|
+
case "TaggedTemplateExpression": return hasGuaranteedAwait(node.tag) || hasGuaranteedAwait(node.quasi);
|
|
1110
|
+
case "UnaryExpression": return hasGuaranteedAwait(node.argument);
|
|
1111
|
+
case "BinaryExpression": return hasGuaranteedAwait(node.left) || hasGuaranteedAwait(node.right);
|
|
1112
|
+
case "AssignmentExpression": return hasGuaranteedAwait(node.right);
|
|
1113
|
+
case "NewExpression": return hasGuaranteedAwait(node.callee) || node.arguments.some(hasGuaranteedAwait);
|
|
1114
|
+
case "CallExpression":
|
|
1115
|
+
if (node.optional) return false;
|
|
1116
|
+
return hasGuaranteedAwait(node.callee) || node.arguments.some(hasGuaranteedAwait);
|
|
1117
|
+
default: return false;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
function statementGuaranteesAwait(stmt) {
|
|
1121
|
+
switch (stmt.type) {
|
|
1122
|
+
case "ExpressionStatement": return hasGuaranteedAwait(stmt.expression);
|
|
1123
|
+
case "VariableDeclaration": return stmt.declarations.some((declarator) => hasGuaranteedAwait(declarator.init));
|
|
1124
|
+
case "BlockStatement": return stmt.body.some(statementGuaranteesAwait);
|
|
1125
|
+
default: return false;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
function collectTypeParts(type, acc = [], seen = /* @__PURE__ */ new Set()) {
|
|
1129
|
+
if (seen.has(type)) return acc;
|
|
1130
|
+
seen.add(type);
|
|
1131
|
+
acc.push(type);
|
|
1132
|
+
if (type.isUnion() || type.isIntersection()) for (const member of type.types) collectTypeParts(member, acc, seen);
|
|
1133
|
+
return acc;
|
|
1134
|
+
}
|
|
1135
|
+
function symbolIsFromSolid(symbol, checker) {
|
|
1136
|
+
const isSolidOrigin = (text) => text.includes("solid-js") || text.includes("@solidjs") || text.includes("solid-signals");
|
|
1137
|
+
if (isSolidOrigin(checker.getFullyQualifiedName(symbol))) return true;
|
|
1138
|
+
for (const declaration of symbol.getDeclarations() ?? []) if (isSolidOrigin(declaration.getSourceFile().fileName)) return true;
|
|
1139
|
+
return false;
|
|
1140
|
+
}
|
|
1141
|
+
var no_reactive_read_after_await_default = createRule({
|
|
1142
|
+
name: "no-reactive-read-after-await",
|
|
1143
|
+
meta: {
|
|
1144
|
+
type: "problem",
|
|
1145
|
+
docs: { description: "Disallow reading reactive state (signal/memo accessors) after an `await` in an async reactive computation, where it is no longer tracked as a dependency." },
|
|
1146
|
+
schema: [{
|
|
1147
|
+
type: "object",
|
|
1148
|
+
properties: { typescriptEnabled: { type: "boolean" } },
|
|
1149
|
+
additionalProperties: false
|
|
1150
|
+
}],
|
|
1151
|
+
messages: { reactiveReadAfterAwait: "Signal '{{name}}' is read after an `await` in this reactive computation. Reactive tracking ends at the first await, so this read does not register a dependency and the computation won't re-run when '{{name}}' changes. Read it before the await, or wrap it in `untrack()` if that's intentional." }
|
|
1152
|
+
},
|
|
1153
|
+
defaultOptions: [{}],
|
|
1154
|
+
create(context) {
|
|
1155
|
+
const sourceCode = context.sourceCode;
|
|
1156
|
+
const services = context.options[0]?.typescriptEnabled ?? false ? getTypeAwareServices(context) : null;
|
|
1157
|
+
const checker = services?.program.getTypeChecker() ?? null;
|
|
1158
|
+
const accessorVars = /* @__PURE__ */ new Set();
|
|
1159
|
+
const computeCallbackCache = /* @__PURE__ */ new Map();
|
|
1160
|
+
function isAfterAwait(call, fn) {
|
|
1161
|
+
let current = call;
|
|
1162
|
+
while (current !== fn && current.parent != null) {
|
|
1163
|
+
const parent = current.parent;
|
|
1164
|
+
if (parent.type === "BlockStatement" || parent.type === "StaticBlock") {
|
|
1165
|
+
const index = parent.body.indexOf(current);
|
|
1166
|
+
for (let i = 0; i < index; i++) if (statementGuaranteesAwait(parent.body[i])) return true;
|
|
1167
|
+
} else if (parent.type === "SequenceExpression") {
|
|
1168
|
+
const index = parent.expressions.indexOf(current);
|
|
1169
|
+
for (let i = 0; i < index; i++) if (hasGuaranteedAwait(parent.expressions[i])) return true;
|
|
1170
|
+
}
|
|
1171
|
+
current = parent;
|
|
1172
|
+
}
|
|
1173
|
+
return false;
|
|
1174
|
+
}
|
|
1175
|
+
function isTypeAwareComputeCallback(fn) {
|
|
1176
|
+
if (!services || !checker) return false;
|
|
1177
|
+
const parent = fn.parent;
|
|
1178
|
+
if (parent?.type !== "CallExpression" || parent.arguments[0] !== fn) return false;
|
|
1179
|
+
const calleeNode = services.esTreeNodeToTSNodeMap.get(parent.callee);
|
|
1180
|
+
if (!calleeNode) return false;
|
|
1181
|
+
let symbol = checker.getSymbolAtLocation(calleeNode);
|
|
1182
|
+
if (symbol && symbol.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
|
|
1183
|
+
return symbol != null && COMPUTE_FACTORIES.has(symbol.getName()) && symbolIsFromSolid(symbol, checker);
|
|
1184
|
+
}
|
|
1185
|
+
function isComputeCallback(fn) {
|
|
1186
|
+
const cached = computeCallbackCache.get(fn);
|
|
1187
|
+
if (cached !== void 0) return cached;
|
|
1188
|
+
const result = isSolidApiCallbackArgument(fn, 0, context, COMPUTE_FACTORIES) || isTypeAwareComputeCallback(fn);
|
|
1189
|
+
computeCallbackCache.set(fn, result);
|
|
1190
|
+
return result;
|
|
1191
|
+
}
|
|
1192
|
+
function isSolidAccessorCallee(callee) {
|
|
1193
|
+
if (!services || !checker) return false;
|
|
1194
|
+
const calleeNode = services.esTreeNodeToTSNodeMap.get(callee);
|
|
1195
|
+
if (!calleeNode) return false;
|
|
1196
|
+
return collectTypeParts(checker.getTypeAtLocation(calleeNode)).some((part) => {
|
|
1197
|
+
const alias = part.aliasSymbol;
|
|
1198
|
+
return alias != null && SOLID_ACCESSOR_TYPE_NAMES.has(alias.getName()) && symbolIsFromSolid(alias, checker);
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
function accessorReadName(call) {
|
|
1202
|
+
if (call.callee.type === "Identifier") {
|
|
1203
|
+
const variable = ASTUtils.findVariable(sourceCode.getScope(call), call.callee);
|
|
1204
|
+
if (variable && accessorVars.has(variable)) return call.callee.name;
|
|
1205
|
+
}
|
|
1206
|
+
if (services && isSolidAccessorCallee(call.callee)) return sourceCode.getText(call.callee);
|
|
1207
|
+
return null;
|
|
1208
|
+
}
|
|
1209
|
+
return {
|
|
1210
|
+
VariableDeclarator(node) {
|
|
1211
|
+
if (node.id.type === "Identifier" && node.init?.type === "CallExpression" && node.init.callee.type === "Identifier" && bindsToSolid(node.init.callee, context, ACCESSOR_FACTORIES$1)) {
|
|
1212
|
+
const variable = ASTUtils.findVariable(sourceCode.getScope(node.id), node.id);
|
|
1213
|
+
if (variable) accessorVars.add(variable);
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
if (node.id.type === "ArrayPattern" && node.init?.type === "CallExpression" && node.init.callee.type === "Identifier" && bindsToSolid(node.init.callee, context, PAIR_ACCESSOR_FACTORIES$1)) {
|
|
1217
|
+
const first = node.id.elements[0];
|
|
1218
|
+
if (first?.type === "Identifier") {
|
|
1219
|
+
const variable = sourceCode.scopeManager?.getDeclaredVariables(node).find((declared) => declared.name === first.name);
|
|
1220
|
+
if (variable) accessorVars.add(variable);
|
|
1221
|
+
}
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
if (node.id.type === "Identifier" && node.init?.type === "Identifier") {
|
|
1225
|
+
const source = ASTUtils.findVariable(sourceCode.getScope(node.init), node.init);
|
|
1226
|
+
if (source && accessorVars.has(source)) {
|
|
1227
|
+
const target = ASTUtils.findVariable(sourceCode.getScope(node.id), node.id);
|
|
1228
|
+
if (target) accessorVars.add(target);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
},
|
|
1232
|
+
CallExpression(node) {
|
|
1233
|
+
const fn = getNearestFunctionAncestor(node);
|
|
1234
|
+
if (!fn || !fn.async || !isComputeCallback(fn) || !isAfterAwait(node, fn)) return;
|
|
1235
|
+
const name = accessorReadName(node);
|
|
1236
|
+
if (name == null) return;
|
|
1237
|
+
context.report({
|
|
1238
|
+
node,
|
|
1239
|
+
messageId: "reactiveReadAfterAwait",
|
|
1240
|
+
data: { name }
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
});
|
|
1246
|
+
//#endregion
|
|
1247
|
+
//#region src/rules/no-stale-props-alias.ts
|
|
1248
|
+
const UNTRACK_NAMES = new Set(["untrack"]);
|
|
1249
|
+
const PROPS_HELPER_NAMES = new Set(["merge", "omit"]);
|
|
1250
|
+
function isPropsVariableIdentifier(node, propsVariables, sourceCode) {
|
|
1251
|
+
if (node.type !== "Identifier") return false;
|
|
1252
|
+
const variable = ASTUtils.findVariable(sourceCode.getScope(node), node);
|
|
1253
|
+
return variable != null && propsVariables.has(variable);
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Whether the expression, evaluated at the component's top level, performs an *eager read* of the
|
|
1257
|
+
* props object: a member access rooted at a props variable (`props.name`, `alias.user.name`,
|
|
1258
|
+
* `props[key]`) or a spread of it (`{ ...props }` reads every property). A *bare* props reference
|
|
1259
|
+
* is not a read — passing the object around (`merge(defaults, props)`, `omit(props, "class")`,
|
|
1260
|
+
* `const alias = props`) keeps reactivity intact, and treating it as one flagged the canonical
|
|
1261
|
+
* merge/omit patterns. A callee that might read eagerly (`format(props)`) is undecidable and is a
|
|
1262
|
+
* tolerated false negative.
|
|
1263
|
+
*/
|
|
1264
|
+
function expressionContainsPropsRead(node, propsVariables, sourceCode, context) {
|
|
1265
|
+
const stack = [node];
|
|
1266
|
+
while (stack.length > 0) {
|
|
1267
|
+
const current = stack.pop();
|
|
1268
|
+
if (isFunctionNode(current)) continue;
|
|
1269
|
+
if (current.type === "CallExpression" && current.arguments[0] != null) {
|
|
1270
|
+
if (current.callee.type === "Identifier" && bindsToSolid(current.callee, context, UNTRACK_NAMES)) {
|
|
1271
|
+
const untracked = current.arguments[0];
|
|
1272
|
+
for (const child of getChildNodes(current)) if (child !== untracked) stack.push(child);
|
|
1273
|
+
continue;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
if (current.type === "MemberExpression") {
|
|
1277
|
+
let root = current;
|
|
1278
|
+
while (root.type === "MemberExpression") root = root.object;
|
|
1279
|
+
if (isPropsVariableIdentifier(root, propsVariables, sourceCode)) return true;
|
|
1280
|
+
}
|
|
1281
|
+
if (current.type === "SpreadElement" && isPropsVariableIdentifier(current.argument, propsVariables, sourceCode)) return true;
|
|
1282
|
+
stack.push(...getChildNodes(current));
|
|
1283
|
+
}
|
|
1284
|
+
return false;
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* Whether `init` is a `merge(...)`/`omit(...)` call (bound to solid-js) that receives the props
|
|
1288
|
+
* object as a bare argument — the canonical defaults/rest pattern whose result is a reactive
|
|
1289
|
+
* props-like proxy.
|
|
1290
|
+
*/
|
|
1291
|
+
function isPropsHelperAliasInit(init, propsVariables, sourceCode, context) {
|
|
1292
|
+
if (init.type !== "CallExpression" || init.callee.type !== "Identifier" || !bindsToSolid(init.callee, context, PROPS_HELPER_NAMES)) return false;
|
|
1293
|
+
return init.arguments.some((argument) => {
|
|
1294
|
+
if (argument.type !== "Identifier") return false;
|
|
1295
|
+
const variable = ASTUtils.findVariable(sourceCode.getScope(argument), argument);
|
|
1296
|
+
return variable != null && propsVariables.has(variable);
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
function getDeclaredIdentifierVariable(declaration, sourceCode) {
|
|
1300
|
+
if (declaration.id.type !== "Identifier") return null;
|
|
1301
|
+
const id = declaration.id;
|
|
1302
|
+
return sourceCode.scopeManager?.getDeclaredVariables(declaration).find((variable) => variable.name === id.name) ?? null;
|
|
1303
|
+
}
|
|
1304
|
+
function hasWriteAfterInit(variable) {
|
|
1305
|
+
return variable.references.some((reference) => !reference.init && reference.isWrite());
|
|
1306
|
+
}
|
|
1307
|
+
function getAssignmentExpression(statement) {
|
|
1308
|
+
if (statement.type !== "ExpressionStatement") return null;
|
|
1309
|
+
const expression = statement.expression;
|
|
1310
|
+
return expression.type === "AssignmentExpression" && expression.operator === "=" && expression.left.type === "Identifier" ? expression : null;
|
|
1311
|
+
}
|
|
1312
|
+
function getChildNodes(node) {
|
|
1313
|
+
const children = [];
|
|
1314
|
+
for (const key in node) {
|
|
1315
|
+
if (key === "parent" || key === "tokens" || key === "comments") continue;
|
|
1316
|
+
const value = node[key];
|
|
1317
|
+
if (Array.isArray(value)) {
|
|
1318
|
+
for (const item of value) if (item != null && typeof item === "object" && typeof item.type === "string") children.push(item);
|
|
1319
|
+
} else if (value != null && typeof value === "object" && typeof value.type === "string") children.push(value);
|
|
1320
|
+
}
|
|
1321
|
+
return children;
|
|
1322
|
+
}
|
|
1323
|
+
var no_stale_props_alias_default = createRule({
|
|
1324
|
+
name: "no-stale-props-alias",
|
|
1325
|
+
meta: {
|
|
1326
|
+
type: "problem",
|
|
1327
|
+
docs: { description: "Disallow top-level aliases of component props, which read props outside tracking." },
|
|
1328
|
+
schema: [{
|
|
1329
|
+
type: "object",
|
|
1330
|
+
properties: { typescriptEnabled: { type: "boolean" } },
|
|
1331
|
+
additionalProperties: false
|
|
1332
|
+
}],
|
|
1333
|
+
messages: { stalePropsAlias: "`{{name}}` aliases a prop read outside tracking. Read from `props` in JSX or a tracked scope instead." }
|
|
1334
|
+
},
|
|
1335
|
+
defaultOptions: [{}],
|
|
1336
|
+
create(context) {
|
|
1337
|
+
const sourceCode = context.sourceCode;
|
|
1338
|
+
const checkFunction = (node) => {
|
|
1339
|
+
if (node.body.type !== "BlockStatement" || !isComponent$1(node, context)) return;
|
|
1340
|
+
const props = node.params[0];
|
|
1341
|
+
if (node.params.length !== 1 || props?.type !== "Identifier") return;
|
|
1342
|
+
const propsVariable = ASTUtils.findVariable(sourceCode.getScope(props), props);
|
|
1343
|
+
if (propsVariable == null) return;
|
|
1344
|
+
const propsVariables = new Set([propsVariable]);
|
|
1345
|
+
for (const statement of node.body.body) {
|
|
1346
|
+
if (statement.type === "VariableDeclaration") {
|
|
1347
|
+
for (const declaration of statement.declarations) {
|
|
1348
|
+
const aliasId = declaration.id;
|
|
1349
|
+
if (aliasId.type !== "Identifier" || declaration.init == null) continue;
|
|
1350
|
+
const isPropsLikeAlias = isPropsVariableIdentifier(declaration.init, propsVariables, sourceCode) || isPropsHelperAliasInit(declaration.init, propsVariables, sourceCode, context);
|
|
1351
|
+
if (!expressionContainsPropsRead(declaration.init, propsVariables, sourceCode, context)) {
|
|
1352
|
+
if (isPropsLikeAlias) {
|
|
1353
|
+
const declared = getDeclaredIdentifierVariable(declaration, sourceCode);
|
|
1354
|
+
if (declared != null && !hasWriteAfterInit(declared)) propsVariables.add(declared);
|
|
1355
|
+
}
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
const declared = getDeclaredIdentifierVariable(declaration, sourceCode);
|
|
1359
|
+
const isStableAlias = declared != null && !hasWriteAfterInit(declared);
|
|
1360
|
+
if (statement.kind === "const" || isStableAlias) context.report({
|
|
1361
|
+
node: declaration,
|
|
1362
|
+
messageId: "stalePropsAlias",
|
|
1363
|
+
data: { name: aliasId.name }
|
|
1364
|
+
});
|
|
1365
|
+
if (isStableAlias) propsVariables.add(declared);
|
|
1366
|
+
}
|
|
1367
|
+
continue;
|
|
1368
|
+
}
|
|
1369
|
+
const expression = getAssignmentExpression(statement);
|
|
1370
|
+
if (expression == null || expression.left.type !== "Identifier") continue;
|
|
1371
|
+
const assigned = expression.left;
|
|
1372
|
+
if (!expressionContainsPropsRead(expression.right, propsVariables, sourceCode, context)) continue;
|
|
1373
|
+
context.report({
|
|
1374
|
+
node: expression,
|
|
1375
|
+
messageId: "stalePropsAlias",
|
|
1376
|
+
data: { name: assigned.name }
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
return {
|
|
1381
|
+
"FunctionDeclaration:exit": checkFunction,
|
|
1382
|
+
"FunctionExpression:exit": checkFunction,
|
|
1383
|
+
"ArrowFunctionExpression:exit": checkFunction
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
});
|
|
1387
|
+
//#endregion
|
|
1388
|
+
//#region src/rules/no-untracked-read-in-effect-apply.ts
|
|
1389
|
+
const EFFECT_NAMES = new Set(["createEffect", "createRenderEffect"]);
|
|
1390
|
+
const ACCESSOR_FACTORIES = new Set(["createMemo", "createProjection"]);
|
|
1391
|
+
const PAIR_ACCESSOR_FACTORIES = new Set(["createOptimistic", "createSignal"]);
|
|
1392
|
+
const STORE_FACTORIES = new Set(["createOptimisticStore", "createStore"]);
|
|
1393
|
+
const SAFE_HELPERS = new Set(["deep", "snapshot"]);
|
|
1394
|
+
var no_untracked_read_in_effect_apply_default = createRule({
|
|
1395
|
+
name: "no-untracked-read-in-effect-apply",
|
|
1396
|
+
meta: {
|
|
1397
|
+
type: "problem",
|
|
1398
|
+
docs: { description: "Disallow reading reactive state (signal accessors or store proxies) in a createEffect apply callback, which runs untracked." },
|
|
1399
|
+
schema: [],
|
|
1400
|
+
messages: {
|
|
1401
|
+
signalRead: "Signal '{{name}}' is called directly in an effect apply callback. The apply phase runs untracked — read it in the compute phase and use the passed value, or wrap it in `untrack()`.",
|
|
1402
|
+
storeProxyRead: "Effect apply callbacks run untracked in Solid 2. Extract store properties in the compute phase or use `deep()` before reading them here."
|
|
1403
|
+
}
|
|
1404
|
+
},
|
|
1405
|
+
defaultOptions: [],
|
|
1406
|
+
create(context) {
|
|
1407
|
+
const sourceCode = context.sourceCode;
|
|
1408
|
+
const reactiveVars = /* @__PURE__ */ new Map();
|
|
1409
|
+
const applyCallbacks = /* @__PURE__ */ new Set();
|
|
1410
|
+
const pendingAccessorCalls = [];
|
|
1411
|
+
const storeVars = /* @__PURE__ */ new Set();
|
|
1412
|
+
const storeShapes = /* @__PURE__ */ new Map();
|
|
1413
|
+
const getMemberPath = (node) => {
|
|
1414
|
+
const path = [];
|
|
1415
|
+
let current = node;
|
|
1416
|
+
while (current.type === "MemberExpression") {
|
|
1417
|
+
if (current.computed || current.property.type !== "Identifier") return null;
|
|
1418
|
+
path.unshift(current.property.name);
|
|
1419
|
+
current = current.object;
|
|
1420
|
+
}
|
|
1421
|
+
return current.type === "Identifier" ? {
|
|
1422
|
+
root: current,
|
|
1423
|
+
path
|
|
1424
|
+
} : null;
|
|
1425
|
+
};
|
|
1426
|
+
const pathLandsOnProxy = (shape, path) => {
|
|
1427
|
+
if (shape == null) return "unknown";
|
|
1428
|
+
let current = shape;
|
|
1429
|
+
for (const key of path) {
|
|
1430
|
+
if (current.type !== "ObjectExpression") return "unknown";
|
|
1431
|
+
let next;
|
|
1432
|
+
for (const candidate of current.properties) if (candidate.type === "Property" && getPropertyName$2(candidate) === key) {
|
|
1433
|
+
next = candidate.value;
|
|
1434
|
+
break;
|
|
1435
|
+
}
|
|
1436
|
+
if (next == null) return "unknown";
|
|
1437
|
+
current = next;
|
|
1438
|
+
}
|
|
1439
|
+
return current.type === "ObjectExpression" || current.type === "ArrayExpression";
|
|
1440
|
+
};
|
|
1441
|
+
const getInlineFunction = (value) => {
|
|
1442
|
+
if (value == null || value.type === "SpreadElement") return null;
|
|
1443
|
+
if (isFunctionNode(value)) return value;
|
|
1444
|
+
const traced = trace(value, context);
|
|
1445
|
+
return isFunctionNode(traced) ? traced : null;
|
|
1446
|
+
};
|
|
1447
|
+
const getInlineApplyCallback = (value) => {
|
|
1448
|
+
const direct = getInlineFunction(value);
|
|
1449
|
+
if (direct) return direct;
|
|
1450
|
+
if (value?.type !== "ObjectExpression") return null;
|
|
1451
|
+
for (const property of value.properties) {
|
|
1452
|
+
if (property.type !== "Property" || getPropertyName$2(property) !== "effect") continue;
|
|
1453
|
+
const fn = getInlineFunction(property.value);
|
|
1454
|
+
if (fn) return fn;
|
|
1455
|
+
}
|
|
1456
|
+
return null;
|
|
1457
|
+
};
|
|
1458
|
+
const findContainingApplyCallback = (node) => {
|
|
1459
|
+
const nearest = getNearestFunctionAncestor(node);
|
|
1460
|
+
return nearest != null && applyCallbacks.has(nearest) ? nearest : null;
|
|
1461
|
+
};
|
|
1462
|
+
const getFunctionValue = (value) => {
|
|
1463
|
+
if (value == null || value.type === "SpreadElement") return null;
|
|
1464
|
+
if (value.type === "FunctionExpression" || value.type === "ArrowFunctionExpression") return value;
|
|
1465
|
+
const traced = trace(value, context);
|
|
1466
|
+
return traced.type === "FunctionExpression" || traced.type === "ArrowFunctionExpression" ? traced : null;
|
|
1467
|
+
};
|
|
1468
|
+
const getTracedApplyCallback = (value) => {
|
|
1469
|
+
const direct = getFunctionValue(value);
|
|
1470
|
+
if (direct) return direct;
|
|
1471
|
+
if (value?.type !== "ObjectExpression") return null;
|
|
1472
|
+
for (const property of value.properties) {
|
|
1473
|
+
if (property.type !== "Property" || getPropertyName$2(property) !== "effect") continue;
|
|
1474
|
+
const effect = getFunctionValue(property.value);
|
|
1475
|
+
if (effect) return effect;
|
|
1476
|
+
}
|
|
1477
|
+
return null;
|
|
1478
|
+
};
|
|
1479
|
+
const isStoreSourceExpression = (node) => {
|
|
1480
|
+
if (node == null) return false;
|
|
1481
|
+
if (node.type === "CallExpression" && resolveSolidCallee(node.callee, context, SAFE_HELPERS) != null) return false;
|
|
1482
|
+
if (node.type === "Identifier") {
|
|
1483
|
+
const variable = ASTUtils.findVariable(sourceCode.getScope(node), node);
|
|
1484
|
+
return variable != null && storeVars.has(variable);
|
|
1485
|
+
}
|
|
1486
|
+
if (node.type === "MemberExpression") {
|
|
1487
|
+
const resolved = getMemberPath(node);
|
|
1488
|
+
if (resolved == null) return false;
|
|
1489
|
+
const variable = ASTUtils.findVariable(sourceCode.getScope(resolved.root), resolved.root);
|
|
1490
|
+
if (variable == null || !storeVars.has(variable)) return false;
|
|
1491
|
+
return pathLandsOnProxy(storeShapes.get(variable) ?? null, resolved.path) === true;
|
|
1492
|
+
}
|
|
1493
|
+
return false;
|
|
1494
|
+
};
|
|
1495
|
+
const checkStoreProxyInApply = (node) => {
|
|
1496
|
+
if (resolveSolidCallee(node.callee, context, EFFECT_NAMES) == null || node.arguments.length < 2) return;
|
|
1497
|
+
const compute = getFunctionValue(node.arguments[0]);
|
|
1498
|
+
const apply = getTracedApplyCallback(node.arguments[1]);
|
|
1499
|
+
if (!compute || !apply) return;
|
|
1500
|
+
const returned = getReturnedExpressions(compute).filter((value) => value != null);
|
|
1501
|
+
if (returned.length === 0) return;
|
|
1502
|
+
if (!returned.some((value) => isStoreSourceExpression(value)) || apply.params.length === 0) return;
|
|
1503
|
+
const applyParam = apply.params[0];
|
|
1504
|
+
if (applyParam.type !== "Identifier") return;
|
|
1505
|
+
const variable = ASTUtils.findVariable(sourceCode.getScope(applyParam), applyParam);
|
|
1506
|
+
if (!variable) return;
|
|
1507
|
+
for (const reference of variable.references) {
|
|
1508
|
+
const identifier = reference.identifier;
|
|
1509
|
+
if (reference.init) continue;
|
|
1510
|
+
if (getNearestFunctionAncestor(identifier) !== apply) continue;
|
|
1511
|
+
if (identifier.parent?.type === "VariableDeclarator" && identifier.parent.init === identifier && (identifier.parent.id.type === "ObjectPattern" || identifier.parent.id.type === "ArrayPattern")) {
|
|
1512
|
+
context.report({
|
|
1513
|
+
node: identifier.parent,
|
|
1514
|
+
messageId: "storeProxyRead"
|
|
1515
|
+
});
|
|
1516
|
+
break;
|
|
1517
|
+
}
|
|
1518
|
+
if (identifier.parent?.type === "SpreadElement") {
|
|
1519
|
+
context.report({
|
|
1520
|
+
node: identifier.parent,
|
|
1521
|
+
messageId: "storeProxyRead"
|
|
1522
|
+
});
|
|
1523
|
+
break;
|
|
1524
|
+
}
|
|
1525
|
+
if (identifier.parent?.type === "MemberExpression" && identifier.parent.object === identifier) {
|
|
1526
|
+
context.report({
|
|
1527
|
+
node: identifier.parent,
|
|
1528
|
+
messageId: "storeProxyRead"
|
|
1529
|
+
});
|
|
1530
|
+
break;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1534
|
+
return {
|
|
1535
|
+
VariableDeclarator(node) {
|
|
1536
|
+
if (node.id.type === "Identifier" && node.init?.type === "CallExpression" && node.init.callee.type === "Identifier" && bindsToSolid(node.init.callee, context, ACCESSOR_FACTORIES)) {
|
|
1537
|
+
const variable = ASTUtils.findVariable(sourceCode.getScope(node.id), node.id);
|
|
1538
|
+
if (variable) reactiveVars.set(variable, "accessor");
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
if (node.id.type === "ArrayPattern" && node.init?.type === "CallExpression" && node.init.callee.type === "Identifier" && bindsToSolid(node.init.callee, context, PAIR_ACCESSOR_FACTORIES)) {
|
|
1542
|
+
const first = node.id.elements[0];
|
|
1543
|
+
if (first?.type === "Identifier") {
|
|
1544
|
+
const variable = sourceCode.scopeManager?.getDeclaredVariables(node).find((declared) => declared.name === first.name);
|
|
1545
|
+
if (variable) reactiveVars.set(variable, "accessor");
|
|
1546
|
+
}
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
if (node.id.type === "Identifier" && node.init?.type === "Identifier") {
|
|
1550
|
+
const source = ASTUtils.findVariable(sourceCode.getScope(node.init), node.init);
|
|
1551
|
+
if (source && reactiveVars.get(source) === "accessor") {
|
|
1552
|
+
const target = ASTUtils.findVariable(sourceCode.getScope(node.id), node.id);
|
|
1553
|
+
if (target) reactiveVars.set(target, "accessor");
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
if (node.id.type === "ArrayPattern" && node.init?.type === "CallExpression" && resolveSolidCallee(node.init.callee, context, STORE_FACTORIES) != null) {
|
|
1557
|
+
const first = node.id.elements[0];
|
|
1558
|
+
if (first?.type === "Identifier") {
|
|
1559
|
+
const variable = sourceCode.scopeManager?.getDeclaredVariables(node).find((declared) => declared.name === first.name);
|
|
1560
|
+
if (variable) {
|
|
1561
|
+
storeVars.add(variable);
|
|
1562
|
+
const initialValue = node.init.arguments[0];
|
|
1563
|
+
storeShapes.set(variable, initialValue?.type === "ObjectExpression" ? initialValue : null);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
},
|
|
1568
|
+
CallExpression(node) {
|
|
1569
|
+
if (node.callee.type === "Identifier" && bindsToSolid(node.callee, context, EFFECT_NAMES) && node.arguments.length >= 2) {
|
|
1570
|
+
const apply = getInlineApplyCallback(node.arguments[1]);
|
|
1571
|
+
if (apply) applyCallbacks.add(apply);
|
|
1572
|
+
}
|
|
1573
|
+
checkStoreProxyInApply(node);
|
|
1574
|
+
if (node.callee.type === "Identifier") pendingAccessorCalls.push(node);
|
|
1575
|
+
},
|
|
1576
|
+
"Program:exit"() {
|
|
1577
|
+
for (const node of pendingAccessorCalls) {
|
|
1578
|
+
const callee = node.callee;
|
|
1579
|
+
if (callee.type !== "Identifier") continue;
|
|
1580
|
+
const variable = ASTUtils.findVariable(sourceCode.getScope(node), callee);
|
|
1581
|
+
if (variable && reactiveVars.get(variable) === "accessor" && findContainingApplyCallback(node) != null) context.report({
|
|
1582
|
+
node,
|
|
1583
|
+
messageId: "signalRead",
|
|
1584
|
+
data: { name: callee.name }
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
});
|
|
1591
|
+
//#endregion
|
|
1592
|
+
//#region src/rules/prefer-for.ts
|
|
1593
|
+
const PURE_ARRAY_TRANSFORMS = new Set([
|
|
1594
|
+
"filter",
|
|
1595
|
+
"slice",
|
|
1596
|
+
"concat",
|
|
1597
|
+
"map",
|
|
1598
|
+
"flat",
|
|
1599
|
+
"flatMap",
|
|
1600
|
+
"sort",
|
|
1601
|
+
"reverse",
|
|
1602
|
+
"toSorted",
|
|
1603
|
+
"toReversed",
|
|
1604
|
+
"toSpliced",
|
|
1605
|
+
"with"
|
|
1606
|
+
]);
|
|
1607
|
+
const getPropertyName = (node) => {
|
|
1608
|
+
if (!node.computed && node.property.type === "Identifier") return node.property.name;
|
|
1609
|
+
if (node.computed && node.property.type === "Literal" && typeof node.property.value === "string") return node.property.value;
|
|
1610
|
+
return null;
|
|
1611
|
+
};
|
|
1612
|
+
function isStaticArrayReceiver(node, context) {
|
|
1613
|
+
if (node.type === "ArrayExpression") return true;
|
|
1614
|
+
if (node.type === "Identifier") {
|
|
1615
|
+
const traced = trace(node, context);
|
|
1616
|
+
return traced !== node && traced.type === "ArrayExpression";
|
|
1617
|
+
}
|
|
1618
|
+
if (node.type === "CallExpression" && node.callee.type === "MemberExpression" && !node.callee.computed && node.callee.property.type === "Identifier" && PURE_ARRAY_TRANSFORMS.has(node.callee.property.name)) return isStaticArrayReceiver(node.callee.object, context);
|
|
1619
|
+
return false;
|
|
1620
|
+
}
|
|
1621
|
+
function isRenderedInJsx(node) {
|
|
1622
|
+
let current = node;
|
|
1623
|
+
for (let parent = current.parent; parent != null; parent = current.parent) {
|
|
1624
|
+
if (parent.type === "ChainExpression") {} else if (parent.type === "LogicalExpression" && parent.operator === "&&" && parent.right === current) {} else if (parent.type === "ConditionalExpression" && (parent.consequent === current || parent.alternate === current)) {} else if (parent.type === "JSXExpressionContainer") return isJSXElementOrFragment(parent.parent);
|
|
1625
|
+
else return false;
|
|
1626
|
+
current = parent;
|
|
1627
|
+
}
|
|
1628
|
+
return false;
|
|
1629
|
+
}
|
|
1630
|
+
var prefer_for_default = createRule({
|
|
1631
|
+
name: "prefer-for",
|
|
1632
|
+
meta: {
|
|
1633
|
+
type: "suggestion",
|
|
1634
|
+
docs: { description: "Prefer Solid's <For /> component over Array#map when rendering JSX lists." },
|
|
1635
|
+
fixable: "code",
|
|
1636
|
+
schema: [{
|
|
1637
|
+
type: "object",
|
|
1638
|
+
properties: { typescriptEnabled: { type: "boolean" } },
|
|
1639
|
+
additionalProperties: false
|
|
1640
|
+
}],
|
|
1641
|
+
messages: { preferFor: "Use Solid's `<For />` component for rendering JSX lists instead of `Array#map(...)`." }
|
|
1642
|
+
},
|
|
1643
|
+
defaultOptions: [{}],
|
|
1644
|
+
create(context) {
|
|
1645
|
+
const sourceCode = context.sourceCode;
|
|
1646
|
+
const typescriptEnabled = context.options[0]?.typescriptEnabled ?? false;
|
|
1647
|
+
return { CallExpression(node) {
|
|
1648
|
+
if (!isRenderedInJsx(node)) return;
|
|
1649
|
+
const callee = node.callee;
|
|
1650
|
+
if (callee.type !== "MemberExpression" || node.arguments.length === 0) return;
|
|
1651
|
+
if (node.arguments[0].type === "SpreadElement") return;
|
|
1652
|
+
if (getPropertyName(callee) !== "map") return;
|
|
1653
|
+
if (isStaticArrayReceiver(callee.object, context)) return;
|
|
1654
|
+
const mapFn = node.arguments[0];
|
|
1655
|
+
if (!isFunctionNode(mapFn) || !functionReturnsJsx(mapFn)) return;
|
|
1656
|
+
let receiverVerdict = "unknown";
|
|
1657
|
+
if (typescriptEnabled) {
|
|
1658
|
+
const services = getTypeAwareServices(context);
|
|
1659
|
+
if (services) {
|
|
1660
|
+
receiverVerdict = getArrayReceiverVerdict(callee.object, services);
|
|
1661
|
+
if (receiverVerdict === "not-array") return;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
const containerNode = node.parent?.type === "ChainExpression" ? node.parent : node;
|
|
1665
|
+
const directContainer = containerNode.parent?.type === "JSXExpressionContainer" && isJSXElementOrFragment(containerNode.parent.parent);
|
|
1666
|
+
const canAutoFix = receiverVerdict === "array" && directContainer && node.arguments.length === 1 && mapFn.type === "ArrowFunctionExpression" && mapFn.params.length <= 2 && mapFn.params.every((param) => param.type === "Identifier") && sourceCode.scopeManager?.acquire(mapFn) != null;
|
|
1667
|
+
if (receiverVerdict !== "array") return;
|
|
1668
|
+
context.report({
|
|
1669
|
+
node,
|
|
1670
|
+
messageId: "preferFor",
|
|
1671
|
+
fix: canAutoFix ? (fixer) => {
|
|
1672
|
+
const importFixes = getSolidImportFixes(context, fixer, ["For"]);
|
|
1673
|
+
if (importFixes == null) return null;
|
|
1674
|
+
const jsxExpressionContainerNode = containerNode.parent;
|
|
1675
|
+
const arrayNode = callee.object;
|
|
1676
|
+
const mapFnNode = node.arguments[0];
|
|
1677
|
+
const scope = sourceCode.scopeManager.acquire(mapFn);
|
|
1678
|
+
const fixes = [
|
|
1679
|
+
...importFixes,
|
|
1680
|
+
fixer.replaceTextRange([jsxExpressionContainerNode.range[0], arrayNode.range[0]], "<For each={"),
|
|
1681
|
+
fixer.replaceTextRange([arrayNode.range[1], mapFnNode.range[0]], "}>{"),
|
|
1682
|
+
fixer.replaceTextRange([mapFnNode.range[1], jsxExpressionContainerNode.range[1]], "}</For>")
|
|
1683
|
+
];
|
|
1684
|
+
const indexParam = mapFn.params[1];
|
|
1685
|
+
if (indexParam?.type === "Identifier") {
|
|
1686
|
+
const variable = scope.set.get(indexParam.name);
|
|
1687
|
+
if (variable) {
|
|
1688
|
+
for (const reference of variable.references) if (reference.isReadOnly()) fixes.push(fixer.replaceText(reference.identifier, `${indexParam.name}()`));
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
return fixes;
|
|
1692
|
+
} : void 0
|
|
1693
|
+
});
|
|
1694
|
+
} };
|
|
1695
|
+
}
|
|
1696
|
+
});
|
|
1697
|
+
//#endregion
|
|
1698
|
+
//#region src/rules/prefer-show.ts
|
|
1699
|
+
const EXPENSIVE_TYPES = new Set(["JSXElement", "JSXFragment"]);
|
|
1700
|
+
var prefer_show_default = createRule({
|
|
1701
|
+
name: "prefer-show",
|
|
1702
|
+
meta: {
|
|
1703
|
+
type: "suggestion",
|
|
1704
|
+
docs: { description: "Prefer Solid's <Show /> component for JSX conditionals." },
|
|
1705
|
+
fixable: "code",
|
|
1706
|
+
hasSuggestions: true,
|
|
1707
|
+
schema: [],
|
|
1708
|
+
messages: {
|
|
1709
|
+
preferShowAnd: "Use Solid's `<Show />` component for conditionally showing content.",
|
|
1710
|
+
preferShowTernary: "Use Solid's `<Show />` component for conditionally showing content with a fallback.",
|
|
1711
|
+
convertToShow: "Convert to `<Show />`."
|
|
1712
|
+
}
|
|
1713
|
+
},
|
|
1714
|
+
defaultOptions: [],
|
|
1715
|
+
create(context) {
|
|
1716
|
+
const sourceCode = context.sourceCode;
|
|
1717
|
+
const putIntoJSX = (node) => {
|
|
1718
|
+
const text = sourceCode.getText(node);
|
|
1719
|
+
return isJSXElementOrFragment(node) ? text : `{${text}}`;
|
|
1720
|
+
};
|
|
1721
|
+
const replaceTarget = (node) => node.parent?.type === "JSXExpressionContainer" && isJSXElementOrFragment(node.parent.parent) ? node.parent : node;
|
|
1722
|
+
const withShowImport = (fixer, replacement) => {
|
|
1723
|
+
const importFixes = getSolidImportFixes(context, fixer, ["Show"]);
|
|
1724
|
+
return importFixes == null ? null : [...importFixes, replacement];
|
|
1725
|
+
};
|
|
1726
|
+
const logicalExpressionHandler = (node) => {
|
|
1727
|
+
if (node.operator !== "&&" || !EXPENSIVE_TYPES.has(node.right.type)) return;
|
|
1728
|
+
context.report({
|
|
1729
|
+
node,
|
|
1730
|
+
messageId: "preferShowAnd",
|
|
1731
|
+
suggest: [{
|
|
1732
|
+
messageId: "convertToShow",
|
|
1733
|
+
fix: (fixer) => withShowImport(fixer, fixer.replaceText(replaceTarget(node), `<Show when={${sourceCode.getText(node.left)}}>${putIntoJSX(node.right)}</Show>`))
|
|
1734
|
+
}]
|
|
1735
|
+
});
|
|
1736
|
+
};
|
|
1737
|
+
const conditionalExpressionHandler = (node) => {
|
|
1738
|
+
if (!EXPENSIVE_TYPES.has(node.consequent.type) && !EXPENSIVE_TYPES.has(node.alternate.type)) return;
|
|
1739
|
+
context.report({
|
|
1740
|
+
node,
|
|
1741
|
+
messageId: "preferShowTernary",
|
|
1742
|
+
fix: (fixer) => withShowImport(fixer, fixer.replaceText(replaceTarget(node), `<Show when={${sourceCode.getText(node.test)}} fallback={${sourceCode.getText(node.alternate)}}>${putIntoJSX(node.consequent)}</Show>`))
|
|
1743
|
+
});
|
|
1744
|
+
};
|
|
1745
|
+
const renderedExpression = (expression) => {
|
|
1746
|
+
if (expression.type !== "ArrowFunctionExpression") return expression;
|
|
1747
|
+
if (expression.body.type !== "BlockStatement") return expression.body;
|
|
1748
|
+
return [...expression.body.body].reverse().find((statement) => statement.type === "ReturnStatement")?.argument ?? expression;
|
|
1749
|
+
};
|
|
1750
|
+
return { JSXExpressionContainer(node) {
|
|
1751
|
+
if (!isJSXElementOrFragment(node.parent)) return;
|
|
1752
|
+
const expression = renderedExpression(node.expression);
|
|
1753
|
+
if (expression.type === "LogicalExpression") logicalExpressionHandler(expression);
|
|
1754
|
+
else if (expression.type === "ConditionalExpression") conditionalExpressionHandler(expression);
|
|
1755
|
+
} };
|
|
1756
|
+
}
|
|
1757
|
+
});
|
|
1758
|
+
//#endregion
|
|
1759
|
+
//#region src/rules/self-closing-comp.ts
|
|
1760
|
+
const voidDOMElementRegex = /^(?:area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/;
|
|
1761
|
+
const isComponent = (node) => node.name.type === "JSXIdentifier" && !isDOMElementName(node.name.name) || node.name.type === "JSXMemberExpression";
|
|
1762
|
+
const isVoidDOMElementName = (name) => voidDOMElementRegex.test(name);
|
|
1763
|
+
const childrenIsEmpty = (node) => node.parent.children.length === 0;
|
|
1764
|
+
const childrenIsMultilineSpaces = (node) => {
|
|
1765
|
+
const children = node.parent.children;
|
|
1766
|
+
return children.length === 1 && children[0].type === "JSXText" && children[0].value.includes("\n") && children[0].value.replace(/(?!\xA0)\s/g, "") === "";
|
|
1767
|
+
};
|
|
1768
|
+
const plugin = {
|
|
1769
|
+
meta: { name: "eslint-plugin-solid" },
|
|
1770
|
+
rules: {
|
|
1771
|
+
"components-return-once": components_return_once_default,
|
|
1772
|
+
"jsx-no-duplicate-props": jsx_no_duplicate_props_default,
|
|
1773
|
+
"no-destructure": no_destructure_default,
|
|
1774
|
+
"no-leaf-owner-operations": no_leaf_owner_operations_default,
|
|
1775
|
+
"no-owned-scope-writes": no_owned_scope_writes_default,
|
|
1776
|
+
"no-reactive-read-after-await": no_reactive_read_after_await_default,
|
|
1777
|
+
"no-stale-props-alias": no_stale_props_alias_default,
|
|
1778
|
+
"no-untracked-read-in-effect-apply": no_untracked_read_in_effect_apply_default,
|
|
1779
|
+
"prefer-for": prefer_for_default,
|
|
1780
|
+
"prefer-show": prefer_show_default,
|
|
1781
|
+
"self-closing-comp": createRule({
|
|
1782
|
+
name: "self-closing-comp",
|
|
1783
|
+
meta: {
|
|
1784
|
+
type: "layout",
|
|
1785
|
+
docs: { description: "Disallow extra closing tags for components without children." },
|
|
1786
|
+
fixable: "code",
|
|
1787
|
+
schema: [{
|
|
1788
|
+
type: "object",
|
|
1789
|
+
properties: {
|
|
1790
|
+
component: {
|
|
1791
|
+
enum: ["all", "none"],
|
|
1792
|
+
type: "string"
|
|
1793
|
+
},
|
|
1794
|
+
html: {
|
|
1795
|
+
enum: [
|
|
1796
|
+
"all",
|
|
1797
|
+
"void",
|
|
1798
|
+
"none"
|
|
1799
|
+
],
|
|
1800
|
+
type: "string"
|
|
1801
|
+
}
|
|
1802
|
+
},
|
|
1803
|
+
additionalProperties: false
|
|
1804
|
+
}],
|
|
1805
|
+
messages: {
|
|
1806
|
+
dontSelfClose: "This element should not be self-closing.",
|
|
1807
|
+
selfClose: "Empty components are self-closing."
|
|
1808
|
+
}
|
|
1809
|
+
},
|
|
1810
|
+
defaultOptions: [],
|
|
1811
|
+
create(context) {
|
|
1812
|
+
const sourceCode = context.sourceCode;
|
|
1813
|
+
const shouldBeSelfClosedWhenPossible = (node) => {
|
|
1814
|
+
if (isComponent(node)) return (context.options[0]?.component ?? "all") === "all";
|
|
1815
|
+
if (isHostElement(node)) switch (context.options[0]?.html ?? "all") {
|
|
1816
|
+
case "all": return true;
|
|
1817
|
+
case "void": return node.name.type === "JSXIdentifier" && isVoidDOMElementName(node.name.name);
|
|
1818
|
+
case "none": return false;
|
|
1819
|
+
}
|
|
1820
|
+
return true;
|
|
1821
|
+
};
|
|
1822
|
+
return { JSXOpeningElement(node) {
|
|
1823
|
+
if (!(childrenIsEmpty(node) || childrenIsMultilineSpaces(node))) return;
|
|
1824
|
+
const shouldSelfClose = shouldBeSelfClosedWhenPossible(node);
|
|
1825
|
+
if (shouldSelfClose && !node.selfClosing) context.report({
|
|
1826
|
+
node,
|
|
1827
|
+
messageId: "selfClose",
|
|
1828
|
+
fix: (fixer) => {
|
|
1829
|
+
const openingElementEnding = node.range[1] - 1;
|
|
1830
|
+
const closingElementEnding = node.parent.closingElement.range[1];
|
|
1831
|
+
return fixer.replaceTextRange([openingElementEnding, closingElementEnding], " />");
|
|
1832
|
+
}
|
|
1833
|
+
});
|
|
1834
|
+
else if (!shouldSelfClose && node.selfClosing) context.report({
|
|
1835
|
+
node,
|
|
1836
|
+
messageId: "dontSelfClose",
|
|
1837
|
+
fix: (fixer) => {
|
|
1838
|
+
const tagName = sourceCode.getText(node.name);
|
|
1839
|
+
const selfCloseEnding = node.range[1];
|
|
1840
|
+
const lastTokens = sourceCode.getLastTokens(node, { count: 3 });
|
|
1841
|
+
const range = [sourceCode.isSpaceBetween(lastTokens[0], lastTokens[1]) ? selfCloseEnding - 3 : selfCloseEnding - 2, selfCloseEnding];
|
|
1842
|
+
return fixer.replaceTextRange(range, `></${tagName}>`);
|
|
1843
|
+
}
|
|
1844
|
+
});
|
|
1845
|
+
} };
|
|
1846
|
+
}
|
|
1847
|
+
})
|
|
1848
|
+
}
|
|
1849
|
+
};
|
|
1850
|
+
//#endregion
|
|
1851
|
+
//#region src/configs/recommended.ts
|
|
1852
|
+
const recommended = {
|
|
1853
|
+
name: "solid/recommended",
|
|
1854
|
+
plugins: { solid: plugin },
|
|
1855
|
+
rules: {
|
|
1856
|
+
"solid/components-return-once": "warn",
|
|
1857
|
+
"solid/jsx-no-duplicate-props": "error",
|
|
1858
|
+
"solid/no-destructure": "warn",
|
|
1859
|
+
"solid/no-leaf-owner-operations": "error",
|
|
1860
|
+
"solid/no-owned-scope-writes": "error",
|
|
1861
|
+
"solid/no-reactive-read-after-await": "warn",
|
|
1862
|
+
"solid/no-stale-props-alias": "warn",
|
|
1863
|
+
"solid/no-untracked-read-in-effect-apply": "warn",
|
|
1864
|
+
"solid/prefer-show": "warn",
|
|
1865
|
+
"solid/self-closing-comp": "warn"
|
|
1866
|
+
}
|
|
1867
|
+
};
|
|
1868
|
+
//#endregion
|
|
1869
|
+
//#region src/configs/recommended-type-checked.ts
|
|
1870
|
+
const typeAwareOverrides = {
|
|
1871
|
+
"solid/components-return-once": ["warn", { typescriptEnabled: true }],
|
|
1872
|
+
"solid/no-destructure": ["warn", { typescriptEnabled: true }],
|
|
1873
|
+
"solid/no-owned-scope-writes": ["error", { typescriptEnabled: true }],
|
|
1874
|
+
"solid/no-reactive-read-after-await": ["warn", { typescriptEnabled: true }],
|
|
1875
|
+
"solid/prefer-for": ["warn", { typescriptEnabled: true }]
|
|
1876
|
+
};
|
|
1877
|
+
const recommendedTypeChecked = {
|
|
1878
|
+
name: "solid/recommended-type-checked",
|
|
1879
|
+
plugins: { solid: plugin },
|
|
1880
|
+
rules: {
|
|
1881
|
+
...recommended.rules,
|
|
1882
|
+
...typeAwareOverrides
|
|
1883
|
+
}
|
|
1884
|
+
};
|
|
1885
|
+
//#endregion
|
|
1886
|
+
//#region src/index.ts
|
|
1887
|
+
const pluginWithConfigs = {
|
|
1888
|
+
...plugin,
|
|
1889
|
+
configs: {
|
|
1890
|
+
recommended,
|
|
1891
|
+
"flat/recommended": recommended,
|
|
1892
|
+
"recommended-type-checked": recommendedTypeChecked,
|
|
1893
|
+
"flat/recommended-type-checked": recommendedTypeChecked
|
|
1894
|
+
}
|
|
1895
|
+
};
|
|
1896
|
+
//#endregion
|
|
1897
|
+
export { pluginWithConfigs as default };
|