@drskillissue/ganko 0.1.22 → 0.1.23

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.
@@ -14,7 +14,7 @@ import {
14
14
  rules3,
15
15
  runCrossFileRules,
16
16
  runPhases
17
- } from "./chunk-5IOPY65Q.js";
17
+ } from "./chunk-V6U7TQCD.js";
18
18
  import "./chunk-EGRHWZRV.js";
19
19
 
20
20
  // src/eslint-adapter.ts
package/dist/index.cjs CHANGED
@@ -12092,7 +12092,8 @@ var CONDITIONAL_MOUNT_TAGS = /* @__PURE__ */ new Set([
12092
12092
  ]);
12093
12093
  var messages16 = {
12094
12094
  loadingMismatch: "createResource '{{name}}' has no initialValue but uses manual loading checks ({{name}}.loading). Without initialValue, Suspense intercepts before your loading UI renders. Add initialValue to the options: createResource(fetcher, { initialValue: ... })",
12095
- conditionalSuspense: "createResource '{{name}}' has no initialValue and is rendered inside a conditional mount point ({{mountTag}}). This will trigger a distant Suspense boundary and unmount the entire subtree. Add initialValue to the options: createResource(fetcher, { initialValue: ... })"
12095
+ conditionalSuspense: "createResource '{{name}}' is rendered inside a conditional mount point ({{mountTag}}) with a distant Suspense boundary. When the fetcher's Promise is pending, the SuspenseContext increment fires and unmounts the entire subtree. initialValue does NOT prevent this \u2014 it only prevents the accessor from returning undefined.",
12096
+ missingErrorBoundary: "createResource '{{name}}' has no <ErrorBoundary> between its component and the nearest <Suspense>. When the fetcher throws (network error, 401/403/503, timeout), the error propagates to Suspense which absorbs it and stays in its fallback state permanently. Wrap the component in <ErrorBoundary fallback={...}> or catch errors inside the fetcher."
12096
12097
  };
12097
12098
  var options16 = {};
12098
12099
  function hasInitialValue(call) {
@@ -12135,35 +12136,92 @@ function hasLoadingRead(resourceVariable) {
12135
12136
  }
12136
12137
  return false;
12137
12138
  }
