@thomaflette/eslint-plugin-solid-2 0.1.0 → 0.2.1
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 +8 -3
- 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 +65 -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/dev-diagnostic-coverage.md +29 -0
- package/docs/jsx-no-duplicate-props.md +34 -0
- package/docs/no-destructure.md +92 -0
- package/docs/no-leaf-owner-operations.md +90 -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.cjs
CHANGED
|
@@ -78,34 +78,218 @@ function* jsxGetAllProps(props) {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
//#endregion
|
|
81
|
+
//#region src/analysis/typescript-semantics.ts
|
|
82
|
+
function getTypeAwareServices(context) {
|
|
83
|
+
const services = context.sourceCode.parserServices;
|
|
84
|
+
if (services?.program != null && services.esTreeNodeToTSNodeMap != null) return services;
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
function resolveTypeScriptAlias(symbol, checker) {
|
|
88
|
+
return symbol.flags & typescript.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
|
|
89
|
+
}
|
|
90
|
+
const SOLID_ACCESSOR_TYPE_NAMES = new Set(["Accessor", "SourceAccessor"]);
|
|
91
|
+
function isSolidOrigin(text) {
|
|
92
|
+
return text.includes("solid-js") || text.includes("@solidjs") || text.includes("solid-signals");
|
|
93
|
+
}
|
|
94
|
+
const solidSymbolCache = /* @__PURE__ */ new WeakMap();
|
|
95
|
+
function symbolIsFromSolid(symbol, checker) {
|
|
96
|
+
const cached = solidSymbolCache.get(symbol);
|
|
97
|
+
if (cached !== void 0) return cached;
|
|
98
|
+
const result = isSolidOrigin(checker.getFullyQualifiedName(symbol)) || (symbol.getDeclarations() ?? []).some((declaration) => isSolidOrigin(declaration.getSourceFile().fileName));
|
|
99
|
+
solidSymbolCache.set(symbol, result);
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
function collectTypeParts(type, acc = [], seen = /* @__PURE__ */ new Set()) {
|
|
103
|
+
if (seen.has(type)) return acc;
|
|
104
|
+
seen.add(type);
|
|
105
|
+
acc.push(type);
|
|
106
|
+
if (type.isUnion() || type.isIntersection()) for (const member of type.types) collectTypeParts(member, acc, seen);
|
|
107
|
+
return acc;
|
|
108
|
+
}
|
|
109
|
+
const solidAccessorNodeCache = /* @__PURE__ */ new WeakMap();
|
|
110
|
+
function isSolidAccessorExpression(node, services) {
|
|
111
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
|
|
112
|
+
if (!tsNode) return false;
|
|
113
|
+
const cached = solidAccessorNodeCache.get(tsNode);
|
|
114
|
+
if (cached !== void 0) return cached;
|
|
115
|
+
const checker = services.program.getTypeChecker();
|
|
116
|
+
const result = collectTypeParts(checker.getTypeAtLocation(tsNode)).some((part) => {
|
|
117
|
+
const alias = part.aliasSymbol;
|
|
118
|
+
return alias != null && SOLID_ACCESSOR_TYPE_NAMES.has(alias.getName()) && symbolIsFromSolid(alias, checker);
|
|
119
|
+
});
|
|
120
|
+
solidAccessorNodeCache.set(tsNode, result);
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
function resolveTypeAwareSolidCallee(node, services, canonicalNames) {
|
|
124
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
|
|
125
|
+
if (!tsNode) return null;
|
|
126
|
+
const checker = services.program.getTypeChecker();
|
|
127
|
+
const symbol = checker.getSymbolAtLocation(tsNode);
|
|
128
|
+
if (!symbol) return null;
|
|
129
|
+
const resolved = resolveTypeScriptAlias(symbol, checker);
|
|
130
|
+
const name = resolved.getName();
|
|
131
|
+
return canonicalNames.has(name) && symbolIsFromSolid(resolved, checker) ? name : null;
|
|
132
|
+
}
|
|
133
|
+
function isTypeAwareSolidCallee(node, services, canonicalNames) {
|
|
134
|
+
return resolveTypeAwareSolidCallee(node, services, canonicalNames) != null;
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/analysis/solid-bindings.ts
|
|
138
|
+
const SINGLE_RESULT_FACTORIES = new Map([
|
|
139
|
+
["action", "action"],
|
|
140
|
+
["createMemo", "accessor"],
|
|
141
|
+
["createProjection", "store"]
|
|
142
|
+
]);
|
|
143
|
+
const PAIR_RESULT_FACTORIES = new Map([
|
|
144
|
+
["createOptimistic", ["accessor", "setter"]],
|
|
145
|
+
["createOptimisticStore", ["store", "setter"]],
|
|
146
|
+
["createSignal", ["accessor", "setter"]],
|
|
147
|
+
["createStore", ["store", "setter"]]
|
|
148
|
+
]);
|
|
149
|
+
const REACTIVE_FACTORIES = new Set([...SINGLE_RESULT_FACTORIES.keys(), ...PAIR_RESULT_FACTORIES.keys()]);
|
|
150
|
+
/** Whether a node is an `import ... from "solid-js"` declaration. */
|
|
151
|
+
function isSolidJsImportDeclaration(node) {
|
|
152
|
+
return node?.type === "ImportDeclaration" && node.source.type === "Literal" && node.source.value === "solid-js";
|
|
153
|
+
}
|
|
154
|
+
function namespaceMemberName(node, context) {
|
|
155
|
+
if (node.type !== "MemberExpression" || node.optional || node.object.type !== "Identifier" || node.property.type !== "Identifier" || node.computed) return null;
|
|
156
|
+
const imported = _typescript_eslint_utils.ASTUtils.findVariable(context.sourceCode.getScope(node.object), node.object)?.defs.some((def) => def.type === "ImportBinding" && def.node.type === "ImportNamespaceSpecifier" && isSolidJsImportDeclaration(def.node.parent));
|
|
157
|
+
return {
|
|
158
|
+
name: node.property.name,
|
|
159
|
+
imported: imported === true
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Resolves a direct, aliased, or namespace-qualified expression to its canonical Solid export.
|
|
164
|
+
* Bare canonical names count only when truly unresolved (auto-import/global).
|
|
165
|
+
*/
|
|
166
|
+
function resolveSolidCallee(node, context, canonicalNames) {
|
|
167
|
+
const traced = trace(node, context);
|
|
168
|
+
if (traced.type === "Identifier" && canonicalNames.has(traced.name)) {
|
|
169
|
+
const variable = _typescript_eslint_utils.ASTUtils.findVariable(context.sourceCode.getScope(traced), traced);
|
|
170
|
+
return variable == null || variable.defs.length === 0 ? traced.name : null;
|
|
171
|
+
}
|
|
172
|
+
if (traced.type === "ImportSpecifier" && isSolidJsImportDeclaration(traced.parent)) {
|
|
173
|
+
const importedName = traced.imported.type === "Identifier" ? traced.imported.name : traced.imported.value;
|
|
174
|
+
return canonicalNames.has(importedName) ? importedName : null;
|
|
175
|
+
}
|
|
176
|
+
const member = namespaceMemberName(traced, context);
|
|
177
|
+
return member?.imported === true && canonicalNames.has(member.name) ? member.name : null;
|
|
178
|
+
}
|
|
179
|
+
function bindsToSolid(node, context, canonicalNames) {
|
|
180
|
+
return resolveSolidCallee(node, context, canonicalNames) != null;
|
|
181
|
+
}
|
|
182
|
+
function getChildNodes$1(node) {
|
|
183
|
+
const children = [];
|
|
184
|
+
for (const key in node) {
|
|
185
|
+
if (key === "parent" || key === "tokens" || key === "comments") continue;
|
|
186
|
+
const value = node[key];
|
|
187
|
+
if (Array.isArray(value)) {
|
|
188
|
+
for (const item of value) if (item != null && typeof item === "object" && typeof item.type === "string") children.push(item);
|
|
189
|
+
} else if (value != null && typeof value === "object" && typeof value.type === "string") children.push(value);
|
|
190
|
+
}
|
|
191
|
+
return children;
|
|
192
|
+
}
|
|
193
|
+
function declaredVariable(declarator, id, sourceCode) {
|
|
194
|
+
return sourceCode.scopeManager?.getDeclaredVariables(declarator).find((variable) => variable.name === id.name) ?? null;
|
|
195
|
+
}
|
|
196
|
+
const baseReactiveBindingCache = /* @__PURE__ */ new WeakMap();
|
|
197
|
+
const typeAwareReactiveBindingCache = /* @__PURE__ */ new WeakMap();
|
|
198
|
+
function buildReactiveBindingFacts(sourceCode, context) {
|
|
199
|
+
const facts = /* @__PURE__ */ new Map();
|
|
200
|
+
const aliases = [];
|
|
201
|
+
const services = context.options[0]?.typescriptEnabled ? getTypeAwareServices(context) : null;
|
|
202
|
+
const stack = [sourceCode.ast];
|
|
203
|
+
while (stack.length > 0) {
|
|
204
|
+
const node = stack.pop();
|
|
205
|
+
if (node.type === "VariableDeclarator") {
|
|
206
|
+
const declaration = node.parent;
|
|
207
|
+
if (declaration?.type === "VariableDeclaration" && declaration.kind === "const" && node.id.type === "Identifier" && node.init?.type === "Identifier") {
|
|
208
|
+
const target = declaredVariable(node, node.id, sourceCode);
|
|
209
|
+
const source = _typescript_eslint_utils.ASTUtils.findVariable(sourceCode.getScope(node.init), node.init);
|
|
210
|
+
if (target && source) aliases.push({
|
|
211
|
+
target,
|
|
212
|
+
source
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (node.init?.type === "CallExpression") {
|
|
216
|
+
const factory = resolveSolidCallee(node.init.callee, context, REACTIVE_FACTORIES) ?? (services == null ? null : resolveTypeAwareSolidCallee(node.init.callee, services, REACTIVE_FACTORIES));
|
|
217
|
+
if (factory != null) {
|
|
218
|
+
const singleRole = SINGLE_RESULT_FACTORIES.get(factory);
|
|
219
|
+
if (singleRole != null && node.id.type === "Identifier") {
|
|
220
|
+
const variable = declaredVariable(node, node.id, sourceCode);
|
|
221
|
+
if (variable) facts.set(variable, {
|
|
222
|
+
role: singleRole,
|
|
223
|
+
factory,
|
|
224
|
+
declaration: node,
|
|
225
|
+
tupleIndex: null
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
const pairRoles = PAIR_RESULT_FACTORIES.get(factory);
|
|
229
|
+
if (pairRoles != null && node.id.type === "ArrayPattern") for (const tupleIndex of [0, 1]) {
|
|
230
|
+
const id = node.id.elements[tupleIndex];
|
|
231
|
+
if (id?.type !== "Identifier") continue;
|
|
232
|
+
const variable = declaredVariable(node, id, sourceCode);
|
|
233
|
+
if (variable) facts.set(variable, {
|
|
234
|
+
role: pairRoles[tupleIndex],
|
|
235
|
+
factory,
|
|
236
|
+
declaration: node,
|
|
237
|
+
tupleIndex
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
stack.push(...getChildNodes$1(node));
|
|
244
|
+
}
|
|
245
|
+
let changed = true;
|
|
246
|
+
while (changed) {
|
|
247
|
+
changed = false;
|
|
248
|
+
for (const { target, source } of aliases) {
|
|
249
|
+
if (facts.has(target)) continue;
|
|
250
|
+
const fact = facts.get(source);
|
|
251
|
+
if (fact) {
|
|
252
|
+
facts.set(target, fact);
|
|
253
|
+
changed = true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return facts;
|
|
258
|
+
}
|
|
259
|
+
function getReactiveBindingFacts(context) {
|
|
260
|
+
const sourceCode = context.sourceCode;
|
|
261
|
+
const cache = context.options[0]?.typescriptEnabled === true ? typeAwareReactiveBindingCache : baseReactiveBindingCache;
|
|
262
|
+
const cached = cache.get(sourceCode);
|
|
263
|
+
if (cached) return cached;
|
|
264
|
+
const facts = buildReactiveBindingFacts(sourceCode, context);
|
|
265
|
+
cache.set(sourceCode, facts);
|
|
266
|
+
return facts;
|
|
267
|
+
}
|
|
268
|
+
function getReactiveBindingFact(node, context) {
|
|
269
|
+
const variable = _typescript_eslint_utils.ASTUtils.findVariable(context.sourceCode.getScope(node), node);
|
|
270
|
+
return variable ? getReactiveBindingFacts(context).get(variable) ?? null : null;
|
|
271
|
+
}
|
|
272
|
+
function getReactiveBindingFactForVariable(variable, context) {
|
|
273
|
+
return getReactiveBindingFacts(context).get(variable) ?? null;
|
|
274
|
+
}
|
|
275
|
+
//#endregion
|
|
81
276
|
//#region src/rules/create-rule.ts
|
|
82
277
|
const DOCS_BASE = "https://github.com/yumemi-thomas/eslint-plugin-solid-2/blob/main/docs";
|
|
83
278
|
const createRule = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `${DOCS_BASE}/${name}.md`);
|
|
84
279
|
//#endregion
|
|
85
|
-
//#region src/
|
|
280
|
+
//#region src/analysis/component-recognition.ts
|
|
86
281
|
const SOLID_COMPONENT_TYPES = new Set([
|
|
87
282
|
"Component",
|
|
88
283
|
"VoidComponent",
|
|
89
284
|
"ParentComponent",
|
|
90
285
|
"FlowComponent"
|
|
91
286
|
]);
|
|
92
|
-
/** Whether a node is an `import ... from "solid-js"` declaration. The single source of truth for
|
|
93
|
-
* "this binding comes from solid-js", shared by the callee, type-annotation, and alias resolvers. */
|
|
94
|
-
function isSolidJsImportDeclaration(node) {
|
|
95
|
-
return node?.type === "ImportDeclaration" && node.source.type === "Literal" && node.source.value === "solid-js";
|
|
96
|
-
}
|
|
97
287
|
function typeReferenceName(node) {
|
|
98
288
|
if (node?.type !== "TSTypeReference") return null;
|
|
99
289
|
if (node.typeName.type === "Identifier") return node.typeName.name;
|
|
100
290
|
if (node.typeName.type === "TSQualifiedName" && node.typeName.right.type === "Identifier") return node.typeName.right.name;
|
|
101
291
|
return null;
|
|
102
292
|
}
|
|
103
|
-
/**
|
|
104
|
-
* The identifier that carries the binding for a type reference: the type name itself for a plain
|
|
105
|
-
* `Component`, or the leftmost segment for a qualified `Solid.Component` (i.e. the namespace root).
|
|
106
|
-
* `qualified` distinguishes the two so the binding check can demand an explicit solid-js import for
|
|
107
|
-
* the qualified form (an unresolved `Solid.Component` is not a Solid auto-import).
|
|
108
|
-
*/
|
|
109
293
|
function typeReferenceRootIdentifier(node) {
|
|
110
294
|
if (node?.type !== "TSTypeReference") return null;
|
|
111
295
|
let name = node.typeName;
|
|
@@ -119,14 +303,6 @@ function typeReferenceRootIdentifier(node) {
|
|
|
119
303
|
qualified
|
|
120
304
|
} : null;
|
|
121
305
|
}
|
|
122
|
-
/**
|
|
123
|
-
* Whether a component type-name identifier actually refers to Solid's type — the type-annotation
|
|
124
|
-
* analogue of {@link resolveSolidCallee} (see ADR-0003). It counts as Solid when the name binds to a
|
|
125
|
-
* `solid-js` import, or (for a bare name only) is unresolved — an ambient/auto-import global. A name
|
|
126
|
-
* that binds to a local `type`/`interface` declaration or an import from another package is *not*
|
|
127
|
-
* Solid, which is what keeps `hasSolidComponentTypeAnnotation` from firing on same-named non-Solid
|
|
128
|
-
* types (a UI kit's `Component`, a local `VoidComponent`, a foreign `Ns.Component`).
|
|
129
|
-
*/
|
|
130
306
|
function typeNameBindsToSolid(id, qualified, context) {
|
|
131
307
|
let scope = context.sourceCode.getScope(id);
|
|
132
308
|
while (scope) {
|
|
@@ -141,35 +317,26 @@ function typeNameBindsToSolid(id, qualified, context) {
|
|
|
141
317
|
}
|
|
142
318
|
return !qualified;
|
|
143
319
|
}
|
|
144
|
-
/**
|
|
145
|
-
* Whether the function is declared with an explicit Solid component type annotation,
|
|
146
|
-
* e.g. `const C: Component<P> = (props) => ...`. This is a *sound* signal of component-hood:
|
|
147
|
-
* it is never true for a plain JSX-returning helper, and holds across file boundaries. The type
|
|
148
|
-
* must actually resolve to Solid's `Component`/`VoidComponent`/… (not a same-named local or
|
|
149
|
-
* third-party type) — see {@link typeNameBindsToSolid} and ADR-0002/0003.
|
|
150
|
-
*/
|
|
151
320
|
function hasSolidComponentTypeAnnotation(node, context) {
|
|
152
321
|
const parent = node.parent;
|
|
153
|
-
if (parent?.type
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
return false;
|
|
322
|
+
if (parent?.type !== "VariableDeclarator" || parent.id.type !== "Identifier") return false;
|
|
323
|
+
const annotation = parent.id.typeAnnotation?.typeAnnotation;
|
|
324
|
+
const name = typeReferenceName(annotation);
|
|
325
|
+
if (name == null || !SOLID_COMPONENT_TYPES.has(name)) return false;
|
|
326
|
+
const root = typeReferenceRootIdentifier(annotation);
|
|
327
|
+
return root != null && typeNameBindsToSolid(root.id, root.qualified, context);
|
|
161
328
|
}
|
|
162
329
|
const inFileComponentVarsCache = /* @__PURE__ */ new WeakMap();
|
|
163
330
|
function getInFileComponentVariables(sourceCode) {
|
|
164
331
|
const cached = inFileComponentVarsCache.get(sourceCode);
|
|
165
332
|
if (cached) return cached;
|
|
166
|
-
const
|
|
333
|
+
const variables = /* @__PURE__ */ new Set();
|
|
167
334
|
const stack = [sourceCode.ast];
|
|
168
335
|
while (stack.length > 0) {
|
|
169
336
|
const node = stack.pop();
|
|
170
337
|
if (node.type === "JSXOpeningElement" && node.name.type === "JSXIdentifier" && !isDOMElementName(node.name.name)) {
|
|
171
338
|
const variable = _typescript_eslint_utils.ASTUtils.findVariable(sourceCode.getScope(node.name), node.name.name);
|
|
172
|
-
if (variable
|
|
339
|
+
if (variable) variables.add(variable);
|
|
173
340
|
}
|
|
174
341
|
for (const key in node) {
|
|
175
342
|
if (key === "parent" || key === "tokens" || key === "comments") continue;
|
|
@@ -179,9 +346,73 @@ function getInFileComponentVariables(sourceCode) {
|
|
|
179
346
|
} else if (value != null && typeof value === "object" && typeof value.type === "string") stack.push(value);
|
|
180
347
|
}
|
|
181
348
|
}
|
|
182
|
-
inFileComponentVarsCache.set(sourceCode,
|
|
183
|
-
return
|
|
349
|
+
inFileComponentVarsCache.set(sourceCode, variables);
|
|
350
|
+
return variables;
|
|
351
|
+
}
|
|
352
|
+
const jsxTagSymbolsCache = /* @__PURE__ */ new WeakMap();
|
|
353
|
+
function getJsxTagSymbols(services) {
|
|
354
|
+
const cached = jsxTagSymbolsCache.get(services.program);
|
|
355
|
+
if (cached) return cached;
|
|
356
|
+
const checker = services.program.getTypeChecker();
|
|
357
|
+
const symbols = /* @__PURE__ */ new Set();
|
|
358
|
+
const visit = (node) => {
|
|
359
|
+
if (typescript.isJsxOpeningElement(node) || typescript.isJsxSelfClosingElement(node)) {
|
|
360
|
+
const symbol = checker.getSymbolAtLocation(node.tagName);
|
|
361
|
+
if (symbol) symbols.add(resolveTypeScriptAlias(symbol, checker));
|
|
362
|
+
}
|
|
363
|
+
typescript.forEachChild(node, visit);
|
|
364
|
+
};
|
|
365
|
+
for (const sourceFile of services.program.getSourceFiles()) if (!sourceFile.isDeclarationFile && !sourceFile.fileName.includes("/node_modules/")) visit(sourceFile);
|
|
366
|
+
jsxTagSymbolsCache.set(services.program, symbols);
|
|
367
|
+
return symbols;
|
|
368
|
+
}
|
|
369
|
+
function getFunctionSymbol(node, services) {
|
|
370
|
+
let nameNode;
|
|
371
|
+
if ((node.type === "FunctionDeclaration" || node.type === "FunctionExpression") && node.id != null) nameNode = node.id;
|
|
372
|
+
else if (node.parent?.type === "VariableDeclarator" && node.parent.id.type === "Identifier") nameNode = node.parent.id;
|
|
373
|
+
if (!nameNode) return;
|
|
374
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(nameNode);
|
|
375
|
+
return tsNode ? services.program.getTypeChecker().getSymbolAtLocation(tsNode) : void 0;
|
|
376
|
+
}
|
|
377
|
+
const baseEvidenceCache = /* @__PURE__ */ new WeakMap();
|
|
378
|
+
const typeAwareEvidenceCache = /* @__PURE__ */ new WeakMap();
|
|
379
|
+
function cachedEvidence(cache, sourceCode, node, compute) {
|
|
380
|
+
let byFunction = cache.get(sourceCode);
|
|
381
|
+
if (!byFunction) {
|
|
382
|
+
byFunction = /* @__PURE__ */ new WeakMap();
|
|
383
|
+
cache.set(sourceCode, byFunction);
|
|
384
|
+
}
|
|
385
|
+
const cached = byFunction.get(node);
|
|
386
|
+
if (cached !== void 0) return cached;
|
|
387
|
+
const evidence = compute();
|
|
388
|
+
byFunction.set(node, evidence);
|
|
389
|
+
return evidence;
|
|
390
|
+
}
|
|
391
|
+
function hasBaseEvidence(node, context) {
|
|
392
|
+
return cachedEvidence(baseEvidenceCache, context.sourceCode, node, () => {
|
|
393
|
+
if (hasSolidComponentTypeAnnotation(node, context)) return true;
|
|
394
|
+
const name = getFunctionName(node);
|
|
395
|
+
if (name == null) return false;
|
|
396
|
+
const variable = _typescript_eslint_utils.ASTUtils.findVariable(context.sourceCode.getScope(node), name);
|
|
397
|
+
return variable != null && getInFileComponentVariables(context.sourceCode).has(variable);
|
|
398
|
+
});
|
|
184
399
|
}
|
|
400
|
+
function hasTypeAwareEvidence(node, context) {
|
|
401
|
+
return cachedEvidence(typeAwareEvidenceCache, context.sourceCode, node, () => {
|
|
402
|
+
const services = getTypeAwareServices(context);
|
|
403
|
+
if (!services) return false;
|
|
404
|
+
const checker = services.program.getTypeChecker();
|
|
405
|
+
const symbol = getFunctionSymbol(node, services);
|
|
406
|
+
return symbol != null && getJsxTagSymbols(services).has(resolveTypeScriptAlias(symbol, checker));
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
/** Sound, self-indexing Solid component recognition. See ADR-0002. */
|
|
410
|
+
function isComponent$1(node, context) {
|
|
411
|
+
if (hasBaseEvidence(node, context)) return true;
|
|
412
|
+
return context.options[0]?.typescriptEnabled === true && hasTypeAwareEvidence(node, context);
|
|
413
|
+
}
|
|
414
|
+
//#endregion
|
|
415
|
+
//#region src/rules/solid-rule-utils.ts
|
|
185
416
|
function expressionCanYieldJsx(node) {
|
|
186
417
|
switch (node?.type) {
|
|
187
418
|
case "JSXElement":
|
|
@@ -226,43 +457,6 @@ function functionReturnsJsx(node) {
|
|
|
226
457
|
}
|
|
227
458
|
return false;
|
|
228
459
|
}
|
|
229
|
-
function getTypeAwareServices(context) {
|
|
230
|
-
const services = context.sourceCode.parserServices;
|
|
231
|
-
if (services?.program != null && services.esTreeNodeToTSNodeMap != null) return services;
|
|
232
|
-
return null;
|
|
233
|
-
}
|
|
234
|
-
const jsxTagSymbolsCache = /* @__PURE__ */ new WeakMap();
|
|
235
|
-
function resolveAlias(symbol, checker) {
|
|
236
|
-
return symbol.flags & typescript.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
|
|
237
|
-
}
|
|
238
|
-
function getJsxTagSymbols(services) {
|
|
239
|
-
const program = services.program;
|
|
240
|
-
const cached = jsxTagSymbolsCache.get(program);
|
|
241
|
-
if (cached) return cached;
|
|
242
|
-
const checker = program.getTypeChecker();
|
|
243
|
-
const symbols = /* @__PURE__ */ new Set();
|
|
244
|
-
const visit = (node) => {
|
|
245
|
-
if (typescript.isJsxOpeningElement(node) || typescript.isJsxSelfClosingElement(node)) {
|
|
246
|
-
const symbol = checker.getSymbolAtLocation(node.tagName);
|
|
247
|
-
if (symbol) symbols.add(resolveAlias(symbol, checker));
|
|
248
|
-
}
|
|
249
|
-
typescript.forEachChild(node, visit);
|
|
250
|
-
};
|
|
251
|
-
for (const sourceFile of program.getSourceFiles()) {
|
|
252
|
-
if (sourceFile.isDeclarationFile || sourceFile.fileName.includes("/node_modules/")) continue;
|
|
253
|
-
visit(sourceFile);
|
|
254
|
-
}
|
|
255
|
-
jsxTagSymbolsCache.set(program, symbols);
|
|
256
|
-
return symbols;
|
|
257
|
-
}
|
|
258
|
-
function getFunctionSymbol(node, services) {
|
|
259
|
-
let nameNode;
|
|
260
|
-
if ((node.type === "FunctionDeclaration" || node.type === "FunctionExpression") && node.id != null) nameNode = node.id;
|
|
261
|
-
else if (node.parent?.type === "VariableDeclarator" && node.parent.id.type === "Identifier") nameNode = node.parent.id;
|
|
262
|
-
if (!nameNode) return;
|
|
263
|
-
const tsNode = services.esTreeNodeToTSNodeMap.get(nameNode);
|
|
264
|
-
return tsNode ? services.program.getTypeChecker().getSymbolAtLocation(tsNode) : void 0;
|
|
265
|
-
}
|
|
266
460
|
/**
|
|
267
461
|
* Verdict on whether `node`'s type is array-like (an array or tuple — i.e. has a numeric index
|
|
268
462
|
* signature), considering every union member:
|
|
@@ -283,57 +477,6 @@ function getArrayReceiverVerdict(node, services) {
|
|
|
283
477
|
if (arrayMembers.length === members.length) return "array";
|
|
284
478
|
return arrayMembers.length === 0 ? "not-array" : "unknown";
|
|
285
479
|
}
|
|
286
|
-
/**
|
|
287
|
-
* Sound component detection (see docs/adr/0002). A function is treated as a Solid component only by
|
|
288
|
-
* signals that are never true for a plain helper — never the leaky "capitalized JSX-returning
|
|
289
|
-
* function" guess:
|
|
290
|
-
*
|
|
291
|
-
* - **Always:** it is annotated `Component`/`VoidComponent`/… or used as `<C/>` in the same file.
|
|
292
|
-
* - **Only when `typescriptEnabled` and type info is present:** additionally, its symbol is used as
|
|
293
|
-
* a JSX tag anywhere in the program (catches exported/cross-file components).
|
|
294
|
-
*
|
|
295
|
-
* Both paths are sound (zero false positives); type information is purely additive — it finds more
|
|
296
|
-
* real components, never flips a verdict on correct code. The trade-off is a tolerated false
|
|
297
|
-
* negative: an unannotated component used only in another file is not seen without type info.
|
|
298
|
-
*
|
|
299
|
-
* The module owns its own whole-file index and reads `typescriptEnabled` from `context`, so a
|
|
300
|
-
* caller needs nothing but the node — no `jsxComponentNames` set to build, thread, or defer. The
|
|
301
|
-
* index is complete on first query (see {@link getInFileComponentVariables}), so this is correct
|
|
302
|
-
* when called inline during traversal.
|
|
303
|
-
*/
|
|
304
|
-
function isComponent$1(node, context) {
|
|
305
|
-
if (hasSolidComponentTypeAnnotation(node, context)) return true;
|
|
306
|
-
const name = getFunctionName(node);
|
|
307
|
-
if (name != null) {
|
|
308
|
-
const fnVariable = _typescript_eslint_utils.ASTUtils.findVariable(context.sourceCode.getScope(node), name);
|
|
309
|
-
if (fnVariable != null && getInFileComponentVariables(context.sourceCode).has(fnVariable)) return true;
|
|
310
|
-
}
|
|
311
|
-
if (context.options[0]?.typescriptEnabled) {
|
|
312
|
-
const services = getTypeAwareServices(context);
|
|
313
|
-
if (services) {
|
|
314
|
-
const symbol = getFunctionSymbol(node, services);
|
|
315
|
-
if (symbol && getJsxTagSymbols(services).has(resolveAlias(symbol, services.program.getTypeChecker()))) return true;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
return false;
|
|
319
|
-
}
|
|
320
|
-
/**
|
|
321
|
-
* Whether `identifier` actually refers to the Solid API for one of `canonicalNames` — a name
|
|
322
|
-
* imported from "solid-js" (directly or aliased), a `const` alias of one (`const c = onCleanup`),
|
|
323
|
-
* or an unresolved global (auto-import). Returns false when it binds to the user's own declaration
|
|
324
|
-
* or an import from another package, which prevents false positives on same-named non-Solid
|
|
325
|
-
* functions (a stream library's `flush`, a state library's `createStore`, a local `onCleanup`, ...).
|
|
326
|
-
*
|
|
327
|
-
* Delegates to {@link resolveSolidCallee}, whose `trace` step resolves both aliased imports and
|
|
328
|
-
* `const` aliases — so no caller needs to pre-collect import aliases (see ADR-0003).
|
|
329
|
-
*/
|
|
330
|
-
function bindsToSolid(identifier, context, canonicalNames) {
|
|
331
|
-
return resolveSolidCallee(identifier, context, canonicalNames) != null;
|
|
332
|
-
}
|
|
333
|
-
/** Whether `node` is the `argumentIndex`th argument of a call whose callee binds to the Solid API. */
|
|
334
|
-
function isSolidApiCallbackArgument(node, argumentIndex, context, canonicalNames) {
|
|
335
|
-
return node.parent?.type === "CallExpression" && node.parent.arguments[argumentIndex] === node && node.parent.callee.type === "Identifier" && bindsToSolid(node.parent.callee, context, canonicalNames);
|
|
336
|
-
}
|
|
337
480
|
function getReturnedExpressions(node) {
|
|
338
481
|
if (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement") return [node.body];
|
|
339
482
|
if (node.body.type !== "BlockStatement") return [];
|
|
@@ -387,26 +530,6 @@ function getNearestFunctionAncestor(node) {
|
|
|
387
530
|
}
|
|
388
531
|
return null;
|
|
389
532
|
}
|
|
390
|
-
/**
|
|
391
|
-
* Resolves an identifier to the canonical Solid API name it refers to, or null. The `trace` step
|
|
392
|
-
* follows both aliased imports (`import { createEffect as fx }`) and `const` aliases to their
|
|
393
|
-
* origin, so an aliased Solid API resolves to its **canonical** name (`"createEffect"`), not the
|
|
394
|
-
* local alias. A bare canonical name is trusted only when unresolved (an auto-import/global); a
|
|
395
|
-
* name bound to a local declaration or a non-solid-js import resolves to null.
|
|
396
|
-
*/
|
|
397
|
-
function resolveSolidCallee(node, context, canonicalNames) {
|
|
398
|
-
if (node.type !== "Identifier") return null;
|
|
399
|
-
const traced = trace(node, context);
|
|
400
|
-
if (traced.type === "Identifier" && canonicalNames.has(traced.name)) {
|
|
401
|
-
const variable = _typescript_eslint_utils.ASTUtils.findVariable(context.sourceCode.getScope(traced), traced);
|
|
402
|
-
return variable == null || variable.defs.length === 0 ? traced.name : null;
|
|
403
|
-
}
|
|
404
|
-
if (traced.type === "ImportSpecifier" && isSolidJsImportDeclaration(traced.parent)) {
|
|
405
|
-
const importedName = traced.imported.type === "Identifier" ? traced.imported.name : traced.imported.value;
|
|
406
|
-
if (canonicalNames.has(importedName)) return importedName;
|
|
407
|
-
}
|
|
408
|
-
return null;
|
|
409
|
-
}
|
|
410
533
|
/** Whether any scope in the file already declares `name`. */
|
|
411
534
|
function isNameTaken(sourceCode, name) {
|
|
412
535
|
return sourceCode.scopeManager?.scopes.some((scope) => scope.set.has(name)) ?? false;
|
|
@@ -435,9 +558,6 @@ function getSolidImportFixes(context, fixer, names) {
|
|
|
435
558
|
}
|
|
436
559
|
//#endregion
|
|
437
560
|
//#region src/rules/components-return-once.ts
|
|
438
|
-
const ACCESSOR_FACTORIES$2 = new Set(["createMemo"]);
|
|
439
|
-
const PAIR_ACCESSOR_FACTORIES$2 = new Set(["createSignal", "createOptimistic"]);
|
|
440
|
-
const STORE_FACTORIES$1 = new Set(["createOptimisticStore", "createStore"]);
|
|
441
561
|
function getVariable(id, context) {
|
|
442
562
|
return _typescript_eslint_utils.ASTUtils.findVariable(context.sourceCode.getScope(id), id);
|
|
443
563
|
}
|
|
@@ -446,27 +566,6 @@ function getDeclarator(variable) {
|
|
|
446
566
|
const def = variable.defs[0];
|
|
447
567
|
return def?.type === "Variable" ? def.node : null;
|
|
448
568
|
}
|
|
449
|
-
/** Whether `variable` is `elements[0]` of an array-pattern declaration whose init calls a factory. */
|
|
450
|
-
function isPairElementZero(variable, factories, context) {
|
|
451
|
-
const declarator = getDeclarator(variable);
|
|
452
|
-
if (declarator?.id.type !== "ArrayPattern" || declarator.init?.type !== "CallExpression" || declarator.init.callee.type !== "Identifier") return false;
|
|
453
|
-
const first = declarator.id.elements[0];
|
|
454
|
-
return first?.type === "Identifier" && first.name === variable.name && bindsToSolid(declarator.init.callee, context, factories);
|
|
455
|
-
}
|
|
456
|
-
/** Whether calling `variable` reads a signal/memo accessor (follows `const c = count` aliases). */
|
|
457
|
-
function variableIsAccessor(variable, context, seen) {
|
|
458
|
-
if (seen.has(variable)) return false;
|
|
459
|
-
seen.add(variable);
|
|
460
|
-
if (isPairElementZero(variable, PAIR_ACCESSOR_FACTORIES$2, context)) return true;
|
|
461
|
-
const declarator = getDeclarator(variable);
|
|
462
|
-
if (declarator?.id.type !== "Identifier" || declarator.init == null) return false;
|
|
463
|
-
if (declarator.init.type === "CallExpression" && declarator.init.callee.type === "Identifier" && bindsToSolid(declarator.init.callee, context, ACCESSOR_FACTORIES$2)) return true;
|
|
464
|
-
if (declarator.init.type === "Identifier") {
|
|
465
|
-
const source = getVariable(declarator.init, context);
|
|
466
|
-
return source != null && variableIsAccessor(source, context, seen);
|
|
467
|
-
}
|
|
468
|
-
return false;
|
|
469
|
-
}
|
|
470
569
|
/**
|
|
471
570
|
* Whether evaluating `node` provably performs a reactive read from `componentFn`'s perspective.
|
|
472
571
|
* Skips nested functions (not evaluated by the guard) and JSX (reads there compile to tracked
|
|
@@ -480,7 +579,7 @@ function containsReactiveRead(node, componentFn, context, seen = /* @__PURE__ */
|
|
|
480
579
|
if (root.type !== "Identifier") return false;
|
|
481
580
|
const variable = getVariable(root, context);
|
|
482
581
|
if (variable == null) return false;
|
|
483
|
-
if (isPropsParameter(variable) ||
|
|
582
|
+
if (isPropsParameter(variable) || getReactiveBindingFactForVariable(variable, context)?.role === "store") return true;
|
|
484
583
|
return derivedConstIsReactive(variable);
|
|
485
584
|
};
|
|
486
585
|
const derivedConstIsReactive = (variable) => {
|
|
@@ -495,7 +594,7 @@ function containsReactiveRead(node, componentFn, context, seen = /* @__PURE__ */
|
|
|
495
594
|
if (isFunctionNode(current) || current.type === "JSXElement" || current.type === "JSXFragment") continue;
|
|
496
595
|
if (current.type === "CallExpression" && current.callee.type === "Identifier") {
|
|
497
596
|
const callee = getVariable(current.callee, context);
|
|
498
|
-
if (callee != null &&
|
|
597
|
+
if (callee != null && getReactiveBindingFactForVariable(callee, context)?.role === "accessor") return true;
|
|
499
598
|
}
|
|
500
599
|
if (current.type === "MemberExpression" && memberRootIsReactive(current)) return true;
|
|
501
600
|
if (current.type === "Identifier") {
|
|
@@ -908,24 +1007,34 @@ var no_leaf_owner_operations_default = createRule({
|
|
|
908
1007
|
meta: {
|
|
909
1008
|
type: "problem",
|
|
910
1009
|
docs: { description: "Disallow onCleanup, reactive-primitive creation, and flush() inside the leaf owners createTrackedEffect and onSettled." },
|
|
911
|
-
schema: [
|
|
1010
|
+
schema: [{
|
|
1011
|
+
type: "object",
|
|
1012
|
+
properties: { typescriptEnabled: { type: "boolean" } },
|
|
1013
|
+
additionalProperties: false
|
|
1014
|
+
}],
|
|
912
1015
|
messages: {
|
|
913
1016
|
noCleanup: "Cannot use `onCleanup` inside `createTrackedEffect` or `onSettled`; return a cleanup function instead.",
|
|
914
1017
|
noFlush: "Cannot call `flush()` from inside `createTrackedEffect` or `onSettled`; schedule work outside instead.",
|
|
915
1018
|
noPrimitives: "Cannot create reactive primitives inside `createTrackedEffect` or `onSettled`; move them to the component body or another owner."
|
|
916
1019
|
}
|
|
917
1020
|
},
|
|
918
|
-
defaultOptions: [],
|
|
1021
|
+
defaultOptions: [{}],
|
|
919
1022
|
create(context) {
|
|
1023
|
+
const services = context.options[0]?.typescriptEnabled ? getTypeAwareServices(context) : null;
|
|
1024
|
+
const isSolidApi = (node, names) => resolveSolidCallee(node, context, names) != null || services != null && resolveTypeAwareSolidCallee(node, services, names) != null;
|
|
1025
|
+
const isSolidCallbackArgument = (node, argumentIndex, names) => {
|
|
1026
|
+
const call = node.parent;
|
|
1027
|
+
return call?.type === "CallExpression" && call.arguments[argumentIndex] === node && isSolidApi(call.callee, names);
|
|
1028
|
+
};
|
|
920
1029
|
const forbiddenStack = [];
|
|
921
1030
|
const onFunctionEnter = (node) => {
|
|
922
1031
|
if (node.type !== "FunctionDeclaration" && node.type !== "FunctionExpression" && node.type !== "ArrowFunctionExpression") return;
|
|
923
|
-
const trackedEffect =
|
|
1032
|
+
const trackedEffect = isSolidCallbackArgument(node, 0, TRACKED_EFFECT_NAMES);
|
|
924
1033
|
let ownerBackedSettled = false;
|
|
925
|
-
if (
|
|
1034
|
+
if (isSolidCallbackArgument(node, 0, ON_SETTLED_NAMES)) {
|
|
926
1035
|
const settledCall = node.parent;
|
|
927
1036
|
const enclosing = getNearestFunctionAncestor(settledCall);
|
|
928
|
-
ownerBackedSettled = enclosing != null && (isComponent$1(enclosing, context) ||
|
|
1037
|
+
ownerBackedSettled = enclosing != null && (isComponent$1(enclosing, context) || isSolidCallbackArgument(enclosing, 0, OWNER_CALLBACK_NAMES) || isSolidCallbackArgument(enclosing, 1, OWNER_CALLBACK_NAMES));
|
|
929
1038
|
}
|
|
930
1039
|
if (trackedEffect || ownerBackedSettled) forbiddenStack.push(node);
|
|
931
1040
|
};
|
|
@@ -941,21 +1050,21 @@ var no_leaf_owner_operations_default = createRule({
|
|
|
941
1050
|
"ArrowFunctionExpression:exit": onFunctionExit,
|
|
942
1051
|
CallExpression(node) {
|
|
943
1052
|
const currentForbidden = forbiddenStack[forbiddenStack.length - 1];
|
|
944
|
-
if (!currentForbidden || getNearestFunctionAncestor(node) !== currentForbidden
|
|
1053
|
+
if (!currentForbidden || getNearestFunctionAncestor(node) !== currentForbidden) return;
|
|
945
1054
|
const callee = node.callee;
|
|
946
|
-
if (
|
|
1055
|
+
if (isSolidApi(callee, ON_CLEANUP_NAMES)) context.report({
|
|
947
1056
|
node: callee,
|
|
948
1057
|
messageId: "noCleanup"
|
|
949
1058
|
});
|
|
950
|
-
else if (
|
|
1059
|
+
else if (isSolidApi(callee, FLUSH_NAMES)) context.report({
|
|
951
1060
|
node: callee,
|
|
952
1061
|
messageId: "noFlush"
|
|
953
1062
|
});
|
|
954
|
-
else if (
|
|
1063
|
+
else if (isSolidApi(callee, PRIMITIVE_NAMES)) context.report({
|
|
955
1064
|
node: callee,
|
|
956
1065
|
messageId: "noPrimitives"
|
|
957
1066
|
});
|
|
958
|
-
else if (
|
|
1067
|
+
else if (isSolidApi(callee, FUNCTION_FORM_PRIMITIVE_NAMES) && hasProvablyFunctionFirstArgument(node, context)) context.report({
|
|
959
1068
|
node: callee,
|
|
960
1069
|
messageId: "noPrimitives"
|
|
961
1070
|
});
|
|
@@ -964,21 +1073,61 @@ var no_leaf_owner_operations_default = createRule({
|
|
|
964
1073
|
}
|
|
965
1074
|
});
|
|
966
1075
|
//#endregion
|
|
967
|
-
//#region src/
|
|
968
|
-
const
|
|
969
|
-
"createOptimistic",
|
|
970
|
-
"createOptimisticStore",
|
|
971
|
-
"createSignal",
|
|
972
|
-
"createStore"
|
|
973
|
-
]);
|
|
974
|
-
const OWNED_WRITE_FACTORIES = new Set(["createSignal", "createOptimistic"]);
|
|
975
|
-
const EFFECT_FACTORIES = new Set(["createEffect", "createRenderEffect"]);
|
|
976
|
-
const COMPUTE_FACTORIES$1 = new Set([
|
|
1076
|
+
//#region src/analysis/computation-roles.ts
|
|
1077
|
+
const CALLBACK_FACTORIES = new Set([
|
|
977
1078
|
"createEffect",
|
|
978
1079
|
"createMemo",
|
|
979
|
-
"
|
|
1080
|
+
"createProjection",
|
|
1081
|
+
"createRenderEffect",
|
|
1082
|
+
"createSignal",
|
|
1083
|
+
"createStore",
|
|
1084
|
+
"createTrackedEffect",
|
|
1085
|
+
"onSettled"
|
|
980
1086
|
]);
|
|
1087
|
+
/** Classifies a function's exact, binding-proven Solid execution role. */
|
|
1088
|
+
function getComputationCallbackRole(node, context) {
|
|
1089
|
+
if (isComponent$1(node, context)) return "component";
|
|
1090
|
+
if (node.type === "FunctionDeclaration") return null;
|
|
1091
|
+
const call = node.parent;
|
|
1092
|
+
if (call?.type !== "CallExpression") return null;
|
|
1093
|
+
const argumentIndex = call.arguments.indexOf(node);
|
|
1094
|
+
if (argumentIndex < 0) return null;
|
|
1095
|
+
const services = context.options[0]?.typescriptEnabled ? getTypeAwareServices(context) : null;
|
|
1096
|
+
switch (resolveSolidCallee(call.callee, context, CALLBACK_FACTORIES) ?? (services == null ? null : resolveTypeAwareSolidCallee(call.callee, services, CALLBACK_FACTORIES))) {
|
|
1097
|
+
case "createMemo": return argumentIndex === 0 ? "memo-compute" : null;
|
|
1098
|
+
case "createProjection": return argumentIndex === 0 ? "projection-compute" : null;
|
|
1099
|
+
case "createSignal": return argumentIndex === 0 ? "derived-signal" : null;
|
|
1100
|
+
case "createStore": return argumentIndex === 0 && call.arguments.length >= 2 ? "derived-store" : null;
|
|
1101
|
+
case "createEffect":
|
|
1102
|
+
case "createRenderEffect":
|
|
1103
|
+
if (call.arguments.length < 2) return argumentIndex === 0 ? "legacy-effect-compute" : null;
|
|
1104
|
+
return argumentIndex === 0 ? "split-effect-compute" : argumentIndex === 1 ? "effect-apply" : null;
|
|
1105
|
+
case "createTrackedEffect": return argumentIndex === 0 ? "leaf-owner" : null;
|
|
1106
|
+
case "onSettled": return argumentIndex === 0 ? "settled" : null;
|
|
1107
|
+
default: return null;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
const OWNED_WRITE_FORBIDDEN_ROLES = new Set([
|
|
1111
|
+
"component",
|
|
1112
|
+
"derived-signal",
|
|
1113
|
+
"derived-store",
|
|
1114
|
+
"memo-compute",
|
|
1115
|
+
"projection-compute",
|
|
1116
|
+
"split-effect-compute"
|
|
1117
|
+
]);
|
|
1118
|
+
const ASYNC_TRACKED_COMPUTE_ROLES = new Set([
|
|
1119
|
+
"derived-signal",
|
|
1120
|
+
"derived-store",
|
|
1121
|
+
"legacy-effect-compute",
|
|
1122
|
+
"memo-compute",
|
|
1123
|
+
"projection-compute",
|
|
1124
|
+
"split-effect-compute"
|
|
1125
|
+
]);
|
|
1126
|
+
//#endregion
|
|
1127
|
+
//#region src/rules/no-owned-scope-writes.ts
|
|
1128
|
+
const OWNED_WRITE_FACTORIES = new Set(["createSignal", "createOptimistic"]);
|
|
981
1129
|
const ACTION_FACTORIES = new Set(["action"]);
|
|
1130
|
+
const REFRESH_NAMES = new Set(["refresh"]);
|
|
982
1131
|
function getPropertyName$1(node) {
|
|
983
1132
|
if (!node.computed && node.key.type === "Identifier") return node.key.name;
|
|
984
1133
|
if (node.key.type === "Literal" && typeof node.key.value === "string") return node.key.value;
|
|
@@ -990,12 +1139,8 @@ function hasOwnedWriteOption(node) {
|
|
|
990
1139
|
return options.properties.some((property) => property.type === "Property" && getPropertyName$1(property) === "ownedWrite" && property.value.type === "Literal" && property.value.value === true);
|
|
991
1140
|
}
|
|
992
1141
|
function isOwnedScopeFunction(node, context) {
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
const callee = node.parent.callee;
|
|
996
|
-
if (!bindsToSolid(callee, context, COMPUTE_FACTORIES$1)) return false;
|
|
997
|
-
if (bindsToSolid(callee, context, EFFECT_FACTORIES)) return node.parent.arguments.length >= 2;
|
|
998
|
-
return true;
|
|
1142
|
+
const role = getComputationCallbackRole(node, context);
|
|
1143
|
+
return role != null && OWNED_WRITE_FORBIDDEN_ROLES.has(role);
|
|
999
1144
|
}
|
|
1000
1145
|
var no_owned_scope_writes_default = createRule({
|
|
1001
1146
|
name: "no-owned-scope-writes",
|
|
@@ -1009,17 +1154,18 @@ var no_owned_scope_writes_default = createRule({
|
|
|
1009
1154
|
}],
|
|
1010
1155
|
messages: {
|
|
1011
1156
|
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.",
|
|
1157
|
+
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.",
|
|
1012
1158
|
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."
|
|
1013
1159
|
}
|
|
1014
1160
|
},
|
|
1015
1161
|
defaultOptions: [{}],
|
|
1016
1162
|
create(context) {
|
|
1017
|
-
const
|
|
1018
|
-
const
|
|
1163
|
+
const services = context.options[0]?.typescriptEnabled ? getTypeAwareServices(context) : null;
|
|
1164
|
+
const isSolidApi = (node, names) => resolveSolidCallee(node, context, names) != null || services != null && resolveTypeAwareSolidCallee(node, services, names) != null;
|
|
1019
1165
|
const functionStack = [];
|
|
1020
1166
|
const pendingCalls = [];
|
|
1021
1167
|
const pendingDirectActions = [];
|
|
1022
|
-
const
|
|
1168
|
+
const pendingRefreshes = [];
|
|
1023
1169
|
const onFunctionEnter = (node) => {
|
|
1024
1170
|
functionStack.push(node);
|
|
1025
1171
|
};
|
|
@@ -1027,30 +1173,6 @@ var no_owned_scope_writes_default = createRule({
|
|
|
1027
1173
|
functionStack.pop();
|
|
1028
1174
|
};
|
|
1029
1175
|
return {
|
|
1030
|
-
VariableDeclarator(node) {
|
|
1031
|
-
if (node.id.type === "Identifier" && node.init?.type === "CallExpression" && resolveSolidCallee(node.init.callee, context, ACTION_FACTORIES) != null) {
|
|
1032
|
-
const actionId = node.id;
|
|
1033
|
-
const variable = sourceCode.scopeManager?.getDeclaredVariables(node).find((declared) => declared.name === actionId.name);
|
|
1034
|
-
if (variable) actionVariables.add(variable);
|
|
1035
|
-
return;
|
|
1036
|
-
}
|
|
1037
|
-
if (node.id.type === "Identifier" && node.init?.type === "Identifier") {
|
|
1038
|
-
const source = _typescript_eslint_utils.ASTUtils.findVariable(sourceCode.getScope(node.init), node.init);
|
|
1039
|
-
if (source && actionVariables.has(source)) {
|
|
1040
|
-
const aliasId = node.id;
|
|
1041
|
-
const variable = sourceCode.scopeManager?.getDeclaredVariables(node).find((declared) => declared.name === aliasId.name);
|
|
1042
|
-
if (variable) actionVariables.add(variable);
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
if (node.id.type !== "ArrayPattern" || node.init?.type !== "CallExpression" || node.init.callee.type !== "Identifier") return;
|
|
1046
|
-
const canonical = resolveSolidCallee(node.init.callee, context, SETTER_FACTORIES);
|
|
1047
|
-
if (canonical == null) return;
|
|
1048
|
-
const setterElement = node.id.elements[1];
|
|
1049
|
-
if (setterElement?.type !== "Identifier") return;
|
|
1050
|
-
const variable = sourceCode.scopeManager?.getDeclaredVariables(node).find((declared) => declared.name === setterElement.name);
|
|
1051
|
-
if (!variable) return;
|
|
1052
|
-
setterVariables.set(variable, { allowOwnedWrite: OWNED_WRITE_FACTORIES.has(canonical) && hasOwnedWriteOption(node.init) });
|
|
1053
|
-
},
|
|
1054
1176
|
FunctionDeclaration: onFunctionEnter,
|
|
1055
1177
|
FunctionExpression: onFunctionEnter,
|
|
1056
1178
|
ArrowFunctionExpression: onFunctionEnter,
|
|
@@ -1060,13 +1182,20 @@ var no_owned_scope_writes_default = createRule({
|
|
|
1060
1182
|
CallExpression(node) {
|
|
1061
1183
|
const enclosingFn = functionStack[functionStack.length - 1];
|
|
1062
1184
|
if (!enclosingFn) return;
|
|
1063
|
-
if (node.callee.type === "CallExpression" &&
|
|
1185
|
+
if (node.callee.type === "CallExpression" && isSolidApi(node.callee.callee, ACTION_FACTORIES)) {
|
|
1064
1186
|
pendingDirectActions.push({
|
|
1065
1187
|
call: node,
|
|
1066
1188
|
enclosingFn
|
|
1067
1189
|
});
|
|
1068
1190
|
return;
|
|
1069
1191
|
}
|
|
1192
|
+
if (isSolidApi(node.callee, REFRESH_NAMES)) {
|
|
1193
|
+
pendingRefreshes.push({
|
|
1194
|
+
call: node,
|
|
1195
|
+
enclosingFn
|
|
1196
|
+
});
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1070
1199
|
if (node.callee.type !== "Identifier") return;
|
|
1071
1200
|
pendingCalls.push({
|
|
1072
1201
|
callee: node.callee,
|
|
@@ -1084,16 +1213,17 @@ var no_owned_scope_writes_default = createRule({
|
|
|
1084
1213
|
return verdict;
|
|
1085
1214
|
};
|
|
1086
1215
|
for (const { callee, enclosingFn } of pendingCalls) {
|
|
1087
|
-
const
|
|
1088
|
-
if (
|
|
1216
|
+
const fact = getReactiveBindingFact(callee, context);
|
|
1217
|
+
if (fact?.role === "action" && isOwnedScope(enclosingFn)) {
|
|
1089
1218
|
context.report({
|
|
1090
1219
|
node: callee,
|
|
1091
1220
|
messageId: "noActionInOwnedScope"
|
|
1092
1221
|
});
|
|
1093
1222
|
continue;
|
|
1094
1223
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1224
|
+
if (fact?.role !== "setter") continue;
|
|
1225
|
+
const factoryCall = fact.declaration.init;
|
|
1226
|
+
if (factoryCall?.type === "CallExpression" && OWNED_WRITE_FACTORIES.has(fact.factory) && hasOwnedWriteOption(factoryCall)) continue;
|
|
1097
1227
|
if (isOwnedScope(enclosingFn)) context.report({
|
|
1098
1228
|
node: callee,
|
|
1099
1229
|
messageId: "noOwnedScopeWrite"
|
|
@@ -1103,6 +1233,10 @@ var no_owned_scope_writes_default = createRule({
|
|
|
1103
1233
|
node: call,
|
|
1104
1234
|
messageId: "noActionInOwnedScope"
|
|
1105
1235
|
});
|
|
1236
|
+
for (const { call, enclosingFn } of pendingRefreshes) if (isOwnedScope(enclosingFn)) context.report({
|
|
1237
|
+
node: call,
|
|
1238
|
+
messageId: "noOwnedScopeRefresh"
|
|
1239
|
+
});
|
|
1106
1240
|
}
|
|
1107
1241
|
};
|
|
1108
1242
|
}
|
|
@@ -1113,11 +1247,10 @@ const COMPUTE_FACTORIES = new Set([
|
|
|
1113
1247
|
"createMemo",
|
|
1114
1248
|
"createEffect",
|
|
1115
1249
|
"createRenderEffect",
|
|
1116
|
-
"createProjection"
|
|
1250
|
+
"createProjection",
|
|
1251
|
+
"createSignal",
|
|
1252
|
+
"createStore"
|
|
1117
1253
|
]);
|
|
1118
|
-
const ACCESSOR_FACTORIES$1 = new Set(["createMemo"]);
|
|
1119
|
-
const PAIR_ACCESSOR_FACTORIES$1 = new Set(["createSignal", "createOptimistic"]);
|
|
1120
|
-
const SOLID_ACCESSOR_TYPE_NAMES = new Set(["Accessor", "SourceAccessor"]);
|
|
1121
1254
|
function hasGuaranteedAwait(node) {
|
|
1122
1255
|
if (node == null) return false;
|
|
1123
1256
|
switch (node.type) {
|
|
@@ -1148,19 +1281,6 @@ function statementGuaranteesAwait(stmt) {
|
|
|
1148
1281
|
default: return false;
|
|
1149
1282
|
}
|
|
1150
1283
|
}
|
|
1151
|
-
function collectTypeParts(type, acc = [], seen = /* @__PURE__ */ new Set()) {
|
|
1152
|
-
if (seen.has(type)) return acc;
|
|
1153
|
-
seen.add(type);
|
|
1154
|
-
acc.push(type);
|
|
1155
|
-
if (type.isUnion() || type.isIntersection()) for (const member of type.types) collectTypeParts(member, acc, seen);
|
|
1156
|
-
return acc;
|
|
1157
|
-
}
|
|
1158
|
-
function symbolIsFromSolid(symbol, checker) {
|
|
1159
|
-
const isSolidOrigin = (text) => text.includes("solid-js") || text.includes("@solidjs") || text.includes("solid-signals");
|
|
1160
|
-
if (isSolidOrigin(checker.getFullyQualifiedName(symbol))) return true;
|
|
1161
|
-
for (const declaration of symbol.getDeclarations() ?? []) if (isSolidOrigin(declaration.getSourceFile().fileName)) return true;
|
|
1162
|
-
return false;
|
|
1163
|
-
}
|
|
1164
1284
|
var no_reactive_read_after_await_default = createRule({
|
|
1165
1285
|
name: "no-reactive-read-after-await",
|
|
1166
1286
|
meta: {
|
|
@@ -1177,8 +1297,6 @@ var no_reactive_read_after_await_default = createRule({
|
|
|
1177
1297
|
create(context) {
|
|
1178
1298
|
const sourceCode = context.sourceCode;
|
|
1179
1299
|
const services = context.options[0]?.typescriptEnabled ?? false ? getTypeAwareServices(context) : null;
|
|
1180
|
-
const checker = services?.program.getTypeChecker() ?? null;
|
|
1181
|
-
const accessorVars = /* @__PURE__ */ new Set();
|
|
1182
1300
|
const computeCallbackCache = /* @__PURE__ */ new Map();
|
|
1183
1301
|
function isAfterAwait(call, fn) {
|
|
1184
1302
|
let current = call;
|
|
@@ -1196,77 +1314,236 @@ var no_reactive_read_after_await_default = createRule({
|
|
|
1196
1314
|
return false;
|
|
1197
1315
|
}
|
|
1198
1316
|
function isTypeAwareComputeCallback(fn) {
|
|
1199
|
-
if (!services
|
|
1317
|
+
if (!services) return false;
|
|
1200
1318
|
const parent = fn.parent;
|
|
1201
1319
|
if (parent?.type !== "CallExpression" || parent.arguments[0] !== fn) return false;
|
|
1202
|
-
|
|
1203
|
-
if (!calleeNode) return false;
|
|
1204
|
-
let symbol = checker.getSymbolAtLocation(calleeNode);
|
|
1205
|
-
if (symbol && symbol.flags & typescript.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
|
|
1206
|
-
return symbol != null && COMPUTE_FACTORIES.has(symbol.getName()) && symbolIsFromSolid(symbol, checker);
|
|
1320
|
+
return isTypeAwareSolidCallee(parent.callee, services, COMPUTE_FACTORIES);
|
|
1207
1321
|
}
|
|
1208
1322
|
function isComputeCallback(fn) {
|
|
1209
1323
|
const cached = computeCallbackCache.get(fn);
|
|
1210
1324
|
if (cached !== void 0) return cached;
|
|
1211
|
-
const result =
|
|
1325
|
+
const result = (() => {
|
|
1326
|
+
const role = getComputationCallbackRole(fn, context);
|
|
1327
|
+
return role != null && ASYNC_TRACKED_COMPUTE_ROLES.has(role);
|
|
1328
|
+
})() || isTypeAwareComputeCallback(fn);
|
|
1212
1329
|
computeCallbackCache.set(fn, result);
|
|
1213
1330
|
return result;
|
|
1214
1331
|
}
|
|
1215
1332
|
function isSolidAccessorCallee(callee) {
|
|
1216
|
-
if (!services
|
|
1217
|
-
|
|
1218
|
-
if (!calleeNode) return false;
|
|
1219
|
-
return collectTypeParts(checker.getTypeAtLocation(calleeNode)).some((part) => {
|
|
1220
|
-
const alias = part.aliasSymbol;
|
|
1221
|
-
return alias != null && SOLID_ACCESSOR_TYPE_NAMES.has(alias.getName()) && symbolIsFromSolid(alias, checker);
|
|
1222
|
-
});
|
|
1333
|
+
if (!services) return false;
|
|
1334
|
+
return isSolidAccessorExpression(callee, services);
|
|
1223
1335
|
}
|
|
1224
1336
|
function accessorReadName(call) {
|
|
1225
1337
|
if (call.callee.type === "Identifier") {
|
|
1226
|
-
|
|
1227
|
-
if (variable && accessorVars.has(variable)) return call.callee.name;
|
|
1338
|
+
if (getReactiveBindingFact(call.callee, context)?.role === "accessor") return call.callee.name;
|
|
1228
1339
|
}
|
|
1229
1340
|
if (services && isSolidAccessorCallee(call.callee)) return sourceCode.getText(call.callee);
|
|
1230
1341
|
return null;
|
|
1231
1342
|
}
|
|
1232
|
-
return {
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
if (variable) accessorVars.add(variable);
|
|
1244
|
-
}
|
|
1245
|
-
return;
|
|
1246
|
-
}
|
|
1247
|
-
if (node.id.type === "Identifier" && node.init?.type === "Identifier") {
|
|
1248
|
-
const source = _typescript_eslint_utils.ASTUtils.findVariable(sourceCode.getScope(node.init), node.init);
|
|
1249
|
-
if (source && accessorVars.has(source)) {
|
|
1250
|
-
const target = _typescript_eslint_utils.ASTUtils.findVariable(sourceCode.getScope(node.id), node.id);
|
|
1251
|
-
if (target) accessorVars.add(target);
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
},
|
|
1255
|
-
CallExpression(node) {
|
|
1256
|
-
const fn = getNearestFunctionAncestor(node);
|
|
1257
|
-
if (!fn || !fn.async || !isComputeCallback(fn) || !isAfterAwait(node, fn)) return;
|
|
1258
|
-
const name = accessorReadName(node);
|
|
1259
|
-
if (name == null) return;
|
|
1260
|
-
context.report({
|
|
1261
|
-
node,
|
|
1262
|
-
messageId: "reactiveReadAfterAwait",
|
|
1263
|
-
data: { name }
|
|
1264
|
-
});
|
|
1265
|
-
}
|
|
1266
|
-
};
|
|
1343
|
+
return { CallExpression(node) {
|
|
1344
|
+
const fn = getNearestFunctionAncestor(node);
|
|
1345
|
+
if (!fn || !fn.async || !isComputeCallback(fn) || !isAfterAwait(node, fn)) return;
|
|
1346
|
+
const name = accessorReadName(node);
|
|
1347
|
+
if (name == null) return;
|
|
1348
|
+
context.report({
|
|
1349
|
+
node,
|
|
1350
|
+
messageId: "reactiveReadAfterAwait",
|
|
1351
|
+
data: { name }
|
|
1352
|
+
});
|
|
1353
|
+
} };
|
|
1267
1354
|
}
|
|
1268
1355
|
});
|
|
1269
1356
|
//#endregion
|
|
1357
|
+
//#region src/analysis/control-flow-children.ts
|
|
1358
|
+
const CONTROL_FLOW_NAMES = new Set([
|
|
1359
|
+
"For",
|
|
1360
|
+
"Match",
|
|
1361
|
+
"Repeat",
|
|
1362
|
+
"Show"
|
|
1363
|
+
]);
|
|
1364
|
+
function importedControlFlowName(name, context) {
|
|
1365
|
+
if (name.type === "JSXIdentifier") {
|
|
1366
|
+
const variable = _typescript_eslint_utils.ASTUtils.findVariable(context.sourceCode.getScope(name), name.name);
|
|
1367
|
+
for (const def of variable?.defs ?? []) if (def.type === "ImportBinding" && def.node.type === "ImportSpecifier" && isSolidJsImportDeclaration(def.node.parent)) {
|
|
1368
|
+
const imported = def.node.imported.type === "Identifier" ? def.node.imported.name : def.node.imported.value;
|
|
1369
|
+
return CONTROL_FLOW_NAMES.has(imported) ? imported : null;
|
|
1370
|
+
}
|
|
1371
|
+
return null;
|
|
1372
|
+
}
|
|
1373
|
+
if (name.type === "JSXMemberExpression" && name.object.type === "JSXIdentifier" && name.property.type === "JSXIdentifier" && CONTROL_FLOW_NAMES.has(name.property.name)) return _typescript_eslint_utils.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;
|
|
1374
|
+
return null;
|
|
1375
|
+
}
|
|
1376
|
+
function provenControlFlowName(name, context) {
|
|
1377
|
+
const imported = importedControlFlowName(name, context);
|
|
1378
|
+
if (imported != null) return imported;
|
|
1379
|
+
const services = context.options[0]?.typescriptEnabled ? getTypeAwareServices(context) : null;
|
|
1380
|
+
return services == null ? null : resolveTypeAwareSolidCallee(name, services, CONTROL_FLOW_NAMES);
|
|
1381
|
+
}
|
|
1382
|
+
function resolveFunction(value, context) {
|
|
1383
|
+
if (value == null || value.type === "SpreadElement") return null;
|
|
1384
|
+
if (isFunctionNode(value)) return value;
|
|
1385
|
+
const resolved = trace(value, context);
|
|
1386
|
+
return isFunctionNode(resolved) ? resolved : null;
|
|
1387
|
+
}
|
|
1388
|
+
function keyedMode(element, context) {
|
|
1389
|
+
const keyed = element.openingElement.attributes.find((attribute) => attribute.type === "JSXAttribute" && attribute.name.type === "JSXIdentifier" && attribute.name.name === "keyed");
|
|
1390
|
+
if (!keyed) return "absent";
|
|
1391
|
+
if (keyed.value == null) return "true";
|
|
1392
|
+
if (keyed.value.type !== "JSXExpressionContainer") return "unknown";
|
|
1393
|
+
const value = keyed.value.expression;
|
|
1394
|
+
if (value.type === "Literal" && value.value === true) return "true";
|
|
1395
|
+
if (value.type === "Literal" && value.value === false) return "false";
|
|
1396
|
+
if (resolveFunction(value, context) != null) return "custom";
|
|
1397
|
+
return "unknown";
|
|
1398
|
+
}
|
|
1399
|
+
function accessorParameters(fn, controlFlowName, element, context) {
|
|
1400
|
+
const identifiers = fn.params.map((param) => param.type === "Identifier" ? param : null);
|
|
1401
|
+
const mode = keyedMode(element, context);
|
|
1402
|
+
if (controlFlowName === "For") {
|
|
1403
|
+
if (mode === "false") return identifiers[0] ? [identifiers[0]] : [];
|
|
1404
|
+
if (mode === "custom") return identifiers.filter((param) => param != null);
|
|
1405
|
+
if (mode === "absent" || mode === "true") return identifiers[1] ? [identifiers[1]] : [];
|
|
1406
|
+
return [];
|
|
1407
|
+
}
|
|
1408
|
+
if (controlFlowName === "Show" || controlFlowName === "Match") return mode === "absent" || mode === "false" ? identifiers[0] ? [identifiers[0]] : [] : [];
|
|
1409
|
+
return [];
|
|
1410
|
+
}
|
|
1411
|
+
function functionsFromElement(element, context) {
|
|
1412
|
+
const controlFlowName = provenControlFlowName(element.openingElement.name, context);
|
|
1413
|
+
if (controlFlowName == null) return [];
|
|
1414
|
+
const functions = [];
|
|
1415
|
+
const add = (fn) => {
|
|
1416
|
+
functions.push({
|
|
1417
|
+
function: fn,
|
|
1418
|
+
accessorParameters: accessorParameters(fn, controlFlowName, element, context)
|
|
1419
|
+
});
|
|
1420
|
+
};
|
|
1421
|
+
for (const attribute of element.openingElement.attributes) if (attribute.type === "JSXAttribute" && attribute.name.type === "JSXIdentifier" && attribute.name.name === "children" && attribute.value?.type === "JSXExpressionContainer") {
|
|
1422
|
+
const fn = resolveFunction(attribute.value.expression, context);
|
|
1423
|
+
if (fn) add(fn);
|
|
1424
|
+
}
|
|
1425
|
+
for (const child of element.children) {
|
|
1426
|
+
if (child.type !== "JSXExpressionContainer") continue;
|
|
1427
|
+
const fn = resolveFunction(child.expression, context);
|
|
1428
|
+
if (fn) add(fn);
|
|
1429
|
+
}
|
|
1430
|
+
return functions;
|
|
1431
|
+
}
|
|
1432
|
+
function childNodes$1(node) {
|
|
1433
|
+
const result = [];
|
|
1434
|
+
for (const key in node) {
|
|
1435
|
+
if (key === "parent" || key === "tokens" || key === "comments") continue;
|
|
1436
|
+
const value = node[key];
|
|
1437
|
+
if (Array.isArray(value)) {
|
|
1438
|
+
for (const item of value) if (item != null && typeof item === "object" && typeof item.type === "string") result.push(item);
|
|
1439
|
+
} else if (value != null && typeof value === "object" && typeof value.type === "string") result.push(value);
|
|
1440
|
+
}
|
|
1441
|
+
return result;
|
|
1442
|
+
}
|
|
1443
|
+
/** Finds binding-proven Solid control-flow function children inside a component body. */
|
|
1444
|
+
function getControlFlowFunctionChildren(component, context) {
|
|
1445
|
+
const functions = [];
|
|
1446
|
+
const seenFunctions = /* @__PURE__ */ new Set();
|
|
1447
|
+
const stack = [component.body];
|
|
1448
|
+
while (stack.length > 0) {
|
|
1449
|
+
const node = stack.pop();
|
|
1450
|
+
if (node !== component.body && isFunctionNode(node)) continue;
|
|
1451
|
+
if (node.type === "JSXElement") for (const child of functionsFromElement(node, context)) {
|
|
1452
|
+
if (seenFunctions.has(child.function)) continue;
|
|
1453
|
+
seenFunctions.add(child.function);
|
|
1454
|
+
functions.push(child);
|
|
1455
|
+
stack.push(child.function.body);
|
|
1456
|
+
}
|
|
1457
|
+
stack.push(...childNodes$1(node));
|
|
1458
|
+
}
|
|
1459
|
+
return functions;
|
|
1460
|
+
}
|
|
1461
|
+
//#endregion
|
|
1462
|
+
//#region src/analysis/reactive-reads.ts
|
|
1463
|
+
const UNTRACK_NAMES$1 = new Set(["untrack"]);
|
|
1464
|
+
function getReactiveReadTypeServices(context) {
|
|
1465
|
+
return context.options[0]?.typescriptEnabled ? getTypeAwareServices(context) : null;
|
|
1466
|
+
}
|
|
1467
|
+
function variableFor(node, context) {
|
|
1468
|
+
return _typescript_eslint_utils.ASTUtils.findVariable(context.sourceCode.getScope(node), node);
|
|
1469
|
+
}
|
|
1470
|
+
function rootIdentifier(node) {
|
|
1471
|
+
let root = node;
|
|
1472
|
+
while (root.type === "MemberExpression") root = root.object;
|
|
1473
|
+
return root.type === "Identifier" ? root : null;
|
|
1474
|
+
}
|
|
1475
|
+
function variableRole(variable, environment, context) {
|
|
1476
|
+
if (variable == null) return null;
|
|
1477
|
+
if (environment.propsVariables?.has(variable)) return "props";
|
|
1478
|
+
if (environment.accessorVariables?.has(variable)) return "accessor";
|
|
1479
|
+
const fact = getReactiveBindingFactForVariable(variable, context);
|
|
1480
|
+
return fact?.role === "accessor" ? "accessor" : fact?.role === "store" ? "store" : null;
|
|
1481
|
+
}
|
|
1482
|
+
function childNodes(node) {
|
|
1483
|
+
const result = [];
|
|
1484
|
+
for (const key in node) {
|
|
1485
|
+
if (key === "parent" || key === "tokens" || key === "comments") continue;
|
|
1486
|
+
const value = node[key];
|
|
1487
|
+
if (Array.isArray(value)) {
|
|
1488
|
+
for (const item of value) if (item != null && typeof item === "object" && typeof item.type === "string") result.push(item);
|
|
1489
|
+
} else if (value != null && typeof value === "object" && typeof value.type === "string") result.push(value);
|
|
1490
|
+
}
|
|
1491
|
+
return result;
|
|
1492
|
+
}
|
|
1493
|
+
/** Finds the first provable reactive read executed directly in a structure-building scope. */
|
|
1494
|
+
function findReactiveRead(root, environment, context) {
|
|
1495
|
+
const stack = [root];
|
|
1496
|
+
while (stack.length > 0) {
|
|
1497
|
+
const node = stack.pop();
|
|
1498
|
+
if (isFunctionNode(node) || node.type === "JSXElement" || node.type === "JSXFragment") continue;
|
|
1499
|
+
if (node.type === "CallExpression") {
|
|
1500
|
+
if (bindsToSolid(node.callee, context, UNTRACK_NAMES$1)) continue;
|
|
1501
|
+
const callee = node.callee;
|
|
1502
|
+
if (callee.type === "Identifier") {
|
|
1503
|
+
if (variableRole(variableFor(callee, context), environment, context) === "accessor") return {
|
|
1504
|
+
kind: "accessor",
|
|
1505
|
+
node
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
if (environment.typescript != null && isSolidAccessorExpression(callee, environment.typescript)) return {
|
|
1509
|
+
kind: "accessor",
|
|
1510
|
+
node
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
if (node.type === "VariableDeclarator" && (node.id.type === "ObjectPattern" || node.id.type === "ArrayPattern") && node.init?.type === "Identifier") {
|
|
1514
|
+
const role = variableRole(variableFor(node.init, context), environment, context);
|
|
1515
|
+
if (role === "props" || role === "store") return {
|
|
1516
|
+
kind: role,
|
|
1517
|
+
node
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
if (node.type === "AssignmentExpression" && (node.left.type === "ObjectPattern" || node.left.type === "ArrayPattern") && node.right.type === "Identifier") {
|
|
1521
|
+
const role = variableRole(variableFor(node.right, context), environment, context);
|
|
1522
|
+
if (role === "props" || role === "store") return {
|
|
1523
|
+
kind: role,
|
|
1524
|
+
node
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
if (node.type === "MemberExpression") {
|
|
1528
|
+
const rootId = rootIdentifier(node);
|
|
1529
|
+
const role = rootId ? variableRole(variableFor(rootId, context), environment, context) : null;
|
|
1530
|
+
if (role === "props" || role === "store") return {
|
|
1531
|
+
kind: role,
|
|
1532
|
+
node
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
if (node.type === "SpreadElement" && node.argument.type === "Identifier") {
|
|
1536
|
+
const role = variableRole(variableFor(node.argument, context), environment, context);
|
|
1537
|
+
if (role === "props" || role === "store") return {
|
|
1538
|
+
kind: role,
|
|
1539
|
+
node
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
stack.push(...childNodes(node));
|
|
1543
|
+
}
|
|
1544
|
+
return null;
|
|
1545
|
+
}
|
|
1546
|
+
//#endregion
|
|
1270
1547
|
//#region src/rules/no-stale-props-alias.ts
|
|
1271
1548
|
const UNTRACK_NAMES = new Set(["untrack"]);
|
|
1272
1549
|
const PROPS_HELPER_NAMES = new Set(["merge", "omit"]);
|
|
@@ -1347,35 +1624,60 @@ var no_stale_props_alias_default = createRule({
|
|
|
1347
1624
|
name: "no-stale-props-alias",
|
|
1348
1625
|
meta: {
|
|
1349
1626
|
type: "problem",
|
|
1350
|
-
docs: { description: "Disallow
|
|
1627
|
+
docs: { description: "Disallow provable untracked reactive reads in component and Solid control-flow structure-building scopes." },
|
|
1351
1628
|
schema: [{
|
|
1352
1629
|
type: "object",
|
|
1353
1630
|
properties: { typescriptEnabled: { type: "boolean" } },
|
|
1354
1631
|
additionalProperties: false
|
|
1355
1632
|
}],
|
|
1356
|
-
messages: {
|
|
1633
|
+
messages: {
|
|
1634
|
+
stalePropsAlias: "`{{name}}` aliases a prop read outside tracking. Read from `props` in JSX or a tracked scope instead.",
|
|
1635
|
+
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.",
|
|
1636
|
+
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."
|
|
1637
|
+
}
|
|
1357
1638
|
},
|
|
1358
1639
|
defaultOptions: [{}],
|
|
1359
1640
|
create(context) {
|
|
1360
1641
|
const sourceCode = context.sourceCode;
|
|
1642
|
+
const typescript = getReactiveReadTypeServices(context);
|
|
1361
1643
|
const checkFunction = (node) => {
|
|
1362
|
-
if (
|
|
1644
|
+
if (!isComponent$1(node, context)) return;
|
|
1363
1645
|
const props = node.params[0];
|
|
1364
|
-
if (node.params.length
|
|
1365
|
-
const
|
|
1366
|
-
if (
|
|
1367
|
-
|
|
1368
|
-
|
|
1646
|
+
if (node.params.length > 1 || props != null && props.type !== "Identifier") return;
|
|
1647
|
+
const propsVariables = /* @__PURE__ */ new Set();
|
|
1648
|
+
if (props?.type === "Identifier") {
|
|
1649
|
+
const propsVariable = _typescript_eslint_utils.ASTUtils.findVariable(sourceCode.getScope(props), props);
|
|
1650
|
+
if (propsVariable == null) return;
|
|
1651
|
+
propsVariables.add(propsVariable);
|
|
1652
|
+
}
|
|
1653
|
+
const environment = {
|
|
1654
|
+
propsVariables,
|
|
1655
|
+
typescript
|
|
1656
|
+
};
|
|
1657
|
+
for (const statement of node.body.type === "BlockStatement" ? node.body.body : []) {
|
|
1369
1658
|
if (statement.type === "VariableDeclaration") {
|
|
1370
1659
|
for (const declaration of statement.declarations) {
|
|
1371
1660
|
const aliasId = declaration.id;
|
|
1372
|
-
if (
|
|
1661
|
+
if (declaration.init == null) continue;
|
|
1662
|
+
if (aliasId.type !== "Identifier") {
|
|
1663
|
+
const read = findReactiveRead(declaration, environment, context);
|
|
1664
|
+
if (read) context.report({
|
|
1665
|
+
node: read.node,
|
|
1666
|
+
messageId: "staleReactiveRead"
|
|
1667
|
+
});
|
|
1668
|
+
continue;
|
|
1669
|
+
}
|
|
1373
1670
|
const isPropsLikeAlias = isPropsVariableIdentifier(declaration.init, propsVariables, sourceCode) || isPropsHelperAliasInit(declaration.init, propsVariables, sourceCode, context);
|
|
1374
1671
|
if (!expressionContainsPropsRead(declaration.init, propsVariables, sourceCode, context)) {
|
|
1375
1672
|
if (isPropsLikeAlias) {
|
|
1376
1673
|
const declared = getDeclaredIdentifierVariable(declaration, sourceCode);
|
|
1377
1674
|
if (declared != null && !hasWriteAfterInit(declared)) propsVariables.add(declared);
|
|
1378
1675
|
}
|
|
1676
|
+
const read = findReactiveRead(declaration.init, environment, context);
|
|
1677
|
+
if (read) context.report({
|
|
1678
|
+
node: read.node,
|
|
1679
|
+
messageId: "staleReactiveRead"
|
|
1680
|
+
});
|
|
1379
1681
|
continue;
|
|
1380
1682
|
}
|
|
1381
1683
|
const declared = getDeclaredIdentifierVariable(declaration, sourceCode);
|
|
@@ -1390,15 +1692,45 @@ var no_stale_props_alias_default = createRule({
|
|
|
1390
1692
|
continue;
|
|
1391
1693
|
}
|
|
1392
1694
|
const expression = getAssignmentExpression(statement);
|
|
1393
|
-
if (expression == null || expression.left.type !== "Identifier")
|
|
1695
|
+
if (expression == null || expression.left.type !== "Identifier") {
|
|
1696
|
+
const read = findReactiveRead(statement, environment, context);
|
|
1697
|
+
if (read) context.report({
|
|
1698
|
+
node: read.node,
|
|
1699
|
+
messageId: "staleReactiveRead"
|
|
1700
|
+
});
|
|
1701
|
+
continue;
|
|
1702
|
+
}
|
|
1394
1703
|
const assigned = expression.left;
|
|
1395
|
-
if (!expressionContainsPropsRead(expression.right, propsVariables, sourceCode, context))
|
|
1704
|
+
if (!expressionContainsPropsRead(expression.right, propsVariables, sourceCode, context)) {
|
|
1705
|
+
const read = findReactiveRead(expression.right, environment, context);
|
|
1706
|
+
if (read) context.report({
|
|
1707
|
+
node: read.node,
|
|
1708
|
+
messageId: "staleReactiveRead"
|
|
1709
|
+
});
|
|
1710
|
+
continue;
|
|
1711
|
+
}
|
|
1396
1712
|
context.report({
|
|
1397
1713
|
node: expression,
|
|
1398
1714
|
messageId: "stalePropsAlias",
|
|
1399
1715
|
data: { name: assigned.name }
|
|
1400
1716
|
});
|
|
1401
1717
|
}
|
|
1718
|
+
for (const child of getControlFlowFunctionChildren(node, context)) {
|
|
1719
|
+
const accessorVariables = /* @__PURE__ */ new Set();
|
|
1720
|
+
for (const parameter of child.accessorParameters) {
|
|
1721
|
+
const variable = _typescript_eslint_utils.ASTUtils.findVariable(sourceCode.getScope(parameter), parameter);
|
|
1722
|
+
if (variable) accessorVariables.add(variable);
|
|
1723
|
+
}
|
|
1724
|
+
const read = findReactiveRead(child.function.body, {
|
|
1725
|
+
propsVariables,
|
|
1726
|
+
accessorVariables,
|
|
1727
|
+
typescript
|
|
1728
|
+
}, context);
|
|
1729
|
+
if (read) context.report({
|
|
1730
|
+
node: read.node,
|
|
1731
|
+
messageId: read.kind === "props" ? "stalePropsRead" : "staleReactiveRead"
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1402
1734
|
};
|
|
1403
1735
|
return {
|
|
1404
1736
|
"FunctionDeclaration:exit": checkFunction,
|
|
@@ -1410,29 +1742,29 @@ var no_stale_props_alias_default = createRule({
|
|
|
1410
1742
|
//#endregion
|
|
1411
1743
|
//#region src/rules/no-untracked-read-in-effect-apply.ts
|
|
1412
1744
|
const EFFECT_NAMES = new Set(["createEffect", "createRenderEffect"]);
|
|
1413
|
-
const ACCESSOR_FACTORIES = new Set(["createMemo", "createProjection"]);
|
|
1414
|
-
const PAIR_ACCESSOR_FACTORIES = new Set(["createOptimistic", "createSignal"]);
|
|
1415
|
-
const STORE_FACTORIES = new Set(["createOptimisticStore", "createStore"]);
|
|
1416
1745
|
const SAFE_HELPERS = new Set(["deep", "snapshot"]);
|
|
1417
1746
|
var no_untracked_read_in_effect_apply_default = createRule({
|
|
1418
1747
|
name: "no-untracked-read-in-effect-apply",
|
|
1419
1748
|
meta: {
|
|
1420
1749
|
type: "problem",
|
|
1421
1750
|
docs: { description: "Disallow reading reactive state (signal accessors or store proxies) in a createEffect apply callback, which runs untracked." },
|
|
1422
|
-
schema: [
|
|
1751
|
+
schema: [{
|
|
1752
|
+
type: "object",
|
|
1753
|
+
properties: { typescriptEnabled: { type: "boolean" } },
|
|
1754
|
+
additionalProperties: false
|
|
1755
|
+
}],
|
|
1423
1756
|
messages: {
|
|
1424
1757
|
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()`.",
|
|
1425
1758
|
storeProxyRead: "Effect apply callbacks run untracked in Solid 2. Extract store properties in the compute phase or use `deep()` before reading them here."
|
|
1426
1759
|
}
|
|
1427
1760
|
},
|
|
1428
|
-
defaultOptions: [],
|
|
1761
|
+
defaultOptions: [{}],
|
|
1429
1762
|
create(context) {
|
|
1430
1763
|
const sourceCode = context.sourceCode;
|
|
1431
|
-
const
|
|
1764
|
+
const services = context.options[0]?.typescriptEnabled ? getTypeAwareServices(context) : null;
|
|
1765
|
+
const isEffectFactory = (node) => resolveSolidCallee(node, context, EFFECT_NAMES) != null || services != null && isTypeAwareSolidCallee(node, services, EFFECT_NAMES);
|
|
1432
1766
|
const applyCallbacks = /* @__PURE__ */ new Set();
|
|
1433
1767
|
const pendingAccessorCalls = [];
|
|
1434
|
-
const storeVars = /* @__PURE__ */ new Set();
|
|
1435
|
-
const storeShapes = /* @__PURE__ */ new Map();
|
|
1436
1768
|
const getMemberPath = (node) => {
|
|
1437
1769
|
const path = [];
|
|
1438
1770
|
let current = node;
|
|
@@ -1461,6 +1793,13 @@ var no_untracked_read_in_effect_apply_default = createRule({
|
|
|
1461
1793
|
}
|
|
1462
1794
|
return current.type === "ObjectExpression" || current.type === "ArrayExpression";
|
|
1463
1795
|
};
|
|
1796
|
+
const getStoreShape = (fact) => {
|
|
1797
|
+
const init = fact.declaration.init;
|
|
1798
|
+
if (init?.type !== "CallExpression") return null;
|
|
1799
|
+
const first = init.arguments[0];
|
|
1800
|
+
const seed = first != null && first.type !== "SpreadElement" && (first.type === "FunctionExpression" || first.type === "ArrowFunctionExpression") ? init.arguments[1] : first;
|
|
1801
|
+
return seed?.type === "ObjectExpression" ? seed : null;
|
|
1802
|
+
};
|
|
1464
1803
|
const getInlineFunction = (value) => {
|
|
1465
1804
|
if (value == null || value.type === "SpreadElement") return null;
|
|
1466
1805
|
if (isFunctionNode(value)) return value;
|
|
@@ -1502,21 +1841,18 @@ var no_untracked_read_in_effect_apply_default = createRule({
|
|
|
1502
1841
|
const isStoreSourceExpression = (node) => {
|
|
1503
1842
|
if (node == null) return false;
|
|
1504
1843
|
if (node.type === "CallExpression" && resolveSolidCallee(node.callee, context, SAFE_HELPERS) != null) return false;
|
|
1505
|
-
if (node.type === "Identifier")
|
|
1506
|
-
const variable = _typescript_eslint_utils.ASTUtils.findVariable(sourceCode.getScope(node), node);
|
|
1507
|
-
return variable != null && storeVars.has(variable);
|
|
1508
|
-
}
|
|
1844
|
+
if (node.type === "Identifier") return getReactiveBindingFact(node, context)?.role === "store";
|
|
1509
1845
|
if (node.type === "MemberExpression") {
|
|
1510
1846
|
const resolved = getMemberPath(node);
|
|
1511
1847
|
if (resolved == null) return false;
|
|
1512
|
-
const
|
|
1513
|
-
if (
|
|
1514
|
-
return pathLandsOnProxy(
|
|
1848
|
+
const fact = getReactiveBindingFact(resolved.root, context);
|
|
1849
|
+
if (fact?.role !== "store") return false;
|
|
1850
|
+
return pathLandsOnProxy(getStoreShape(fact), resolved.path) === true;
|
|
1515
1851
|
}
|
|
1516
1852
|
return false;
|
|
1517
1853
|
};
|
|
1518
1854
|
const checkStoreProxyInApply = (node) => {
|
|
1519
|
-
if (
|
|
1855
|
+
if (!isEffectFactory(node.callee) || node.arguments.length < 2) return;
|
|
1520
1856
|
const compute = getFunctionValue(node.arguments[0]);
|
|
1521
1857
|
const apply = getTracedApplyCallback(node.arguments[1]);
|
|
1522
1858
|
if (!compute || !apply) return;
|
|
@@ -1555,56 +1891,23 @@ var no_untracked_read_in_effect_apply_default = createRule({
|
|
|
1555
1891
|
}
|
|
1556
1892
|
};
|
|
1557
1893
|
return {
|
|
1558
|
-
VariableDeclarator(node) {
|
|
1559
|
-
if (node.id.type === "Identifier" && node.init?.type === "CallExpression" && node.init.callee.type === "Identifier" && bindsToSolid(node.init.callee, context, ACCESSOR_FACTORIES)) {
|
|
1560
|
-
const variable = _typescript_eslint_utils.ASTUtils.findVariable(sourceCode.getScope(node.id), node.id);
|
|
1561
|
-
if (variable) reactiveVars.set(variable, "accessor");
|
|
1562
|
-
return;
|
|
1563
|
-
}
|
|
1564
|
-
if (node.id.type === "ArrayPattern" && node.init?.type === "CallExpression" && node.init.callee.type === "Identifier" && bindsToSolid(node.init.callee, context, PAIR_ACCESSOR_FACTORIES)) {
|
|
1565
|
-
const first = node.id.elements[0];
|
|
1566
|
-
if (first?.type === "Identifier") {
|
|
1567
|
-
const variable = sourceCode.scopeManager?.getDeclaredVariables(node).find((declared) => declared.name === first.name);
|
|
1568
|
-
if (variable) reactiveVars.set(variable, "accessor");
|
|
1569
|
-
}
|
|
1570
|
-
return;
|
|
1571
|
-
}
|
|
1572
|
-
if (node.id.type === "Identifier" && node.init?.type === "Identifier") {
|
|
1573
|
-
const source = _typescript_eslint_utils.ASTUtils.findVariable(sourceCode.getScope(node.init), node.init);
|
|
1574
|
-
if (source && reactiveVars.get(source) === "accessor") {
|
|
1575
|
-
const target = _typescript_eslint_utils.ASTUtils.findVariable(sourceCode.getScope(node.id), node.id);
|
|
1576
|
-
if (target) reactiveVars.set(target, "accessor");
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
if (node.id.type === "ArrayPattern" && node.init?.type === "CallExpression" && resolveSolidCallee(node.init.callee, context, STORE_FACTORIES) != null) {
|
|
1580
|
-
const first = node.id.elements[0];
|
|
1581
|
-
if (first?.type === "Identifier") {
|
|
1582
|
-
const variable = sourceCode.scopeManager?.getDeclaredVariables(node).find((declared) => declared.name === first.name);
|
|
1583
|
-
if (variable) {
|
|
1584
|
-
storeVars.add(variable);
|
|
1585
|
-
const initialValue = node.init.arguments[0];
|
|
1586
|
-
storeShapes.set(variable, initialValue?.type === "ObjectExpression" ? initialValue : null);
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
},
|
|
1591
1894
|
CallExpression(node) {
|
|
1592
|
-
if (
|
|
1895
|
+
if (isEffectFactory(node.callee) && node.arguments.length >= 2) {
|
|
1593
1896
|
const apply = getInlineApplyCallback(node.arguments[1]);
|
|
1594
1897
|
if (apply) applyCallbacks.add(apply);
|
|
1595
1898
|
}
|
|
1596
1899
|
checkStoreProxyInApply(node);
|
|
1597
|
-
|
|
1900
|
+
pendingAccessorCalls.push(node);
|
|
1598
1901
|
},
|
|
1599
1902
|
"Program:exit"() {
|
|
1600
1903
|
for (const node of pendingAccessorCalls) {
|
|
1601
1904
|
const callee = node.callee;
|
|
1602
|
-
|
|
1603
|
-
const
|
|
1604
|
-
if (
|
|
1905
|
+
const astAccessor = callee.type === "Identifier" && getReactiveBindingFact(callee, context)?.role === "accessor";
|
|
1906
|
+
const typedAccessor = services != null && isSolidAccessorExpression(callee, services);
|
|
1907
|
+
if ((astAccessor || typedAccessor) && findContainingApplyCallback(node) != null) context.report({
|
|
1605
1908
|
node,
|
|
1606
1909
|
messageId: "signalRead",
|
|
1607
|
-
data: { name: callee
|
|
1910
|
+
data: { name: sourceCode.getText(callee) }
|
|
1608
1911
|
});
|
|
1609
1912
|
}
|
|
1610
1913
|
}
|
|
@@ -1788,122 +2091,161 @@ const childrenIsMultilineSpaces = (node) => {
|
|
|
1788
2091
|
const children = node.parent.children;
|
|
1789
2092
|
return children.length === 1 && children[0].type === "JSXText" && children[0].value.includes("\n") && children[0].value.replace(/(?!\xA0)\s/g, "") === "";
|
|
1790
2093
|
};
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
"
|
|
1795
|
-
"
|
|
1796
|
-
"
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
type: "object",
|
|
1812
|
-
properties: {
|
|
1813
|
-
component: {
|
|
1814
|
-
enum: ["all", "none"],
|
|
1815
|
-
type: "string"
|
|
1816
|
-
},
|
|
1817
|
-
html: {
|
|
1818
|
-
enum: [
|
|
1819
|
-
"all",
|
|
1820
|
-
"void",
|
|
1821
|
-
"none"
|
|
1822
|
-
],
|
|
1823
|
-
type: "string"
|
|
1824
|
-
}
|
|
1825
|
-
},
|
|
1826
|
-
additionalProperties: false
|
|
1827
|
-
}],
|
|
1828
|
-
messages: {
|
|
1829
|
-
dontSelfClose: "This element should not be self-closing.",
|
|
1830
|
-
selfClose: "Empty components are self-closing."
|
|
2094
|
+
var self_closing_comp_default = createRule({
|
|
2095
|
+
name: "self-closing-comp",
|
|
2096
|
+
meta: {
|
|
2097
|
+
type: "layout",
|
|
2098
|
+
docs: { description: "Disallow extra closing tags for components without children." },
|
|
2099
|
+
fixable: "code",
|
|
2100
|
+
schema: [{
|
|
2101
|
+
type: "object",
|
|
2102
|
+
properties: {
|
|
2103
|
+
component: {
|
|
2104
|
+
enum: ["all", "none"],
|
|
2105
|
+
type: "string"
|
|
2106
|
+
},
|
|
2107
|
+
html: {
|
|
2108
|
+
enum: [
|
|
2109
|
+
"all",
|
|
2110
|
+
"void",
|
|
2111
|
+
"none"
|
|
2112
|
+
],
|
|
2113
|
+
type: "string"
|
|
1831
2114
|
}
|
|
1832
2115
|
},
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
node,
|
|
1850
|
-
messageId: "selfClose",
|
|
1851
|
-
fix: (fixer) => {
|
|
1852
|
-
const openingElementEnding = node.range[1] - 1;
|
|
1853
|
-
const closingElementEnding = node.parent.closingElement.range[1];
|
|
1854
|
-
return fixer.replaceTextRange([openingElementEnding, closingElementEnding], " />");
|
|
1855
|
-
}
|
|
1856
|
-
});
|
|
1857
|
-
else if (!shouldSelfClose && node.selfClosing) context.report({
|
|
1858
|
-
node,
|
|
1859
|
-
messageId: "dontSelfClose",
|
|
1860
|
-
fix: (fixer) => {
|
|
1861
|
-
const tagName = sourceCode.getText(node.name);
|
|
1862
|
-
const selfCloseEnding = node.range[1];
|
|
1863
|
-
const lastTokens = sourceCode.getLastTokens(node, { count: 3 });
|
|
1864
|
-
const range = [sourceCode.isSpaceBetween(lastTokens[0], lastTokens[1]) ? selfCloseEnding - 3 : selfCloseEnding - 2, selfCloseEnding];
|
|
1865
|
-
return fixer.replaceTextRange(range, `></${tagName}>`);
|
|
1866
|
-
}
|
|
1867
|
-
});
|
|
1868
|
-
} };
|
|
2116
|
+
additionalProperties: false
|
|
2117
|
+
}],
|
|
2118
|
+
messages: {
|
|
2119
|
+
dontSelfClose: "This element should not be self-closing.",
|
|
2120
|
+
selfClose: "Empty components are self-closing."
|
|
2121
|
+
}
|
|
2122
|
+
},
|
|
2123
|
+
defaultOptions: [],
|
|
2124
|
+
create(context) {
|
|
2125
|
+
const sourceCode = context.sourceCode;
|
|
2126
|
+
const shouldBeSelfClosedWhenPossible = (node) => {
|
|
2127
|
+
if (isComponent(node)) return (context.options[0]?.component ?? "all") === "all";
|
|
2128
|
+
if (isHostElement(node)) switch (context.options[0]?.html ?? "all") {
|
|
2129
|
+
case "all": return true;
|
|
2130
|
+
case "void": return node.name.type === "JSXIdentifier" && isVoidDOMElementName(node.name.name);
|
|
2131
|
+
case "none": return false;
|
|
1869
2132
|
}
|
|
1870
|
-
|
|
2133
|
+
return true;
|
|
2134
|
+
};
|
|
2135
|
+
return { JSXOpeningElement(node) {
|
|
2136
|
+
if (!(childrenIsEmpty(node) || childrenIsMultilineSpaces(node))) return;
|
|
2137
|
+
const shouldSelfClose = shouldBeSelfClosedWhenPossible(node);
|
|
2138
|
+
if (shouldSelfClose && !node.selfClosing) context.report({
|
|
2139
|
+
node,
|
|
2140
|
+
messageId: "selfClose",
|
|
2141
|
+
fix: (fixer) => {
|
|
2142
|
+
const openingElementEnding = node.range[1] - 1;
|
|
2143
|
+
const closingElementEnding = node.parent.closingElement.range[1];
|
|
2144
|
+
return fixer.replaceTextRange([openingElementEnding, closingElementEnding], " />");
|
|
2145
|
+
}
|
|
2146
|
+
});
|
|
2147
|
+
else if (!shouldSelfClose && node.selfClosing) context.report({
|
|
2148
|
+
node,
|
|
2149
|
+
messageId: "dontSelfClose",
|
|
2150
|
+
fix: (fixer) => {
|
|
2151
|
+
const tagName = sourceCode.getText(node.name);
|
|
2152
|
+
const selfCloseEnding = node.range[1];
|
|
2153
|
+
const lastTokens = sourceCode.getLastTokens(node, { count: 3 });
|
|
2154
|
+
const range = [sourceCode.isSpaceBetween(lastTokens[0], lastTokens[1]) ? selfCloseEnding - 3 : selfCloseEnding - 2, selfCloseEnding];
|
|
2155
|
+
return fixer.replaceTextRange(range, `></${tagName}>`);
|
|
2156
|
+
}
|
|
2157
|
+
});
|
|
2158
|
+
} };
|
|
1871
2159
|
}
|
|
2160
|
+
});
|
|
2161
|
+
//#endregion
|
|
2162
|
+
//#region src/rule-catalog.ts
|
|
2163
|
+
/**
|
|
2164
|
+
* The authoritative internal catalog for public rule registration and recommendation policy.
|
|
2165
|
+
* Documentation remains hand-authored and is verified through public-interface invariant tests.
|
|
2166
|
+
*/
|
|
2167
|
+
const ruleCatalog = {
|
|
2168
|
+
"components-return-once": {
|
|
2169
|
+
rule: components_return_once_default,
|
|
2170
|
+
recommended: "warn",
|
|
2171
|
+
recommendedTypeChecked: ["warn", { typescriptEnabled: true }]
|
|
2172
|
+
},
|
|
2173
|
+
"jsx-no-duplicate-props": {
|
|
2174
|
+
rule: jsx_no_duplicate_props_default,
|
|
2175
|
+
recommended: "error"
|
|
2176
|
+
},
|
|
2177
|
+
"no-destructure": {
|
|
2178
|
+
rule: no_destructure_default,
|
|
2179
|
+
recommended: "warn",
|
|
2180
|
+
recommendedTypeChecked: ["warn", { typescriptEnabled: true }]
|
|
2181
|
+
},
|
|
2182
|
+
"no-leaf-owner-operations": {
|
|
2183
|
+
rule: no_leaf_owner_operations_default,
|
|
2184
|
+
recommended: "error",
|
|
2185
|
+
recommendedTypeChecked: ["error", { typescriptEnabled: true }]
|
|
2186
|
+
},
|
|
2187
|
+
"no-owned-scope-writes": {
|
|
2188
|
+
rule: no_owned_scope_writes_default,
|
|
2189
|
+
recommended: "error",
|
|
2190
|
+
recommendedTypeChecked: ["error", { typescriptEnabled: true }]
|
|
2191
|
+
},
|
|
2192
|
+
"no-reactive-read-after-await": {
|
|
2193
|
+
rule: no_reactive_read_after_await_default,
|
|
2194
|
+
recommended: "warn",
|
|
2195
|
+
recommendedTypeChecked: ["warn", { typescriptEnabled: true }]
|
|
2196
|
+
},
|
|
2197
|
+
"no-stale-props-alias": {
|
|
2198
|
+
rule: no_stale_props_alias_default,
|
|
2199
|
+
recommended: "warn",
|
|
2200
|
+
recommendedTypeChecked: ["warn", { typescriptEnabled: true }]
|
|
2201
|
+
},
|
|
2202
|
+
"no-untracked-read-in-effect-apply": {
|
|
2203
|
+
rule: no_untracked_read_in_effect_apply_default,
|
|
2204
|
+
recommended: "warn",
|
|
2205
|
+
recommendedTypeChecked: ["warn", { typescriptEnabled: true }]
|
|
2206
|
+
},
|
|
2207
|
+
"prefer-for": {
|
|
2208
|
+
rule: prefer_for_default,
|
|
2209
|
+
recommendedTypeChecked: ["warn", { typescriptEnabled: true }]
|
|
2210
|
+
},
|
|
2211
|
+
"prefer-show": {
|
|
2212
|
+
rule: prefer_show_default,
|
|
2213
|
+
recommended: "warn"
|
|
2214
|
+
},
|
|
2215
|
+
"self-closing-comp": {
|
|
2216
|
+
rule: self_closing_comp_default,
|
|
2217
|
+
recommended: "warn"
|
|
2218
|
+
}
|
|
2219
|
+
};
|
|
2220
|
+
const rules = Object.fromEntries(Object.entries(ruleCatalog).map(([name, entry]) => [name, entry.rule]));
|
|
2221
|
+
function buildRuleMap(policy) {
|
|
2222
|
+
return Object.fromEntries(Object.entries(ruleCatalog).flatMap(([name, entry]) => {
|
|
2223
|
+
const typedEntry = entry;
|
|
2224
|
+
const config = policy === "recommended" ? typedEntry.recommended : typedEntry.recommendedTypeChecked ?? typedEntry.recommended;
|
|
2225
|
+
return config == null ? [] : [[`solid/${name}`, config]];
|
|
2226
|
+
}));
|
|
2227
|
+
}
|
|
2228
|
+
const recommendedRules = buildRuleMap("recommended");
|
|
2229
|
+
const recommendedTypeCheckedRules = buildRuleMap("recommendedTypeChecked");
|
|
2230
|
+
//#endregion
|
|
2231
|
+
//#region src/plugin.ts
|
|
2232
|
+
const plugin = {
|
|
2233
|
+
meta: { name: "eslint-plugin-solid" },
|
|
2234
|
+
rules
|
|
1872
2235
|
};
|
|
1873
2236
|
//#endregion
|
|
1874
2237
|
//#region src/configs/recommended.ts
|
|
1875
2238
|
const recommended = {
|
|
1876
2239
|
name: "solid/recommended",
|
|
1877
2240
|
plugins: { solid: plugin },
|
|
1878
|
-
rules:
|
|
1879
|
-
"solid/components-return-once": "warn",
|
|
1880
|
-
"solid/jsx-no-duplicate-props": "error",
|
|
1881
|
-
"solid/no-destructure": "warn",
|
|
1882
|
-
"solid/no-leaf-owner-operations": "error",
|
|
1883
|
-
"solid/no-owned-scope-writes": "error",
|
|
1884
|
-
"solid/no-reactive-read-after-await": "warn",
|
|
1885
|
-
"solid/no-stale-props-alias": "warn",
|
|
1886
|
-
"solid/no-untracked-read-in-effect-apply": "warn",
|
|
1887
|
-
"solid/prefer-show": "warn",
|
|
1888
|
-
"solid/self-closing-comp": "warn"
|
|
1889
|
-
}
|
|
2241
|
+
rules: recommendedRules
|
|
1890
2242
|
};
|
|
1891
2243
|
//#endregion
|
|
1892
2244
|
//#region src/configs/recommended-type-checked.ts
|
|
1893
|
-
const typeAwareOverrides = {
|
|
1894
|
-
"solid/components-return-once": ["warn", { typescriptEnabled: true }],
|
|
1895
|
-
"solid/no-destructure": ["warn", { typescriptEnabled: true }],
|
|
1896
|
-
"solid/no-owned-scope-writes": ["error", { typescriptEnabled: true }],
|
|
1897
|
-
"solid/no-reactive-read-after-await": ["warn", { typescriptEnabled: true }],
|
|
1898
|
-
"solid/prefer-for": ["warn", { typescriptEnabled: true }]
|
|
1899
|
-
};
|
|
1900
2245
|
const recommendedTypeChecked = {
|
|
1901
2246
|
name: "solid/recommended-type-checked",
|
|
1902
2247
|
plugins: { solid: plugin },
|
|
1903
|
-
rules:
|
|
1904
|
-
...recommended.rules,
|
|
1905
|
-
...typeAwareOverrides
|
|
1906
|
-
}
|
|
2248
|
+
rules: recommendedTypeCheckedRules
|
|
1907
2249
|
};
|
|
1908
2250
|
//#endregion
|
|
1909
2251
|
//#region src/index.ts
|