@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/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/rules/solid-rule-utils.ts
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 === "VariableDeclarator" && parent.id.type === "Identifier") {
131
- const annotation = parent.id.typeAnnotation?.typeAnnotation;
132
- const name = typeReferenceName(annotation);
133
- if (name == null || !SOLID_COMPONENT_TYPES.has(name)) return false;
134
- const root = typeReferenceRootIdentifier(annotation);
135
- return root != null && typeNameBindsToSolid(root.id, root.qualified, context);
136
- }
137
- return false;
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 vars = /* @__PURE__ */ new Set();
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 != null) vars.add(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, vars);
160
- return vars;
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) || isPairElementZero(variable, STORE_FACTORIES$1, context)) return true;
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 && variableIsAccessor(callee, context, /* @__PURE__ */ new Set())) return true;
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 = isSolidApiCallbackArgument(node, 0, context, TRACKED_EFFECT_NAMES);
1009
+ const trackedEffect = isSolidCallbackArgument(node, 0, TRACKED_EFFECT_NAMES);
901
1010
  let ownerBackedSettled = false;
902
- if (isSolidApiCallbackArgument(node, 0, context, ON_SETTLED_NAMES)) {
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) || isSolidApiCallbackArgument(enclosing, 0, context, OWNER_CALLBACK_NAMES) || isSolidApiCallbackArgument(enclosing, 1, context, OWNER_CALLBACK_NAMES));
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 || node.callee.type !== "Identifier") return;
1030
+ if (!currentForbidden || getNearestFunctionAncestor(node) !== currentForbidden) return;
922
1031
  const callee = node.callee;