12138
- function findConditionalMountAncestor(graph, componentName) {
12139
+ function resolveFetcherFunction(graph, call) {
12140
+ const args = call.node.arguments;
12141
+ if (args.length === 0) return null;
12142
+ let fetcherNode;
12143
+ if (args.length === 1) {
12144
+ fetcherNode = args[0];
12145
+ } else if (args.length === 2) {
12146
+ const lastArg = args[1];
12147
+ fetcherNode = lastArg && lastArg.type === "ObjectExpression" ? args[0] : args[1];
12148
+ } else {
12149
+ fetcherNode = args[1];
12150
+ }
12151
+ if (!fetcherNode) return null;
12152
+ if (fetcherNode.type === "ArrowFunctionExpression" || fetcherNode.type === "FunctionExpression") {
12153
+ return graph.functionsByNode.get(fetcherNode) ?? null;
12154
+ }
12155
+ if (fetcherNode.type === "Identifier") {
12156
+ const fns = graph.functionsByName.get(fetcherNode.name);
12157
+ if (fns && fns.length > 0) {
12158
+ const fn = fns[0];
12159
+ if (fn) return fn;
12160
+ }
12161
+ }
12162
+ return null;
12163
+ }
12164
+ function fetcherCanThrow(graph, fn, visited) {
12165
+ if (visited.has(fn.id)) return false;
12166
+ visited.add(fn.id);
12167
+ if (fn.async && fn.awaitRanges.length > 0) return true;
12168
+ if (fn.hasThrowStatement) return true;
12169
+ const callSites = fn.callSites;
12170
+ for (let i = 0, len = callSites.length; i < len; i++) {
12171
+ const callSite = callSites[i];
12172
+ if (!callSite) continue;
12173
+ if (!callSite.resolvedTarget) return true;
12174
+ if (fetcherCanThrow(graph, callSite.resolvedTarget, visited)) return true;
12175
+ }
12176
+ return false;
12177
+ }
12178
+ function analyzeComponentBoundaries(graph, componentName) {
12179
+ const result = {
12180
+ conditionalMountTag: null,
12181
+ suspenseDistance: 0,
12182
+ lacksErrorBoundary: false
12183
+ };
12139
12184
  const usages = graph.jsxByTag.get(componentName) ?? [];
12185
+ if (usages.length === 0) return result;
12140
12186
  for (let i = 0, len = usages.length; i < len; i++) {
12141
12187
  const usage = usages[i];
12142
12188
  if (!usage) continue;
12143
12189
  let current = usage.parent;
12144
12190
  let conditionalTag = null;
12145
12191
  let componentLevels = 0;
12192
+ let foundErrorBoundary = false;
12193
+ let foundSuspense = false;
12146
12194
  while (current) {
12147
12195
  const tag = current.tag;
12148
12196
  if (tag && !current.isDomElement) {
12149
12197
  componentLevels++;
12150
- if (tag === "Suspense") {
12198
+ if (tag === "ErrorBoundary") {
12199
+ foundErrorBoundary = true;
12200
+ } else if (tag === "Suspense") {
12201
+ foundSuspense = true;
12202
+ if (!foundErrorBoundary) {
12203
+ result.lacksErrorBoundary = true;
12204
+ }
12151
12205
  if (conditionalTag !== null && componentLevels > 1) {
12152
- return { tag: conditionalTag, suspenseDistance: componentLevels };
12206
+ result.conditionalMountTag = conditionalTag;
12207
+ result.suspenseDistance = componentLevels;
12153
12208
  }
12154
- return null;
12155
- }
12156
- if (conditionalTag === null && CONDITIONAL_MOUNT_TAGS.has(tag)) {
12209
+ break;
12210
+ } else if (conditionalTag === null && CONDITIONAL_MOUNT_TAGS.has(tag)) {
12157
12211
  conditionalTag = tag;
12158
12212
  }
12159
12213
  }
12160
12214
  current = current.parent;
12161
12215
  }
12162
- if (conditionalTag !== null) {
12163
- return { tag: conditionalTag, suspenseDistance: componentLevels };
12216
+ if (!foundSuspense && !foundErrorBoundary) {
12217
+ result.lacksErrorBoundary = true;
12218
+ if (conditionalTag !== null) {
12219
+ result.conditionalMountTag = conditionalTag;
12220
+ result.suspenseDistance = componentLevels;
12221
+ }
12164
12222
  }
12165
12223
  }
12166
- return null;
12224
+ return result;
12167
12225
  }
12168
12226
  function getContainingComponentName(graph, call) {
12169
12227
  const selfComponent = graph.componentScopes.get(call.scope);
@@ -12185,7 +12243,7 @@ var resourceImplicitSuspense = defineSolidRule({
12185
12243
  severity: "warn",
12186
12244
  messages: messages16,
12187
12245
  meta: {
12188
- description: "Detect createResource without initialValue that implicitly triggers Suspense boundaries.",
12246
+ description: "Detect createResource that implicitly triggers or permanently breaks Suspense boundaries.",
12189
12247
  fixable: false,
12190
12248
  category: "reactivity"
12191
12249
  },
@@ -12193,30 +12251,37 @@ var resourceImplicitSuspense = defineSolidRule({
12193
12251
  check(graph, emit) {
12194
12252
  const resourceCalls = getCallsByPrimitive(graph, "createResource");
12195
12253
  if (resourceCalls.length === 0) return;
12254
+ const throwVisited = /* @__PURE__ */ new Set();
12255
+ const boundaryCache = /* @__PURE__ */ new Map();
12196
12256
  for (let i = 0, len = resourceCalls.length; i < len; i++) {
12197
12257
  const call = resourceCalls[i];
12198
12258
  if (!call) continue;
12199
- if (hasInitialValue(call)) continue;
12200
12259
  const resourceName = getResourceVariableName(call);
12201
12260
  if (!resourceName) continue;
12202
- const resourceVariable = findResourceVariable(graph, resourceName);
12203
- if (resourceVariable && hasLoadingRead(resourceVariable)) {
12204
- emit(
12205
- createDiagnostic(
12206
- graph.file,
12207
- call.node,
12208
- "resource-implicit-suspense",
12209
- "loadingMismatch",
12210
- resolveMessage(messages16.loadingMismatch, { name: resourceName }),
12211
- "warn"
12212
- )
12213
- );
12214
- continue;
12215
- }
12261
+ const hasInitial = hasInitialValue(call);
12216
12262
  const componentName = getContainingComponentName(graph, call);
12263
+ if (!hasInitial) {
12264
+ const resourceVariable = findResourceVariable(graph, resourceName);
12265
+ if (resourceVariable && hasLoadingRead(resourceVariable)) {
12266
+ emit(
12267
+ createDiagnostic(
12268
+ graph.file,
12269
+ call.node,
12270
+ "resource-implicit-suspense",
12271
+ "loadingMismatch",
12272
+ resolveMessage(messages16.loadingMismatch, { name: resourceName }),
12273
+ "warn"
12274
+ )
12275
+ );
12276
+ }
12277
+ }
12217
12278
  if (!componentName) continue;
12218
- const conditional = findConditionalMountAncestor(graph, componentName);
12219
- if (conditional) {
12279
+ let analysis = boundaryCache.get(componentName);
12280
+ if (!analysis) {
12281
+ analysis = analyzeComponentBoundaries(graph, componentName);
12282
+ boundaryCache.set(componentName, analysis);
12283
+ }
12284
+ if (analysis.conditionalMountTag) {
12220
12285
  emit(
12221
12286
  createDiagnostic(
12222
12287
  graph.file,
@@ -12225,12 +12290,27 @@ var resourceImplicitSuspense = defineSolidRule({
12225
12290
  "conditionalSuspense",
12226
12291
  resolveMessage(messages16.conditionalSuspense, {
12227
12292
  name: resourceName,
12228
- mountTag: conditional.tag
12293
+ mountTag: analysis.conditionalMountTag
12229
12294
  }),
12230
12295
  "error"
12231
12296
  )
12232
12297
  );
12233
12298
  }
12299
+ if (analysis.lacksErrorBoundary) {
12300
+ const fetcherFn = resolveFetcherFunction(graph, call);
12301
+ if (fetcherFn && fetcherCanThrow(graph, fetcherFn, throwVisited)) {
12302
+ emit(
12303
+ createDiagnostic(
12304
+ graph.file,
12305
+ call.node,
12306
+ "resource-implicit-suspense",
12307
+ "missingErrorBoundary",
12308
+ resolveMessage(messages16.missingErrorBoundary, { name: resourceName }),
12309
+ "error"
12310
+ )
12311
+ );
12312
+ }
12313
+ }
12234
12314
  }
12235
12315
  }
12236
12316
  });
@@ -42304,13 +42384,14 @@ var RULES = [
42304
42384
  {
42305
42385
  "id": "resource-implicit-suspense",
42306
42386
  "severity": "warn",
42307
- "description": "Detect createResource without initialValue that implicitly triggers Suspense boundaries.",
42387
+ "description": "Detect createResource that implicitly triggers or permanently breaks Suspense boundaries.",
42308
42388
  "fixable": false,
42309
42389
  "category": "reactivity",
42310
42390
  "plugin": "solid",
42311
42391
  "messages": {
42312
42392
  "loadingMismatch": "createResource '{{name}}' has no initialValue but uses manual loading checks ({{name}}.loading). Without initialValue, Suspense intercepts before your loading UI renders. Add initialValue to the options: createResource(fetcher, { initialValue: ... })",
42313
- "conditionalSuspense": "createResource '{{name}}' has no initialValue and is rendered inside a conditional mount point ({{mountTag}}). This will trigger a distant Suspense boundary and unmount the entire subtree. Add initialValue to the options: createResource(fetcher, { initialValue: ... })"
42393
+ "conditionalSuspense": "createResource '{{name}}' is rendered inside a conditional mount point ({{mountTag}}) with a distant Suspense boundary. When the fetcher's Promise is pending, the SuspenseContext increment fires and unmounts the entire subtree. initialValue does NOT prevent this \u2014 it only prevents the accessor from returning undefined.",
42394
+ "missingErrorBoundary": "createResource '{{name}}' has no <ErrorBoundary> between its component and the nearest <Suspense>. When the fetcher throws (network error, 401/403/503, timeout), the error propagates to Suspense which absorbs it and stays in its fallback state permanently. Wrap the component in <ErrorBoundary fallback={...}> or catch errors inside the fetcher."
42314
42395
  }
42315
42396
  },
42316
42397
  {
@@ -42513,7 +42594,7 @@ var RULES_BY_CATEGORY = {
42513
42594
  "css-structure": [{ "id": "css-no-empty-rule", "severity": "warn", "description": "Disallow empty CSS rules.", "fixable": false, "category": "css-structure", "plugin": "css", "messages": { "emptyRule": "Empty rule `{{selector}}` should be removed." } }, { "id": "css-no-unknown-container-name", "severity": "error", "description": "Disallow unknown named containers in @container queries.", "fixable": false, "category": "css-structure", "plugin": "css", "messages": { "unknownContainer": "Unknown container name `{{name}}` in @container query." } }, { "id": "css-no-unused-container-name", "severity": "warn", "description": "Disallow unused named containers.", "fixable": false, "category": "css-structure", "plugin": "css", "messages": { "unusedContainer": "Container name `{{name}}` is declared but never queried." } }, { "id": "layer-requirement-for-component-rules", "severity": "warn", "description": "Require style rules to be inside @layer when the file defines layers.", "fixable": false, "category": "css-structure", "plugin": "css", "messages": { "missingLayer": "Rule `{{selector}}` is not inside any @layer block while this file uses @layer. Place component rules inside an explicit layer." } }],
42514
42595
  "jsx": [{ "id": "components-return-once", "severity": "error", "description": "Disallow early returns in components. Solid components only run once, and so conditionals should be inside JSX.", "fixable": true, "category": "jsx", "plugin": "solid", "messages": { "noEarlyReturn": "Early returns in Solid components break reactivity because the component function only runs once. Use <Show> or <Switch>/<Match> inside the JSX to conditionally render content instead of returning early from the function.", "noConditionalReturn": "Conditional expressions in return statements break reactivity because Solid components only run once. Wrap the condition in <Show when={...}> for a single condition, or <Switch>/<Match> for multiple conditions." } }, { "id": "jsx-no-duplicate-props", "severity": "error", "description": "Disallow passing the same prop twice in JSX.", "fixable": true, "category": "jsx", "plugin": "solid", "messages": { "noDuplicateProps": "Duplicate prop detected. Each prop should only be specified once; the second value will override the first.", "noDuplicateClass": "Duplicate `class` prop detected. While this might appear to work, it can break unexpectedly because only one class binding is applied. Use `classList` to conditionally apply multiple classes.", "noDuplicateChildren": "Conflicting children: {{used}}. Only one method of setting children is allowed at a time." } }, { "id": "jsx-no-script-url", "severity": "error", "description": "Disallow javascript: URLs.", "fixable": true, "category": "jsx", "plugin": "solid", "messages": { "noJSURL": "Using javascript: URLs is a security risk because it can enable cross-site scripting (XSS) attacks. Use an event handler like onClick instead, or navigate programmatically with useNavigate()." } }, { "id": "jsx-no-undef", "severity": "error", "description": "Disallow references to undefined variables in JSX. Handles custom directives.", "fixable": false, "category": "jsx", "plugin": "solid", "messages": { "customDirectiveUndefined": "Custom directive '{{identifier}}' is not defined. Directives must be imported or declared in scope before use (e.g., `const {{identifier}} = (el, accessor) => { ... }`)." } }, { "id": "jsx-uses-vars", "severity": "warn", "description": "Detect imported components and directives that are never used in JSX.", "fixable": false, "category": "jsx", "plugin": "solid", "messages": { "unusedComponent": "Component '{{name}}' is imported but never used in JSX.", "unusedDirective": "Directive '{{name}}' is imported but never used in JSX." } }, { "id": "no-innerhtml", "severity": "error", "description": "Disallow usage of the innerHTML attribute, which can lead to security vulnerabilities.", "fixable": true, "category": "jsx", "plugin": "solid", "messages": { "dangerous": "Using innerHTML with dynamic content is a security risk. Unsanitized user input can lead to cross-site scripting (XSS) attacks. Use a sanitization library or render content safely.", "conflict": "The innerHTML prop will overwrite all child elements. Remove the children or use innerHTML on an empty element.", "notHtml": "The innerHTML value doesn't appear to be HTML. If you're setting text content, use innerText instead for clarity and safety.", "dangerouslySetInnerHTML": "The dangerouslySetInnerHTML is a React prop that Solid doesn't support. Use innerHTML instead." } }, { "id": "no-unknown-namespaces", "severity": "error", "description": "Enforce using only Solid-specific namespaced attribute names (i.e. `'on:'` in `<div on:click={...} />`).", "fixable": false, "category": "jsx", "plugin": "solid", "messages": { "unknownNamespace": "'{{namespace}}:' is not a recognized Solid namespace. Valid namespaces are: {{validNamespaces}}.", "styleNamespace": "The 'style:' namespace works but is discouraged. Use the style prop with an object instead: style={{ {{property}}: value }}.", "classNamespace": `The 'class:' namespace works but is discouraged. Use the classList prop instead: classList={{ "{{className}}": condition }}.`, "componentNamespace": "Namespaced attributes like '{{namespace}}:' only work on DOM elements, not components. The '{{fullName}}' attribute will be passed as a regular prop named '{{fullName}}'." } }, { "id": "show-truthy-conversion", "severity": "error", "description": "Detect <Show when={expr}> where expr is not explicitly boolean, which may have unexpected truthy/falsy behavior.", "fixable": true, "category": "jsx", "plugin": "solid", "messages": { "showNonBoolean": "<Show when={{{{expr}}}}> uses truthy/falsy conversion. Value '0' or empty string '' will hide content. Use explicit boolean: when={Boolean({{expr}})} or when={{{expr}}} != null}" } }, { "id": "suspense-boundary-missing", "severity": "error", "description": "Detect missing fallback props on Suspense/ErrorBoundary, and lazy components without Suspense wrapper.", "fixable": false, "category": "jsx", "plugin": "solid", "messages": { "suspenseNoFallback": "<Suspense> should have a fallback prop to show while children are loading. Add: fallback={<Loading />}", "errorBoundaryNoFallback": "<ErrorBoundary> should have a fallback prop to show when an error occurs. Add: fallback={(err) => <Error error={err} />}", "lazyNoSuspense": "Lazy component '{{name}}' must be wrapped in a <Suspense> boundary. Add a <Suspense fallback={...}> ancestor." } }, { "id": "validate-jsx-nesting", "severity": "error", "description": "Validates that HTML elements are nested according to the HTML5 specification.", "fixable": false, "category": "jsx", "plugin": "solid", "messages": { "invalidNesting": "Invalid HTML nesting: <{{child}}> cannot be a child of <{{parent}}>. {{reason}}.", "voidElementWithChildren": "<{{parent}}> is a void element and cannot have children. Found <{{child}}> as a child.", "invalidListChild": "<{{child}}> is not a valid direct child of <{{parent}}>. Only <li> elements can be direct children of <ul> and <ol>.", "invalidSelectChild": "<{{child}}> is not a valid direct child of <select>. Only <option> and <optgroup> elements are allowed.", "invalidTableChild": "<{{child}}> is not a valid direct child of <{{parent}}>. Expected: {{expected}}.", "invalidDlChild": "<{{child}}> is not a valid direct child of <dl>. Only <dt>, <dd>, and <div> elements are allowed." } }],
42515
42596
  "performance": [{ "id": "avoid-arguments-object", "severity": "warn", "description": "Disallow arguments object (use rest parameters instead).", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "avoidArguments": "arguments object can prevent V8 optimization. Use rest parameters (...args) instead." } }, { "id": "avoid-chained-array-methods", "severity": "warn", "description": "Flags chained array methods creating 3+ intermediate arrays, or filter().map() pattern.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "avoidChainedArrayMethods": "Chain creates {{count}} intermediate array(s). Consider reduce() or a loop. Chain: {{chain}}", "mapJoinHotPath": "map().join() inside loops allocates intermediate arrays on a hot path. Prefer single-pass string construction." } }, { "id": "avoid-defensive-copy-for-scalar-stat", "severity": "warn", "description": "Disallow defensive array copies passed into scalar statistic calls.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "defensiveCopy": "Defensive copy before scalar statistic '{{stat}}' allocates unnecessarily. Prefer readonly/non-mutating scalar computation." } }, { "id": "avoid-delete-operator", "severity": "warn", "description": "Disallow delete operator on objects (causes V8 deoptimization).", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "avoidDelete": "delete operator transitions object to slow mode. Use `obj.prop = undefined` or destructuring instead." } }, { "id": "avoid-function-allocation-in-hot-loop", "severity": "warn", "description": "Disallow creating closures inside loops.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "closureInLoop": "Function created inside loop allocates new closure per iteration. Consider hoisting or using event delegation." } }, { "id": "avoid-hidden-class-transition", "severity": "warn", "description": "Suggest consistent object shapes to avoid V8 hidden class transitions.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "hiddenClassTransition": "Property '{{property}}' added conditionally to '{{object}}' creates inconsistent object shapes. Initialize '{{property}}' in the object literal." } }, { "id": "avoid-intermediate-map-copy", "severity": "warn", "description": "Disallow temporary Map allocations that are copied key-for-key into another Map.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "intermediateMapCopy": "Intermediate Map '{{tempName}}' is copied into '{{outName}}' key-for-key. Build output directly to avoid extra allocation." } }, { "id": "avoid-megamorphic-property-access", "severity": "warn", "description": "Avoid property access on `any` or wide union types to prevent V8 deoptimization.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "megamorphicAccess": "Property access on `any` or wide union type causes V8 deoptimization. Consider narrowing the type." } }, { "id": "avoid-quadratic-pair-comparison", "severity": "warn", "description": "Disallow nested for-loops over the same collection creating O(n\xB2) pair comparison.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "quadraticPair": "Nested loops over `{{collection}}` create O(n\xB2) pair comparison. Group by a key property first." } }, { "id": "avoid-quadratic-spread", "severity": "error", "description": "Disallow spreading accumulator in reduce callbacks (O(n\xB2) complexity).", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "quadraticSpread": "Spreading accumulator in reduce creates O(n\xB2) complexity. Use push() instead." } }, { "id": "avoid-repeated-indexof-check", "severity": "warn", "description": "Disallow 3+ .indexOf() calls on the same array variable in one function.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "repeatedIndexOf": "{{count}} .indexOf() calls on `{{name}}` in the same function. Use a Set, regex, or single-pass scan instead." } }, { "id": "avoid-slice-sort-pattern", "severity": "warn", "description": "Disallow .slice().sort() and .slice().reverse() chains. Use .toSorted()/.toReversed().", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "sliceSort": ".slice().sort() creates an intermediate array. Use .toSorted() instead.", "sliceReverse": ".slice().reverse() creates an intermediate array. Use .toReversed() instead.", "spreadSort": "[...array].sort() creates an intermediate array. Use .toSorted() instead.", "spreadReverse": "[...array].reverse() creates an intermediate array. Use .toReversed() instead." } }, { "id": "avoid-sparse-arrays", "severity": "warn", "description": "Disallow new Array(n) without fill (creates holey array).", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "sparseArray": "new Array(n) creates a holey array. Use Array.from() or .fill() instead." } }, { "id": "avoid-spread-sort-map-join-pipeline", "severity": "warn", "description": "Disallow [...iterable].sort().map().join() pipelines on hot paths.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "spreadSortMapJoin": "Spread+sort+map+join pipeline allocates multiple intermediates. Prefer single-pass string construction on hot paths." } }, { "id": "bounded-worklist-traversal", "severity": "warn", "description": "Detect queue/worklist traversals with unbounded growth and no guard.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "boundedWorklist": "Worklist '{{name}}' grows via push() without visited set or explicit size bound. Add traversal guard to prevent pathological growth." } }, { "id": "closure-captured-scope", "severity": "warn", "description": "Detect closures returned from scopes containing large allocations that may be retained.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "capturedScope": "Returned closure shares scope with large allocation '{{name}}'. V8 may retain the allocation via scope capture even though the closure doesn't reference it. Move the allocation to an inner scope." } }, { "id": "closure-dom-circular", "severity": "warn", "description": "Detect event handler property assignments that create closure-DOM circular references.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "circularRef": "Event handler on '{{param}}' creates a closure that captures '{{param}}', forming a closure-DOM circular reference. Use addEventListener with a named handler for easier cleanup." } }, { "id": "create-root-dispose", "severity": "warn", "description": "Detect createRoot with unused dispose parameter.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "unusedDispose": "createRoot() dispose parameter is never used. The reactive tree will never be cleaned up. Call dispose(), return it, or pass it to onCleanup()." } }, { "id": "detached-dom-reference", "severity": "warn", "description": "Detect DOM query results stored in module-scoped variables that may hold detached nodes.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "detachedRef": "DOM query result from '{{method}}' stored in module-scoped variable '{{name}}'. If the DOM node is removed, this reference prevents garbage collection. Use a local variable or WeakRef instead." } }, { "id": "effect-outside-root", "severity": "error", "description": "Detect reactive computations created outside a reactive root (no Owner).", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "orphanedEffect": "{{primitive}}() called outside a reactive root. Without an Owner, this computation is never disposed and leaks memory. Wrap in a component, createRoot, or runWithOwner." } }, { "id": "finalization-registry-leak", "severity": "error", "description": "Detect FinalizationRegistry.register() where heldValue references the target.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "selfReference": "FinalizationRegistry.register() heldValue references the target '{{name}}'. This strong reference prevents the target from being garbage collected, defeating the purpose of the registry." } }, { "id": "no-char-array-materialization", "severity": "warn", "description": 'Disallow split(""), Array.from(str), or [...str] in parsing loops.', "fixable": false, "category": "performance", "plugin": "solid", "messages": { "charArrayMaterialization": "Character array materialization via {{pattern}} in parsing loops allocates O(n) extra memory. Prefer index-based scanning." } }, { "id": "no-double-pass-delimiter-count", "severity": "warn", "description": "Disallow split-based delimiter counting followed by additional split passes.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "doublePassDelimiterCount": "Delimiter counting via `split(...).length` plus another `split(...)` repeats full-string passes. Prefer one indexed scan." } }, { "id": "no-full-split-in-hot-parse", "severity": "warn", "description": "Disallow full split() materialization inside hot string parsing loops.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "fullSplitInHotParse": "`split()` inside parsing loops materializes full token arrays each iteration. Prefer cursor/index scanning." } }, { "id": "no-heavy-parser-constructor-in-loop", "severity": "warn", "description": "Disallow constructing heavy parsing helpers inside loops.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "heavyParserConstructor": "`new {{ctor}}(...)` inside parsing loops repeatedly allocates heavy parser helpers. Hoist and reuse instances." } }, { "id": "no-leaked-abort-controller", "severity": "warn", "description": "Detect AbortController in effects without abort() in onCleanup.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "leakedAbort": "new AbortController() inside a reactive effect without onCleanup. Add onCleanup(() => controller.abort())." } }, { "id": "no-leaked-animation-frame", "severity": "warn", "description": "Detect requestAnimationFrame in effects without cancelAnimationFrame in onCleanup.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "leakedRaf": "requestAnimationFrame() inside a reactive effect without onCleanup. Add onCleanup(() => cancelAnimationFrame(id))." } }, { "id": "no-leaked-event-listener", "severity": "warn", "description": "Detect addEventListener in effects without removeEventListener in onCleanup.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "leakedListener": "addEventListener() inside a reactive effect without onCleanup. Each re-run leaks a listener. Add onCleanup(() => removeEventListener(...))." } }, { "id": "no-leaked-observer", "severity": "warn", "description": "Detect Observer APIs in effects without disconnect() in onCleanup.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "leakedObserver": "new {{type}}() inside a reactive effect without onCleanup. Add onCleanup(() => observer.disconnect())." } }, { "id": "no-leaked-subscription", "severity": "warn", "description": "Detect WebSocket/EventSource/BroadcastChannel in effects without close() in onCleanup.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "leakedSubscription": "new {{type}}() inside a reactive effect without onCleanup. Add onCleanup(() => instance.close())." } }, { "id": "no-leaked-timer", "severity": "warn", "description": "Detect setInterval/setTimeout in effects without onCleanup to clear them.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "leakedTimer": "{{setter}}() inside a reactive effect without onCleanup. Each re-run leaks a timer. Add onCleanup(() => {{clearer}}(id))." } }, { "id": "no-loop-string-plus-equals", "severity": "warn", "description": "Disallow repeated string += accumulation in parsing loops.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "loopStringPlusEquals": "Repeated string `+=` in parsing loops creates avoidable allocations. Buffer chunks and join once." } }, { "id": "no-multipass-split-pipeline", "severity": "warn", "description": "Disallow multipass split/map/filter pipelines in parsing code.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "multipassSplit": "`split()` followed by multiple array passes allocates heavily on parsing paths. Prefer single-pass parsing." } }, { "id": "no-per-char-substring-scan", "severity": "warn", "description": "Disallow per-character substring/charAt scanning patterns in loops.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "perCharSubstring": "Per-character `{{method}}()` scanning in loops allocates extra strings. Prefer index + charCodeAt scanning." } }, { "id": "no-repeated-token-normalization", "severity": "warn", "description": "Disallow repeated trim/lower/upper normalization chains on the same token in one function.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "repeatedTokenNormalization": "Repeated token normalization `{{chain}}` on `{{name}}` in one function. Compute once and reuse." } }, { "id": "no-rescan-indexof-loop", "severity": "warn", "description": "Disallow repeated indexOf/includes scans from start in parsing loops.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "rescanIndexOf": "Repeated `{{method}}()` from string start inside loops rescans prior text. Pass a cursor start index." } }, { "id": "no-rest-slice-loop", "severity": "warn", "description": "Disallow repeated self-slice reassignment loops in string parsing code.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "restSliceLoop": "Repeated `{{name}} = {{name}}.{{method}}(...)` in loops creates string churn. Track cursor indexes instead." } }, { "id": "no-shift-splice-head-consume", "severity": "warn", "description": "Disallow shift/splice(0,1) head-consume patterns in loops.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "headConsume": "Head-consuming `{{method}}()` inside loops causes array reindexing costs. Use index cursor iteration instead." } }, { "id": "no-write-only-index", "severity": "warn", "description": "Detect index structures that are written but never queried by key.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "writeOnlyIndex": "Index '{{name}}' is built via writes but never queried by key. Remove it or use direct collection flow." } }, { "id": "prefer-charcode-over-regex-test", "severity": "warn", "description": "Prefer charCodeAt() range checks over regex .test() for single-character classification.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "regexTest": "Regex `{{pattern}}`.test() on a single character. Use charCodeAt() range checks instead." } }, { "id": "prefer-index-scan-over-string-iterator", "severity": "warn", "description": "Prefer index-based string scanning over for-of iteration in ASCII parser code.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "preferIndexScan": "ASCII parsing loops should avoid `for...of` string iteration. Prefer indexed scanning with charCodeAt for lower overhead." } }, { "id": "prefer-lazy-property-access", "severity": "warn", "description": "Suggests moving property access after early returns when not used immediately.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "preferLazyPropertyAccess": "Property '{{propertyName}}' assigned to '{{variableName}}' before early return but not used there. Move assignment after early returns." } }, { "id": "prefer-map-lookup-over-linear-scan", "severity": "warn", "description": "Disallow repeated linear scans over fixed literal collections in hot paths.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "preferMapLookup": "Linear scan over fixed collection '{{name}}' in '{{fnName}}'. Precompute Map/Set lookup for O(1) access." } }, { "id": "prefer-map-over-object-dictionary", "severity": "warn", "description": "Suggest Map for dictionary-like objects with dynamic keys.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "preferMap": "Dynamic key assignment on dictionary object causes hidden class transitions. Consider using Map." } }, { "id": "prefer-precompiled-regex", "severity": "warn", "description": "Prefer hoisting regex literals to module-level constants to avoid repeated compilation.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "inlineRegex": "Regex `{{pattern}}` is compiled on every call. Hoist to a module-level constant." } }, { "id": "prefer-set-has-over-equality-chain", "severity": "warn", "description": "Disallow 4+ guard-style equality checks against string literals on the same variable. Use a Set.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "equalityChain": "{{count}} equality checks against `{{name}}`. Extract literals to a Set and use .has() instead." } }, { "id": "prefer-set-lookup-in-loop", "severity": "warn", "description": "Disallow linear search methods (.includes/.indexOf) on arrays inside loops.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "preferSet": "`.{{method}}()` on `{{name}}` called inside a loop. Convert to a Set for O(1) lookups." } }, { "id": "recursive-timer", "severity": "warn", "description": "Detect setTimeout that recursively calls its enclosing function.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "recursiveTimer": "setTimeout() recursively calls '{{name}}', creating an unbreakable polling loop. Add a termination condition or use setInterval with cleanup." } }, { "id": "self-referencing-store", "severity": "error", "description": "Detect setStore() where the value argument references the store itself.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "selfReference": "setStore() value references the store variable '{{name}}', creating a circular proxy reference. This prevents garbage collection and can cause infinite loops." } }, { "id": "unbounded-collection", "severity": "warn", "description": "Detect module-scoped Map/Set/Array that only grow without removal.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "unboundedCollection": "Module-scoped {{type}} '{{name}}' only uses additive methods ({{methods}}). Without removal or clearing, this grows unbounded. Consider WeakMap, LRU eviction, or periodic clear()." } }, { "id": "unbounded-signal-accumulation", "severity": "warn", "description": "Detect signal setters that accumulate data without truncation via spread+append pattern.", "fixable": false, "category": "performance", "plugin": "solid", "messages": { "unbounded": "Signal setter '{{name}}' accumulates data without bounds. The array grows monotonically via spread+append. Add truncation (e.g. prev.slice(-limit)) to prevent unbounded growth." } }],
42516
- "reactivity": [{ "id": "async-tracked", "severity": "error", "description": "Disallow async functions in tracked scopes (createEffect, createMemo, etc.)", "fixable": false, "category": "reactivity", "plugin": "solid", "messages": { "asyncCreateEffect": "Async function{{fnName}} in createEffect loses tracking after await. Read all signals before the first await, or use createResource for async data fetching.", "asyncCreateMemo": "Async function{{fnName}} in createMemo won't work correctly. createMemo must be synchronous. For async derived data, use createResource instead.", "asyncCreateComputed": "Async function{{fnName}} in createComputed won't track properly. createComputed must be synchronous\u2014signal reads after await won't trigger re-computation.", "asyncCreateRenderEffect": "Async function{{fnName}} in createRenderEffect breaks DOM update timing. createRenderEffect must be synchronous. Move async work to onMount or createResource.", "asyncTrackedGeneric": "Async function{{fnName}} in {{source}} won't track reactivity after await. Solid's tracking only works synchronously\u2014signal reads after await are ignored." } }, { "id": "children-helper-misuse", "severity": "error", "description": "Detect misuse of the children() helper that causes unnecessary re-computation or breaks reactivity", "fixable": false, "category": "reactivity", "plugin": "solid", "messages": { "multipleChildrenCalls": "The children() helper should only be called once per component. Each call re-resolves children, causing unnecessary computation. Store the result and reuse the accessor.", "directChildrenAccess": "Access props.children through the children() helper in reactive contexts. Direct access won't properly resolve or track children. Use: const resolved = children(() => props.children);" } }, { "id": "cleanup-scope", "severity": "error", "description": "Detect onCleanup called outside of a valid reactive scope", "fixable": false, "category": "reactivity", "plugin": "solid", "messages": { "cleanupOutsideScope": "onCleanup() called outside a reactive scope ({{location}}). The cleanup function will never execute unless this code runs within a component, effect, createRoot, or runWithOwner." } }, { "id": "derived-signal", "severity": "error", "description": "Detect functions that capture reactive values but are called in untracked contexts", "fixable": false, "category": "reactivity", "plugin": "solid", "messages": { "moduleScopeInit": "Assigning '{{fnName}}()' to '{{varName}}' at module scope runs once at startup. It captures {{vars}} which won't trigger updates.", "moduleScopeCall": "'{{fnName}}()' at module scope executes once when the module loads. It captures {{vars}}\u2014changes won't cause this to re-run.", "componentTopLevelInit": "'{{fnName}}()' assigned to '{{varName}}' in '{{componentName}}' captures a one-time snapshot of {{vars}}. Changes won't update '{{varName}}'. Call in JSX or use createMemo().", "componentTopLevelCall": "'{{fnName}}()' at top-level of '{{componentName}}' runs once and captures a snapshot of {{vars}}. Changes won't re-run this. Move inside JSX: {{{fnName}}()} or wrap with createMemo().", "utilityFnCall": "'{{fnName}}()' inside '{{utilityName}}' won't be reactive. Call '{{utilityName}}' from a tracked scope (createEffect, JSX), or pass {{vars}} as parameters.", "syncCallbackCall": "'{{fnName}}()' inside {{methodName}}() callback runs outside a tracking scope. The result captures a snapshot of {{vars}} that won't update.", "untrackedCall": "'{{fnName}}()' called in an untracked context. It captures {{vars}} which won't trigger updates here. Move to JSX or a tracked scope." } }, { "id": "effect-as-memo", "severity": "error", "description": "Detect createEffect that only sets a derived signal value, which should be createMemo instead", "fixable": true, "category": "reactivity", "plugin": "solid", "messages": { "effectAsMemo": "This createEffect only computes a derived value. Use createMemo() instead: const {{signalName}} = createMemo(() => {{expression}});" } }, { "id": "effect-as-mount", "severity": "error", "description": "Detect createEffect/createRenderEffect with no reactive dependencies that should be onMount instead", "fixable": true, "category": "reactivity", "plugin": "solid", "messages": { "effectAsMount": "This {{primitive}} has no reactive dependencies and runs only once. Use onMount() for initialization logic that doesn't need to re-run." } }, { "id": "inline-component", "severity": "error", "description": "Detect component functions defined inside other components, which causes remount on every parent update", "fixable": false, "category": "reactivity", "plugin": "solid", "messages": { "inlineComponent": "Component '{{name}}' is defined inside another component. This creates a new component type on every render, causing unmount/remount. Move the component definition outside." } }, { "id": "no-top-level-signal-call", "severity": "error", "description": "Disallow calling signals at component top-level (captures stale snapshots)", "fixable": false, "category": "reactivity", "plugin": "solid", "messages": { "assignedToVar": "'{{name}}()' assigned to '{{varName}}' in {{componentName}} captures a one-time snapshot. '{{varName}}' won't update when {{name}} changes. Use createMemo(): `const {{varName}} = createMemo(() => {{name}}());`", "computedValue": "'{{name}}()' in computation at top-level of {{componentName}} captures a stale snapshot. Wrap with createMemo(): `const {{varName}} = createMemo(() => /* computation using {{name}}() */);`", "templateLiteral": "'{{name}}()' in template literal at top-level of {{componentName}} captures a stale snapshot. Use createMemo() or compute directly in JSX: `{`Hello, ${{{name}}()}!`}`", "destructuring": "Destructuring '{{name}}()' at top-level of {{componentName}} captures a stale snapshot. Access properties in JSX or createMemo(): `{{{name}}().propertyName}`", "objectLiteral": "'{{name}}()' in object literal at top-level of {{componentName}} captures a stale snapshot. Use createMemo() for the object, or spread in JSX.", "arrayCreation": "'{{name}}()' in array creation at top-level of {{componentName}} captures a stale snapshot. Wrap with createMemo(): `const items = createMemo(() => Array.from(...));`", "earlyReturn": "'{{name}}()' in early return at top-level of {{componentName}} captures a stale snapshot. Use <Show when={{{name}}()}> for conditional rendering instead.", "conditionalAssign": "'{{name}}()' in ternary at top-level of {{componentName}} captures a stale snapshot. Use createMemo() or compute in JSX: `{{{name}}() ? 'Yes' : 'No'}`", "functionArgument": "'{{name}}()' passed as argument at top-level of {{componentName}} captures a stale snapshot. Move to createEffect() or compute in JSX.", "syncCallback": "'{{name}}()' inside {{methodName}}() at top-level of {{componentName}} captures a stale snapshot. Wrap the entire computation in createMemo(): `const result = createMemo(() => items.{{methodName}}(...));`", "topLevelCall": "'{{name}}()' at top-level of {{componentName}} captures a one-time snapshot. Changes to {{name}} won't update the result. Call directly in JSX or wrap in createMemo()." } }, { "id": "ref-early-access", "severity": "error", "description": "Detect accessing refs before they are assigned (before mount)", "fixable": false, "category": "reactivity", "plugin": "solid", "messages": { "refBeforeMount": "Ref '{{name}}' is accessed before component mounts. Refs are undefined until after mount. Access in onMount(), createEffect(), or event handlers." } }, { "id": "resource-access-unchecked", "severity": "error", "description": "Detect accessing resource data without checking loading/error state.", "fixable": false, "category": "reactivity", "plugin": "solid", "messages": { "resourceUnchecked": "Accessing resource '{{name}}' without checking loading/error state may return undefined. Wrap in <Show when={!{{name}}.loading}> or <Suspense>." } }, { "id": "resource-implicit-suspense", "severity": "warn", "description": "Detect createResource without initialValue that implicitly triggers Suspense boundaries.", "fixable": false, "category": "reactivity", "plugin": "solid", "messages": { "loadingMismatch": "createResource '{{name}}' has no initialValue but uses manual loading checks ({{name}}.loading). Without initialValue, Suspense intercepts before your loading UI renders. Add initialValue to the options: createResource(fetcher, { initialValue: ... })", "conditionalSuspense": "createResource '{{name}}' has no initialValue and is rendered inside a conditional mount point ({{mountTag}}). This will trigger a distant Suspense boundary and unmount the entire subtree. Add initialValue to the options: createResource(fetcher, { initialValue: ... })" } }, { "id": "resource-refetch-loop", "severity": "error", "description": "Detect refetch() calls inside createEffect which can cause infinite loops", "fixable": false, "category": "reactivity", "plugin": "solid", "messages": { "refetchInEffect": "Calling {{name}}.refetch() inside createEffect may cause infinite loops. The resource tracks its own dependencies. Move refetch to an event handler or use on() to control dependencies." } }, { "id": "signal-call", "severity": "error", "description": "Require signals to be called as functions when used in tracked contexts", "fixable": true, "category": "reactivity", "plugin": "solid", "messages": { "signalInJsxText": "Signal '{{name}}' in JSX text should be called: {{{name}}()}. Without (), you're rendering the function, not its value.", "signalInJsxAttribute": "Signal '{{name}}' in JSX attribute should be called: {{attr}}={{{name}}()}. Without (), the attribute won't update reactively.", "signalInTernary": "Signal '{{name}}' in ternary should be called: {{name}}() ? ... : .... The condition won't react to changes without ().", "signalInLogical": "Signal '{{name}}' in logical expression should be called: {{name}}() && .... Without (), this always evaluates to truthy (functions are truthy).", "signalInComparison": "Signal '{{name}}' in comparison should be called: {{name}}() === .... Comparing functions always returns false.", "signalInArithmetic": "Signal '{{name}}' in arithmetic should be called: {{name}}() + .... Math on functions produces NaN.", "signalInTemplate": "Signal '{{name}}' in template literal should be called: `...${{{name}}()}...`. Without (), you're embedding '[Function]'.", "signalInTrackedScope": "Signal '{{name}}' in {{where}} should be called: {{name}}(). Without (), reactivity is lost.", "badSignal": "The reactive variable '{{name}}' should be called as a function when used in {{where}}." } }, { "id": "signal-in-loop", "severity": "error", "description": "Detect problematic signal usage inside For/Index loop callbacks", "fixable": false, "category": "reactivity", "plugin": "solid", "messages": { "signalInLoop": "Creating signals inside <{{component}}> callback creates new signals on each render. Use a store at the parent level, or derive state from the index.", "signalCallInvariant": "Signal '{{name}}' called inside <{{component}}> produces the same value for every item. Extract to a variable or memoize with createMemo() before the loop.", "derivedCallInvariant": "'{{name}}()' inside <{{component}}> captures {{captures}} but doesn't use the loop item. Extract the call before the loop or pass the item as a parameter." } }, { "id": "store-reactive-break", "severity": "error", "description": "Detect patterns that break store reactivity: spreading stores, top-level property extraction, or destructuring", "fixable": false, "category": "reactivity", "plugin": "solid", "messages": { "storeSpread": "Spreading a store ({...store}) creates a static snapshot that won't update. Access store properties directly in JSX or tracked contexts.", "storeTopLevelAccess": "Accessing store property '{{property}}' at component top-level captures the value once. Access store.{{property}} directly in JSX or wrap in createMemo().", "storeDestructure": "Destructuring a store breaks reactivity. Access properties via store.{{property}} instead of destructuring." } }, { "id": "transition-pending-unchecked", "severity": "error", "description": "Detect useTransition usage without handling the isPending state", "fixable": false, "category": "reactivity", "plugin": "solid", "messages": { "pendingUnchecked": "useTransition returns [isPending, startTransition]. The isPending state should be used to show loading UI during transitions." } }],
42597
+ "reactivity": [{ "id": "async-tracked", "severity": "error", "description": "Disallow async functions in tracked scopes (createEffect, createMemo, etc.)", "fixable": false, "category": "reactivity", "plugin": "solid", "messages": { "asyncCreateEffect": "Async function{{fnName}} in createEffect loses tracking after await. Read all signals before the first await, or use createResource for async data fetching.", "asyncCreateMemo": "Async function{{fnName}} in createMemo won't work correctly. createMemo must be synchronous. For async derived data, use createResource instead.", "asyncCreateComputed": "Async function{{fnName}} in createComputed won't track properly. createComputed must be synchronous\u2014signal reads after await won't trigger re-computation.", "asyncCreateRenderEffect": "Async function{{fnName}} in createRenderEffect breaks DOM update timing. createRenderEffect must be synchronous. Move async work to onMount or createResource.", "asyncTrackedGeneric": "Async function{{fnName}} in {{source}} won't track reactivity after await. Solid's tracking only works synchronously\u2014signal reads after await are ignored." } }, { "id": "children-helper-misuse", "severity": "error", "description": "Detect misuse of the children() helper that causes unnecessary re-computation or breaks reactivity", "fixable": false, "category": "reactivity", "plugin": "solid", "messages": { "multipleChildrenCalls": "The children() helper should only be called once per component. Each call re-resolves children, causing unnecessary computation. Store the result and reuse the accessor.", "directChildrenAccess": "Access props.children through the children() helper in reactive contexts. Direct access won't properly resolve or track children. Use: const resolved = children(() => props.children);" } }, { "id": "cleanup-scope", "severity": "error", "description": "Detect onCleanup called outside of a valid reactive scope", "fixable": false, "category": "reactivity", "plugin": "solid", "messages": { "cleanupOutsideScope": "onCleanup() called outside a reactive scope ({{location}}). The cleanup function will never execute unless this code runs within a component, effect, createRoot, or runWithOwner." } }, { "id": "derived-signal", "severity": "error", "description": "Detect functions that capture reactive values but are called in untracked contexts", "fixable": false, "category": "reactivity", "plugin": "solid", "messages": { "moduleScopeInit": "Assigning '{{fnName}}()' to '{{varName}}' at module scope runs once at startup. It captures {{vars}} which won't trigger updates.", "moduleScopeCall": "'{{fnName}}()' at module scope executes once when the module loads. It captures {{vars}}\u2014changes won't cause this to re-run.", "componentTopLevelInit": "'{{fnName}}()' assigned to '{{varName}}' in '{{componentName}}' captures a one-time snapshot of {{vars}}. Changes won't update '{{varName}}'. Call in JSX or use createMemo().", "componentTopLevelCall": "'{{fnName}}()' at top-level of '{{componentName}}' runs once and captures a snapshot of {{vars}}. Changes won't re-run this. Move inside JSX: {{{fnName}}()} or wrap with createMemo().", "utilityFnCall": "'{{fnName}}()' inside '{{utilityName}}' won't be reactive. Call '{{utilityName}}' from a tracked scope (createEffect, JSX), or pass {{vars}} as parameters.", "syncCallbackCall": "'{{fnName}}()' inside {{methodName}}() callback runs outside a tracking scope. The result captures a snapshot of {{vars}} that won't update.", "untrackedCall": "'{{fnName}}()' called in an untracked context. It captures {{vars}} which won't trigger updates here. Move to JSX or a tracked scope." } }, { "id": "effect-as-memo", "severity": "error", "description": "Detect createEffect that only sets a derived signal value, which should be createMemo instead", "fixable": true, "category": "reactivity", "plugin": "solid", "messages": { "effectAsMemo": "This createEffect only computes a derived value. Use createMemo() instead: const {{signalName}} = createMemo(() => {{expression}});" } }, { "id": "effect-as-mount", "severity": "error", "description": "Detect createEffect/createRenderEffect with no reactive dependencies that should be onMount instead", "fixable": true, "category": "reactivity", "plugin": "solid", "messages": { "effectAsMount": "This {{primitive}} has no reactive dependencies and runs only once. Use onMount() for initialization logic that doesn't need to re-run." } }, { "id": "inline-component", "severity": "error", "description": "Detect component functions defined inside other components, which causes remount on every parent update", "fixable": false, "category": "reactivity", "plugin": "solid", "messages": { "inlineComponent": "Component '{{name}}' is defined inside another component. This creates a new component type on every render, causing unmount/remount. Move the component definition outside." } }, { "id": "no-top-level-signal-call", "severity": "error", "description": "Disallow calling signals at component top-level (captures stale snapshots)", "fixable": false, "category": "reactivity", "plugin": "solid", "messages": { "assignedToVar": "'{{name}}()' assigned to '{{varName}}' in {{componentName}} captures a one-time snapshot. '{{varName}}' won't update when {{name}} changes. Use createMemo(): `const {{varName}} = createMemo(() => {{name}}());`", "computedValue": "'{{name}}()' in computation at top-level of {{componentName}} captures a stale snapshot. Wrap with createMemo(): `const {{varName}} = createMemo(() => /* computation using {{name}}() */);`", "templateLiteral": "'{{name}}()' in template literal at top-level of {{componentName}} captures a stale snapshot. Use createMemo() or compute directly in JSX: `{`Hello, ${{{name}}()}!`}`", "destructuring": "Destructuring '{{name}}()' at top-level of {{componentName}} captures a stale snapshot. Access properties in JSX or createMemo(): `{{{name}}().propertyName}`", "objectLiteral": "'{{name}}()' in object literal at top-level of {{componentName}} captures a stale snapshot. Use createMemo() for the object, or spread in JSX.", "arrayCreation": "'{{name}}()' in array creation at top-level of {{componentName}} captures a stale snapshot. Wrap with createMemo(): `const items = createMemo(() => Array.from(...));`", "earlyReturn": "'{{name}}()' in early return at top-level of {{componentName}} captures a stale snapshot. Use <Show when={{{name}}()}> for conditional rendering instead.", "conditionalAssign": "'{{name}}()' in ternary at top-level of {{componentName}} captures a stale snapshot. Use createMemo() or compute in JSX: `{{{name}}() ? 'Yes' : 'No'}`", "functionArgument": "'{{name}}()' passed as argument at top-level of {{componentName}} captures a stale snapshot. Move to createEffect() or compute in JSX.", "syncCallback": "'{{name}}()' inside {{methodName}}() at top-level of {{componentName}} captures a stale snapshot. Wrap the entire computation in createMemo(): `const result = createMemo(() => items.{{methodName}}(...));`", "topLevelCall": "'{{name}}()' at top-level of {{componentName}} captures a one-time snapshot. Changes to {{name}} won't update the result. Call directly in JSX or wrap in createMemo()." } }, { "id": "ref-early-access", "severity": "error", "description": "Detect accessing refs before they are assigned (before mount)", "fixable": false, "category": "reactivity", "plugin": "solid", "messages": { "refBeforeMount": "Ref '{{name}}' is accessed before component mounts. Refs are undefined until after mount. Access in onMount(), createEffect(), or event handlers." } }, { "id": "resource-access-unchecked", "severity": "error", "description": "Detect accessing resource data without checking loading/error state.", "fixable": false, "category": "reactivity", "plugin": "solid", "messages": { "resourceUnchecked": "Accessing resource '{{name}}' without checking loading/error state may return undefined. Wrap in <Show when={!{{name}}.loading}> or <Suspense>." } }, { "id": "resource-implicit-suspense", "severity": "warn", "description": "Detect createResource that implicitly triggers or permanently breaks Suspense boundaries.", "fixable": false, "category": "reactivity", "plugin": "solid", "messages": { "loadingMismatch": "createResource '{{name}}' has no initialValue but uses manual loading checks ({{name}}.loading). Without initialValue, Suspense intercepts before your loading UI renders. Add initialValue to the options: createResource(fetcher, { initialValue: ... })", "conditionalSuspense": "createResource '{{name}}' is rendered inside a conditional mount point ({{mountTag}}) with a distant Suspense boundary. When the fetcher's Promise is pending, the SuspenseContext increment fires and unmounts the entire subtree. initialValue does NOT prevent this \u2014 it only prevents the accessor from returning undefined.", "missingErrorBoundary": "createResource '{{name}}' has no <ErrorBoundary> between its component and the nearest <Suspense>. When the fetcher throws (network error, 401/403/503, timeout), the error propagates to Suspense which absorbs it and stays in its fallback state permanently. Wrap the component in <ErrorBoundary fallback={...}> or catch errors inside the fetcher." } }, { "id": "resource-refetch-loop", "severity": "error", "description": "Detect refetch() calls inside createEffect which can cause infinite loops", "fixable": false, "category": "reactivity", "plugin": "solid", "messages": { "refetchInEffect": "Calling {{name}}.refetch() inside createEffect may cause infinite loops. The resource tracks its own dependencies. Move refetch to an event handler or use on() to control dependencies." } }, { "id": "signal-call", "severity": "error", "description": "Require signals to be called as functions when used in tracked contexts", "fixable": true, "category": "reactivity", "plugin": "solid", "messages": { "signalInJsxText": "Signal '{{name}}' in JSX text should be called: {{{name}}()}. Without (), you're rendering the function, not its value.", "signalInJsxAttribute": "Signal '{{name}}' in JSX attribute should be called: {{attr}}={{{name}}()}. Without (), the attribute won't update reactively.", "signalInTernary": "Signal '{{name}}' in ternary should be called: {{name}}() ? ... : .... The condition won't react to changes without ().", "signalInLogical": "Signal '{{name}}' in logical expression should be called: {{name}}() && .... Without (), this always evaluates to truthy (functions are truthy).", "signalInComparison": "Signal '{{name}}' in comparison should be called: {{name}}() === .... Comparing functions always returns false.", "signalInArithmetic": "Signal '{{name}}' in arithmetic should be called: {{name}}() + .... Math on functions produces NaN.", "signalInTemplate": "Signal '{{name}}' in template literal should be called: `...${{{name}}()}...`. Without (), you're embedding '[Function]'.", "signalInTrackedScope": "Signal '{{name}}' in {{where}} should be called: {{name}}(). Without (), reactivity is lost.", "badSignal": "The reactive variable '{{name}}' should be called as a function when used in {{where}}." } }, { "id": "signal-in-loop", "severity": "error", "description": "Detect problematic signal usage inside For/Index loop callbacks", "fixable": false, "category": "reactivity", "plugin": "solid", "messages": { "signalInLoop": "Creating signals inside <{{component}}> callback creates new signals on each render. Use a store at the parent level, or derive state from the index.", "signalCallInvariant": "Signal '{{name}}' called inside <{{component}}> produces the same value for every item. Extract to a variable or memoize with createMemo() before the loop.", "derivedCallInvariant": "'{{name}}()' inside <{{component}}> captures {{captures}} but doesn't use the loop item. Extract the call before the loop or pass the item as a parameter." } }, { "id": "store-reactive-break", "severity": "error", "description": "Detect patterns that break store reactivity: spreading stores, top-level property extraction, or destructuring", "fixable": false, "category": "reactivity", "plugin": "solid", "messages": { "storeSpread": "Spreading a store ({...store}) creates a static snapshot that won't update. Access store properties directly in JSX or tracked contexts.", "storeTopLevelAccess": "Accessing store property '{{property}}' at component top-level captures the value once. Access store.{{property}} directly in JSX or wrap in createMemo().", "storeDestructure": "Destructuring a store breaks reactivity. Access properties via store.{{property}} instead of destructuring." } }, { "id": "transition-pending-unchecked", "severity": "error", "description": "Detect useTransition usage without handling the isPending state", "fixable": false, "category": "reactivity", "plugin": "solid", "messages": { "pendingUnchecked": "useTransition returns [isPending, startTransition]. The isPending state should be used to show loading UI during transitions." } }],
42517
42598
  "solid": [{ "id": "batch-optimization", "severity": "warn", "description": "Suggest using batch() when multiple signal setters are called in the same synchronous scope", "fixable": true, "category": "solid", "plugin": "solid", "messages": { "multipleSetters": "Multiple signal updates in the same scope cause multiple re-renders. Wrap in batch() for a single update: batch(() => { {{setters}} });" } }, { "id": "imports", "severity": "error", "description": 'Enforce consistent imports from "solid-js", "solid-js/web", and "solid-js/store".', "fixable": false, "category": "solid", "plugin": "solid", "messages": { "preferSource": 'Prefer importing {{name}} from "{{source}}".' } }, { "id": "index-vs-for", "severity": "warn", "description": "Suggest <For> for object arrays and <Index> for primitive arrays.", "fixable": true, "category": "solid", "plugin": "solid", "messages": { "indexWithObjects": "<Index> with object arrays causes the item accessor to change on any array mutation. Use <For> for objects to maintain reference stability.", "forWithPrimitives": "<For> with primitive arrays (strings, numbers) keys by value, which may cause unexpected re-renders. Consider <Index> if index stability is preferred." } }, { "id": "no-react-deps", "severity": "error", "description": "Disallow usage of dependency arrays in `createEffect`, `createMemo`, and `createRenderEffect`.", "fixable": true, "category": "solid", "plugin": "solid", "messages": { "noUselessDep": "In Solid, `{{name}}` doesn't accept a dependency array because it automatically tracks its dependencies. If you really need to override the list of dependencies, use `on`." } }, { "id": "no-react-specific-props", "severity": "error", "description": "Disallow usage of React-specific `className`/`htmlFor` props, which were deprecated in v1.4.0.", "fixable": true, "category": "solid", "plugin": "solid", "messages": { "prefer": "Prefer the `{{to}}` prop over the deprecated `{{from}}` prop.", "noUselessKey": "Elements in a <For> or <Index> list do not need a key prop." } }, { "id": "prefer-for", "severity": "warn", "description": "Enforce using Solid's `<For />` component for mapping an array to JSX elements.", "fixable": true, "category": "solid", "plugin": "solid", "messages": { "preferFor": "Prefer Solid's `<For each={...}>` component for rendering lists of objects. Array#map recreates all DOM elements on every update, while <For> updates only changed items by keying on reference.", "preferIndex": "Prefer Solid's `<Index each={...}>` component for rendering lists of primitives. Array#map recreates all DOM elements on every update, while <Index> updates only changed items by keying on index position.", "preferForOrIndex": "Prefer Solid's `<For />` or `<Index />` component for rendering lists. Use <For> when items are objects (keys by reference), or <Index> when items are primitives like strings/numbers (keys by index). Array#map recreates all DOM elements on every update." } }, { "id": "prefer-memo-complex-styles", "severity": "warn", "description": "Enforce extracting complex style computations to createMemo for better approach. Complex inline style objects are rebuilt on every render, which can impact approach.", "fixable": false, "category": "solid", "plugin": "solid", "messages": { "preferMemoComplexStyle": "Complex style computation should be extracted to createMemo() for better approach. This style object contains {{complexity}} conditional expressions that are recalculated on every render.", "preferMemoConditionalSpread": "Conditional spread operators in style objects should be extracted to createMemo(). Pattern like `...(condition ? {...} : {})` creates new objects on every render." } }, { "id": "prefer-show", "severity": "warn", "description": "Enforce using Solid's `<Show />` component for conditionally showing content. Solid's compiler covers this case, so it's a stylistic rule only.", "fixable": true, "category": "solid", "plugin": "solid", "messages": { "preferShowAnd": "Prefer Solid's `<Show when={...}>` component for conditional rendering. While Solid's compiler handles `&&` expressions, <Show> is more explicit and provides better readability for conditional content.", "preferShowTernary": "Prefer Solid's `<Show when={...} fallback={...}>` component for conditional rendering with a fallback. This provides clearer intent and better readability than ternary expressions." } }, { "id": "self-closing-comp", "severity": "warn", "description": "Disallow extra closing tags for components without children.", "fixable": true, "category": "solid", "plugin": "solid", "messages": { "selfClose": "Empty elements should be self-closing. Use `<{{name}} />` instead of `<{{name}}></{{name}}>` for cleaner, more concise JSX.", "dontSelfClose": "This element should not be self-closing based on your configuration. Use `<{{name}}></{{name}}>` instead of `<{{name}} />` for explicit opening and closing tags." } }, { "id": "style-prop", "severity": "warn", "description": "Require CSS properties in the `style` prop to be valid and kebab-cased (ex. 'font-size'), not camel-cased (ex. 'fontSize') like in React, and that property values with dimensions are strings, not numbers with implicit 'px' units.", "fixable": true, "category": "solid", "plugin": "solid", "messages": { "kebabStyleProp": "Solid uses kebab-case for CSS property names, not camelCase like React. Use '{{kebabName}}' instead of '{{name}}'.", "invalidStyleProp": "'{{name}}' is not a valid CSS property. Check for typos, or if this is a custom property, prefix it with '--' (e.g., '--{{name}}').", "numericStyleValue": "Numeric values for dimensional properties need explicit units in Solid. Unlike React, Solid does not auto-append 'px'. Use '{{value}}px' or another appropriate unit.", "stringStyle": "Use an object for the style prop instead of a string for better approach and type safety. Example: style={{ '{{prop}}': '{{value}}' }}." } }]
42518
42599
  };
42519
42600
  function getRule2(id) {