@drskillissue/ganko 0.1.21 → 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.
package/README.md CHANGED
@@ -299,8 +299,8 @@ bun run --cwd packages/ganko generate
299
299
  <!-- BEGIN AUTO-GENERATED:rule-catalog -->
300
300
  Totals:
301
301
 
302
- - 163 rules total
303
- - 100 Solid rules, 33 CSS rules, 30 cross-file rules
302
+ - 164 rules total
303
+ - 101 Solid rules, 33 CSS rules, 30 cross-file rules
304
304
  - 23 fixable rules
305
305
 
306
306
  Category breakdown:
@@ -318,7 +318,7 @@ Category breakdown:
318
318
  | `css-structure` | 4 |
319
319
  | `jsx` | 10 |
320
320
  | `performance` | 51 |
321
- | `reactivity` | 15 |
321
+ | `reactivity` | 16 |
322
322
  | `solid` | 10 |
323
323
 
324
324
  For full, up-to-date rule IDs and descriptions, read the generated manifest via API:
@@ -1722,6 +1722,19 @@ var RULES = [
1722
1722
  "resourceUnchecked": "Accessing resource '{{name}}' without checking loading/error state may return undefined. Wrap in <Show when={!{{name}}.loading}> or <Suspense>."
1723
1723
  }
1724
1724
  },
1725
+ {
1726
+ "id": "resource-implicit-suspense",
1727
+ "severity": "warn",
1728
+ "description": "Detect createResource that implicitly triggers or permanently breaks Suspense boundaries.",
1729
+ "fixable": false,
1730
+ "category": "reactivity",
1731
+ "plugin": "solid",
1732
+ "messages": {
1733
+ "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: ... })",
1734
+ "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.",
1735
+ "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."
1736
+ }
1737
+ },
1725
1738
  {
1726
1739
  "id": "resource-refetch-loop",
1727
1740
  "severity": "error",
@@ -1922,7 +1935,7 @@ var RULES_BY_CATEGORY = {
1922
1935
  "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." } }],
1923
1936
  "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." } }],
1924
1937
  "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." } }],
1925
- "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-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." } }],
1938
+ "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." } }],
1926
1939
  "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}}' }}." } }]
1927
1940
  };
1928
1941
  var RULE_CATEGORIES = ["correctness", "css-a11y", "css-animation", "css-cascade", "css-jsx", "css-layout", "css-property", "css-selector", "css-structure", "jsx", "performance", "reactivity", "solid"];
@@ -1940,4 +1953,4 @@ export {
1940
1953
  RULE_CATEGORIES,
1941
1954
  getRule
1942
1955
  };
1943
- //# sourceMappingURL=chunk-JBBGDG7M.js.map
1956
+ //# sourceMappingURL=chunk-4PJEULOI.js.map