923
- if (bindsToSolid(callee, context, ON_CLEANUP_NAMES)) context.report({
1032
+ if (isSolidApi(callee, ON_CLEANUP_NAMES)) context.report({
924
1033
  node: callee,
925
1034
  messageId: "noCleanup"
926
1035
  });
927
- else if (bindsToSolid(callee, context, FLUSH_NAMES)) context.report({
1036
+ else if (isSolidApi(callee, FLUSH_NAMES)) context.report({
928
1037
  node: callee,
929
1038
  messageId: "noFlush"
930
1039
  });
931
- else if (bindsToSolid(callee, context, PRIMITIVE_NAMES)) context.report({
1040
+ else if (isSolidApi(callee, PRIMITIVE_NAMES)) context.report({
932
1041
  node: callee,
933
1042
  messageId: "noPrimitives"
934
1043
  });
935
- else if (bindsToSolid(callee, context, FUNCTION_FORM_PRIMITIVE_NAMES) && hasProvablyFunctionFirstArgument(node, context)) context.report({
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/rules/no-owned-scope-writes.ts
945
- const SETTER_FACTORIES = new Set([
946
- "createOptimistic",
947
- "createOptimisticStore",
948
- "createSignal",
949
- "createStore"
950
- ]);
951
- const OWNED_WRITE_FACTORIES = new Set(["createSignal", "createOptimistic"]);
952
- const EFFECT_FACTORIES = new Set(["createEffect", "createRenderEffect"]);
953
- const COMPUTE_FACTORIES$1 = new Set([
1053
+ //#region src/analysis/computation-roles.ts
1054
+ const CALLBACK_FACTORIES = new Set([
954
1055
  "createEffect",
955
1056
  "createMemo",
956
- "createRenderEffect"
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
- if (isComponent$1(node, context)) return true;
971
- if (node.parent?.type !== "CallExpression" || node.parent.arguments[0] !== node || node.parent.callee.type !== "Identifier") return false;
972
- const callee = node.parent.callee;
973
- if (!bindsToSolid(callee, context, COMPUTE_FACTORIES$1)) return false;
974
- if (bindsToSolid(callee, context, EFFECT_FACTORIES)) return node.parent.arguments.length >= 2;
975
- return true;
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 setterVariables = /* @__PURE__ */ new Map();
995
- const actionVariables = /* @__PURE__ */ new Set();
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 sourceCode = context.sourceCode;
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" && resolveSolidCallee(node.callee.callee, context, ACTION_FACTORIES) != null) {
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 variable = ASTUtils.findVariable(sourceCode.getScope(callee), callee);
1065
- if (variable && actionVariables.has(variable) && isOwnedScope(enclosingFn)) {
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
- const setter = variable && setterVariables.get(variable);
1073
- if (!setter || setter.allowOwnedWrite) continue;
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 || !checker) return false;
1294
+ if (!services) return false;
1177
1295
  const parent = fn.parent;
1178
1296
  if (parent?.type !== "CallExpression" || parent.arguments[0] !== fn) return false;
1179
- const calleeNode = services.esTreeNodeToTSNodeMap.get(parent.callee);
1180
- if (!calleeNode) return false;
1181
- let symbol = checker.getSymbolAtLocation(calleeNode);
1182
- if (symbol && symbol.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
1183
- return symbol != null && COMPUTE_FACTORIES.has(symbol.getName()) && symbolIsFromSolid(symbol, checker);
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 = isSolidApiCallbackArgument(fn, 0, context, COMPUTE_FACTORIES) || isTypeAwareComputeCallback(fn);
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 || !checker) return false;
1194
- const calleeNode = services.esTreeNodeToTSNodeMap.get(callee);
1195
- if (!calleeNode) return false;
1196
- return collectTypeParts(checker.getTypeAtLocation(calleeNode)).some((part) => {
1197
- const alias = part.aliasSymbol;
1198
- return alias != null && SOLID_ACCESSOR_TYPE_NAMES.has(alias.getName()) && symbolIsFromSolid(alias, checker);
1199
- });
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
- const variable = ASTUtils.findVariable(sourceCode.getScope(call), call.callee);
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
- VariableDeclarator(node) {
1211
- if (node.id.type === "Identifier" && node.init?.type === "CallExpression" && node.init.callee.type === "Identifier" && bindsToSolid(node.init.callee, context, ACCESSOR_FACTORIES$1)) {
1212
- const variable = ASTUtils.findVariable(sourceCode.getScope(node.id), node.id);
1213
- if (variable) accessorVars.add(variable);
1214
- return;
1215
- }
1216
- if (node.id.type === "ArrayPattern" && node.init?.type === "CallExpression" && node.init.callee.type === "Identifier" && bindsToSolid(node.init.callee, context, PAIR_ACCESSOR_FACTORIES$1)) {
1217
- const first = node.id.elements[0];
1218
- if (first?.type === "Identifier") {
1219
- const variable = sourceCode.scopeManager?.getDeclaredVariables(node).find((declared) => declared.name === first.name);
1220
- if (variable) accessorVars.add(variable);
1221
- }
1222
- return;
1223
- }
1224
- if (node.id.type === "Identifier" && node.init?.type === "Identifier") {
1225
- const source = ASTUtils.findVariable(sourceCode.getScope(node.init), node.init);
1226
- if (source && accessorVars.has(source)) {
1227
- const target = ASTUtils.findVariable(sourceCode.getScope(node.id), node.id);
1228
- if (target) accessorVars.add(target);
1229
- }
1230
- }
1231
- },
1232
- CallExpression(node) {
1233
- const fn = getNearestFunctionAncestor(node);
1234
- if (!fn || !fn.async || !isComputeCallback(fn) || !isAfterAwait(node, fn)) return;
1235
- const name = accessorReadName(node);
1236
- if (name == null) return;
1237
- context.report({
1238
- node,
1239
- messageId: "reactiveReadAfterAwait",
1240
- data: { name }
1241
- });
1242
- }
1243
- };
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 top-level aliases of component props, which read props outside tracking." },
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: { stalePropsAlias: "`{{name}}` aliases a prop read outside tracking. Read from `props` in JSX or a tracked scope instead." }
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 (node.body.type !== "BlockStatement" || !isComponent$1(node, context)) return;
1621
+ if (!isComponent$1(node, context)) return;
1340
1622
  const props = node.params[0];
1341
- if (node.params.length !== 1 || props?.type !== "Identifier") return;
1342
- const propsVariable = ASTUtils.findVariable(sourceCode.getScope(props), props);
1343
- if (propsVariable == null) return;
1344
- const propsVariables = new Set([propsVariable]);
1345
- for (const statement of node.body.body) {
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 (aliasId.type !== "Identifier" || declaration.init == null) continue;
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") continue;
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)) continue;
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 reactiveVars = /* @__PURE__ */ new Map();
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 variable = ASTUtils.findVariable(sourceCode.getScope(resolved.root), resolved.root);
1490
- if (variable == null || !storeVars.has(variable)) return false;
1491
- return pathLandsOnProxy(storeShapes.get(variable) ?? null, resolved.path) === true;
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 (resolveSolidCallee(node.callee, context, EFFECT_NAMES) == null || node.arguments.length < 2) return;
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 (node.callee.type === "Identifier" && bindsToSolid(node.callee, context, EFFECT_NAMES) && node.arguments.length >= 2) {
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
- if (node.callee.type === "Identifier") pendingAccessorCalls.push(node);
1877
+ pendingAccessorCalls.push(node);
1575
1878
  },
1576
1879
  "Program:exit"() {
1577
1880
  for (const node of pendingAccessorCalls) {
1578
1881
  const callee = node.callee;
1579
- if (callee.type !== "Identifier") continue;
1580
- const variable = ASTUtils.findVariable(sourceCode.getScope(node), callee);
1581
- if (variable && reactiveVars.get(variable) === "accessor" && findContainingApplyCallback(node) != null) context.report({
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.name }
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
- const plugin = {
1769
- meta: { name: "eslint-plugin-solid" },
1770
- rules: {
1771
- "components-return-once": components_return_once_default,
1772
- "jsx-no-duplicate-props": jsx_no_duplicate_props_default,
1773
- "no-destructure": no_destructure_default,
1774
- "no-leaf-owner-operations": no_leaf_owner_operations_default,
1775
- "no-owned-scope-writes": no_owned_scope_writes_default,
1776
- "no-reactive-read-after-await": no_reactive_read_after_await_default,
1777
- "no-stale-props-alias": no_stale_props_alias_default,
1778
- "no-untracked-read-in-effect-apply": no_untracked_read_in_effect_apply_default,
1779
- "prefer-for": prefer_for_default,
1780
- "prefer-show": prefer_show_default,
1781
- "self-closing-comp": createRule({
1782
- name: "self-closing-comp",
1783
- meta: {
1784
- type: "layout",
1785
- docs: { description: "Disallow extra closing tags for components without children." },
1786
- fixable: "code",
1787
- schema: [{
1788
- type: "object",
1789
- properties: {
1790
- component: {
1791
- enum: ["all", "none"],
1792
- type: "string"
1793
- },
1794
- html: {
1795
- enum: [
1796
- "all",
1797
- "void",
1798
- "none"
1799
- ],
1800
- type: "string"
1801
- }
1802
- },
1803
- additionalProperties: false
1804
- }],
1805
- messages: {
1806
- dontSelfClose: "This element should not be self-closing.",
1807
- selfClose: "Empty components are self-closing."
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
- defaultOptions: [],
1811
- create(context) {
1812
- const sourceCode = context.sourceCode;
1813
- const shouldBeSelfClosedWhenPossible = (node) => {
1814
- if (isComponent(node)) return (context.options[0]?.component ?? "all") === "all";
1815
- if (isHostElement(node)) switch (context.options[0]?.html ?? "all") {
1816
- case "all": return true;
1817
- case "void": return node.name.type === "JSXIdentifier" && isVoidDOMElementName(node.name.name);
1818
- case "none": return false;
1819
- }
1820
- return true;
1821
- };
1822
- return { JSXOpeningElement(node) {
1823
- if (!(childrenIsEmpty(node) || childrenIsMultilineSpaces(node))) return;
1824
- const shouldSelfClose = shouldBeSelfClosedWhenPossible(node);
1825
- if (shouldSelfClose && !node.selfClosing) context.report({
1826
- node,
1827
- messageId: "selfClose",
1828
- fix: (fixer) => {
1829
- const openingElementEnding = node.range[1] - 1;
1830
- const closingElementEnding = node.parent.closingElement.range[1];
1831
- return fixer.replaceTextRange([openingElementEnding, closingElementEnding], " />");
1832
- }
1833
- });
1834
- else if (!shouldSelfClose && node.selfClosing) context.report({
1835
- node,
1836
- messageId: "dontSelfClose",
1837
- fix: (fixer) => {
1838
- const tagName = sourceCode.getText(node.name);
1839
- const selfCloseEnding = node.range[1];
1840
- const lastTokens = sourceCode.getLastTokens(node, { count: 3 });
1841
- const range = [sourceCode.isSpaceBetween(lastTokens[0], lastTokens[1]) ? selfCloseEnding - 3 : selfCloseEnding - 2, selfCloseEnding];
1842
- return fixer.replaceTextRange(range, `></${tagName}>`);
1843
- }
1844
- });
1845
- } };
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