@thomaflette/eslint-plugin-solid-2 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/index.cjs +810 -468
- package/dist/index.d.cts +79 -63
- package/dist/index.d.mts +79 -63
- package/dist/index.mjs +810 -468
- package/docs/README.md +24 -0
- package/docs/adr/0001-only-false-positive-free-rules.md +60 -0
- package/docs/adr/0002-sound-component-detection.md +66 -0
- package/docs/adr/0003-require-solid-binding.md +50 -0
- package/docs/adr/0004-correctness-guard-not-migration-tool.md +48 -0
- package/docs/adr/0005-recommended-type-checked-config.md +27 -0
- package/docs/adr/0006-merge-leaf-owner-rules.md +42 -0
- package/docs/adr/0007-typescript-audience.md +35 -0
- package/docs/components-return-once.md +89 -0
- package/docs/jsx-no-duplicate-props.md +34 -0
- package/docs/no-destructure.md +92 -0
- package/docs/no-leaf-owner-operations.md +82 -0
- package/docs/no-owned-scope-writes.md +117 -0
- package/docs/no-reactive-read-after-await.md +87 -0
- package/docs/no-stale-props-alias.md +157 -0
- package/docs/no-untracked-read-in-effect-apply.md +97 -0
- package/docs/prefer-for.md +81 -0
- package/docs/prefer-show.md +59 -0
- package/docs/self-closing-comp.md +42 -0
- package/package.json +7 -5
package/dist/index.mjs
CHANGED
|
@@ -55,34 +55,218 @@ function* jsxGetAllProps(props) {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
//#endregion
|
|
58
|
+
//#region src/analysis/typescript-semantics.ts
|
|
59
|
+
function getTypeAwareServices(context) {
|
|
60
|
+
const services = context.sourceCode.parserServices;
|
|
61
|
+
if (services?.program != null && services.esTreeNodeToTSNodeMap != null) return services;
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
function resolveTypeScriptAlias(symbol, checker) {
|
|
65
|
+
return symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
|
|
66
|
+
}
|
|
67
|
+
const SOLID_ACCESSOR_TYPE_NAMES = new Set(["Accessor", "SourceAccessor"]);
|
|
68
|
+
function isSolidOrigin(text) {
|
|
69
|
+
return text.includes("solid-js") || text.includes("@solidjs") || text.includes("solid-signals");
|
|
70
|
+
}
|
|
71
|
+
const solidSymbolCache = /* @__PURE__ */ new WeakMap();
|
|
72
|
+
function symbolIsFromSolid(symbol, checker) {
|
|
73
|
+
const cached = solidSymbolCache.get(symbol);
|
|
74
|
+
if (cached !== void 0) return cached;
|
|
75
|
+
const result = isSolidOrigin(checker.getFullyQualifiedName(symbol)) || (symbol.getDeclarations() ?? []).some((declaration) => isSolidOrigin(declaration.getSourceFile().fileName));
|
|
76
|
+
solidSymbolCache.set(symbol, result);
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
function collectTypeParts(type, acc = [], seen = /* @__PURE__ */ new Set()) {
|
|
80
|
+
if (seen.has(type)) return acc;
|
|
81
|
+
seen.add(type);
|
|
82
|
+
acc.push(type);
|
|
83
|
+
if (type.isUnion() || type.isIntersection()) for (const member of type.types) collectTypeParts(member, acc, seen);
|
|
84
|
+
return acc;
|
|
85
|
+
}
|
|
86
|
+
const solidAccessorNodeCache = /* @__PURE__ */ new WeakMap();
|
|
87
|
+
function isSolidAccessorExpression(node, services) {
|
|
88
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
|
|
89
|
+
if (!tsNode) return false;
|
|
90
|
+
const cached = solidAccessorNodeCache.get(tsNode);
|
|
91
|
+
if (cached !== void 0) return cached;
|
|
92
|
+
const checker = services.program.getTypeChecker();
|
|
93
|
+
const result = collectTypeParts(checker.getTypeAtLocation(tsNode)).some((part) => {
|
|
94
|
+
const alias = part.aliasSymbol;
|
|
95
|
+
return alias != null && SOLID_ACCESSOR_TYPE_NAMES.has(alias.getName()) && symbolIsFromSolid(alias, checker);
|
|
96
|
+
});
|
|
97
|
+
solidAccessorNodeCache.set(tsNode, result);
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
function resolveTypeAwareSolidCallee(node, services, canonicalNames) {
|
|
101
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
|
|
102
|
+
if (!tsNode) return null;
|
|
103
|
+
const checker = services.program.getTypeChecker();
|
|
104
|
+
const symbol = checker.getSymbolAtLocation(tsNode);
|
|
105
|
+
if (!symbol) return null;
|
|
106
|
+
const resolved = resolveTypeScriptAlias(symbol, checker);
|
|
107
|
+
const name = resolved.getName();
|
|
108
|
+
return canonicalNames.has(name) && symbolIsFromSolid(resolved, checker) ? name : null;
|
|
109
|
+
}
|
|
110
|
+
function isTypeAwareSolidCallee(node, services, canonicalNames) {
|
|
111
|
+
return resolveTypeAwareSolidCallee(node, services, canonicalNames) != null;
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/analysis/solid-bindings.ts
|
|
115
|
+
const SINGLE_RESULT_FACTORIES = new Map([
|
|
116
|
+
["action", "action"],
|
|
117
|
+
["createMemo", "accessor"],
|
|
118
|
+
["createProjection", "store"]
|
|
119
|
+
]);
|
|
120
|
+
const PAIR_RESULT_FACTORIES = new Map([
|
|
121
|
+
["createOptimistic", ["accessor", "setter"]],
|
|
122
|
+
["createOptimisticStore", ["store", "setter"]],
|
|
123
|
+
["createSignal", ["accessor", "setter"]],
|
|
124
|
+
["createStore", ["store", "setter"]]
|
|
125
|
+
]);
|
|
126
|
+
const REACTIVE_FACTORIES = new Set([...SINGLE_RESULT_FACTORIES.keys(), ...PAIR_RESULT_FACTORIES.keys()]);
|
|
127
|
+
/** Whether a node is an `import ... from "solid-js"` declaration. */
|
|
128
|
+
function isSolidJsImportDeclaration(node) {
|
|
129
|
+
return node?.type === "ImportDeclaration" && node.source.type === "Literal" && node.source.value === "solid-js";
|
|
130
|
+
}
|
|
131
|
+
function namespaceMemberName(node, context) {
|
|
132
|
+
if (node.type !== "MemberExpression" || node.optional || node.object.type !== "Identifier" || node.property.type !== "Identifier" || node.computed) return null;
|
|
133
|
+
const imported = ASTUtils.findVariable(context.sourceCode.getScope(node.object), node.object)?.defs.some((def) => def.type === "ImportBinding" && def.node.type === "ImportNamespaceSpecifier" && isSolidJsImportDeclaration(def.node.parent));
|
|
134
|
+
return {
|
|
135
|
+
name: node.property.name,
|
|
136
|
+
imported: imported === true
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Resolves a direct, aliased, or namespace-qualified expression to its canonical Solid export.
|
|
141
|
+
* Bare canonical names count only when truly unresolved (auto-import/global).
|
|
142
|
+
*/
|
|
143
|
+
function resolveSolidCallee(node, context, canonicalNames) {
|
|
144
|
+
const traced = trace(node, context);
|
|
145
|
+
if (traced.type === "Identifier" && canonicalNames.has(traced.name)) {
|
|
146
|
+
const variable = ASTUtils.findVariable(context.sourceCode.getScope(traced), traced);
|
|
147
|
+
return variable == null || variable.defs.length === 0 ? traced.name : null;
|
|
148
|
+
}
|
|
149
|
+
if (traced.type === "ImportSpecifier" && isSolidJsImportDeclaration(traced.parent)) {
|
|
150
|
+
const importedName = traced.imported.type === "Identifier" ? traced.imported.name : traced.imported.value;
|
|
151
|
+
return canonicalNames.has(importedName) ? importedName : null;
|
|
152
|
+
}
|
|
153
|
+
const member = namespaceMemberName(traced, context);
|
|
154
|
+
return member?.imported === true && canonicalNames.has(member.name) ? member.name : null;
|
|
155
|
+
}
|
|
156
|
+
function bindsToSolid(node, context, canonicalNames) {
|
|
157
|
+
return resolveSolidCallee(node, context, canonicalNames) != null;
|
|
158
|
+
}
|
|
159
|
+
function getChildNodes$1(node) {
|
|
160
|
+
const children = [];
|
|
161
|
+
for (const key in node) {
|
|
162
|
+
if (key === "parent" || key === "tokens" || key === "comments") continue;
|
|
163
|
+
const value = node[key];
|
|
164
|
+
if (Array.isArray(value)) {
|
|
165
|
+
for (const item of value) if (item != null && typeof item === "object" && typeof item.type === "string") children.push(item);
|
|
166
|
+
} else if (value != null && typeof value === "object" && typeof value.type === "string") children.push(value);
|
|
167
|
+
}
|
|
168
|
+
return children;
|
|
169
|
+
}
|
|
170
|
+
function declaredVariable(declarator, id, sourceCode) {
|
|
171
|
+
return sourceCode.scopeManager?.getDeclaredVariables(declarator).find((variable) => variable.name === id.name) ?? null;
|
|
172
|
+
}
|
|
173
|
+
const baseReactiveBindingCache = /* @__PURE__ */ new WeakMap();
|
|
174
|
+
const typeAwareReactiveBindingCache = /* @__PURE__ */ new WeakMap();
|
|
175
|
+
function buildReactiveBindingFacts(sourceCode, context) {
|
|
176
|
+
const facts = /* @__PURE__ */ new Map();
|
|
177
|
+
const aliases = [];
|
|
178
|
+
const services = context.options[0]?.typescriptEnabled ? getTypeAwareServices(context) : null;
|
|
179
|
+
const stack = [sourceCode.ast];
|
|
180
|
+
while (stack.length > 0) {
|
|
181
|
+
const node = stack.pop();
|
|
182
|
+
if (node.type === "VariableDeclarator") {
|
|
183
|
+
const declaration = node.parent;
|
|
184
|
+
if (declaration?.type === "VariableDeclaration" && declaration.kind === "const" && node.id.type === "Identifier" && node.init?.type === "Identifier") {
|
|
185
|
+
const target = declaredVariable(node, node.id, sourceCode);
|
|
186
|
+
const source = ASTUtils.findVariable(sourceCode.getScope(node.init), node.init);
|
|
187
|
+
if (target && source) aliases.push({
|
|
188
|
+
target,
|
|
189
|
+
source
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
if (node.init?.type === "CallExpression") {
|
|
193
|
+
const factory = resolveSolidCallee(node.init.callee, context, REACTIVE_FACTORIES) ?? (services == null ? null : resolveTypeAwareSolidCallee(node.init.callee, services, REACTIVE_FACTORIES));
|
|
194
|
+
if (factory != null) {
|
|
195
|
+
const singleRole = SINGLE_RESULT_FACTORIES.get(factory);
|
|
196
|
+
if (singleRole != null && node.id.type === "Identifier") {
|
|
197
|
+
const variable = declaredVariable(node, node.id, sourceCode);
|
|
198
|
+
if (variable) facts.set(variable, {
|
|
199
|
+
role: singleRole,
|
|
200
|
+
factory,
|
|
201
|
+
declaration: node,
|
|
202
|
+
tupleIndex: null
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
const pairRoles = PAIR_RESULT_FACTORIES.get(factory);
|
|
206
|
+
if (pairRoles != null && node.id.type === "ArrayPattern") for (const tupleIndex of [0, 1]) {
|
|
207
|
+
const id = node.id.elements[tupleIndex];
|
|
208
|
+
if (id?.type !== "Identifier") continue;
|
|
209
|
+
const variable = declaredVariable(node, id, sourceCode);
|
|
210
|
+
if (variable) facts.set(variable, {
|
|
211
|
+
role: pairRoles[tupleIndex],
|
|
212
|
+
factory,
|
|
213
|
+
declaration: node,
|
|
214
|
+
tupleIndex
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
stack.push(...getChildNodes$1(node));
|
|
221
|
+
}
|
|
222
|
+
let changed = true;
|
|
223
|
+
while (changed) {
|
|
224
|
+
changed = false;
|
|
225
|
+
for (const { target, source } of aliases) {
|
|
226
|
+
if (facts.has(target)) continue;
|
|
227
|
+
const fact = facts.get(source);
|
|
228
|
+
if (fact) {
|
|
229
|
+
facts.set(target, fact);
|
|
230
|
+
changed = true;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return facts;
|
|
235
|
+
}
|
|
236
|
+
function getReactiveBindingFacts(context) {
|
|
237
|
+
const sourceCode = context.sourceCode;
|
|
238
|
+
const cache = context.options[0]?.typescriptEnabled === true ? typeAwareReactiveBindingCache : baseReactiveBindingCache;
|
|
239
|
+
const cached = cache.get(sourceCode);
|
|
240
|
+
if (cached) return cached;
|
|
241
|
+
const facts = buildReactiveBindingFacts(sourceCode, context);
|
|
242
|
+
cache.set(sourceCode, facts);
|
|
243
|
+
return facts;
|
|
244
|
+
}
|
|
245
|
+
function getReactiveBindingFact(node, context) {
|
|
246
|
+
const variable = ASTUtils.findVariable(context.sourceCode.getScope(node), node);
|
|
247
|
+
return variable ? getReactiveBindingFacts(context).get(variable) ?? null : null;
|
|
248
|
+
}
|
|
249
|
+
function getReactiveBindingFactForVariable(variable, context) {
|
|
250
|
+
return getReactiveBindingFacts(context).get(variable) ?? null;
|
|
251
|
+
}
|
|
252
|
+
//#endregion
|
|
58
253
|
//#region src/rules/create-rule.ts
|
|
59
254
|
const DOCS_BASE = "https://github.com/yumemi-thomas/eslint-plugin-solid-2/blob/main/docs";
|
|
60
255
|
const createRule = ESLintUtils.RuleCreator((name) => `${DOCS_BASE}/${name}.md`);
|
|
61
256
|
//#endregion
|
|
62
|
-
//#region src/
|
|
257
|
+
//#region src/analysis/component-recognition.ts
|
|
63
258
|
const SOLID_COMPONENT_TYPES = new Set([
|
|
64
259
|
"Component",
|
|
65
260
|
"VoidComponent",
|
|
66
261
|
"ParentComponent",
|
|
67
262
|
"FlowComponent"
|
|
68
263
|
]);
|
|
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
264
|
function typeReferenceName(node) {
|
|
75
265
|
if (node?.type !== "TSTypeReference") return null;
|
|
76
266
|
if (node.typeName.type === "Identifier") return node.typeName.name;
|
|
77
267
|
if (node.typeName.type === "TSQualifiedName" && node.typeName.right.type === "Identifier") return node.typeName.right.name;
|
|
78
268
|
return null;
|
|
79
269
|
}
|
|
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
270
|
function typeReferenceRootIdentifier(node) {
|
|
87
271
|
if (node?.type !== "TSTypeReference") return null;
|
|
88
272
|
let name = node.typeName;
|
|
@@ -96,14 +280,6 @@ function typeReferenceRootIdentifier(node) {
|
|
|
96
280
|
qualified
|
|
97
281
|
} : null;
|
|
98
282
|
}
|
|
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
283
|
function typeNameBindsToSolid(id, qualified, context) {
|
|
108
284
|
let scope = context.sourceCode.getScope(id);
|
|
109
285
|
while (scope) {
|
|
@@ -118,35 +294,26 @@ function typeNameBindsToSolid(id, qualified, context) {
|
|
|
118
294
|
}
|
|
119
295
|
return !qualified;
|
|
120
296
|
}
|
|
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
297
|
function hasSolidComponentTypeAnnotation(node, context) {
|
|
129
298
|
const parent = node.parent;
|
|
130
|
-
if (parent?.type
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
return false;
|
|
299
|
+
if (parent?.type !== "VariableDeclarator" || parent.id.type !== "Identifier") return false;
|
|
300
|
+
const annotation = parent.id.typeAnnotation?.typeAnnotation;
|
|
301
|
+
const name = typeReferenceName(annotation);
|
|
302
|
+
if (name == null || !SOLID_COMPONENT_TYPES.has(name)) return false;
|
|
303
|
+
const root = typeReferenceRootIdentifier(annotation);
|
|
304
|
+
return root != null && typeNameBindsToSolid(root.id, root.qualified, context);
|
|
138
305
|
}
|
|
139
306
|
const inFileComponentVarsCache = /* @__PURE__ */ new WeakMap();
|
|
140
307
|
function getInFileComponentVariables(sourceCode) {
|
|
141
308
|
const cached = inFileComponentVarsCache.get(sourceCode);
|
|
142
309
|
if (cached) return cached;
|
|
143
|
-
const
|
|
310
|
+
const variables = /* @__PURE__ */ new Set();
|
|
144
311
|
const stack = [sourceCode.ast];
|
|
145
312
|
while (stack.length > 0) {
|
|
146
313
|
const node = stack.pop();
|
|
147
314
|
if (node.type === "JSXOpeningElement" && node.name.type === "JSXIdentifier" && !isDOMElementName(node.name.name)) {
|
|
148
315
|
const variable = ASTUtils.findVariable(sourceCode.getScope(node.name), node.name.name);
|
|
149
|
-
if (variable
|
|
316
|
+
if (variable) variables.add(variable);
|
|
150
317
|
}
|
|
151
318
|
for (const key in node) {
|
|
152
319
|
if (key === "parent" || key === "tokens" || key === "comments") continue;
|
|
@@ -156,9 +323,73 @@ function getInFileComponentVariables(sourceCode) {
|
|
|
156
323
|
} else if (value != null && typeof value === "object" && typeof value.type === "string") stack.push(value);
|
|
157
324
|
}
|
|
158
325
|
}
|
|
159
|
-
inFileComponentVarsCache.set(sourceCode,
|
|
160
|
-
return
|
|
326
|
+
inFileComponentVarsCache.set(sourceCode, variables);
|
|
327
|
+
return variables;
|
|
328
|
+
}
|
|
329
|
+
const jsxTagSymbolsCache = /* @__PURE__ */ new WeakMap();
|
|
330
|
+
function getJsxTagSymbols(services) {
|
|
331
|
+
const cached = jsxTagSymbolsCache.get(services.program);
|
|
332
|
+
if (cached) return cached;
|
|
333
|
+
const checker = services.program.getTypeChecker();
|
|
334
|
+
const symbols = /* @__PURE__ */ new Set();
|
|
335
|
+
const visit = (node) => {
|
|
336
|
+
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
337
|
+
const symbol = checker.getSymbolAtLocation(node.tagName);
|
|
338
|
+
if (symbol) symbols.add(resolveTypeScriptAlias(symbol, checker));
|
|
339
|
+
}
|
|
340
|
+
ts.forEachChild(node, visit);
|
|
341
|
+
};
|
|
342
|
+
for (const sourceFile of services.program.getSourceFiles()) if (!sourceFile.isDeclarationFile && !sourceFile.fileName.includes("/node_modules/")) visit(sourceFile);
|
|
343
|
+
jsxTagSymbolsCache.set(services.program, symbols);
|
|
344
|
+
return symbols;
|
|
345
|
+
}
|
|
346
|
+
function getFunctionSymbol(node, services) {
|
|
347
|
+
let nameNode;
|
|
348
|
+
if ((node.type === "FunctionDeclaration" || node.type === "FunctionExpression") && node.id != null) nameNode = node.id;
|
|
349
|
+
else if (node.parent?.type === "VariableDeclarator" && node.parent.id.type === "Identifier") nameNode = node.parent.id;
|
|
350
|
+
if (!nameNode) return;
|
|
351
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(nameNode);
|
|
352
|
+
return tsNode ? services.program.getTypeChecker().getSymbolAtLocation(tsNode) : void 0;
|
|
353
|
+
}
|
|
354
|
+
const baseEvidenceCache = /* @__PURE__ */ new WeakMap();
|
|
355
|
+
const typeAwareEvidenceCache = /* @__PURE__ */ new WeakMap();
|
|
356
|
+
function cachedEvidence(cache, sourceCode, node, compute) {
|
|
357
|
+
let byFunction = cache.get(sourceCode);
|
|
358
|
+
if (!byFunction) {
|
|
359
|
+
byFunction = /* @__PURE__ */ new WeakMap();
|
|
360
|
+
cache.set(sourceCode, byFunction);
|
|
361
|
+
}
|
|
362
|
+
const cached = byFunction.get(node);
|
|
363
|
+
if (cached !== void 0) return cached;
|
|
364
|
+
const evidence = compute();
|
|
365
|
+
byFunction.set(node, evidence);
|
|
366
|
+
return evidence;
|
|
367
|
+
}
|
|
368
|
+
function hasBaseEvidence(node, context) {
|
|
369
|
+
return cachedEvidence(baseEvidenceCache, context.sourceCode, node, () => {
|
|
370
|
+
if (hasSolidComponentTypeAnnotation(node, context)) return true;
|
|
371
|
+
const name = getFunctionName(node);
|
|
372
|
+
if (name == null) return false;
|
|
373
|
+
const variable = ASTUtils.findVariable(context.sourceCode.getScope(node), name);
|
|
374
|
+
return variable != null && getInFileComponentVariables(context.sourceCode).has(variable);
|
|
375
|
+
});
|
|
161
376
|
}
|
|
377
|
+
function hasTypeAwareEvidence(node, context) {
|
|
378
|
+
return cachedEvidence(typeAwareEvidenceCache, context.sourceCode, node, () => {
|
|
379
|
+
const services = getTypeAwareServices(context);
|
|
380
|
+
if (!services) return false;
|
|
381
|
+
const checker = services.program.getTypeChecker();
|
|
382
|
+
const symbol = getFunctionSymbol(node, services);
|
|
383
|
+
return symbol != null && getJsxTagSymbols(services).has(resolveTypeScriptAlias(symbol, checker));
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
/** Sound, self-indexing Solid component recognition. See ADR-0002. */
|
|
387
|
+
function isComponent$1(node, context) {
|
|
388
|
+
if (hasBaseEvidence(node, context)) return true;
|
|
389
|
+
return context.options[0]?.typescriptEnabled === true && hasTypeAwareEvidence(node, context);
|
|
390
|
+
}
|
|
391
|
+
//#endregion
|
|
392
|
+
//#region src/rules/solid-rule-utils.ts
|
|
162
393
|
function expressionCanYieldJsx(node) {
|
|
163
394
|
switch (node?.type) {
|
|
164
395
|
case "JSXElement":
|
|
@@ -203,43 +434,6 @@ function functionReturnsJsx(node) {
|
|
|
203
434
|
}
|
|
204
435
|
return false;
|
|
205
436
|
}
|
|
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
437
|
/**
|
|
244
438
|
* Verdict on whether `node`'s type is array-like (an array or tuple — i.e. has a numeric index
|
|
245
439
|
* signature), considering every union member:
|
|
@@ -260,57 +454,6 @@ function getArrayReceiverVerdict(node, services) {
|
|
|
260
454
|
if (arrayMembers.length === members.length) return "array";
|
|
261
455
|
return arrayMembers.length === 0 ? "not-array" : "unknown";
|
|
262
456
|
}
|
|
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
457
|
function getReturnedExpressions(node) {
|
|
315
458
|
if (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement") return [node.body];
|
|
316
459
|
if (node.body.type !== "BlockStatement") return [];
|
|
@@ -364,26 +507,6 @@ function getNearestFunctionAncestor(node) {
|
|
|
364
507
|
}
|
|
365
508
|
return null;
|
|
366
509
|
}
|
|
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
510
|
/** Whether any scope in the file already declares `name`. */
|
|
388
511
|
function isNameTaken(sourceCode, name) {
|
|
389
512
|
return sourceCode.scopeManager?.scopes.some((scope) => scope.set.has(name)) ?? false;
|
|
@@ -412,9 +535,6 @@ function getSolidImportFixes(context, fixer, names) {
|
|
|
412
535
|
}
|
|
413
536
|
//#endregion
|
|
414
537
|
//#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
538
|
function getVariable(id, context) {
|
|
419
539
|
return ASTUtils.findVariable(context.sourceCode.getScope(id), id);
|
|
420
540
|
}
|
|
@@ -423,27 +543,6 @@ function getDeclarator(variable) {
|
|
|
423
543
|
const def = variable.defs[0];
|
|
424
544
|
return def?.type === "Variable" ? def.node : null;
|
|
425
545
|
}
|
|
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
546
|
/**
|
|
448
547
|
* Whether evaluating `node` provably performs a reactive read from `componentFn`'s perspective.
|
|
449
548
|
* Skips nested functions (not evaluated by the guard) and JSX (reads there compile to tracked
|
|
@@ -457,7 +556,7 @@ function containsReactiveRead(node, componentFn, context, seen = /* @__PURE__ */
|
|
|
457
556
|
if (root.type !== "Identifier") return false;
|
|
458
557
|
const variable = getVariable(root, context);
|
|
459
558
|
if (variable == null) return false;
|
|
460
|
-
if (isPropsParameter(variable) ||
|
|
559
|
+
if (isPropsParameter(variable) || getReactiveBindingFactForVariable(variable, context)?.role === "store") return true;
|
|
461
560
|
return derivedConstIsReactive(variable);
|
|
462
561
|
};
|
|
463
562
|
const derivedConstIsReactive = (variable) => {
|
|
@@ -472,7 +571,7 @@ function containsReactiveRead(node, componentFn, context, seen = /* @__PURE__ */
|
|
|
472
571
|
if (isFunctionNode(current) || current.type === "JSXElement" || current.type === "JSXFragment") continue;
|
|
473
572
|
if (current.type === "CallExpression" && current.callee.type === "Identifier") {
|
|
474
573
|
const callee = getVariable(current.callee, context);
|
|
475
|
-
if (callee != null &&
|
|
574
|
+
if (callee != null && getReactiveBindingFactForVariable(callee, context)?.role === "accessor") return true;
|
|
476
575
|
}
|
|
477
576
|
if (current.type === "MemberExpression" && memberRootIsReactive(current)) return true;
|
|
478
577
|
if (current.type === "Identifier") {
|
|
@@ -885,24 +984,34 @@ var no_leaf_owner_operations_default = createRule({
|
|
|
885
984
|
meta: {
|
|
886
985
|
type: "problem",
|
|
887
986
|
docs: { description: "Disallow onCleanup, reactive-primitive creation, and flush() inside the leaf owners createTrackedEffect and onSettled." },
|
|
888
|
-
schema: [
|
|
987
|
+
schema: [{
|
|
988
|
+
type: "object",
|
|
989
|
+
properties: { typescriptEnabled: { type: "boolean" } },
|
|
990
|
+
additionalProperties: false
|
|
991
|
+
}],
|
|
889
992
|
messages: {
|
|
890
993
|
noCleanup: "Cannot use `onCleanup` inside `createTrackedEffect` or `onSettled`; return a cleanup function instead.",
|
|
891
994
|
noFlush: "Cannot call `flush()` from inside `createTrackedEffect` or `onSettled`; schedule work outside instead.",
|
|
892
995
|
noPrimitives: "Cannot create reactive primitives inside `createTrackedEffect` or `onSettled`; move them to the component body or another owner."
|
|
893
996
|
}
|
|
894
997
|
},
|
|
895
|
-
defaultOptions: [],
|
|
998
|
+
defaultOptions: [{}],
|
|
896
999
|
create(context) {
|
|
1000
|
+
const services = context.options[0]?.typescriptEnabled ? getTypeAwareServices(context) : null;
|
|
1001
|
+
const isSolidApi = (node, names) => resolveSolidCallee(node, context, names) != null || services != null && resolveTypeAwareSolidCallee(node, services, names) != null;
|
|
1002
|
+
const isSolidCallbackArgument = (node, argumentIndex, names) => {
|
|
1003
|
+
const call = node.parent;
|
|
1004
|
+
return call?.type === "CallExpression" && call.arguments[argumentIndex] === node && isSolidApi(call.callee, names);
|
|
1005
|
+
};
|
|
897
1006
|
const forbiddenStack = [];
|
|
898
1007
|
const onFunctionEnter = (node) => {
|
|
899
1008
|
if (node.type !== "FunctionDeclaration" && node.type !== "FunctionExpression" && node.type !== "ArrowFunctionExpression") return;
|
|
900
|
-
const trackedEffect =
|
|
1009
|
+
const trackedEffect = isSolidCallbackArgument(node, 0, TRACKED_EFFECT_NAMES);
|
|
901
1010
|
let ownerBackedSettled = false;
|
|
902
|
-
if (
|
|
1011
|
+
if (isSolidCallbackArgument(node, 0, ON_SETTLED_NAMES)) {
|
|
903
1012
|
const settledCall = node.parent;
|
|
904
1013
|
const enclosing = getNearestFunctionAncestor(settledCall);
|
|
905
|
-
ownerBackedSettled = enclosing != null && (isComponent$1(enclosing, context) ||
|
|
1014
|
+
ownerBackedSettled = enclosing != null && (isComponent$1(enclosing, context) || isSolidCallbackArgument(enclosing, 0, OWNER_CALLBACK_NAMES) || isSolidCallbackArgument(enclosing, 1, OWNER_CALLBACK_NAMES));
|
|
906
1015
|
}
|
|
907
1016
|
if (trackedEffect || ownerBackedSettled) forbiddenStack.push(node);
|
|
908
1017
|
};
|
|
@@ -918,21 +1027,21 @@ var no_leaf_owner_operations_default = createRule({
|
|
|
918
1027
|
"ArrowFunctionExpression:exit": onFunctionExit,
|
|
919
1028
|
CallExpression(node) {
|
|
920
1029
|
const currentForbidden = forbiddenStack[forbiddenStack.length - 1];
|
|
921
|
-
if (!currentForbidden || getNearestFunctionAncestor(node) !== currentForbidden
|
|
1030
|
+
if (!currentForbidden || getNearestFunctionAncestor(node) !== currentForbidden) return;
|
|
922
1031
|
const callee = node.callee;
|
|
923
|
-
if (
|
|
1032
|
+
if (isSolidApi(callee, ON_CLEANUP_NAMES)) context.report({
|
|
924
1033
|
node: callee,
|
|
925
1034
|
messageId: "noCleanup"
|
|
926
1035
|
});
|
|
927
|
-
else if (
|
|
1036
|
+
else if (isSolidApi(callee, FLUSH_NAMES)) context.report({
|
|
928
1037
|
node: callee,
|
|
929
1038
|
messageId: "noFlush"
|
|
930
1039
|
});
|
|
931
|
-
else if (
|
|
1040
|
+
else if (isSolidApi(callee, PRIMITIVE_NAMES)) context.report({
|
|
932
1041
|
node: callee,
|
|
933
1042
|
messageId: "noPrimitives"
|
|
934
1043
|
});
|
|
935
|
-
else if (
|
|
1044
|
+
else if (isSolidApi(callee, FUNCTION_FORM_PRIMITIVE_NAMES) && hasProvablyFunctionFirstArgument(node, context)) context.report({
|
|
936
1045
|
node: callee,
|
|
937
1046
|
messageId: "noPrimitives"
|
|
938
1047
|
});
|
|
@@ -941,21 +1050,61 @@ var no_leaf_owner_operations_default = createRule({
|
|
|
941
1050
|
}
|
|
942
1051
|
});
|
|
943
1052
|
//#endregion
|
|
944
|
-
//#region src/
|
|
945
|
-
const
|
|
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([
|
|
1053
|
+
//#region src/analysis/computation-roles.ts
|
|
1054
|
+
const CALLBACK_FACTORIES = new Set([
|
|
954
1055
|
"createEffect",
|
|
955
1056
|
"createMemo",
|
|
956
|
-
"
|
|
1057
|
+
"createProjection",
|
|
1058
|
+
"createRenderEffect",
|
|
1059
|
+
"createSignal",
|
|
1060
|
+
"createStore",
|
|
1061
|
+
"createTrackedEffect",
|
|
1062
|
+
"onSettled"
|
|
957
1063
|
]);
|
|
1064
|
+
/** Classifies a function's exact, binding-proven Solid execution role. */
|
|
1065
|
+
function getComputationCallbackRole(node, context) {
|
|
1066
|
+
if (isComponent$1(node, context)) return "component";
|
|
1067
|
+
if (node.type === "FunctionDeclaration") return null;
|
|
1068
|
+
const call = node.parent;
|
|
1069
|
+
if (call?.type !== "CallExpression") return null;
|
|
1070
|
+
const argumentIndex = call.arguments.indexOf(node);
|
|
1071
|
+
if (argumentIndex < 0) return null;
|
|
1072
|
+
const services = context.options[0]?.typescriptEnabled ? getTypeAwareServices(context) : null;
|
|
1073
|
+
switch (resolveSolidCallee(call.callee, context, CALLBACK_FACTORIES) ?? (services == null ? null : resolveTypeAwareSolidCallee(call.callee, services, CALLBACK_FACTORIES))) {
|
|
1074
|
+
case "createMemo": return argumentIndex === 0 ? "memo-compute" : null;
|
|
1075
|
+
case "createProjection": return argumentIndex === 0 ? "projection-compute" : null;
|
|
1076
|
+
case "createSignal": return argumentIndex === 0 ? "derived-signal" : null;
|
|
1077
|
+
case "createStore": return argumentIndex === 0 && call.arguments.length >= 2 ? "derived-store" : null;
|
|
1078
|
+
case "createEffect":
|
|
1079
|
+
case "createRenderEffect":
|
|
1080
|
+
if (call.arguments.length < 2) return argumentIndex === 0 ? "legacy-effect-compute" : null;
|
|
1081
|
+
return argumentIndex === 0 ? "split-effect-compute" : argumentIndex === 1 ? "effect-apply" : null;
|
|
1082
|
+
case "createTrackedEffect": return argumentIndex === 0 ? "leaf-owner" : null;
|
|
1083
|
+
case "onSettled": return argumentIndex === 0 ? "settled" : null;
|
|
1084
|
+
default: return null;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
const OWNED_WRITE_FORBIDDEN_ROLES = new Set([
|
|
1088
|
+
"component",
|
|
1089
|
+
"derived-signal",
|
|
1090
|
+
"derived-store",
|
|
1091
|
+
"memo-compute",
|
|
1092
|
+
"projection-compute",
|
|
1093
|
+
"split-effect-compute"
|
|
1094
|
+
]);
|
|
1095
|
+
const ASYNC_TRACKED_COMPUTE_ROLES = new Set([
|
|
1096
|
+
"derived-signal",
|
|
1097
|
+
"derived-store",
|
|
1098
|
+
"legacy-effect-compute",
|
|
1099
|
+
"memo-compute",
|
|
1100
|
+
"projection-compute",
|
|
1101
|
+
"split-effect-compute"
|
|
1102
|
+
]);
|
|
1103
|
+
//#endregion
|
|
1104
|
+
//#region src/rules/no-owned-scope-writes.ts
|
|
1105
|
+
const OWNED_WRITE_FACTORIES = new Set(["createSignal", "createOptimistic"]);
|
|
958
1106
|
const ACTION_FACTORIES = new Set(["action"]);
|
|
1107
|
+
const REFRESH_NAMES = new Set(["refresh"]);
|
|
959
1108
|
function getPropertyName$1(node) {
|
|
960
1109
|
if (!node.computed && node.key.type === "Identifier") return node.key.name;
|
|
961
1110
|
if (node.key.type === "Literal" && typeof node.key.value === "string") return node.key.value;
|
|
@@ -967,12 +1116,8 @@ function hasOwnedWriteOption(node) {
|
|
|
967
1116
|
return options.properties.some((property) => property.type === "Property" && getPropertyName$1(property) === "ownedWrite" && property.value.type === "Literal" && property.value.value === true);
|
|
968
1117
|
}
|
|
969
1118
|
function isOwnedScopeFunction(node, context) {
|
|
970
|
-
|
|
971
|
-
|
|
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;
|
|
1119
|
+
const role = getComputationCallbackRole(node, context);
|
|
1120
|
+
return role != null && OWNED_WRITE_FORBIDDEN_ROLES.has(role);
|
|
976
1121
|
}
|
|
977
1122
|
var no_owned_scope_writes_default = createRule({
|
|
978
1123
|
name: "no-owned-scope-writes",
|
|
@@ -986,17 +1131,18 @@ var no_owned_scope_writes_default = createRule({
|
|
|
986
1131
|
}],
|
|
987
1132
|
messages: {
|
|
988
1133
|
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.",
|
|
1134
|
+
noOwnedScopeRefresh: "Calling refresh() inside a component or reactive compute scope is not allowed in Solid 2. Move the invalidation to an event handler, effect apply phase, or another imperative scope.",
|
|
989
1135
|
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
1136
|
}
|
|
991
1137
|
},
|
|
992
1138
|
defaultOptions: [{}],
|
|
993
1139
|
create(context) {
|
|
994
|
-
const
|
|
995
|
-
const
|
|
1140
|
+
const services = context.options[0]?.typescriptEnabled ? getTypeAwareServices(context) : null;
|
|
1141
|
+
const isSolidApi = (node, names) => resolveSolidCallee(node, context, names) != null || services != null && resolveTypeAwareSolidCallee(node, services, names) != null;
|
|
996
1142
|
const functionStack = [];
|
|
997
1143
|
const pendingCalls = [];
|
|
998
1144
|
const pendingDirectActions = [];
|
|
999
|
-
const
|
|
1145
|
+
const pendingRefreshes = [];
|
|
1000
1146
|
const onFunctionEnter = (node) => {
|
|
1001
1147
|
functionStack.push(node);
|
|
1002
1148
|
};
|
|
@@ -1004,30 +1150,6 @@ var no_owned_scope_writes_default = createRule({
|
|
|
1004
1150
|
functionStack.pop();
|
|
1005
1151
|
};
|
|
1006
1152
|
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
1153
|
FunctionDeclaration: onFunctionEnter,
|
|
1032
1154
|
FunctionExpression: onFunctionEnter,
|
|
1033
1155
|
ArrowFunctionExpression: onFunctionEnter,
|
|
@@ -1037,13 +1159,20 @@ var no_owned_scope_writes_default = createRule({
|
|
|
1037
1159
|
CallExpression(node) {
|
|
1038
1160
|
const enclosingFn = functionStack[functionStack.length - 1];
|
|
1039
1161
|
if (!enclosingFn) return;
|
|
1040
|
-
if (node.callee.type === "CallExpression" &&
|
|
1162
|
+
if (node.callee.type === "CallExpression" && isSolidApi(node.callee.callee, ACTION_FACTORIES)) {
|
|
1041
1163
|
pendingDirectActions.push({
|
|
1042
1164
|
call: node,
|
|
1043
1165
|
enclosingFn
|
|
1044
1166
|
});
|
|
1045
1167
|
return;
|
|
1046
1168
|
}
|
|
1169
|
+
if (isSolidApi(node.callee, REFRESH_NAMES)) {
|
|
1170
|
+
pendingRefreshes.push({
|
|
1171
|
+
call: node,
|
|
1172
|
+
enclosingFn
|
|
1173
|
+
});
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1047
1176
|
if (node.callee.type !== "Identifier") return;
|
|
1048
1177
|
pendingCalls.push({
|
|
1049
1178
|
callee: node.callee,
|
|
@@ -1061,16 +1190,17 @@ var no_owned_scope_writes_default = createRule({
|
|
|
1061
1190
|
return verdict;
|
|
1062
1191
|
};
|
|
1063
1192
|
for (const { callee, enclosingFn } of pendingCalls) {
|
|
1064
|
-
const
|
|
1065
|
-
if (
|
|
1193
|
+
const fact = getReactiveBindingFact(callee, context);
|
|
1194
|
+
if (fact?.role === "action" && isOwnedScope(enclosingFn)) {
|
|
1066
1195
|
context.report({
|
|
1067
1196
|
node: callee,
|
|
1068
1197
|
messageId: "noActionInOwnedScope"
|
|
1069
1198
|
});
|
|
1070
1199
|
continue;
|
|
1071
1200
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1201
|
+
if (fact?.role !== "setter") continue;
|
|
1202
|
+
const factoryCall = fact.declaration.init;
|
|
1203
|
+
if (factoryCall?.type === "CallExpression" && OWNED_WRITE_FACTORIES.has(fact.factory) && hasOwnedWriteOption(factoryCall)) continue;
|
|
1074
1204
|
if (isOwnedScope(enclosingFn)) context.report({
|
|
1075
1205
|
node: callee,
|
|
1076
1206
|
messageId: "noOwnedScopeWrite"
|
|
@@ -1080,6 +1210,10 @@ var no_owned_scope_writes_default = createRule({
|
|
|
1080
1210
|
node: call,
|
|
1081
1211
|
messageId: "noActionInOwnedScope"
|
|
1082
1212
|
});
|
|
1213
|
+
for (const { call, enclosingFn } of pendingRefreshes) if (isOwnedScope(enclosingFn)) context.report({
|
|
1214
|
+
node: call,
|
|
1215
|
+
messageId: "noOwnedScopeRefresh"
|
|
1216
|
+
});
|
|
1083
1217
|
}
|
|
1084
1218
|
};
|
|
1085
1219
|
}
|
|
@@ -1090,11 +1224,10 @@ const COMPUTE_FACTORIES = new Set([
|
|
|
1090
1224
|
"createMemo",
|
|
1091
1225
|
"createEffect",
|
|
1092
1226
|
"createRenderEffect",
|
|
1093
|
-
"createProjection"
|
|
1227
|
+
"createProjection",
|
|
1228
|
+
"createSignal",
|
|
1229
|
+
"createStore"
|
|
1094
1230
|
]);
|
|
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
1231
|
function hasGuaranteedAwait(node) {
|
|
1099
1232
|
if (node == null) return false;
|
|
1100
1233
|
switch (node.type) {
|
|
@@ -1125,19 +1258,6 @@ function statementGuaranteesAwait(stmt) {
|
|
|
1125
1258
|
default: return false;
|
|
1126
1259
|
}
|
|
1127
1260
|
}
|
|
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
1261
|
var no_reactive_read_after_await_default = createRule({
|
|
1142
1262
|
name: "no-reactive-read-after-await",
|
|
1143
1263
|
meta: {
|
|
@@ -1154,8 +1274,6 @@ var no_reactive_read_after_await_default = createRule({
|
|
|
1154
1274
|
create(context) {
|
|
1155
1275
|
const sourceCode = context.sourceCode;
|
|
1156
1276
|
const services = context.options[0]?.typescriptEnabled ?? false ? getTypeAwareServices(context) : null;
|
|
1157
|
-
const checker = services?.program.getTypeChecker() ?? null;
|
|
1158
|
-
const accessorVars = /* @__PURE__ */ new Set();
|
|
1159
1277
|
const computeCallbackCache = /* @__PURE__ */ new Map();
|
|
1160
1278
|
function isAfterAwait(call, fn) {
|
|
1161
1279
|
let current = call;
|
|
@@ -1173,77 +1291,236 @@ var no_reactive_read_after_await_default = createRule({
|
|
|
1173
1291
|
return false;
|
|
1174
1292
|
}
|
|
1175
1293
|
function isTypeAwareComputeCallback(fn) {
|
|
1176
|
-
if (!services
|
|
1294
|
+
if (!services) return false;
|
|
1177
1295
|
const parent = fn.parent;
|
|
1178
1296
|
if (parent?.type !== "CallExpression" || parent.arguments[0] !== fn) return false;
|
|
1179
|
-
|
|
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);
|
|
1297
|
+
return isTypeAwareSolidCallee(parent.callee, services, COMPUTE_FACTORIES);
|
|
1184
1298
|
}
|
|
1185
1299
|
function isComputeCallback(fn) {
|
|
1186
1300
|
const cached = computeCallbackCache.get(fn);
|
|
1187
1301
|
if (cached !== void 0) return cached;
|
|
1188
|
-
const result =
|
|
1302
|
+
const result = (() => {
|
|
1303
|
+
const role = getComputationCallbackRole(fn, context);
|
|
1304
|
+
return role != null && ASYNC_TRACKED_COMPUTE_ROLES.has(role);
|
|
1305
|
+
})() || isTypeAwareComputeCallback(fn);
|
|
1189
1306
|
computeCallbackCache.set(fn, result);
|
|
1190
1307
|
return result;
|
|
1191
1308
|
}
|
|
1192
1309
|
function isSolidAccessorCallee(callee) {
|
|
1193
|
-
if (!services
|
|
1194
|
-
|
|
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
|
-
});
|
|
1310
|
+
if (!services) return false;
|
|
1311
|
+
return isSolidAccessorExpression(callee, services);
|
|
1200
1312
|
}
|
|
1201
1313
|
function accessorReadName(call) {
|
|
1202
1314
|
if (call.callee.type === "Identifier") {
|
|
1203
|
-
|
|
1204
|
-
if (variable && accessorVars.has(variable)) return call.callee.name;
|
|
1315
|
+
if (getReactiveBindingFact(call.callee, context)?.role === "accessor") return call.callee.name;
|
|
1205
1316
|
}
|
|
1206
1317
|
if (services && isSolidAccessorCallee(call.callee)) return sourceCode.getText(call.callee);
|
|
1207
1318
|
return null;
|
|
1208
1319
|
}
|
|
1209
|
-
return {
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
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
|
-
};
|
|
1320
|
+
return { CallExpression(node) {
|
|
1321
|
+
const fn = getNearestFunctionAncestor(node);
|
|
1322
|
+
if (!fn || !fn.async || !isComputeCallback(fn) || !isAfterAwait(node, fn)) return;
|
|
1323
|
+
const name = accessorReadName(node);
|
|
1324
|
+
if (name == null) return;
|
|
1325
|
+
context.report({
|
|
1326
|
+
node,
|
|
1327
|
+
messageId: "reactiveReadAfterAwait",
|
|
1328
|
+
data: { name }
|
|
1329
|
+
});
|
|
1330
|
+
} };
|
|
1244
1331
|
}
|
|
1245
1332
|
});
|
|
1246
1333
|
//#endregion
|
|
1334
|
+
//#region src/analysis/control-flow-children.ts
|
|
1335
|
+
const CONTROL_FLOW_NAMES = new Set([
|
|
1336
|
+
"For",
|
|
1337
|
+
"Match",
|
|
1338
|
+
"Repeat",
|
|
1339
|
+
"Show"
|
|
1340
|
+
]);
|
|
1341
|
+
function importedControlFlowName(name, context) {
|
|
1342
|
+
if (name.type === "JSXIdentifier") {
|
|
1343
|
+
const variable = ASTUtils.findVariable(context.sourceCode.getScope(name), name.name);
|
|
1344
|
+
for (const def of variable?.defs ?? []) if (def.type === "ImportBinding" && def.node.type === "ImportSpecifier" && isSolidJsImportDeclaration(def.node.parent)) {
|
|
1345
|
+
const imported = def.node.imported.type === "Identifier" ? def.node.imported.name : def.node.imported.value;
|
|
1346
|
+
return CONTROL_FLOW_NAMES.has(imported) ? imported : null;
|
|
1347
|
+
}
|
|
1348
|
+
return null;
|
|
1349
|
+
}
|
|
1350
|
+
if (name.type === "JSXMemberExpression" && name.object.type === "JSXIdentifier" && name.property.type === "JSXIdentifier" && CONTROL_FLOW_NAMES.has(name.property.name)) return ASTUtils.findVariable(context.sourceCode.getScope(name.object), name.object.name)?.defs.some((def) => def.type === "ImportBinding" && def.node.type === "ImportNamespaceSpecifier" && isSolidJsImportDeclaration(def.node.parent)) === true ? name.property.name : null;
|
|
1351
|
+
return null;
|
|
1352
|
+
}
|
|
1353
|
+
function provenControlFlowName(name, context) {
|
|
1354
|
+
const imported = importedControlFlowName(name, context);
|
|
1355
|
+
if (imported != null) return imported;
|
|
1356
|
+
const services = context.options[0]?.typescriptEnabled ? getTypeAwareServices(context) : null;
|
|
1357
|
+
return services == null ? null : resolveTypeAwareSolidCallee(name, services, CONTROL_FLOW_NAMES);
|
|
1358
|
+
}
|
|
1359
|
+
function resolveFunction(value, context) {
|
|
1360
|
+
if (value == null || value.type === "SpreadElement") return null;
|
|
1361
|
+
if (isFunctionNode(value)) return value;
|
|
1362
|
+
const resolved = trace(value, context);
|
|
1363
|
+
return isFunctionNode(resolved) ? resolved : null;
|
|
1364
|
+
}
|
|
1365
|
+
function keyedMode(element, context) {
|
|
1366
|
+
const keyed = element.openingElement.attributes.find((attribute) => attribute.type === "JSXAttribute" && attribute.name.type === "JSXIdentifier" && attribute.name.name === "keyed");
|
|
1367
|
+
if (!keyed) return "absent";
|
|
1368
|
+
if (keyed.value == null) return "true";
|
|
1369
|
+
if (keyed.value.type !== "JSXExpressionContainer") return "unknown";
|
|
1370
|
+
const value = keyed.value.expression;
|
|
1371
|
+
if (value.type === "Literal" && value.value === true) return "true";
|
|
1372
|
+
if (value.type === "Literal" && value.value === false) return "false";
|
|
1373
|
+
if (resolveFunction(value, context) != null) return "custom";
|
|
1374
|
+
return "unknown";
|
|
1375
|
+
}
|
|
1376
|
+
function accessorParameters(fn, controlFlowName, element, context) {
|
|
1377
|
+
const identifiers = fn.params.map((param) => param.type === "Identifier" ? param : null);
|
|
1378
|
+
const mode = keyedMode(element, context);
|
|
1379
|
+
if (controlFlowName === "For") {
|
|
1380
|
+
if (mode === "false") return identifiers[0] ? [identifiers[0]] : [];
|
|
1381
|
+
if (mode === "custom") return identifiers.filter((param) => param != null);
|
|
1382
|
+
if (mode === "absent" || mode === "true") return identifiers[1] ? [identifiers[1]] : [];
|
|
1383
|
+
return [];
|
|
1384
|
+
}
|
|
1385
|
+
if (controlFlowName === "Show" || controlFlowName === "Match") return mode === "absent" || mode === "false" ? identifiers[0] ? [identifiers[0]] : [] : [];
|
|
1386
|
+
return [];
|
|
1387
|
+
}
|
|
1388
|
+
function functionsFromElement(element, context) {
|
|
1389
|
+
const controlFlowName = provenControlFlowName(element.openingElement.name, context);
|
|
1390
|
+
if (controlFlowName == null) return [];
|
|
1391
|
+
const functions = [];
|
|
1392
|
+
const add = (fn) => {
|
|
1393
|
+
functions.push({
|
|
1394
|
+
function: fn,
|
|
1395
|
+
accessorParameters: accessorParameters(fn, controlFlowName, element, context)
|
|
1396
|
+
});
|
|
1397
|
+
};
|
|
1398
|
+
for (const attribute of element.openingElement.attributes) if (attribute.type === "JSXAttribute" && attribute.name.type === "JSXIdentifier" && attribute.name.name === "children" && attribute.value?.type === "JSXExpressionContainer") {
|
|
1399
|
+
const fn = resolveFunction(attribute.value.expression, context);
|
|
1400
|
+
if (fn) add(fn);
|
|
1401
|
+
}
|
|
1402
|
+
for (const child of element.children) {
|
|
1403
|
+
if (child.type !== "JSXExpressionContainer") continue;
|
|
1404
|
+
const fn = resolveFunction(child.expression, context);
|
|
1405
|
+
if (fn) add(fn);
|
|
1406
|
+
}
|
|
1407
|
+
return functions;
|
|
1408
|
+
}
|
|
1409
|
+
function childNodes$1(node) {
|
|
1410
|
+
const result = [];
|
|
1411
|
+
for (const key in node) {
|
|
1412
|
+
if (key === "parent" || key === "tokens" || key === "comments") continue;
|
|
1413
|
+
const value = node[key];
|
|
1414
|
+
if (Array.isArray(value)) {
|
|
1415
|
+
for (const item of value) if (item != null && typeof item === "object" && typeof item.type === "string") result.push(item);
|
|
1416
|
+
} else if (value != null && typeof value === "object" && typeof value.type === "string") result.push(value);
|
|
1417
|
+
}
|
|
1418
|
+
return result;
|
|
1419
|
+
}
|
|
1420
|
+
/** Finds binding-proven Solid control-flow function children inside a component body. */
|
|
1421
|
+
function getControlFlowFunctionChildren(component, context) {
|
|
1422
|
+
const functions = [];
|
|
1423
|
+
const seenFunctions = /* @__PURE__ */ new Set();
|
|
1424
|
+
const stack = [component.body];
|
|
1425
|
+
while (stack.length > 0) {
|
|
1426
|
+
const node = stack.pop();
|
|
1427
|
+
if (node !== component.body && isFunctionNode(node)) continue;
|
|
1428
|
+
if (node.type === "JSXElement") for (const child of functionsFromElement(node, context)) {
|
|
1429
|
+
if (seenFunctions.has(child.function)) continue;
|
|
1430
|
+
seenFunctions.add(child.function);
|
|
1431
|
+
functions.push(child);
|
|
1432
|
+
stack.push(child.function.body);
|
|
1433
|
+
}
|
|
1434
|
+
stack.push(...childNodes$1(node));
|
|
1435
|
+
}
|
|
1436
|
+
return functions;
|
|
1437
|
+
}
|
|
1438
|
+
//#endregion
|
|
1439
|
+
//#region src/analysis/reactive-reads.ts
|
|
1440
|
+
const UNTRACK_NAMES$1 = new Set(["untrack"]);
|
|
1441
|
+
function getReactiveReadTypeServices(context) {
|
|
1442
|
+
return context.options[0]?.typescriptEnabled ? getTypeAwareServices(context) : null;
|
|
1443
|
+
}
|
|
1444
|
+
function variableFor(node, context) {
|
|
1445
|
+
return ASTUtils.findVariable(context.sourceCode.getScope(node), node);
|
|
1446
|
+
}
|
|
1447
|
+
function rootIdentifier(node) {
|
|
1448
|
+
let root = node;
|
|
1449
|
+
while (root.type === "MemberExpression") root = root.object;
|
|
1450
|
+
return root.type === "Identifier" ? root : null;
|
|
1451
|
+
}
|
|
1452
|
+
function variableRole(variable, environment, context) {
|
|
1453
|
+
if (variable == null) return null;
|
|
1454
|
+
if (environment.propsVariables?.has(variable)) return "props";
|
|
1455
|
+
if (environment.accessorVariables?.has(variable)) return "accessor";
|
|
1456
|
+
const fact = getReactiveBindingFactForVariable(variable, context);
|
|
1457
|
+
return fact?.role === "accessor" ? "accessor" : fact?.role === "store" ? "store" : null;
|
|
1458
|
+
}
|
|
1459
|
+
function childNodes(node) {
|
|
1460
|
+
const result = [];
|
|
1461
|
+
for (const key in node) {
|
|
1462
|
+
if (key === "parent" || key === "tokens" || key === "comments") continue;
|
|
1463
|
+
const value = node[key];
|
|
1464
|
+
if (Array.isArray(value)) {
|
|
1465
|
+
for (const item of value) if (item != null && typeof item === "object" && typeof item.type === "string") result.push(item);
|
|
1466
|
+
} else if (value != null && typeof value === "object" && typeof value.type === "string") result.push(value);
|
|
1467
|
+
}
|
|
1468
|
+
return result;
|
|
1469
|
+
}
|
|
1470
|
+
/** Finds the first provable reactive read executed directly in a structure-building scope. */
|
|
1471
|
+
function findReactiveRead(root, environment, context) {
|
|
1472
|
+
const stack = [root];
|
|
1473
|
+
while (stack.length > 0) {
|
|
1474
|
+
const node = stack.pop();
|
|
1475
|
+
if (isFunctionNode(node) || node.type === "JSXElement" || node.type === "JSXFragment") continue;
|
|
1476
|
+
if (node.type === "CallExpression") {
|
|
1477
|
+
if (bindsToSolid(node.callee, context, UNTRACK_NAMES$1)) continue;
|
|
1478
|
+
const callee = node.callee;
|
|
1479
|
+
if (callee.type === "Identifier") {
|
|
1480
|
+
if (variableRole(variableFor(callee, context), environment, context) === "accessor") return {
|
|
1481
|
+
kind: "accessor",
|
|
1482
|
+
node
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
if (environment.typescript != null && isSolidAccessorExpression(callee, environment.typescript)) return {
|
|
1486
|
+
kind: "accessor",
|
|
1487
|
+
node
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
if (node.type === "VariableDeclarator" && (node.id.type === "ObjectPattern" || node.id.type === "ArrayPattern") && node.init?.type === "Identifier") {
|
|
1491
|
+
const role = variableRole(variableFor(node.init, context), environment, context);
|
|
1492
|
+
if (role === "props" || role === "store") return {
|
|
1493
|
+
kind: role,
|
|
1494
|
+
node
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
if (node.type === "AssignmentExpression" && (node.left.type === "ObjectPattern" || node.left.type === "ArrayPattern") && node.right.type === "Identifier") {
|
|
1498
|
+
const role = variableRole(variableFor(node.right, context), environment, context);
|
|
1499
|
+
if (role === "props" || role === "store") return {
|
|
1500
|
+
kind: role,
|
|
1501
|
+
node
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
if (node.type === "MemberExpression") {
|
|
1505
|
+
const rootId = rootIdentifier(node);
|
|
1506
|
+
const role = rootId ? variableRole(variableFor(rootId, context), environment, context) : null;
|
|
1507
|
+
if (role === "props" || role === "store") return {
|
|
1508
|
+
kind: role,
|
|
1509
|
+
node
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
if (node.type === "SpreadElement" && node.argument.type === "Identifier") {
|
|
1513
|
+
const role = variableRole(variableFor(node.argument, context), environment, context);
|
|
1514
|
+
if (role === "props" || role === "store") return {
|
|
1515
|
+
kind: role,
|
|
1516
|
+
node
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
stack.push(...childNodes(node));
|
|
1520
|
+
}
|
|
1521
|
+
return null;
|
|
1522
|
+
}
|
|
1523
|
+
//#endregion
|
|
1247
1524
|
//#region src/rules/no-stale-props-alias.ts
|
|
1248
1525
|
const UNTRACK_NAMES = new Set(["untrack"]);
|
|
1249
1526
|
const PROPS_HELPER_NAMES = new Set(["merge", "omit"]);
|
|
@@ -1324,35 +1601,60 @@ var no_stale_props_alias_default = createRule({
|
|
|
1324
1601
|
name: "no-stale-props-alias",
|
|
1325
1602
|
meta: {
|
|
1326
1603
|
type: "problem",
|
|
1327
|
-
docs: { description: "Disallow
|
|
1604
|
+
docs: { description: "Disallow provable untracked reactive reads in component and Solid control-flow structure-building scopes." },
|
|
1328
1605
|
schema: [{
|
|
1329
1606
|
type: "object",
|
|
1330
1607
|
properties: { typescriptEnabled: { type: "boolean" } },
|
|
1331
1608
|
additionalProperties: false
|
|
1332
1609
|
}],
|
|
1333
|
-
messages: {
|
|
1610
|
+
messages: {
|
|
1611
|
+
stalePropsAlias: "`{{name}}` aliases a prop read outside tracking. Read from `props` in JSX or a tracked scope instead.",
|
|
1612
|
+
stalePropsRead: "This prop is read directly in a Solid control-flow function child, where it will not update. Read it inside returned JSX or an explicit tracked scope instead.",
|
|
1613
|
+
staleReactiveRead: "Reactive state is read directly in a component or Solid control-flow function child, where it will not update. Read it inside JSX, a reactive computation, or explicit `untrack()` instead."
|
|
1614
|
+
}
|
|
1334
1615
|
},
|
|
1335
1616
|
defaultOptions: [{}],
|
|
1336
1617
|
create(context) {
|
|
1337
1618
|
const sourceCode = context.sourceCode;
|
|
1619
|
+
const typescript = getReactiveReadTypeServices(context);
|
|
1338
1620
|
const checkFunction = (node) => {
|
|
1339
|
-
if (
|
|
1621
|
+
if (!isComponent$1(node, context)) return;
|
|
1340
1622
|
const props = node.params[0];
|
|
1341
|
-
if (node.params.length
|
|
1342
|
-
const
|
|
1343
|
-
if (
|
|
1344
|
-
|
|
1345
|
-
|
|
1623
|
+
if (node.params.length > 1 || props != null && props.type !== "Identifier") return;
|
|
1624
|
+
const propsVariables = /* @__PURE__ */ new Set();
|
|
1625
|
+
if (props?.type === "Identifier") {
|
|
1626
|
+
const propsVariable = ASTUtils.findVariable(sourceCode.getScope(props), props);
|
|
1627
|
+
if (propsVariable == null) return;
|
|
1628
|
+
propsVariables.add(propsVariable);
|
|
1629
|
+
}
|
|
1630
|
+
const environment = {
|
|
1631
|
+
propsVariables,
|
|
1632
|
+
typescript
|
|
1633
|
+
};
|
|
1634
|
+
for (const statement of node.body.type === "BlockStatement" ? node.body.body : []) {
|
|
1346
1635
|
if (statement.type === "VariableDeclaration") {
|
|
1347
1636
|
for (const declaration of statement.declarations) {
|
|
1348
1637
|
const aliasId = declaration.id;
|
|
1349
|
-
if (
|
|
1638
|
+
if (declaration.init == null) continue;
|
|
1639
|
+
if (aliasId.type !== "Identifier") {
|
|
1640
|
+
const read = findReactiveRead(declaration, environment, context);
|
|
1641
|
+
if (read) context.report({
|
|
1642
|
+
node: read.node,
|
|
1643
|
+
messageId: "staleReactiveRead"
|
|
1644
|
+
});
|
|
1645
|
+
continue;
|
|
1646
|
+
}
|
|
1350
1647
|
const isPropsLikeAlias = isPropsVariableIdentifier(declaration.init, propsVariables, sourceCode) || isPropsHelperAliasInit(declaration.init, propsVariables, sourceCode, context);
|
|
1351
1648
|
if (!expressionContainsPropsRead(declaration.init, propsVariables, sourceCode, context)) {
|
|
1352
1649
|
if (isPropsLikeAlias) {
|
|
1353
1650
|
const declared = getDeclaredIdentifierVariable(declaration, sourceCode);
|
|
1354
1651
|
if (declared != null && !hasWriteAfterInit(declared)) propsVariables.add(declared);
|
|
1355
1652
|
}
|
|
1653
|
+
const read = findReactiveRead(declaration.init, environment, context);
|
|
1654
|
+
if (read) context.report({
|
|
1655
|
+
node: read.node,
|
|
1656
|
+
messageId: "staleReactiveRead"
|
|
1657
|
+
});
|
|
1356
1658
|
continue;
|
|
1357
1659
|
}
|
|
1358
1660
|
const declared = getDeclaredIdentifierVariable(declaration, sourceCode);
|
|
@@ -1367,15 +1669,45 @@ var no_stale_props_alias_default = createRule({
|
|
|
1367
1669
|
continue;
|
|
1368
1670
|
}
|
|
1369
1671
|
const expression = getAssignmentExpression(statement);
|
|
1370
|
-
if (expression == null || expression.left.type !== "Identifier")
|
|
1672
|
+
if (expression == null || expression.left.type !== "Identifier") {
|
|
1673
|
+
const read = findReactiveRead(statement, environment, context);
|
|
1674
|
+
if (read) context.report({
|
|
1675
|
+
node: read.node,
|
|
1676
|
+
messageId: "staleReactiveRead"
|
|
1677
|
+
});
|
|
1678
|
+
continue;
|
|
1679
|
+
}
|
|
1371
1680
|
const assigned = expression.left;
|
|
1372
|
-
if (!expressionContainsPropsRead(expression.right, propsVariables, sourceCode, context))
|
|
1681
|
+
if (!expressionContainsPropsRead(expression.right, propsVariables, sourceCode, context)) {
|
|
1682
|
+
const read = findReactiveRead(expression.right, environment, context);
|
|
1683
|
+
if (read) context.report({
|
|
1684
|
+
node: read.node,
|
|
1685
|
+
messageId: "staleReactiveRead"
|
|
1686
|
+
});
|
|
1687
|
+
continue;
|
|
1688
|
+
}
|
|
1373
1689
|
context.report({
|
|
1374
1690
|
node: expression,
|
|
1375
1691
|
messageId: "stalePropsAlias",
|
|
1376
1692
|
data: { name: assigned.name }
|
|
1377
1693
|
});
|
|
1378
1694
|
}
|
|
1695
|
+
for (const child of getControlFlowFunctionChildren(node, context)) {
|
|
1696
|
+
const accessorVariables = /* @__PURE__ */ new Set();
|
|
1697
|
+
for (const parameter of child.accessorParameters) {
|
|
1698
|
+
const variable = ASTUtils.findVariable(sourceCode.getScope(parameter), parameter);
|
|
1699
|
+
if (variable) accessorVariables.add(variable);
|
|
1700
|
+
}
|
|
1701
|
+
const read = findReactiveRead(child.function.body, {
|
|
1702
|
+
propsVariables,
|
|
1703
|
+
accessorVariables,
|
|
1704
|
+
typescript
|
|
1705
|
+
}, context);
|
|
1706
|
+
if (read) context.report({
|
|
1707
|
+
node: read.node,
|
|
1708
|
+
messageId: read.kind === "props" ? "stalePropsRead" : "staleReactiveRead"
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1379
1711
|
};
|
|
1380
1712
|
return {
|
|
1381
1713
|
"FunctionDeclaration:exit": checkFunction,
|
|
@@ -1387,29 +1719,29 @@ var no_stale_props_alias_default = createRule({
|
|
|
1387
1719
|
//#endregion
|
|
1388
1720
|
//#region src/rules/no-untracked-read-in-effect-apply.ts
|
|
1389
1721
|
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
1722
|
const SAFE_HELPERS = new Set(["deep", "snapshot"]);
|
|
1394
1723
|
var no_untracked_read_in_effect_apply_default = createRule({
|
|
1395
1724
|
name: "no-untracked-read-in-effect-apply",
|
|
1396
1725
|
meta: {
|
|
1397
1726
|
type: "problem",
|
|
1398
1727
|
docs: { description: "Disallow reading reactive state (signal accessors or store proxies) in a createEffect apply callback, which runs untracked." },
|
|
1399
|
-
schema: [
|
|
1728
|
+
schema: [{
|
|
1729
|
+
type: "object",
|
|
1730
|
+
properties: { typescriptEnabled: { type: "boolean" } },
|
|
1731
|
+
additionalProperties: false
|
|
1732
|
+
}],
|
|
1400
1733
|
messages: {
|
|
1401
1734
|
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
1735
|
storeProxyRead: "Effect apply callbacks run untracked in Solid 2. Extract store properties in the compute phase or use `deep()` before reading them here."
|
|
1403
1736
|
}
|
|
1404
1737
|
},
|
|
1405
|
-
defaultOptions: [],
|
|
1738
|
+
defaultOptions: [{}],
|
|
1406
1739
|
create(context) {
|
|
1407
1740
|
const sourceCode = context.sourceCode;
|
|
1408
|
-
const
|
|
1741
|
+
const services = context.options[0]?.typescriptEnabled ? getTypeAwareServices(context) : null;
|
|
1742
|
+
const isEffectFactory = (node) => resolveSolidCallee(node, context, EFFECT_NAMES) != null || services != null && isTypeAwareSolidCallee(node, services, EFFECT_NAMES);
|
|
1409
1743
|
const applyCallbacks = /* @__PURE__ */ new Set();
|
|
1410
1744
|
const pendingAccessorCalls = [];
|
|
1411
|
-
const storeVars = /* @__PURE__ */ new Set();
|
|
1412
|
-
const storeShapes = /* @__PURE__ */ new Map();
|
|
1413
1745
|
const getMemberPath = (node) => {
|
|
1414
1746
|
const path = [];
|
|
1415
1747
|
let current = node;
|
|
@@ -1438,6 +1770,13 @@ var no_untracked_read_in_effect_apply_default = createRule({
|
|
|
1438
1770
|
}
|
|
1439
1771
|
return current.type === "ObjectExpression" || current.type === "ArrayExpression";
|
|
1440
1772
|
};
|
|
1773
|
+
const getStoreShape = (fact) => {
|
|
1774
|
+
const init = fact.declaration.init;
|
|
1775
|
+
if (init?.type !== "CallExpression") return null;
|
|
1776
|
+
const first = init.arguments[0];
|
|
1777
|
+
const seed = first != null && first.type !== "SpreadElement" && (first.type === "FunctionExpression" || first.type === "ArrowFunctionExpression") ? init.arguments[1] : first;
|
|
1778
|
+
return seed?.type === "ObjectExpression" ? seed : null;
|
|
1779
|
+
};
|
|
1441
1780
|
const getInlineFunction = (value) => {
|
|
1442
1781
|
if (value == null || value.type === "SpreadElement") return null;
|
|
1443
1782
|
if (isFunctionNode(value)) return value;
|
|
@@ -1479,21 +1818,18 @@ var no_untracked_read_in_effect_apply_default = createRule({
|
|
|
1479
1818
|
const isStoreSourceExpression = (node) => {
|
|
1480
1819
|
if (node == null) return false;
|
|
1481
1820
|
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
|
-
}
|
|
1821
|
+
if (node.type === "Identifier") return getReactiveBindingFact(node, context)?.role === "store";
|
|
1486
1822
|
if (node.type === "MemberExpression") {
|
|
1487
1823
|
const resolved = getMemberPath(node);
|
|
1488
1824
|
if (resolved == null) return false;
|
|
1489
|
-
const
|
|
1490
|
-
if (
|
|
1491
|
-
return pathLandsOnProxy(
|
|
1825
|
+
const fact = getReactiveBindingFact(resolved.root, context);
|
|
1826
|
+
if (fact?.role !== "store") return false;
|
|
1827
|
+
return pathLandsOnProxy(getStoreShape(fact), resolved.path) === true;
|
|
1492
1828
|
}
|
|
1493
1829
|
return false;
|
|
1494
1830
|
};
|
|
1495
1831
|
const checkStoreProxyInApply = (node) => {
|
|
1496
|
-
if (
|
|
1832
|
+
if (!isEffectFactory(node.callee) || node.arguments.length < 2) return;
|
|
1497
1833
|
const compute = getFunctionValue(node.arguments[0]);
|
|
1498
1834
|
const apply = getTracedApplyCallback(node.arguments[1]);
|
|
1499
1835
|
if (!compute || !apply) return;
|
|
@@ -1532,56 +1868,23 @@ var no_untracked_read_in_effect_apply_default = createRule({
|
|
|
1532
1868
|
}
|
|
1533
1869
|
};
|
|
1534
1870
|
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
1871
|
CallExpression(node) {
|
|
1569
|
-
if (
|
|
1872
|
+
if (isEffectFactory(node.callee) && node.arguments.length >= 2) {
|
|
1570
1873
|
const apply = getInlineApplyCallback(node.arguments[1]);
|
|
1571
1874
|
if (apply) applyCallbacks.add(apply);
|
|
1572
1875
|
}
|
|
1573
1876
|
checkStoreProxyInApply(node);
|
|
1574
|
-
|
|
1877
|
+
pendingAccessorCalls.push(node);
|
|
1575
1878
|
},
|
|
1576
1879
|
"Program:exit"() {
|
|
1577
1880
|
for (const node of pendingAccessorCalls) {
|
|
1578
1881
|
const callee = node.callee;
|
|
1579
|
-
|
|
1580
|
-
const
|
|
1581
|
-
if (
|
|
1882
|
+
const astAccessor = callee.type === "Identifier" && getReactiveBindingFact(callee, context)?.role === "accessor";
|
|
1883
|
+
const typedAccessor = services != null && isSolidAccessorExpression(callee, services);
|
|
1884
|
+
if ((astAccessor || typedAccessor) && findContainingApplyCallback(node) != null) context.report({
|
|
1582
1885
|
node,
|
|
1583
1886
|
messageId: "signalRead",
|
|
1584
|
-
data: { name: callee
|
|
1887
|
+
data: { name: sourceCode.getText(callee) }
|
|
1585
1888
|
});
|
|
1586
1889
|
}
|
|
1587
1890
|
}
|
|
@@ -1765,122 +2068,161 @@ const childrenIsMultilineSpaces = (node) => {
|
|
|
1765
2068
|
const children = node.parent.children;
|
|
1766
2069
|
return children.length === 1 && children[0].type === "JSXText" && children[0].value.includes("\n") && children[0].value.replace(/(?!\xA0)\s/g, "") === "";
|
|
1767
2070
|
};
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
"
|
|
1772
|
-
"
|
|
1773
|
-
"
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
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."
|
|
2071
|
+
var self_closing_comp_default = createRule({
|
|
2072
|
+
name: "self-closing-comp",
|
|
2073
|
+
meta: {
|
|
2074
|
+
type: "layout",
|
|
2075
|
+
docs: { description: "Disallow extra closing tags for components without children." },
|
|
2076
|
+
fixable: "code",
|
|
2077
|
+
schema: [{
|
|
2078
|
+
type: "object",
|
|
2079
|
+
properties: {
|
|
2080
|
+
component: {
|
|
2081
|
+
enum: ["all", "none"],
|
|
2082
|
+
type: "string"
|
|
2083
|
+
},
|
|
2084
|
+
html: {
|
|
2085
|
+
enum: [
|
|
2086
|
+
"all",
|
|
2087
|
+
"void",
|
|
2088
|
+
"none"
|
|
2089
|
+
],
|
|
2090
|
+
type: "string"
|
|
1808
2091
|
}
|
|
1809
2092
|
},
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
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
|
-
} };
|
|
2093
|
+
additionalProperties: false
|
|
2094
|
+
}],
|
|
2095
|
+
messages: {
|
|
2096
|
+
dontSelfClose: "This element should not be self-closing.",
|
|
2097
|
+
selfClose: "Empty components are self-closing."
|
|
2098
|
+
}
|
|
2099
|
+
},
|
|
2100
|
+
defaultOptions: [],
|
|
2101
|
+
create(context) {
|
|
2102
|
+
const sourceCode = context.sourceCode;
|
|
2103
|
+
const shouldBeSelfClosedWhenPossible = (node) => {
|
|
2104
|
+
if (isComponent(node)) return (context.options[0]?.component ?? "all") === "all";
|
|
2105
|
+
if (isHostElement(node)) switch (context.options[0]?.html ?? "all") {
|
|
2106
|
+
case "all": return true;
|
|
2107
|
+
case "void": return node.name.type === "JSXIdentifier" && isVoidDOMElementName(node.name.name);
|
|
2108
|
+
case "none": return false;
|
|
1846
2109
|
}
|
|
1847
|
-
|
|
2110
|
+
return true;
|
|
2111
|
+
};
|
|
2112
|
+
return { JSXOpeningElement(node) {
|
|
2113
|
+
if (!(childrenIsEmpty(node) || childrenIsMultilineSpaces(node))) return;
|
|
2114
|
+
const shouldSelfClose = shouldBeSelfClosedWhenPossible(node);
|
|
2115
|
+
if (shouldSelfClose && !node.selfClosing) context.report({
|
|
2116
|
+
node,
|
|
2117
|
+
messageId: "selfClose",
|
|
2118
|
+
fix: (fixer) => {
|
|
2119
|
+
const openingElementEnding = node.range[1] - 1;
|
|
2120
|
+
const closingElementEnding = node.parent.closingElement.range[1];
|
|
2121
|
+
return fixer.replaceTextRange([openingElementEnding, closingElementEnding], " />");
|
|
2122
|
+
}
|
|
2123
|
+
});
|
|
2124
|
+
else if (!shouldSelfClose && node.selfClosing) context.report({
|
|
2125
|
+
node,
|
|
2126
|
+
messageId: "dontSelfClose",
|
|
2127
|
+
fix: (fixer) => {
|
|
2128
|
+
const tagName = sourceCode.getText(node.name);
|
|
2129
|
+
const selfCloseEnding = node.range[1];
|
|
2130
|
+
const lastTokens = sourceCode.getLastTokens(node, { count: 3 });
|
|
2131
|
+
const range = [sourceCode.isSpaceBetween(lastTokens[0], lastTokens[1]) ? selfCloseEnding - 3 : selfCloseEnding - 2, selfCloseEnding];
|
|
2132
|
+
return fixer.replaceTextRange(range, `></${tagName}>`);
|
|
2133
|
+
}
|
|
2134
|
+
});
|
|
2135
|
+
} };
|
|
1848
2136
|
}
|
|
2137
|
+
});
|
|
2138
|
+
//#endregion
|
|
2139
|
+
//#region src/rule-catalog.ts
|
|
2140
|
+
/**
|
|
2141
|
+
* The authoritative internal catalog for public rule registration and recommendation policy.
|
|
2142
|
+
* Documentation remains hand-authored and is verified through public-interface invariant tests.
|
|
2143
|
+
*/
|
|
2144
|
+
const ruleCatalog = {
|
|
2145
|
+
"components-return-once": {
|
|
2146
|
+
rule: components_return_once_default,
|
|
2147
|
+
recommended: "warn",
|
|
2148
|
+
recommendedTypeChecked: ["warn", { typescriptEnabled: true }]
|
|
2149
|
+
},
|
|
2150
|
+
"jsx-no-duplicate-props": {
|
|
2151
|
+
rule: jsx_no_duplicate_props_default,
|
|
2152
|
+
recommended: "error"
|
|
2153
|
+
},
|
|
2154
|
+
"no-destructure": {
|
|
2155
|
+
rule: no_destructure_default,
|
|
2156
|
+
recommended: "warn",
|
|
2157
|
+
recommendedTypeChecked: ["warn", { typescriptEnabled: true }]
|
|
2158
|
+
},
|
|
2159
|
+
"no-leaf-owner-operations": {
|
|
2160
|
+
rule: no_leaf_owner_operations_default,
|
|
2161
|
+
recommended: "error",
|
|
2162
|
+
recommendedTypeChecked: ["error", { typescriptEnabled: true }]
|
|
2163
|
+
},
|
|
2164
|
+
"no-owned-scope-writes": {
|
|
2165
|
+
rule: no_owned_scope_writes_default,
|
|
2166
|
+
recommended: "error",
|
|
2167
|
+
recommendedTypeChecked: ["error", { typescriptEnabled: true }]
|
|
2168
|
+
},
|
|
2169
|
+
"no-reactive-read-after-await": {
|
|
2170
|
+
rule: no_reactive_read_after_await_default,
|
|
2171
|
+
recommended: "warn",
|
|
2172
|
+
recommendedTypeChecked: ["warn", { typescriptEnabled: true }]
|
|
2173
|
+
},
|
|
2174
|
+
"no-stale-props-alias": {
|
|
2175
|
+
rule: no_stale_props_alias_default,
|
|
2176
|
+
recommended: "warn",
|
|
2177
|
+
recommendedTypeChecked: ["warn", { typescriptEnabled: true }]
|
|
2178
|
+
},
|
|
2179
|
+
"no-untracked-read-in-effect-apply": {
|
|
2180
|
+
rule: no_untracked_read_in_effect_apply_default,
|
|
2181
|
+
recommended: "warn",
|
|
2182
|
+
recommendedTypeChecked: ["warn", { typescriptEnabled: true }]
|
|
2183
|
+
},
|
|
2184
|
+
"prefer-for": {
|
|
2185
|
+
rule: prefer_for_default,
|
|
2186
|
+
recommendedTypeChecked: ["warn", { typescriptEnabled: true }]
|
|
2187
|
+
},
|
|
2188
|
+
"prefer-show": {
|
|
2189
|
+
rule: prefer_show_default,
|
|
2190
|
+
recommended: "warn"
|
|
2191
|
+
},
|
|
2192
|
+
"self-closing-comp": {
|
|
2193
|
+
rule: self_closing_comp_default,
|
|
2194
|
+
recommended: "warn"
|
|
2195
|
+
}
|
|
2196
|
+
};
|
|
2197
|
+
const rules = Object.fromEntries(Object.entries(ruleCatalog).map(([name, entry]) => [name, entry.rule]));
|
|
2198
|
+
function buildRuleMap(policy) {
|
|
2199
|
+
return Object.fromEntries(Object.entries(ruleCatalog).flatMap(([name, entry]) => {
|
|
2200
|
+
const typedEntry = entry;
|
|
2201
|
+
const config = policy === "recommended" ? typedEntry.recommended : typedEntry.recommendedTypeChecked ?? typedEntry.recommended;
|
|
2202
|
+
return config == null ? [] : [[`solid/${name}`, config]];
|
|
2203
|
+
}));
|
|
2204
|
+
}
|
|
2205
|
+
const recommendedRules = buildRuleMap("recommended");
|
|
2206
|
+
const recommendedTypeCheckedRules = buildRuleMap("recommendedTypeChecked");
|
|
2207
|
+
//#endregion
|
|
2208
|
+
//#region src/plugin.ts
|
|
2209
|
+
const plugin = {
|
|
2210
|
+
meta: { name: "eslint-plugin-solid" },
|
|
2211
|
+
rules
|
|
1849
2212
|
};
|
|
1850
2213
|
//#endregion
|
|
1851
2214
|
//#region src/configs/recommended.ts
|
|
1852
2215
|
const recommended = {
|
|
1853
2216
|
name: "solid/recommended",
|
|
1854
2217
|
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
|
-
}
|
|
2218
|
+
rules: recommendedRules
|
|
1867
2219
|
};
|
|
1868
2220
|
//#endregion
|
|
1869
2221
|
//#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
2222
|
const recommendedTypeChecked = {
|
|
1878
2223
|
name: "solid/recommended-type-checked",
|
|
1879
2224
|
plugins: { solid: plugin },
|
|
1880
|
-
rules:
|
|
1881
|
-
...recommended.rules,
|
|
1882
|
-
...typeAwareOverrides
|
|
1883
|
-
}
|
|
2225
|
+
rules: recommendedTypeCheckedRules
|
|
1884
2226
|
};
|
|
1885
2227
|
//#endregion
|
|
1886
2228
|
//#region src/index.ts
|