@ashstack/lint 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +1 -1
  2. package/dist/core/rules/core/components-tsx-only.js +1 -1
  3. package/dist/core/rules/core/hoist-intl.js +1 -1
  4. package/dist/core/rules/core/no-comments.js +1 -1
  5. package/dist/core/rules/core/no-packed-condition.js +1 -1
  6. package/dist/core/rules/core/use-design-system.js +1 -1
  7. package/dist/core/rules/zod/prefer-enum.js +1 -1
  8. package/dist/lib/registry.js +1 -1
  9. package/dist/lib/rule-types/react-native.d.ts +8 -4
  10. package/dist/lib/types.d.ts +6 -0
  11. package/dist/react/index.js +1 -1
  12. package/dist/react/rules/query/next-page-param-undefined.js +1 -1
  13. package/dist/react/rules/query/no-deprecated-filters.js +1 -1
  14. package/dist/react/rules/query/no-fetch-in-query-fn.js +1 -1
  15. package/dist/react/rules/query/no-inline-keys.js +1 -1
  16. package/dist/react/rules/query/require-destructured-hooks.js +1 -1
  17. package/dist/react/rules/react/no-svg-without-title.js +1 -1
  18. package/dist/react/rules/react/no-unlabeled-icon-button.js +1 -1
  19. package/dist/react/rules/tailwind/prefer-cn.js +1 -1
  20. package/dist/react/rules/tailwind/use-logical-classes.js +1 -1
  21. package/dist/react/rules/tanstack-router/no-search-casts.js +1 -1
  22. package/dist/react/rules/tanstack-router/require-selector.js +1 -1
  23. package/dist/react/rules/zustand/require-selector.js +1 -1
  24. package/dist/react-native/index.js +1 -1
  25. package/dist/react-native/rules/keyboard/index.js +1 -1
  26. package/dist/react-native/rules/legend-list/no-flex-in-content-container.js +1 -1
  27. package/dist/react-native/rules/legend-list/no-index-key-extractor.js +1 -1
  28. package/dist/react-native/rules/legend-list/no-inline-render-item-props.js +1 -1
  29. package/dist/react-native/rules/legend-list/no-mixed-children.js +1 -1
  30. package/dist/react-native/rules/legend-list/no-remount-key.js +1 -1
  31. package/dist/react-native/rules/legend-list/no-scrollview-map.js +1 -1
  32. package/dist/react-native/rules/legend-list/no-unsupported-props.js +1 -1
  33. package/dist/react-native/rules/legend-list/required-props.js +1 -1
  34. package/dist/react-native/rules/legend-list/typed-items-need-item-type.js +1 -1
  35. package/dist/react-native/rules/legend-state/naming.js +1 -1
  36. package/dist/react-native/rules/legend-state/no-assignment.js +1 -1
  37. package/dist/react-native/rules/legend-state/no-nested-observable.js +1 -1
  38. package/dist/react-native/rules/legend-state/no-object-selector.js +1 -1
  39. package/dist/react-native/rules/legend-state/no-observable-in-component.js +1 -1
  40. package/dist/react-native/rules/legend-state/no-peek-in-selector.js +1 -1
  41. package/dist/react-native/rules/legend-state/no-react-mirror.js +1 -1
  42. package/dist/react-native/rules/legend-state/no-untracked-get-in-jsx.js +1 -1
  43. package/dist/react-native/rules/react-native/hoist-stateless-function.js +1 -1
  44. package/dist/react-native/rules/react-native/no-conditional-style-array.js +1 -1
  45. package/dist/react-native/rules/react-native/no-dynamic-import.js +1 -1
  46. package/dist/react-native/rules/react-native/no-keyboard-will-events.js +1 -1
  47. package/dist/react-native/rules/react-native/no-leaked-render.js +1 -1
  48. package/dist/react-native/rules/react-native/no-manual-memo.js +1 -1
  49. package/dist/react-native/rules/react-native/no-redundant-view-nesting.js +1 -1
  50. package/dist/react-native/rules/react-native/no-rn-image-network-source.js +1 -1
  51. package/dist/react-native/rules/react-native/no-rn-namespace-import.js +1 -1
  52. package/dist/react-native/rules/react-native/no-scroll-position-state.js +1 -1
  53. package/dist/react-native/rules/react-native/no-unlabeled-icon-pressable.js +1 -1
  54. package/dist/react-native/rules/reanimated/animated-reaction-safety.js +1 -1
  55. package/dist/react-native/rules/reanimated/animated-style-needs-animated-component.js +1 -1
  56. package/dist/react-native/rules/reanimated/animated-updater-purity.js +1 -1
  57. package/dist/react-native/rules/reanimated/gpu-properties-only.js +1 -1
  58. package/dist/react-native/rules/reanimated/interpolate-needs-clamp.js +1 -1
  59. package/dist/react-native/rules/reanimated/no-react-state-from-continuous-worklet.js +1 -1
  60. package/dist/react-native/rules/reanimated/prefer-lazy-shared-value-initializer.js +1 -1
  61. package/dist/react-native/rules/reanimated/schedule-on-rn-scope.js +1 -1
  62. package/dist/react-native/rules/reanimated/shared-value-usage.js +1 -1
  63. package/dist/react-native/rules/skia/canvas-opaque.js +1 -1
  64. package/dist/react-native/rules/skia/index.js +1 -1
  65. package/dist/react-native/rules/turbo-image/require-resize.js +1 -1
  66. package/dist/react-native/rules/unistyles/animated-theme.js +1 -1
  67. package/dist/react-native/rules/unistyles/content-container.js +1 -1
  68. package/dist/react-native/rules/unistyles/in-sheet.js +1 -1
  69. package/dist/react-native/rules/unistyles/index.js +1 -1
  70. package/dist/react-native/rules/unistyles/insets.js +1 -1
  71. package/dist/react-native/rules/unistyles/no-hardcoded-color.js +1 -1
  72. package/dist/react-native/rules/unistyles/no-hardcoded-spacing.js +1 -1
  73. package/dist/react-native/rules/unistyles/no-margin.js +1 -1
  74. package/dist/react-native/rules/unistyles/no-paramless-dynamic-function.js +1 -0
  75. package/dist/react-native/rules/unistyles/no-style-spread.js +1 -1
  76. package/dist/react-native/rules/unistyles/no-unused-styles.js +1 -1
  77. package/dist/react-native/rules/unistyles/theme-screen-component.js +1 -1
  78. package/dist/react-native/rules/unistyles/theme-style-attr.js +1 -1
  79. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @ashstack/lint
2
2
 
3
- Strict shared [oxlint](https://oxc.rs) config: <!-- rule-counts -->118 rules on plain TypeScript, 202 with React, 257 on React Native, 74 of them custom-built<!-- /rule-counts -->. Three entries, each containing the one before it: `core()`, `react()`, `react-native()`.
3
+ Strict shared [oxlint](https://oxc.rs) config: <!-- rule-counts -->118 rules on plain TypeScript, 201 with React, 258 on React Native, 75 of them custom-built<!-- /rule-counts -->. Three entries, each containing the one before it: `core()`, `react()`, `react-native()`.
4
4
 
5
5
  Library-specific rules ship only when you depend on that library, so nothing fires about code you don't have.
6
6
 
@@ -1 +1 @@
1
- import{optionsOf as t}from"../../../lib/ast.js";const s="Move this file to `src/utils` (helpers, pure logic), `src/hooks` (a hook), or `src/api/<feature>/` (data access). `components/` holds only files that render JSX, plus a barrel.",l="src/components",n=(r)=>r.every((e)=>e.type==="ImportDeclaration"||e.type==="ExportAllDeclaration"||e.type==="ExportNamedDeclaration"&&e.declaration==null);export const componentsTsxOnly={meta:{type:"problem",docs:{description:"Require every file under the components directory to render JSX or be a re-export barrel. `dir` says which directory, defaulting to `src/components`."},schema:[{type:"object",properties:{dir:{type:"string",minLength:1}},additionalProperties:!1}],defaultOff:!0},createOnce(r){let e=!1;return{before(){e=!1;const{dir:o=l}=t(r,{});return r.filename.replaceAll("\\","/").includes(`/${o}/`)},JSXElement(){e=!0},JSXFragment(){e=!0},"Program:exit"(o){if(e||n(o.body))return;r.report({node:o,message:s})}}}};
1
+ import{optionsOf as t}from"../../../lib/ast.js";const s="Move this file to `src/utils`, `src/hooks`, or `src/api/<feature>/` — `components/` holds only files that render JSX.",n="src/components",i=(r)=>r.every((e)=>e.type==="ImportDeclaration"||e.type==="ExportAllDeclaration"||e.type==="ExportNamedDeclaration"&&e.declaration==null);export const componentsTsxOnly={meta:{type:"problem",docs:{description:"Require every file under the components directory to render JSX or be a re-export barrel. `dir` says which directory, defaulting to `src/components`."},schema:[{type:"object",properties:{dir:{type:"string",minLength:1}},additionalProperties:!1}],defaultOff:!0},createOnce(r){let e=!1;return{before(){e=!1;const{dir:o=n}=t(r,{});return r.filename.replaceAll("\\","/").includes(`/${o}/`)},JSXElement(){e=!0},JSXFragment(){e=!0},"Program:exit"(o){if(e||i(o.body))return;r.report({node:o,message:s})}}}};
@@ -1 +1 @@
1
- import{ancestors as o,calleeName as a,hasAncestor as i,isFunction as l,problem as c,subtreeHas as p}from"../../../lib/ast.js";const u="Move this `Intl` formatter to module scope when the locale and options are static, or wrap it in `useMemo` keyed on the locale — constructing one per render is expensive.",m=new Set(["useMemo","useCallback"]),f=(t)=>p(t,(e)=>e.type==="JSXElement"||e.type==="JSXFragment"),h=(t)=>{for(const e of o(t))if(l(e)&&f(e))return!0;return!1};export const hoistIntl=c("Disallow `new Intl.*` inside a function that renders JSX, unless the call already sits in `useMemo` or `useCallback`.",{createOnce(t){return{NewExpression(e){const{callee:n}=e;if(n.type!=="MemberExpression")return;const{object:s}=n;if(s.type!=="Identifier"||s.name!=="Intl")return;if(i(e,(r)=>r.type==="CallExpression"&&m.has(a(r))))return;if(!h(e))return;t.report({node:e,message:u})}}}});
1
+ import{ancestors as o,calleeName as a,hasAncestor as l,isFunction as i,problem as c,subtreeHas as u}from"../../../lib/ast.js";const p="Move this `Intl` formatter to module scope when the locale and options are static, or wrap it in `useMemo` keyed on the locale.",m=new Set(["useMemo","useCallback"]),f=(t)=>u(t,(e)=>e.type==="JSXElement"||e.type==="JSXFragment"),h=(t)=>{for(const e of o(t))if(i(e)&&f(e))return!0;return!1};export const hoistIntl=c("Disallow `new Intl.*` inside a function that renders JSX, unless the call already sits in `useMemo` or `useCallback`.",{createOnce(t){return{NewExpression(e){const{callee:s}=e;if(s.type!=="MemberExpression")return;const{object:n}=s;if(n.type!=="Identifier"||n.name!=="Intl")return;if(l(e,(r)=>r.type==="CallExpression"&&m.has(a(r))))return;if(!h(e))return;t.report({node:e,message:p})}}}});
@@ -1 +1 @@
1
- import{optionsOf as b}from"../../../lib/ast.js";const v=["@ts-expect-error","@ts-ignore","@ts-nocheck","@ts-check","@jsx","@jsxRuntime","@jsxImportSource","prettier-ignore","eslint-disable","eslint-enable","oxlint-disable","oxlint-enable","oxfmt-ignore","react-doctor-disable","@type","<reference","#__PURE__","@__PURE__","v8 ignore","c8 ignore","istanbul ignore","@vitest-environment","EXPECT_PASS","EXPECT_FAIL"],E=/^(?<kind>what|why):\s*(?<fact>[\s\S]+)$/i,m=new Set(["Block","MultiLine"]),k=new Set(["Shebang","Hashbang"]),u=10,f=120,C=2,D="Make the code say this, then delete the line — nearly every comment here is one rename or one refactor away from unnecessary. Reach for the move that fits: Rename the value, function or type to the plain word this sentence uses; Extract Function for the block it summarises, named after the claim it makes; Guard Clause for the nesting it walks the reader through; split the clever expression it decodes into named steps. You are done when the sentence's content is legible in an identifier, a signature or the control flow.",A=" The `// what:` hatch carries one kind of fact: one that outlives this code — a platform bug, an upstream contract, a number measured on a device. A fact about what this code does fails that test, and the refactor is still owed.",_=" This project runs with the `// what:` hatch turned off, so the refactor is the only way out. The one line left is `// why:`, which a kept `memo` requires.",x=(e)=>D+(e?A:_),H="Attach this block to the declaration it documents, or Rename and Extract Function until the code reads without it. A `/** */` block earns its place as documentation of the symbol directly beneath it.",d={block:"Rewrite this as a single `// what: <fact>` line comment. If the fact needs a paragraph, name the pieces in code until it fits on one line.",shortFact:`Write the fact after \`what:\` (at least ${u} characters), or delete the comment.`,tooLong:`Trim this \`what:\` line under ${f} characters, moving what is left into names in the code.`,stacked:"Keep one `what:` line here — the single irreducible fact — and refactor what the others explain into named values and functions."},M=(e,t)=>`Delete \`what:\` comments from this file until at most ${e} remain (it has ${t}): move the logic each one annotates into a function whose name carries what the comment says.`,P=new Set(["ExportNamedDeclaration","ExportDefaultDeclaration","ExportAllDeclaration","FunctionDeclaration","VariableDeclaration","ClassDeclaration","MethodDefinition","PropertyDefinition","Property","TSInterfaceDeclaration","TSTypeAliasDeclaration","TSEnumDeclaration","TSEnumMember","TSModuleDeclaration","TSDeclareFunction","TSPropertySignature","TSMethodSignature","TSIndexSignature","TSCallSignatureDeclaration","TSConstructSignatureDeclaration","TSAbstractMethodDefinition","TSAbstractPropertyDefinition","AccessorProperty","ImportDeclaration"]),O=(e)=>(m.has(e.type)?e.value.replace(/^[*\s]+/,""):e.value).trim(),R=(e)=>v.some((t)=>e.startsWith(t)),F=(e)=>m.has(e.type)&&e.value.startsWith("*"),I=(e)=>{const t=[];for(const o of e.sourceCode.getAllComments()){if(k.has(o.type))continue;const l=O(o);if(R(l))continue;t.push({comment:o,body:l})}return t},L=(e,t)=>{let o=t;while(/\s/.test(e.charAt(o)))o+=1;return o},p=(e,t)=>{const o=E.exec(e.body)?.groups;if(!o?.kind||!o.fact)return[];const l=o.kind.toLowerCase()==="why"?"why":"what";if(l==="what"&&!t)return[];return[{...e,kind:l,fact:o.fact}]},N=(e,t,o)=>{if(m.has(e.type))return d.block;if(o.trim().length<u)return d.shortFact;if(t.length>f)return d.tooLong;return null},j=(e,t,o)=>e.slice(t.end,o.start).trim()==="",w=(e)=>e.escapeHatch!==!1;export const noComments={meta:{type:"problem",docs:{description:'Disallow every comment that is neither a `// what: <fact>` line, a `// why:` marker, nor a tooling directive. The message names the refactoring that removes it: Rename, Extract Function, Guard Clause. Surviving `// what:` lines are held to one short line each, at most `budget` per file (default 2); `escapeHatch: false` removes that hatch, so no discretionary prose survives. A `// why:` line is the marker `@ashstack/react-native/no-manual-memo` requires above a kept `memo`: held to the same one-line shape, never counted against `budget`, and kept even with `escapeHatch: false`, so the two rules run together. With `jsdoc: "allow"`, a `/** */` block documenting the declaration directly beneath it is kept, while a floating one still reports.'},schema:[{type:"object",properties:{jsdoc:{enum:["allow","report"]},escapeHatch:{type:"boolean"},budget:{type:"integer",minimum:0}},additionalProperties:!1}]},createOnce(e){const t=new Set,o=(n)=>{t.add(n.start)},l={};for(const n of P)l[n]=o;const h=(n,r)=>{e.report({node:n,message:r})},g=(n,r)=>{const{text:s}=e.sourceCode,c=w(r);for(const a of n){const{comment:i}=a;if(p(a,c).length>0)continue;if(r.jsdoc!=="allow"||!F(i))h(i,x(c));else if(!t.has(L(s,i.end)))h(i,H)}},S=(n,r)=>{const s=[];for(const c of n)for(const a of p(c,r)){const i=N(a.comment,a.body,a.fact);if(i===null)s.push(a);else h(a.comment,i)}return s},T=(n)=>{const r=e.sourceCode.getText(),s=n.map((c)=>c.comment);for(const[c,a]of s.entries()){const i=s[c-1];if(i&&j(r,i,a))h(a,d.stacked)}},y=(n,r)=>{const s=r.budget??C,c=n.filter((i)=>i.kind==="what"),a=c[s];if(a)h(a.comment,M(s,c.length))};return{...l,before(){t.clear();return!0},"Program:exit"(){const n=b(e,{}),r=I(e);g(r,n);const s=S(r,w(n));T(s);y(s,n)}}}};
1
+ import{optionsOf as y}from"../../../lib/ast.js";const b=["@ts-expect-error","@ts-ignore","@ts-nocheck","@ts-check","@jsx","@jsxRuntime","@jsxImportSource","prettier-ignore","eslint-disable","eslint-enable","oxlint-disable","oxlint-enable","oxfmt-ignore","react-doctor-disable","@type","<reference","#__PURE__","@__PURE__","v8 ignore","c8 ignore","istanbul ignore","@vitest-environment","EXPECT_PASS","EXPECT_FAIL"],D=/^(?<kind>what|why):\s*(?<fact>[\s\S]+)$/i,m=new Set(["Block","MultiLine"]),C=new Set(["Shebang","Hashbang"]),u=10,f=120,v=2,k="Make the code say this, then delete the line — rename, Extract Function, Guard Clause, or named steps. Done when the sentence reads off a name or the control flow.",A=" `// what:` fits only a fact that outlives the code — a platform bug, an upstream contract, a measured number; what the code does is not one, and still owes the refactor.",_=" This project runs with the `// what:` hatch turned off. The one line left is `// why:`, which a kept `memo` requires.",x=(e)=>k+(e?A:_),H="Attach this block to the declaration it documents, or Rename and Extract Function until the code reads without it.",d={block:"Rewrite this as a single `// what: <fact>` line comment; if the fact needs a paragraph, name the pieces in code instead.",shortFact:`Write the fact after \`what:\` (at least ${u} characters), or delete the comment.`,tooLong:`Trim this \`what:\` line under ${f} characters, moving what is left into names in the code.`,stacked:"Keep one `what:` line here and refactor what the others explain into names."},M=(e,t)=>`Delete \`what:\` comments from this file until at most ${e} remain (it has ${t}): move each one's logic into a function named for it.`,P=new Set(["ExportNamedDeclaration","ExportDefaultDeclaration","ExportAllDeclaration","FunctionDeclaration","VariableDeclaration","ClassDeclaration","MethodDefinition","PropertyDefinition","Property","TSInterfaceDeclaration","TSTypeAliasDeclaration","TSEnumDeclaration","TSEnumMember","TSModuleDeclaration","TSDeclareFunction","TSPropertySignature","TSMethodSignature","TSIndexSignature","TSCallSignatureDeclaration","TSConstructSignatureDeclaration","TSAbstractMethodDefinition","TSAbstractPropertyDefinition","AccessorProperty","ImportDeclaration"]),O=(e)=>(m.has(e.type)?e.value.replace(/^[*\s]+/,""):e.value).trim(),F=(e)=>b.some((t)=>e.startsWith(t)),R=(e)=>m.has(e.type)&&e.value.startsWith("*"),I=(e)=>{const t=[];for(const o of e.sourceCode.getAllComments()){if(C.has(o.type))continue;const l=O(o);if(F(l))continue;t.push({comment:o,body:l})}return t},L=(e,t)=>{let o=t;while(/\s/.test(e.charAt(o)))o+=1;return o},p=(e,t)=>{const o=D.exec(e.body)?.groups;if(!o?.kind||!o.fact)return[];const l=o.kind.toLowerCase()==="why"?"why":"what";if(l==="what"&&!t)return[];return[{...e,kind:l,fact:o.fact}]},N=(e,t,o)=>{if(m.has(e.type))return d.block;if(o.trim().length<u)return d.shortFact;if(t.length>f)return d.tooLong;return null},j=(e,t,o)=>e.slice(t.end,o.start).trim()==="",w=(e)=>e.escapeHatch!==!1;export const noComments={meta:{type:"problem",docs:{description:'Disallow every comment that is neither a `// what: <fact>` line, a `// why:` marker, nor a tooling directive. The message names the refactoring that removes it: Rename, Extract Function, Guard Clause. Surviving `// what:` lines are held to one short line each, at most `budget` per file (default 2); `escapeHatch: false` removes that hatch, so no discretionary prose survives. A `// why:` line is the marker `@ashstack/react-native/no-manual-memo` requires above a kept `memo`: held to the same one-line shape, never counted against `budget`, and kept even with `escapeHatch: false`, so the two rules run together. With `jsdoc: "allow"`, a `/** */` block documenting the declaration directly beneath it is kept, while a floating one still reports.'},schema:[{type:"object",properties:{jsdoc:{enum:["allow","report"]},escapeHatch:{type:"boolean"},budget:{type:"integer",minimum:0}},additionalProperties:!1}]},createOnce(e){const t=new Set,o=(n)=>{t.add(n.start)},l={};for(const n of P)l[n]=o;const h=(n,r)=>{e.report({node:n,message:r})},g=(n,r)=>{const{text:s}=e.sourceCode,i=w(r);for(const a of n){const{comment:c}=a;if(p(a,i).length>0)continue;if(r.jsdoc!=="allow"||!R(c))h(c,x(i));else if(!t.has(L(s,c.end)))h(c,H)}},S=(n,r)=>{const s=[];for(const i of n)for(const a of p(i,r)){const c=N(a.comment,a.body,a.fact);if(c===null)s.push(a);else h(a.comment,c)}return s},T=(n)=>{const r=e.sourceCode.getText(),s=n.map((i)=>i.comment);for(const[i,a]of s.entries()){const c=s[i-1];if(c&&j(r,c,a))h(a,d.stacked)}},E=(n,r)=>{const s=r.budget??v,i=n.filter((c)=>c.kind==="what"),a=i[s];if(a)h(a.comment,M(s,i.length))};return{...l,before(){t.clear();return!0},"Program:exit"(){const n=y(e,{}),r=I(e);g(r,n);const s=S(r,w(n));T(s);E(s,n)}}}};
@@ -1 +1 @@
1
- import{gate as s}from"../../../lib/ast.js";const p=new Set(["===","!==","==","!=","<","<=",">",">=","in","instanceof"]),a=5,n=(t)=>{if(!t)return 0;if(t.type==="LogicalExpression")return 1+n(t.left)+n(t.right);if(t.type==="BinaryExpression")return(p.has(t.operator)?1:0)+n(t.left)+n(t.right);if(t.type==="UnaryExpression")return t.operator==="!"?n(t.argument):0;if(t.type==="ConditionalExpression")return 1+n(t.test)+n(t.consequent)+n(t.alternate);return 0};export const noPackedCondition={meta:{type:"problem",docs:{description:"Require a condition to be split into named booleans once it holds too many boolean operators and comparisons. The option says how many, defaulting to 5."},schema:[{type:"integer",minimum:1}]},createOnce(t){let r=a;const o=(e)=>{if(!e)return;const i=n(e);if(i<=r)return;t.report({node:e,message:`This condition holds ${i} boolean operators and comparisons, past the ${r} a reader takes in at once. Extract each part into an explaining variable, named so it reads on its own, like \`hasValue\` or \`priceChanged\`. Stop when the condition is just those names joined by \`&&\`/\`||\`.`})};return{before(){r=typeof t.options[0]==="number"?t.options[0]:a;return s(t,"&&","||","??")},IfStatement(e){o(e.test)},ConditionalExpression(e){o(e.test)},WhileStatement(e){o(e.test)},DoWhileStatement(e){o(e.test)},ForStatement(e){o(e.test)}}}};
1
+ import{gate as s}from"../../../lib/ast.js";const p=new Set(["===","!==","==","!=","<","<=",">",">=","in","instanceof"]),o=5,r=(t)=>{if(!t)return 0;if(t.type==="LogicalExpression")return 1+r(t.left)+r(t.right);if(t.type==="BinaryExpression")return(p.has(t.operator)?1:0)+r(t.left)+r(t.right);if(t.type==="UnaryExpression")return t.operator==="!"?r(t.argument):0;if(t.type==="ConditionalExpression")return 1+r(t.test)+r(t.consequent)+r(t.alternate);return 0};export const noPackedCondition={meta:{type:"problem",docs:{description:"Require a condition to be split into named booleans once it holds too many boolean operators and comparisons. The option says how many, defaulting to 5."},schema:[{type:"integer",minimum:1}]},createOnce(t){let a=o;const n=(e)=>{if(!e)return;const i=r(e);if(i<=a)return;t.report({node:e,message:`Extract each part into a named variable like \`hasValue\`, until this condition reads as those names joined by \`&&\`/\`||\` — it packs ${i} operators, past the ${a} a reader takes in.`})};return{before(){a=typeof t.options[0]==="number"?t.options[0]:o;return s(t,"&&","||","??")},IfStatement(e){n(e.test)},ConditionalExpression(e){n(e.test)},WhileStatement(e){n(e.test)},DoWhileStatement(e){n(e.test)},ForStatement(e){n(e.test)}}}};
@@ -1 +1 @@
1
- import{readdirSync as l}from"node:fs";import{isAbsolute as d,join as y}from"node:path";import{optionsOf as u}from"../../../lib/ast.js";const g="src/components/ui",h="@/components/ui",b=/\.(?:ios|android|native|web)$/,S={Pressable:["TouchableOpacity","TouchableHighlight","TouchableWithoutFeedback","TouchableNativeFeedback"]},m={anyOf:[{type:"string"},{type:"array",items:{type:"string"}}]},A={anyOf:[...m.anyOf,{type:"object",properties:{replaces:m,from:{type:"string"},path:{type:"string"},reason:{type:"string"}},required:["replaces"],additionalProperties:!1}]},E=(e)=>e.split("-").filter(Boolean).map((t)=>t.charAt(0).toUpperCase()+t.slice(1)).join(""),w=(e)=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),p=(e,t,r,o)=>{const n=e.get(t)??new Map;n.set(r,o);e.set(t,n)},I=(e,t,r)=>{let o;try{o=l(d(t)?t:y(process.cwd(),t))}catch{return}for(const n of o){if(!n.endsWith(".tsx"))continue;const s=n.slice(0,-4).replace(b,"");if(s==="index")continue;const i=E(s),a=`${r}/${s}`;p(e,"react-native",i,{name:i,from:a});for(const c of S[i]??[])p(e,"react-native",c,{name:i,from:a})}},O=(e,t,r)=>{for(const[o,n]of Object.entries(r)){const s=typeof n==="string"||Array.isArray(n)?{replaces:n}:n,i={name:o,from:s.path??`${t}/${w(o)}`,reason:s.reason};for(const a of[s.replaces].flat())p(e,s.from??"react-native",a,i)}},f=new Map,T=(e,t)=>!!e&&t.some((r)=>e.includes(r)),_=(e)=>{const t=e.dir??g,r=e.alias??h,o=e.exempt??[t],n=JSON.stringify([t,r,e.use]);let s=f.get(n);if(s===void 0){s=new Map;I(s,t,r);if(e.use)O(s,r,e.use);f.set(n,s)}return{banned:s,exempt:o}};export const useDesignSystem={meta:{type:"problem",docs:{description:"Disallow importing a raw primitive your design system already wraps. Wrapped components come from scanning the design-system directory, plus the explicit `use` map for names, paths and source modules the scan cannot infer. Files under the design-system directory (or `exempt`) are skipped."},schema:[{type:"object",properties:{dir:{type:"string",minLength:1},alias:{type:"string",minLength:1},use:{type:"object",additionalProperties:A},exempt:{type:"array",items:{type:"string"}}},additionalProperties:!1}],defaultOff:!0},createOnce(e){let t=new Map;return{before(){const r=u(e,{}),o=_(r);t=o.banned;if(t.size===0)return!1;const{filename:n}=e;if(T(n,o.exempt))return!1;return!0},ImportDeclaration(r){const o=r.source.value,n=t.get(o);if(n===void 0)return;for(const s of r.specifiers){if(s.type!=="ImportSpecifier")continue;const{imported:i}=s;if(i.type!=="Identifier")continue;const a=n.get(i.name);if(a===void 0)continue;const c=a.reason?` ${a.reason}`:"";e.report({node:s,message:`Import \`${a.name}\` from "${a.from}" instead of \`${i.name}\` from ${o}. The wrapper is where the theme tokens and app behavior live.${c}`})}}}}};
1
+ import{readdirSync as l}from"node:fs";import{isAbsolute as d,join as y}from"node:path";import{optionsOf as u}from"../../../lib/ast.js";const g="src/components/ui",b="@/components/ui",h=/\.(?:ios|android|native|web)$/,S={Pressable:["TouchableOpacity","TouchableHighlight","TouchableWithoutFeedback","TouchableNativeFeedback"]},m={anyOf:[{type:"string"},{type:"array",items:{type:"string"}}]},A={anyOf:[...m.anyOf,{type:"object",properties:{replaces:m,from:{type:"string"},path:{type:"string"},reason:{type:"string"}},required:["replaces"],additionalProperties:!1}]},E=(e)=>e.split("-").filter(Boolean).map((t)=>t.charAt(0).toUpperCase()+t.slice(1)).join(""),I=(e)=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),p=(e,t,r,o)=>{const n=e.get(t)??new Map;n.set(r,o);e.set(t,n)},O=(e,t,r)=>{let o;try{o=l(d(t)?t:y(process.cwd(),t))}catch{return}for(const n of o){if(!n.endsWith(".tsx"))continue;const s=n.slice(0,-4).replace(h,"");if(s==="index")continue;const i=E(s),a=`${r}/${s}`;p(e,"react-native",i,{name:i,from:a});for(const c of S[i]??[])p(e,"react-native",c,{name:i,from:a})}},w=(e,t,r)=>{for(const[o,n]of Object.entries(r)){const s=typeof n==="string"||Array.isArray(n)?{replaces:n}:n,i={name:o,from:s.path??`${t}/${I(o)}`,reason:s.reason};for(const a of[s.replaces].flat())p(e,s.from??"react-native",a,i)}},f=new Map,_=(e,t)=>!!e&&t.some((r)=>e.includes(r)),M=(e)=>{const t=e.dir??g,r=e.alias??b,o=e.exempt??[t],n=JSON.stringify([t,r,e.use]);let s=f.get(n);if(s===void 0){s=new Map;O(s,t,r);if(e.use)w(s,r,e.use);f.set(n,s)}return{banned:s,exempt:o}};export const useDesignSystem={meta:{type:"problem",docs:{description:"Disallow importing a raw primitive your design system already wraps. Wrapped components come from scanning the design-system directory, plus the explicit `use` map for names, paths and source modules the scan cannot infer. Files under the design-system directory (or `exempt`) are skipped."},schema:[{type:"object",properties:{dir:{type:"string",minLength:1},alias:{type:"string",minLength:1},use:{type:"object",additionalProperties:A},exempt:{type:"array",items:{type:"string"}}},additionalProperties:!1}],defaultOff:!0},createOnce(e){let t=new Map;return{before(){const r=u(e,{}),o=M(r);t=o.banned;if(t.size===0)return!1;const{filename:n}=e;if(_(n,o.exempt))return!1;return!0},ImportDeclaration(r){const o=r.source.value,n=t.get(o);if(n===void 0)return;for(const s of r.specifiers){if(s.type!=="ImportSpecifier")continue;const{imported:i}=s;if(i.type!=="Identifier")continue;const a=n.get(i.name);if(a===void 0)continue;const c=a.reason?` ${a.reason}`:"";e.report({node:s,message:`Import \`${a.name}\` from "${a.from}" instead of \`${i.name}\` from ${o}.${c}`})}}}}};
@@ -1 +1 @@
1
- import{problem as l}from"../../../lib/ast.js";const a={nativeEnum:"Call `z.enum()` here — it takes the same native enum object and params, and `z.nativeEnum()` is deprecated in Zod 4.",literalUnion:"Replace this union with `z.enum([...])` listing the same string values, so invalid input reports one issue and the options stay reusable."},i=(t,e)=>{const{callee:n}=t;if(n.type!=="MemberExpression")return!1;const{object:r,property:s}=n;if(r.type!=="Identifier"||r.name!=="z")return!1;return s.type==="Identifier"&&s.name===e},o=(t)=>{if(t?.type!=="CallExpression"||!i(t,"literal"))return!1;const e=t.arguments;if(e.length!==1)return!1;const[n]=e;return n?.type==="Literal"&&typeof n.value==="string"};export const preferEnum=l("Disallow `z.nativeEnum()` and any `z.union()` whose members are all `z.literal()` strings.",{createOnce(t){return{CallExpression(e){if(i(e,"nativeEnum")){t.report({node:e,message:a.nativeEnum});return}if(!i(e,"union"))return;const[n]=e.arguments;if(n?.type!=="ArrayExpression")return;const{elements:r}=n;if(r.length===0||!r.every((s)=>o(s)))return;t.report({node:e,message:a.literalUnion})}}}});
1
+ import{problem as l}from"../../../lib/ast.js";const a={nativeEnum:"Call `z.enum()` here — same arguments, and `z.nativeEnum()` is deprecated in Zod 4.",literalUnion:"Replace this union with `z.enum([...])` listing the same string values."},i=(r,e)=>{const{callee:t}=r;if(t.type!=="MemberExpression")return!1;const{object:n,property:s}=t;if(n.type!=="Identifier"||n.name!=="z")return!1;return s.type==="Identifier"&&s.name===e},u=(r)=>{if(r?.type!=="CallExpression"||!i(r,"literal"))return!1;const e=r.arguments;if(e.length!==1)return!1;const[t]=e;return t?.type==="Literal"&&typeof t.value==="string"};export const preferEnum=l("Disallow `z.nativeEnum()` and any `z.union()` whose members are all `z.literal()` strings.",{createOnce(r){return{CallExpression(e){if(i(e,"nativeEnum")){r.report({node:e,message:a.nativeEnum});return}if(!i(e,"union"))return;const[t]=e.arguments;if(t?.type!=="ArrayExpression")return;const{elements:n}=t;if(n.length===0||!n.every((s)=>u(s)))return;r.report({node:e,message:a.literalUnion})}}}});
@@ -1 +1 @@
1
- const e=(t)=>new URL(t,import.meta.url).href;export const coreRegistry=[{meta:{name:"@ashstack/core"},url:e("../core/rules/core/index.js"),rules:{"no-comments":{},"no-naming-convention":{},"no-packed-condition":{},"use-design-system":{defaultOff:!0},"components-tsx-only":{defaultOff:!0},"hoist-intl":{}}},{meta:{name:"@ashstack/zod"},url:e("../core/rules/zod/index.js"),option:"zod",packages:["zod"],rules:{"prefer-enum":{}}}],reactRegistry=[{meta:{name:"@ashstack/react"},url:e("../react/rules/react/index.js"),rules:{"no-unlabeled-icon-button":{},"no-svg-without-title":{}}},{meta:{name:"@ashstack/query"},url:e("../react/rules/query/index.js"),option:"query",packages:["@tanstack/react-query"],rules:{"no-inline-keys":{},"no-deprecated-filters":{},"require-destructured-hooks":{},"no-fetch-in-query-fn":{},"next-page-param-undefined":{}}},{meta:{name:"@ashstack/zustand"},url:e("../react/rules/zustand/index.js"),option:"zustand",packages:["zustand"],rules:{"require-selector":{}}},{meta:{name:"@ashstack/i18n"},url:e("../react/rules/i18n/index.js"),option:"i18n",packages:["i18next","react-i18next","@lingui/core","react-intl","use-intl","next-intl","expo-localization"],rules:{"no-bare-text":{},"no-bare-attrs":{},"no-bare-toast":{}}},{meta:{name:"@ashstack/tailwind"},url:e("../react/rules/tailwind/index.js"),option:"tailwind",packages:["tailwindcss"],rules:{"prefer-cn":{},"use-logical-classes":{}}},{meta:{name:"@ashstack/tanstack-router"},url:e("../react/rules/tanstack-router/index.js"),option:"tanstackRouter",packages:["@tanstack/react-router"],rules:{"require-selector":{},"no-search-casts":{}}}],reactNativeRegistry=[{meta:{name:"@ashstack/react-native"},url:e("../react-native/rules/react-native/index.js"),restrictedImports:{paths:[{name:"react",importNames:["forwardRef"],message:"Accept `ref` as a regular prop and delete the `forwardRef` wrapper — React 19 passes `ref` like any other prop."},{name:"react",importNames:["useContext"],message:"Call `use(Context)` instead of `useContext(Context)`; React 19 replaces the hook with it."}],patterns:[{group:["expo-linear-gradient","react-native-linear-gradient"],message:"Apply the gradient through the `experimental_backgroundImage` style property instead of a gradient component."}]},rules:{"no-keyboard-will-events":{},"no-scroll-position-state":{},"no-conditional-style-array":{},"no-leaked-render":{},"no-rn-image-network-source":{},"no-redundant-view-nesting":{},"no-rn-namespace-import":{},"no-unlabeled-icon-pressable":{},"hoist-stateless-function":{},"no-manual-memo":{packages:["babel-plugin-react-compiler","react-compiler-runtime","react-compiler-marker"]},"no-dynamic-import":{}}},{meta:{name:"@ashstack/unistyles"},url:e("../react-native/rules/unistyles/index.js"),option:"unistyles",packages:["react-native-unistyles"],restrictedImports:{paths:[{name:"react-native",importNames:["StyleSheet"],message:"Import `StyleSheet` from `react-native-unistyles` instead of `react-native`."},{name:"react-native",importNames:["useWindowDimensions","Dimensions"],message:"Read screen size from `rt.screen` inside `StyleSheet.create`, or `UnistylesRuntime.screen` outside it, instead of the `Dimensions` APIs."},{name:"react-native",importNames:["SafeAreaView"],message:'Set `contentInsetAdjustmentBehavior="automatic"` on the scroll view, or apply `rt.insets` from `react-native-unistyles`, instead of `SafeAreaView`.'},{name:"react-native-safe-area-context",importNames:["useSafeAreaInsets","useSafeAreaFrame","SafeAreaView"],message:"Read safe-area values from `rt.insets`, or `UnistylesRuntime.insets` outside a stylesheet, in `react-native-unistyles`."}]},rules:{"animated-theme":{},"content-container":{},"in-sheet":{},insets:{},"no-hardcoded-color":{},"no-hardcoded-spacing":{},"no-margin":{},"no-style-spread":{},"no-unused-styles":{},"rtl-style-call":{},"theme-screen-component":{},"theme-style-attr":{}}},{meta:{name:"@ashstack/legend-list"},url:e("../react-native/rules/legend-list/index.js"),option:"legendList",packages:["@legendapp/list"],restrictedImports:{paths:[{name:"react-native",importNames:["FlatList","SectionList","VirtualizedList"],message:"Render this list with `LegendList` from `@legendapp/list/react-native`; a `ScrollView` plus `map` is fine for fewer than 20 static items."},{name:"@shopify/flash-list",message:"Import `LegendList` from `@legendapp/list/react-native` — Legend List replaced FlashList in this stack."}],patterns:[{group:["@legendapp/list"],message:"Import from the platform entrypoint `@legendapp/list/react-native`, not the package root."}]},rules:{"required-props":{},"no-index-key-extractor":{},"no-remount-key":{},"no-inline-render-item-props":{},"no-mixed-children":{},"no-flex-in-content-container":{},"typed-items-need-item-type":{},"no-scrollview-map":{},"no-unsupported-props":{}}},{meta:{name:"@ashstack/legend-state"},url:e("../react-native/rules/legend-state/index.js"),option:"legendState",packages:["@legendapp/state"],restrictedImports:{paths:[{name:"@legendapp/state/react",importNames:["use$","useSelector"],message:"Use `useValue` from `@legendapp/state/react`; `use$` and `useSelector` are deprecated."}]},rules:{"no-assignment":{},naming:{},"no-nested-observable":{},"no-react-mirror":{},"no-untracked-get-in-jsx":{},"no-peek-in-selector":{},"no-object-selector":{},"no-observable-in-component":{}}},{meta:{name:"@ashstack/reanimated"},url:e("../react-native/rules/reanimated/index.js"),option:"reanimated",packages:["react-native-reanimated"],restrictedImports:{paths:[{name:"react-native",importNames:["Animated"],message:"Use `react-native-reanimated` instead of React Native's `Animated` API."},{name:"react-native-reanimated",importNames:["runOnJS","runOnUI"],message:"Use `scheduleOnRN` and `scheduleOnUI` from `react-native-worklets`; `runOnJS` and `runOnUI` are deprecated."},{name:"react-native-worklets",importNames:["runOnJS","runOnUI"],message:"Use `scheduleOnRN` and `scheduleOnUI` from `react-native-worklets`; `runOnJS` and `runOnUI` are deprecated."}]},rules:{"animated-reaction-safety":{},"animated-style-needs-animated-component":{},"animated-updater-purity":{},"gpu-properties-only":{},"hoist-layout-animation-builder":{},"interpolate-needs-clamp":{},"no-shared-value-dot-value":{},"no-react-state-from-continuous-worklet":{},"prefer-lazy-shared-value-initializer":{},"schedule-on-rn-scope":{},"shared-value-usage":{}}},{meta:{name:"@ashstack/turbo-image"},url:e("../react-native/rules/turbo-image/index.js"),option:"turboImage",packages:["react-native-turbo-image"],rules:{"require-resize":{},"require-cache-policy":{}}},{meta:{name:"@ashstack/skia"},url:e("../react-native/rules/skia/index.js"),option:"skia",packages:["@shopify/react-native-skia"],restrictedImports:{paths:[{name:"@shopify/react-native-skia",importNames:["usePathValue","usePathInterpolation"],message:"Keep one stable `SkPath` buffer and mutate it inside `useDerivedValue`, with consumers reading the driving shared value. These legacy path-value hooks self-dirty Reanimated mappers and re-record idle canvases."}]},rules:{"canvas-opaque":{}}},{meta:{name:"@ashstack/keyboard"},url:e("../react-native/rules/keyboard/index.js"),option:"keyboard",packages:["react-native-keyboard-controller"],restrictedImports:{paths:[{name:"react-native",importNames:["KeyboardAvoidingView"],message:"Import `KeyboardAvoidingView` from `react-native-keyboard-controller` — same props, and it tracks the keyboard per frame instead of waiting for Android's post-animation `keyboardDidShow`. For a scrolling form use its `KeyboardAwareScrollView`; to just shift a view, use `rt.insets.ime`."}]},rules:{}}];
1
+ const e=(t)=>new URL(t,import.meta.url).href;export const coreRegistry=[{meta:{name:"@ashstack/core"},url:e("../core/rules/core/index.js"),rules:{"no-comments":{},"no-naming-convention":{},"no-packed-condition":{},"use-design-system":{defaultOff:!0},"components-tsx-only":{defaultOff:!0},"hoist-intl":{}}},{meta:{name:"@ashstack/zod"},url:e("../core/rules/zod/index.js"),option:"zod",packages:["zod"],rules:{"prefer-enum":{}}}],reactRegistry=[{meta:{name:"@ashstack/react"},url:e("../react/rules/react/index.js"),rules:{"no-unlabeled-icon-button":{},"no-svg-without-title":{}}},{meta:{name:"@ashstack/query"},url:e("../react/rules/query/index.js"),option:"query",packages:["@tanstack/react-query"],rules:{"no-inline-keys":{},"no-deprecated-filters":{},"require-destructured-hooks":{},"no-fetch-in-query-fn":{},"next-page-param-undefined":{}}},{meta:{name:"@ashstack/zustand"},url:e("../react/rules/zustand/index.js"),option:"zustand",packages:["zustand"],rules:{"require-selector":{}}},{meta:{name:"@ashstack/i18n"},url:e("../react/rules/i18n/index.js"),option:"i18n",packages:["i18next","react-i18next","@lingui/core","react-intl","use-intl","next-intl","expo-localization"],rules:{"no-bare-text":{},"no-bare-attrs":{},"no-bare-toast":{}}},{meta:{name:"@ashstack/tailwind"},url:e("../react/rules/tailwind/index.js"),option:"tailwind",packages:["tailwindcss"],rules:{"prefer-cn":{},"use-logical-classes":{}}},{meta:{name:"@ashstack/tanstack-router"},url:e("../react/rules/tanstack-router/index.js"),option:"tanstackRouter",packages:["@tanstack/react-router"],rules:{"require-selector":{},"no-search-casts":{}}}],reactNativeRegistry=[{meta:{name:"@ashstack/react-native"},url:e("../react-native/rules/react-native/index.js"),restrictedImports:{paths:[{name:"react",importNames:["forwardRef"],message:"Accept `ref` as a regular prop and delete the `forwardRef` wrapper — React 19 passes `ref` like any other prop."},{name:"react",importNames:["useContext"],message:"Call `use(Context)` instead of `useContext(Context)`; React 19 replaces the hook with it."}],patterns:[{group:["expo-linear-gradient","react-native-linear-gradient"],message:"Apply the gradient through the `experimental_backgroundImage` style property instead of a gradient component."}]},rules:{"no-keyboard-will-events":{},"no-scroll-position-state":{},"no-conditional-style-array":{},"no-leaked-render":{},"no-rn-image-network-source":{},"no-redundant-view-nesting":{},"no-rn-namespace-import":{},"no-unlabeled-icon-pressable":{},"hoist-stateless-function":{},"no-manual-memo":{},"no-dynamic-import":{}}},{meta:{name:"@ashstack/unistyles"},url:e("../react-native/rules/unistyles/index.js"),option:"unistyles",packages:["react-native-unistyles"],restrictedImports:{paths:[{name:"react-native",importNames:["StyleSheet"],message:"Import `StyleSheet` from `react-native-unistyles` instead of `react-native`."},{name:"react-native",importNames:["useWindowDimensions","Dimensions"],message:"Read screen size from `rt.screen` inside `StyleSheet.create`, or `UnistylesRuntime.screen` outside it, instead of the `Dimensions` APIs."},{name:"react-native",importNames:["SafeAreaView"],message:'Set `contentInsetAdjustmentBehavior="automatic"` on the scroll view, or apply `rt.insets` from `react-native-unistyles`, instead of `SafeAreaView`.'},{name:"react-native-safe-area-context",importNames:["useSafeAreaInsets","useSafeAreaFrame","SafeAreaView"],message:"Read safe-area values from `rt.insets`, or `UnistylesRuntime.insets` outside a stylesheet, in `react-native-unistyles`."}]},rules:{"animated-theme":{},"content-container":{},"in-sheet":{},insets:{},"no-hardcoded-color":{},"no-hardcoded-spacing":{},"no-margin":{},"no-paramless-dynamic-function":{},"no-style-spread":{},"no-unused-styles":{},"rtl-style-call":{},"theme-screen-component":{},"theme-style-attr":{}}},{meta:{name:"@ashstack/legend-list"},url:e("../react-native/rules/legend-list/index.js"),option:"legendList",packages:["@legendapp/list"],restrictedImports:{paths:[{name:"react-native",importNames:["FlatList","SectionList","VirtualizedList"],message:"Render this list with `LegendList` from `@legendapp/list/react-native`; a `ScrollView` plus `map` is fine for fewer than 20 static items."},{name:"@shopify/flash-list",message:"Import `LegendList` from `@legendapp/list/react-native` — Legend List replaced FlashList in this stack."}],patterns:[{group:["@legendapp/list"],message:"Import from the platform entrypoint `@legendapp/list/react-native`, not the package root."}]},rules:{"required-props":{},"no-index-key-extractor":{},"no-remount-key":{},"no-inline-render-item-props":{},"no-mixed-children":{},"no-flex-in-content-container":{},"typed-items-need-item-type":{},"no-scrollview-map":{},"no-unsupported-props":{}}},{meta:{name:"@ashstack/legend-state"},url:e("../react-native/rules/legend-state/index.js"),option:"legendState",packages:["@legendapp/state"],restrictedImports:{paths:[{name:"@legendapp/state/react",importNames:["use$","useSelector"],message:"Use `useValue` from `@legendapp/state/react`; `use$` and `useSelector` are deprecated."}]},rules:{"no-assignment":{},naming:{},"no-nested-observable":{},"no-react-mirror":{},"no-untracked-get-in-jsx":{},"no-peek-in-selector":{},"no-object-selector":{},"no-observable-in-component":{}}},{meta:{name:"@ashstack/reanimated"},url:e("../react-native/rules/reanimated/index.js"),option:"reanimated",packages:["react-native-reanimated"],restrictedImports:{paths:[{name:"react-native",importNames:["Animated"],message:"Use `react-native-reanimated` instead of React Native's `Animated` API."},{name:"react-native-reanimated",importNames:["runOnJS","runOnUI"],message:"Use `scheduleOnRN` and `scheduleOnUI` from `react-native-worklets`; `runOnJS` and `runOnUI` are deprecated."},{name:"react-native-worklets",importNames:["runOnJS","runOnUI"],message:"Use `scheduleOnRN` and `scheduleOnUI` from `react-native-worklets`; `runOnJS` and `runOnUI` are deprecated."}]},rules:{"animated-reaction-safety":{},"animated-style-needs-animated-component":{},"animated-updater-purity":{},"gpu-properties-only":{},"hoist-layout-animation-builder":{},"interpolate-needs-clamp":{},"no-shared-value-dot-value":{},"no-react-state-from-continuous-worklet":{},"prefer-lazy-shared-value-initializer":{},"schedule-on-rn-scope":{},"shared-value-usage":{}}},{meta:{name:"@ashstack/turbo-image"},url:e("../react-native/rules/turbo-image/index.js"),option:"turboImage",packages:["react-native-turbo-image"],rules:{"require-resize":{},"require-cache-policy":{}}},{meta:{name:"@ashstack/skia"},url:e("../react-native/rules/skia/index.js"),option:"skia",packages:["@shopify/react-native-skia"],restrictedImports:{paths:[{name:"@shopify/react-native-skia",importNames:["usePathValue","usePathInterpolation"],message:"Keep one stable `SkPath` buffer and mutate it inside `useDerivedValue`, with consumers reading the driving shared value."}]},rules:{"canvas-opaque":{}}},{meta:{name:"@ashstack/keyboard"},url:e("../react-native/rules/keyboard/index.js"),option:"keyboard",packages:["react-native-keyboard-controller"],restrictedImports:{paths:[{name:"react-native",importNames:["KeyboardAvoidingView"],message:"Import `KeyboardAvoidingView` from `react-native-keyboard-controller` — same props, tracked per frame. For a scrolling form, its `KeyboardAwareScrollView`."}]},rules:{}}];
@@ -2,7 +2,7 @@ import type { AllowWarnDeny } from "oxlint";
2
2
  /** A severity, or a severity followed by as many of the rule's options as you want to set. */
3
3
  type RuleSetting<Options extends unknown[] = []> = AllowWarnDeny | [AllowWarnDeny, ...Partial<Options>];
4
4
  /** Every rule id `react-native()` adds on top of the entry below it. */
5
- export type ReactNativeRuleId = "@ashstack/react-native/no-keyboard-will-events" | "@ashstack/react-native/no-scroll-position-state" | "@ashstack/react-native/no-conditional-style-array" | "@ashstack/react-native/no-leaked-render" | "@ashstack/react-native/no-rn-image-network-source" | "@ashstack/react-native/no-redundant-view-nesting" | "@ashstack/react-native/no-rn-namespace-import" | "@ashstack/react-native/no-unlabeled-icon-pressable" | "@ashstack/react-native/hoist-stateless-function" | "@ashstack/react-native/no-manual-memo" | "@ashstack/react-native/no-dynamic-import" | "@ashstack/unistyles/animated-theme" | "@ashstack/unistyles/content-container" | "@ashstack/unistyles/in-sheet" | "@ashstack/unistyles/insets" | "@ashstack/unistyles/no-hardcoded-color" | "@ashstack/unistyles/no-hardcoded-spacing" | "@ashstack/unistyles/no-margin" | "@ashstack/unistyles/no-style-spread" | "@ashstack/unistyles/no-unused-styles" | "@ashstack/unistyles/rtl-style-call" | "@ashstack/unistyles/theme-screen-component" | "@ashstack/unistyles/theme-style-attr" | "@ashstack/legend-list/required-props" | "@ashstack/legend-list/no-index-key-extractor" | "@ashstack/legend-list/no-remount-key" | "@ashstack/legend-list/no-inline-render-item-props" | "@ashstack/legend-list/no-mixed-children" | "@ashstack/legend-list/no-flex-in-content-container" | "@ashstack/legend-list/typed-items-need-item-type" | "@ashstack/legend-list/no-scrollview-map" | "@ashstack/legend-list/no-unsupported-props" | "@ashstack/legend-state/no-assignment" | "@ashstack/legend-state/naming" | "@ashstack/legend-state/no-nested-observable" | "@ashstack/legend-state/no-react-mirror" | "@ashstack/legend-state/no-untracked-get-in-jsx" | "@ashstack/legend-state/no-peek-in-selector" | "@ashstack/legend-state/no-object-selector" | "@ashstack/legend-state/no-observable-in-component" | "@ashstack/reanimated/animated-reaction-safety" | "@ashstack/reanimated/animated-style-needs-animated-component" | "@ashstack/reanimated/animated-updater-purity" | "@ashstack/reanimated/gpu-properties-only" | "@ashstack/reanimated/hoist-layout-animation-builder" | "@ashstack/reanimated/interpolate-needs-clamp" | "@ashstack/reanimated/no-shared-value-dot-value" | "@ashstack/reanimated/no-react-state-from-continuous-worklet" | "@ashstack/reanimated/prefer-lazy-shared-value-initializer" | "@ashstack/reanimated/schedule-on-rn-scope" | "@ashstack/reanimated/shared-value-usage" | "@ashstack/turbo-image/require-resize" | "@ashstack/turbo-image/require-cache-policy" | "@ashstack/skia/canvas-opaque";
5
+ export type ReactNativeRuleId = "@ashstack/react-native/no-keyboard-will-events" | "@ashstack/react-native/no-scroll-position-state" | "@ashstack/react-native/no-conditional-style-array" | "@ashstack/react-native/no-leaked-render" | "@ashstack/react-native/no-rn-image-network-source" | "@ashstack/react-native/no-redundant-view-nesting" | "@ashstack/react-native/no-rn-namespace-import" | "@ashstack/react-native/no-unlabeled-icon-pressable" | "@ashstack/react-native/hoist-stateless-function" | "@ashstack/react-native/no-manual-memo" | "@ashstack/react-native/no-dynamic-import" | "@ashstack/unistyles/animated-theme" | "@ashstack/unistyles/content-container" | "@ashstack/unistyles/in-sheet" | "@ashstack/unistyles/insets" | "@ashstack/unistyles/no-hardcoded-color" | "@ashstack/unistyles/no-hardcoded-spacing" | "@ashstack/unistyles/no-margin" | "@ashstack/unistyles/no-paramless-dynamic-function" | "@ashstack/unistyles/no-style-spread" | "@ashstack/unistyles/no-unused-styles" | "@ashstack/unistyles/rtl-style-call" | "@ashstack/unistyles/theme-screen-component" | "@ashstack/unistyles/theme-style-attr" | "@ashstack/legend-list/required-props" | "@ashstack/legend-list/no-index-key-extractor" | "@ashstack/legend-list/no-remount-key" | "@ashstack/legend-list/no-inline-render-item-props" | "@ashstack/legend-list/no-mixed-children" | "@ashstack/legend-list/no-flex-in-content-container" | "@ashstack/legend-list/typed-items-need-item-type" | "@ashstack/legend-list/no-scrollview-map" | "@ashstack/legend-list/no-unsupported-props" | "@ashstack/legend-state/no-assignment" | "@ashstack/legend-state/naming" | "@ashstack/legend-state/no-nested-observable" | "@ashstack/legend-state/no-react-mirror" | "@ashstack/legend-state/no-untracked-get-in-jsx" | "@ashstack/legend-state/no-peek-in-selector" | "@ashstack/legend-state/no-object-selector" | "@ashstack/legend-state/no-observable-in-component" | "@ashstack/reanimated/animated-reaction-safety" | "@ashstack/reanimated/animated-style-needs-animated-component" | "@ashstack/reanimated/animated-updater-purity" | "@ashstack/reanimated/gpu-properties-only" | "@ashstack/reanimated/hoist-layout-animation-builder" | "@ashstack/reanimated/interpolate-needs-clamp" | "@ashstack/reanimated/no-shared-value-dot-value" | "@ashstack/reanimated/no-react-state-from-continuous-worklet" | "@ashstack/reanimated/prefer-lazy-shared-value-initializer" | "@ashstack/reanimated/schedule-on-rn-scope" | "@ashstack/reanimated/shared-value-usage" | "@ashstack/turbo-image/require-resize" | "@ashstack/turbo-image/require-cache-policy" | "@ashstack/skia/canvas-opaque";
6
6
  declare module "oxlint" {
7
7
  interface DummyRuleMap {
8
8
  /**
@@ -60,9 +60,7 @@ declare module "oxlint" {
60
60
  */
61
61
  "@ashstack/react-native/hoist-stateless-function"?: RuleSetting;
62
62
  /**
63
- * Require a `// why:` line above every kept `useMemo`, `useCallback` and `memo`. The React Compiler memoises on a best-effort basis, not a guarantee, so a memo is allowed where the cost is real: something rendered per list row, or a computation measured as heavy. The `// why:` line names which of the two applies. `@ashstack/core/no-comments` keeps that line and never counts it against its `budget`, so both rules run together.
64
- *
65
- * Enabled only when one of `babel-plugin-react-compiler`, `react-compiler-runtime`, `react-compiler-marker` is a dependency.
63
+ * Require a `// why:` line above every kept `useMemo`, `useCallback` and `memo`. The React Compiler memoises on a best-effort basis, not a guarantee, so a memo is allowed where the cost is real: something rendered per list row, or a computation measured as heavy. The `// why:` line names which of the two applies. `@ashstack/core/no-comments` keeps that line and never counts it against its `budget`, so both rules run together. Assumes the compiler is on — pass `reactCompiler: false` to the entry and this rule turns off, since without it a hand-written memo is the only memo there is.
66
64
  *
67
65
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreact-nativeno-manual-memo
68
66
  */
@@ -115,6 +113,12 @@ declare module "oxlint" {
115
113
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackunistylesno-margin
116
114
  */
117
115
  "@ashstack/unistyles/no-margin"?: RuleSetting;
116
+ /**
117
+ * Disallow a style written as a function that takes no arguments. It returns the same object on every render, and `theme` and `rt` reach a static style anyway, so the function only adds a call at each use site. The suggestion turns `() => ({ ... })` back into the object.
118
+ *
119
+ * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackunistylesno-paramless-dynamic-function
120
+ */
121
+ "@ashstack/unistyles/no-paramless-dynamic-function"?: RuleSetting;
118
122
  /**
119
123
  * Disallow spreading a stylesheet style into another object. The spread reads through the Unistyles C++ proxy once, so the result stops reacting to the theme.
120
124
  *
@@ -102,6 +102,12 @@ export type CoreOptions = {
102
102
  zod?: boolean;
103
103
  };
104
104
  export type ReactOptions = CoreOptions & {
105
+ /**
106
+ * Is the React Compiler on? Defaults to `true` — every other React setting here assumes it.
107
+ * Set `false` and the four [`react-perf/` inline-prop rules](https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#react)
108
+ * come back on, since without the compiler a fresh object, array, function or element in a prop really does re-render the child.
109
+ */
110
+ reactCompiler?: boolean;
105
111
  /** [`@ashstack/query/` rules](https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackquery) — detected from `@tanstack/react-query` */
106
112
  query?: boolean;
107
113
  /** [`@ashstack/zustand/` rules](https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackzustand) — detected from `zustand` */
@@ -1 +1 @@
1
- import o from"../core/index.js";import{EFFECT_RULES as t,effectPlugin as a}from"../lib/effect-plugin.js";import{mergeConfigs as s}from"../lib/merge.js";import{composeModules as n}from"../lib/module.js";import{coreRegistry as c,reactRegistry as i}from"../lib/registry.js";const l={"eslint/no-empty":"off","eslint/no-empty-function":"off"},u={"react/error-boundaries":"error","react/globals":"error","react/immutability":"error","react/incompatible-library":"error","react/preserve-manual-memoization":"error","react/purity":"error","react/refs":"error","react/set-state-in-effect":"off","react/set-state-in-render":"error","react/static-components":"error","react/use-memo":"error","react/void-use-memo":"error","react/capitalized-calls":"error","react/exhaustive-effect-dependencies":"error","react/hooks":"error","react/memo-dependencies":"error","react/no-deriving-state-in-effects":"off","react/invariant":"error","react/rule-suppression":"error","react/syntax":"error","react/todo":"error","react/unsupported-syntax":"error"},p=["**/routes/**","**/src/app/**","**/app/**/_layout.tsx","**/app/**/+*.tsx"],f={...l,"max-lines-per-function":["error",{max:120,skipBlankLines:!0,skipComments:!0}],"react/react-in-jsx-scope":"off","react/style-prop-object":"off","react/only-export-components":"off","react/rules-of-hooks":"error","react/exhaustive-deps":"error","react/button-has-type":"error","react/checked-requires-onchange-or-readonly":"error","react/iframe-missing-sandbox":"error","react/jsx-boolean-value":"error","react/jsx-key":"error","react/jsx-no-comment-textnodes":"error","react/jsx-no-constructed-context-values":"error","react/jsx-no-duplicate-props":"error","react/jsx-no-target-blank":"error","react/jsx-no-useless-fragment":"error","react/no-array-index-key":"error","react/no-children-prop":"error","react/no-danger":"error","react/no-danger-with-children":"error","react/no-direct-mutation-state":"error","react/no-is-mounted":"error","react/no-render-return-value":"error","react/no-string-refs":"error","react/no-this-in-sfc":"error","react/no-unsafe":"error","react/no-unescaped-entities":"error","react/no-unstable-nested-components":["error",{allowAsProps:!0}],"react/require-render-return":"error","react/self-closing-comp":"error","react/void-dom-elements-no-children":"error",...u,"jsx-a11y/alt-text":"error","jsx-a11y/anchor-has-content":"error","jsx-a11y/anchor-is-valid":"error","jsx-a11y/click-events-have-key-events":"off","jsx-a11y/control-has-associated-label":"off","jsx-a11y/heading-has-content":"error","jsx-a11y/img-redundant-alt":"error","jsx-a11y/interactive-supports-focus":"error","jsx-a11y/label-has-associated-control":"error","jsx-a11y/no-aria-hidden-on-focusable":"error","jsx-a11y/no-autofocus":["error",{ignoreNonDOM:!0}],"jsx-a11y/no-noninteractive-element-interactions":"off","jsx-a11y/no-noninteractive-tabindex":"error","jsx-a11y/no-redundant-roles":"error","jsx-a11y/no-static-element-interactions":"error","jsx-a11y/prefer-tag-over-role":"off","jsx-a11y/role-has-required-aria-props":"error","jsx-a11y/role-supports-aria-props":"error","unicorn/filename-case":["error",{cases:{kebabCase:!0,pascalCase:!0}}]},react=(r={})=>{const e=n([...c,...i],r);return s(o(r),{plugins:["react","jsx-a11y","react-perf"],jsPlugins:[a,...e.jsPlugins],rules:{...f,...t,...e.rules},overrides:[{files:p,rules:{"unicorn/filename-case":"off"}}]})};export{react};export default react;
1
+ import o from"../core/index.js";import{EFFECT_RULES as t,effectPlugin as a}from"../lib/effect-plugin.js";import{mergeConfigs as s}from"../lib/merge.js";import{composeModules as n}from"../lib/module.js";import{coreRegistry as c,reactRegistry as i}from"../lib/registry.js";const p={"eslint/no-empty":"off","eslint/no-empty-function":"off"},l={"react/error-boundaries":"error","react/globals":"error","react/immutability":"error","react/incompatible-library":"error","react/preserve-manual-memoization":"error","react/purity":"error","react/refs":"error","react/set-state-in-effect":"off","react/set-state-in-render":"error","react/static-components":"error","react/use-memo":"error","react/void-use-memo":"error","react/capitalized-calls":"error","react/exhaustive-effect-dependencies":"error","react/hooks":"error","react/memo-dependencies":"error","react/no-deriving-state-in-effects":"off","react/invariant":"error","react/rule-suppression":"error","react/syntax":"error","react/todo":"error","react/unsupported-syntax":"error"},f=(e)=>{const r=e?"off":"error";return{"react-perf/jsx-no-jsx-as-prop":r,"react-perf/jsx-no-new-array-as-prop":r,"react-perf/jsx-no-new-function-as-prop":r,"react-perf/jsx-no-new-object-as-prop":r,"react/jsx-no-constructed-context-values":r,"react/no-object-type-as-default-prop":r}},u=["**/routes/**","**/src/app/**","**/app/**/_layout.tsx","**/app/**/+*.tsx"],d={...p,"max-lines-per-function":["error",{max:120,skipBlankLines:!0,skipComments:!0}],"react/react-in-jsx-scope":"off","react/style-prop-object":"off","react/only-export-components":"off","react/rules-of-hooks":"error","react/exhaustive-deps":"error","react/button-has-type":"error","react/checked-requires-onchange-or-readonly":"error","react/iframe-missing-sandbox":"error","react/jsx-boolean-value":"error","react/jsx-key":"error","react/jsx-no-comment-textnodes":"error","react/jsx-no-duplicate-props":"error","react/jsx-no-target-blank":"error","react/jsx-no-useless-fragment":"error","react/no-array-index-key":"error","react/no-children-prop":"error","react/no-danger":"error","react/no-danger-with-children":"error","react/no-direct-mutation-state":"error","react/no-is-mounted":"error","react/no-render-return-value":"error","react/no-string-refs":"error","react/no-this-in-sfc":"error","react/no-unsafe":"error","react/no-unescaped-entities":"error","react/no-unstable-nested-components":["error",{allowAsProps:!0}],"react/require-render-return":"error","react/self-closing-comp":"error","react/void-dom-elements-no-children":"error",...l,"jsx-a11y/alt-text":"error","jsx-a11y/anchor-has-content":"error","jsx-a11y/anchor-is-valid":"error","jsx-a11y/click-events-have-key-events":"off","jsx-a11y/control-has-associated-label":"off","jsx-a11y/heading-has-content":"error","jsx-a11y/img-redundant-alt":"error","jsx-a11y/interactive-supports-focus":"error","jsx-a11y/label-has-associated-control":"error","jsx-a11y/no-aria-hidden-on-focusable":"error","jsx-a11y/no-autofocus":["error",{ignoreNonDOM:!0}],"jsx-a11y/no-noninteractive-element-interactions":"off","jsx-a11y/no-noninteractive-tabindex":"error","jsx-a11y/no-redundant-roles":"error","jsx-a11y/no-static-element-interactions":"error","jsx-a11y/prefer-tag-over-role":"off","jsx-a11y/role-has-required-aria-props":"error","jsx-a11y/role-supports-aria-props":"error","unicorn/filename-case":["error",{cases:{kebabCase:!0,pascalCase:!0}}]},react=(e={})=>{const r=n([...c,...i],e);return s(o(e),{plugins:["react","jsx-a11y","react-perf"],jsPlugins:[a,...r.jsPlugins],rules:{...d,...f(e.reactCompiler??!0),...t,...r.rules},overrides:[{files:u,rules:{"unicorn/filename-case":"off"}}]})};export{react};export default react;
@@ -1 +1 @@
1
- import{crossesFunctionBefore as n,findInSubtree as o,FUNCTION_TYPES as u,gate as l,problem as s,propertyKeyName as i}from"../../../lib/ast.js";const g="Return `undefined` to signal there are no more pages. `null` is a valid page param, so returning it reads as a real next cursor and the list keeps fetching forever.",p=(e)=>e.type==="ReturnStatement"&&e.argument?.type==="Literal"&&e.argument.value===null,m=(e)=>e.type==="ArrowFunctionExpression"&&e.body.type==="Literal"&&e.body.value===null?e.body:null;export const nextPageParamUndefined=s("Disallow `return null` in the body of a `getNextPageParam`.",{createOnce(e){return{before(){return l(e,"getNextPageParam")},Property(t){if(i(t)!=="getNextPageParam")return;const{value:r}=t,a=m(r)??o(r,p);if(!a||n(a,r,u))return;e.report({node:r,message:g})}}}});
1
+ import{crossesFunctionBefore as a,findInSubtree as o,FUNCTION_TYPES as u,gate as l,problem as s,propertyKeyName as i}from"../../../lib/ast.js";const g="Return `undefined` to signal there are no more pages — `null` reads as a real next cursor, so the list keeps fetching forever.",p=(e)=>e.type==="ReturnStatement"&&e.argument?.type==="Literal"&&e.argument.value===null,m=(e)=>e.type==="ArrowFunctionExpression"&&e.body.type==="Literal"&&e.body.value===null?e.body:null;export const nextPageParamUndefined=s("Disallow `return null` in the body of a `getNextPageParam`.",{createOnce(e){return{before(){return l(e,"getNextPageParam")},Property(t){if(i(t)!=="getNextPageParam")return;const{value:r}=t,n=m(r)??o(r,p);if(!n||a(n,r,u))return;e.report({node:r,message:g})}}}});
@@ -1 +1 @@
1
- const n="Pass a filter object here: `.invalidateQueries({ queryKey: someKeys.scope(...) })`. TanStack Query v5 removed the positional `(queryKey)` form.",o=new Set(["invalidateQueries","removeQueries","refetchQueries","cancelQueries","resetQueries"]),a=(e)=>{if(e.type!=="CallExpression")return!1;const{callee:r}=e;if(r.type!=="MemberExpression"||r.computed)return!1;const{property:t}=r;return t.type==="Identifier"&&o.has(t.name)},l=(e)=>{if(e.type!=="CallExpression"||e.arguments.length!==1)return;return e.arguments[0]},u=(e)=>e.type==="Literal"&&typeof e.value==="string",c=(e,r)=>{if(r.type==="ArrayExpression")return`{ queryKey: ${e.sourceCode.getText(r)} }`;if(u(r))return`{ queryKey: [${e.sourceCode.getText(r)}] }`;return null};export const noDeprecatedFilters={meta:{type:"problem",docs:{description:"Disallow the positional key argument that TanStack Query v5 removed from `invalidateQueries` and its sibling methods. The suggestion rewrites it to the filter-object form."},hasSuggestions:!0},createOnce(e){return{CallExpression(r){if(!a(r))return;const t=l(r);if(t===void 0)return;const s=c(e,t);if(s===null)return;e.report({node:t,message:n,suggest:[{desc:"Wrap in a filter object",fix:(i)=>i.replaceText(t,s)}]})}}}};
1
+ const n="Pass a filter object here: `.invalidateQueries({ queryKey: someKeys.scope(...) })`; v5 removed the positional form.",o=new Set(["invalidateQueries","removeQueries","refetchQueries","cancelQueries","resetQueries"]),l=(e)=>{if(e.type!=="CallExpression")return!1;const{callee:t}=e;if(t.type!=="MemberExpression"||t.computed)return!1;const{property:r}=t;return r.type==="Identifier"&&o.has(r.name)},a=(e)=>{if(e.type!=="CallExpression"||e.arguments.length!==1)return;return e.arguments[0]},u=(e)=>e.type==="Literal"&&typeof e.value==="string",c=(e,t)=>{if(t.type==="ArrayExpression")return`{ queryKey: ${e.sourceCode.getText(t)} }`;if(u(t))return`{ queryKey: [${e.sourceCode.getText(t)}] }`;return null};export const noDeprecatedFilters={meta:{type:"problem",docs:{description:"Disallow the positional key argument that TanStack Query v5 removed from `invalidateQueries` and its sibling methods. The suggestion rewrites it to the filter-object form."},hasSuggestions:!0},createOnce(e){return{CallExpression(t){if(!l(t))return;const r=a(t);if(r===void 0)return;const s=c(e,r);if(s===null)return;e.report({node:r,message:n,suggest:[{desc:"Wrap in a filter object",fix:(i)=>i.replaceText(r,s)}]})}}}};
@@ -1 +1 @@
1
- import{gate as r,problem as a,propertyKeyName as n,subtreeHas as s}from"../../../lib/ast.js";const o="Call a typed function from the feature's `*.requests.ts` module here instead of `fetch`. That client is what sends the auth header, applies the timeout, retries a 429 or 5xx, and throws the app's error type.",i=new Set(["queryFn","mutationFn"]),u=(e)=>e.type==="CallExpression"&&e.callee.type==="Identifier"&&e.callee.name==="fetch";export const noFetchInQueryFn=a("Disallow a bare `fetch(` inside a `queryFn` or `mutationFn`.",{createOnce(e){return{before(){return r(e,"queryFn","mutationFn")},Property(t){if(!i.has(n(t)))return;if(!s(t.value,u))return;e.report({node:t.value,message:o})}}}});
1
+ import{gate as t,problem as a,propertyKeyName as n,subtreeHas as s}from"../../../lib/ast.js";const o="Call a typed function from the feature's `*.requests.ts` module here instead of `fetch` — it carries auth, retries and the app's error type.",u=new Set(["queryFn","mutationFn"]),i=(e)=>e.type==="CallExpression"&&e.callee.type==="Identifier"&&e.callee.name==="fetch";export const noFetchInQueryFn=a("Disallow a bare `fetch(` inside a `queryFn` or `mutationFn`.",{createOnce(e){return{before(){return t(e,"queryFn","mutationFn")},Property(r){if(!u.has(n(r)))return;if(!s(r.value,i))return;e.report({node:r.value,message:o})}}}});
@@ -1 +1 @@
1
- import{problem as a,propertyKeyName as y}from"../../../lib/ast.js";const i={inlineQueryKey:"Define this key in the feature's `*.keys.ts` factory and reference it: `queryKey: someKeys.scope(...)`, so invalidation elsewhere matches the same key.",inlineGetQueryData:"Read through the feature's `*.keys.ts` factory: `getQueryData(someKeys.scope())`.",inlineSetQueryData:"Write through the feature's `*.keys.ts` factory: `setQueryData(someKeys.scope(), updater)`."},o=new Set(["useQuery","useSuspenseQuery","useInfiniteQuery","useSuspenseInfiniteQuery","useQueries"]),p=new Set(["invalidateQueries","removeQueries","refetchQueries","cancelQueries","resetQueries","fetchQuery","prefetchQuery","ensureQueryData","getQueriesData","setQueriesData"]),c=(t)=>{if(t.type!=="CallExpression")return"";const{callee:e}=t;if(e.type!=="MemberExpression"||e.computed)return"";const{property:r}=e;return r.type==="Identifier"?r.name:""},f=(t)=>{if(t.type!=="CallExpression")return;const[e]=t.arguments;return e?.type==="ArrayExpression"?e:void 0},l=(t,e)=>{const r=t.type==="CallExpression"?t.callee:void 0;return r?.type==="Identifier"&&o.has(r.name)||p.has(e)},Q=(t)=>{if(t.type!=="Property"||t.computed)return!1;const{key:e}=t;if(e.type==="Identifier")return e.name==="queryKey";return e.type==="Literal"&&e.value==="queryKey"},m=(t)=>{if(t.type!=="ObjectExpression")return[];const e=t.properties.find((r)=>r.type==="Property"&&y(r)==="queries");if(e?.type!=="Property"||e.value.type!=="ArrayExpression")return[t];return[t,...e.value.elements.filter((r)=>r!==null)]},h=(t)=>{if(t.type!=="CallExpression")return[];const[e]=t.arguments;if(e===void 0)return[];const r=[];for(const s of m(e)){if(s.type!=="ObjectExpression")continue;for(const n of s.properties){if(n.type!=="Property"||!Q(n))continue;const{value:u}=n;if(u.type==="ArrayExpression")r.push(u)}}return r};export const noInlineKeys=a("Disallow a query key written as an array literal at the call site instead of coming from a keys factory.",{createOnce(t){return{CallExpression(e){const r=c(e);if(r==="getQueryData"||r==="setQueryData"){const s=f(e);if(s===void 0)return;t.report({node:s,message:r==="getQueryData"?i.inlineGetQueryData:i.inlineSetQueryData});return}if(!l(e,r))return;for(const s of h(e))t.report({node:s,message:i.inlineQueryKey})}}}});
1
+ import{problem as a,propertyKeyName as y}from"../../../lib/ast.js";const i={inlineQueryKey:"Define this key in the feature's `*.keys.ts` factory and reference it: `queryKey: someKeys.scope(...)`.",inlineGetQueryData:"Read through the feature's `*.keys.ts` factory: `getQueryData(someKeys.scope())`.",inlineSetQueryData:"Write through the feature's `*.keys.ts` factory: `setQueryData(someKeys.scope(), updater)`."},o=new Set(["useQuery","useSuspenseQuery","useInfiniteQuery","useSuspenseInfiniteQuery","useQueries"]),p=new Set(["invalidateQueries","removeQueries","refetchQueries","cancelQueries","resetQueries","fetchQuery","prefetchQuery","ensureQueryData","getQueriesData","setQueriesData"]),f=(r)=>{if(r.type!=="CallExpression")return"";const{callee:e}=r;if(e.type!=="MemberExpression"||e.computed)return"";const{property:t}=e;return t.type==="Identifier"?t.name:""},c=(r)=>{if(r.type!=="CallExpression")return;const[e]=r.arguments;return e?.type==="ArrayExpression"?e:void 0},l=(r,e)=>{const t=r.type==="CallExpression"?r.callee:void 0;return t?.type==="Identifier"&&o.has(t.name)||p.has(e)},Q=(r)=>{if(r.type!=="Property"||r.computed)return!1;const{key:e}=r;if(e.type==="Identifier")return e.name==="queryKey";return e.type==="Literal"&&e.value==="queryKey"},m=(r)=>{if(r.type!=="ObjectExpression")return[];const e=r.properties.find((t)=>t.type==="Property"&&y(t)==="queries");if(e?.type!=="Property"||e.value.type!=="ArrayExpression")return[r];return[r,...e.value.elements.filter((t)=>t!==null)]},d=(r)=>{if(r.type!=="CallExpression")return[];const[e]=r.arguments;if(e===void 0)return[];const t=[];for(const s of m(e)){if(s.type!=="ObjectExpression")continue;for(const n of s.properties){if(n.type!=="Property"||!Q(n))continue;const{value:u}=n;if(u.type==="ArrayExpression")t.push(u)}}return t};export const noInlineKeys=a("Disallow a query key written as an array literal at the call site instead of coming from a keys factory.",{createOnce(r){return{CallExpression(e){const t=f(e);if(t==="getQueryData"||t==="setQueryData"){const s=c(e);if(s===void 0)return;r.report({node:s,message:t==="getQueryData"?i.inlineGetQueryData:i.inlineSetQueryData});return}if(!l(e,t))return;for(const s of d(e))r.report({node:s,message:i.inlineQueryKey})}}}});
@@ -1 +1 @@
1
- import{problem as s}from"../../../lib/ast.js";const c="Destructure this hook at the call site: `const { data } = useFooQuery()` rather than binding the whole result to one name.",u=/^@\/api\/.*\.(?:queries|mutations)$/;export const requireDestructuredHooks=s("Require destructuring the result of a hook imported from an `@/api/*.queries` or `@/api/*.mutations` module.",{createOnce(a){const o=new Set,r=[];return{before(){o.clear();r.length=0},ImportDeclaration(e){if(!u.test(e.source.value))return;for(const t of e.specifiers)if(t.type==="ImportSpecifier")o.add(t.local.name)},VariableDeclarator(e){const{id:t,init:n}=e;if(t.type!=="Identifier")return;if(n?.type!=="CallExpression")return;const{callee:i}=n;if(i.type!=="Identifier"||!/^use[A-Z]/.test(i.name))return;r.push({node:t,hook:i.name})},"Program:exit"(){for(const e of r){if(!o.has(e.hook))continue;a.report({node:e.node,message:c})}}}}});
1
+ import{problem as a}from"../../../lib/ast.js";const c="Destructure this hook at the call site: `const { data } = useFooQuery()`.",u=/^@\/api\/.*\.(?:queries|mutations)$/;export const requireDestructuredHooks=a("Require destructuring the result of a hook imported from an `@/api/*.queries` or `@/api/*.mutations` module.",{createOnce(n){const o=new Set,r=[];return{before(){o.clear();r.length=0},ImportDeclaration(e){if(!u.test(e.source.value))return;for(const t of e.specifiers)if(t.type==="ImportSpecifier")o.add(t.local.name)},VariableDeclarator(e){const{id:t,init:s}=e;if(t.type!=="Identifier")return;if(s?.type!=="CallExpression")return;const{callee:i}=s;if(i.type!=="Identifier"||!/^use[A-Z]/.test(i.name))return;r.push({node:t,hook:i.name})},"Program:exit"(){for(const e of r){if(!o.has(e.hook))continue;n.report({node:e.node,message:c})}}}}});
@@ -1 +1 @@
1
- import{attributeName as a,gate as s,problem as o}from"../../../lib/ast.js";import{childNames as l,namedBy as m,NAMING_ATTRIBUTES as c}from"./shared.js";const f=new Set(["presentation","none"]),p='Give this `<svg>` a `<title>` child with content, or an `aria-label`, so a screen reader can name it. If it carries no meaning of its own, mark it `aria-hidden="true"` or `role="presentation"` and let the text beside it do the naming.',u=(e)=>{if(e?.type==="Literal")return e.value!=="false";if(e?.type!=="JSXExpressionContainer")return!0;const{expression:t}=e;return!(t.type==="Literal"&&(t.value===!1||t.value==="false"))},d=(e)=>e?.type==="Literal"&&typeof e.value==="string"&&f.has(e.value),g=(e)=>{if(e.type!=="JSXAttribute")return!1;const t=a(e);if(t==="aria-hidden")return u(e.value);if(t==="role")return d(e.value);return m(e,c)};export const noSvgWithoutTitle=o("Require an accessible name on an inline `<svg>`: a `<title>` child with content, an `aria-label`, or a marker that it is decorative. Every child counts, and a self-closing `<svg />` reports.",{createOnce(e){const t=(r)=>{if(r.type!=="JSXElement")return!1;const{name:n}=r.openingElement;if(n.type!=="JSXIdentifier"||n.name!=="title")return!1;return r.children.some((i)=>l(i,e)||i.type==="JSXElement"||i.type==="JSXFragment")};return{before(){return s(e,"<svg")},JSXElement(r){const n=r.openingElement;if(n.name.type!=="JSXIdentifier"||n.name.name!=="svg")return;if(n.attributes.some(g))return;if(r.children.some(t))return;e.report({node:n.name,message:p})}}}});
1
+ import{attributeName as a,gate as s,problem as o}from"../../../lib/ast.js";import{childNames as l,namedBy as m,NAMING_ATTRIBUTES as f}from"./shared.js";const c=new Set(["presentation","none"]),p='Give this `<svg>` a `<title>` child with content, or an `aria-label`. If it carries no meaning of its own, mark it `aria-hidden="true"`.',u=(e)=>{if(e?.type==="Literal")return e.value!=="false";if(e?.type!=="JSXExpressionContainer")return!0;const{expression:t}=e;return!(t.type==="Literal"&&(t.value===!1||t.value==="false"))},g=(e)=>e?.type==="Literal"&&typeof e.value==="string"&&c.has(e.value),d=(e)=>{if(e.type!=="JSXAttribute")return!1;const t=a(e);if(t==="aria-hidden")return u(e.value);if(t==="role")return g(e.value);return m(e,f)};export const noSvgWithoutTitle=o("Require an accessible name on an inline `<svg>`: a `<title>` child with content, an `aria-label`, or a marker that it is decorative. Every child counts, and a self-closing `<svg />` reports.",{createOnce(e){const t=(r)=>{if(r.type!=="JSXElement")return!1;const{name:n}=r.openingElement;if(n.type!=="JSXIdentifier"||n.name!=="title")return!1;return r.children.some((i)=>l(i,e)||i.type==="JSXElement"||i.type==="JSXFragment")};return{before(){return s(e,"<svg")},JSXElement(r){const n=r.openingElement;if(n.name.type!=="JSXIdentifier"||n.name.name!=="svg")return;if(n.attributes.some(d))return;if(r.children.some(t))return;e.report({node:n.name,message:p})}}}});
@@ -1 +1 @@
1
- import{attributeName as o,findInSubtree as i,gate as s,problem as l,tagIdentifier as m}from"../../../lib/ast.js";import{attributeHasContent as u,childNames as c,namedBy as b,NAMING_ATTRIBUTES as p}from"./shared.js";const g=new Set(["button","Button"]),S="Add an `aria-label` (or `aria-labelledby`), or render visible text, so this icon-only button has an accessible name. An `<img alt>` or an `<svg><title>` inside it counts too.",d=(e)=>e.type==="JSXAttribute"&&o(e)==="alt"&&u(e.value),f=(e)=>e.type==="JSXOpeningElement"&&m(e.name)==="img"&&e.attributes.some(d);export const noUnlabeledIconButton=l("Require an accessible name on an icon-only `<button>` or `<Button>`. Without a label, visible text or image alt text, a screen reader cannot reach the control.",{createOnce(e){const r=(t)=>c(t,e)||f(t);return{before(){return s(e,"button","Button")},JSXElement(t){const a=t.openingElement;if(a.name.type!=="JSXIdentifier"||!g.has(a.name.name))return;if(a.attributes.some((n)=>b(n,p)))return;if(!t.children.some((n)=>n.type==="JSXElement"||n.type==="JSXFragment"))return;if(t.children.some((n)=>i(n,r)!==null))return;e.report({node:a.name,message:S})}}}});
1
+ import{attributeName as o,findInSubtree as i,gate as s,problem as l,tagIdentifier as m}from"../../../lib/ast.js";import{attributeHasContent as u,childNames as b,namedBy as c,NAMING_ATTRIBUTES as p}from"./shared.js";const S=new Set(["button","Button"]),f="Add an `aria-label` (or `aria-labelledby`), or render visible text, so this icon-only button has an accessible name.",g=(e)=>e.type==="JSXAttribute"&&o(e)==="alt"&&u(e.value),d=(e)=>e.type==="JSXOpeningElement"&&m(e.name)==="img"&&e.attributes.some(g);export const noUnlabeledIconButton=l("Require an accessible name on an icon-only `<button>` or `<Button>`. Without a label, visible text or image alt text, a screen reader cannot reach the control.",{createOnce(e){const r=(t)=>b(t,e)||d(t);return{before(){return s(e,"button","Button")},JSXElement(t){const a=t.openingElement;if(a.name.type!=="JSXIdentifier"||!S.has(a.name.name))return;if(a.attributes.some((n)=>c(n,p)))return;if(!t.children.some((n)=>n.type==="JSXElement"||n.type==="JSXFragment"))return;if(t.children.some((n)=>i(n,r)!==null))return;e.report({node:a.name,message:f})}}}});
@@ -1 +1 @@
1
- import{attributeName as f,importedNames as h,problem as m}from"../../../lib/ast.js";import{CLASS_ATTRIBUTE as d,CLASS_BINDING as S,CLASS_MARKER as y,COMPOSER_MODULES as b,KNOWN_COMPOSERS as c}from"./shared.js";const g=new Set(["join","concat"]),w=new Set(["ConditionalExpression","LogicalExpression"]),a={attribute:"Wrap this class value in `cn(...)`. It folds the conditional inputs into one string and lets a later Tailwind utility win over an earlier one that sets the same property.",precomputed:"Wrap this value in `cn(...)` where it reaches the class prop, or build it with `cn(...)` where it is declared. Conflicting Tailwind utilities survive into the DOM otherwise.",declaration:"Build this class value with `cn(...)` instead of joining it by hand, so conflicting Tailwind utilities resolve to the last one and falsy branches drop out."},E=(e,r)=>{if(e.type==="MemberExpression")return e.property.type==="Identifier"&&g.has(e.property.name);if(e.type!=="Identifier")return!1;return c.has(e.name)?e.name!=="cn":r.has(e.name)},p=(e,r)=>{if(!e)return!1;if(w.has(e.type))return!0;if(e.type==="TemplateLiteral")return e.expressions.length>0;if(e.type==="BinaryExpression")return e.operator==="+";return e.type==="CallExpression"&&E(e.callee,r)},C=(e)=>{if(e.type!=="JSXAttribute")return null;const{value:r}=e;return r?.type==="JSXExpressionContainer"?r.expression:null};export const preferCn=m("Require a dynamic class value to go through `cn(...)` before it reaches a `class`, `className` or `*ClassName` prop. Reports both where the value reaches the prop and where a variable named after classes is declared.",{createOnce(e){const r=new Set,s=new Map,o=[],u=()=>{const t=new Set;for(const[n,i]of s){if(!S.test(n))continue;if(!p(i,r))continue;t.add(n);e.report({node:i.parent??i,message:a.declaration})}return t},l=(t)=>{for(const n of o){const i=C(n);if(!i)continue;if(p(i,r)){e.report({node:n,message:a.attribute});continue}if(i.type!=="Identifier"||t.has(i.name))continue;if(!p(s.get(i.name),r))continue;e.report({node:n,message:a.precomputed})}};return{before(){r.clear();s.clear();o.length=0;return y.test(e.sourceCode.text)},ImportDeclaration(t){for(const n of b)for(const{local:i}of h(t,n))if(!c.has(i))r.add(i)},VariableDeclarator(t){if(t.id.type==="Identifier"&&t.init)s.set(t.id.name,t.init)},JSXAttribute(t){if(d.test(f(t)))o.push(t)},"Program:exit"(){l(u())}}}});
1
+ import{attributeName as f,importedNames as m,problem as h}from"../../../lib/ast.js";import{CLASS_ATTRIBUTE as d,CLASS_BINDING as S,CLASS_MARKER as y,COMPOSER_MODULES as b,KNOWN_COMPOSERS as c}from"./shared.js";const g=new Set(["join","concat"]),E=new Set(["ConditionalExpression","LogicalExpression"]),a={attribute:"Wrap this class value in `cn(...)`, so a later Tailwind utility wins over an earlier one setting the same property.",precomputed:"Wrap this value in `cn(...)` where it reaches the class prop, or build it with `cn(...)` where it is declared.",declaration:"Build this class value with `cn(...)` instead of joining it by hand, so conflicting utilities resolve and falsy branches drop out."},w=(e,r)=>{if(e.type==="MemberExpression")return e.property.type==="Identifier"&&g.has(e.property.name);if(e.type!=="Identifier")return!1;return c.has(e.name)?e.name!=="cn":r.has(e.name)},p=(e,r)=>{if(!e)return!1;if(E.has(e.type))return!0;if(e.type==="TemplateLiteral")return e.expressions.length>0;if(e.type==="BinaryExpression")return e.operator==="+";return e.type==="CallExpression"&&w(e.callee,r)},C=(e)=>{if(e.type!=="JSXAttribute")return null;const{value:r}=e;return r?.type==="JSXExpressionContainer"?r.expression:null};export const preferCn=h("Require a dynamic class value to go through `cn(...)` before it reaches a `class`, `className` or `*ClassName` prop. Reports both where the value reaches the prop and where a variable named after classes is declared.",{createOnce(e){const r=new Set,s=new Map,o=[],u=()=>{const t=new Set;for(const[n,i]of s){if(!S.test(n))continue;if(!p(i,r))continue;t.add(n);e.report({node:i.parent??i,message:a.declaration})}return t},l=(t)=>{for(const n of o){const i=C(n);if(!i)continue;if(p(i,r)){e.report({node:n,message:a.attribute});continue}if(i.type!=="Identifier"||t.has(i.name))continue;if(!p(s.get(i.name),r))continue;e.report({node:n,message:a.precomputed})}};return{before(){r.clear();s.clear();o.length=0;return y.test(e.sourceCode.text)},ImportDeclaration(t){for(const n of b)for(const{local:i}of m(t,n))if(!c.has(i))r.add(i)},VariableDeclarator(t){if(t.id.type==="Identifier"&&t.init)s.set(t.id.name,t.init)},JSXAttribute(t){if(d.test(f(t)))o.push(t)},"Program:exit"(){l(u())}}}});
@@ -1 +1 @@
1
- import{attributeName as l,hasAncestor as o,problem as n}from"../../../lib/ast.js";import{CLASS_ATTRIBUTE as d,CLASS_BINDING as f}from"./shared.js";const u="ml|mr|pl|pr|left|right|scroll-ml|scroll-mr|scroll-pl|scroll-pr",c="border-l|border-r|rounded-l|rounded-r|rounded-tl|rounded-tr|rounded-bl|rounded-br",m="text-left|text-right|float-left|float-right|clear-left|clear-right",p=String.raw`(?:(?:${u})-\S+|(?:${c})(?:-\S+)?|${m})`,s=String.raw`[\s"'\`{}]`,r=new RegExp(String.raw`(?:^|${s})(?:[^\s:]+:)*!?-?${p}(?:$|${s})`),a="Swap this for its logical twin so the layout mirrors in Arabic and Hebrew: `ms`/`me` instead of `ml`/`mr`, `text-start` instead of `text-left`. Padding, insets, borders and corners follow the same pattern. Keep the physical utility only where the coordinate really is the screen rather than the reading order.",i=(t)=>o(t,(e)=>{if(e.type==="JSXAttribute")return d.test(l(e));if(e.type!=="VariableDeclarator")return!1;return e.id.type==="Identifier"&&f.test(e.id.name)});export const useLogicalClasses=n("Require a logical Tailwind utility over its physical left/right twin, so a right-to-left layout mirrors. Reads class props and bindings named after classes, through variant prefixes and the `!` and `-` modifiers.",{createOnce(t){return{before(){return r.test(t.sourceCode.text)},Literal(e){if(typeof e.value!=="string")return;if(!r.test(e.value))return;if(!i(e))return;t.report({node:e,message:a})},TemplateElement(e){if(!r.test(e.value.raw))return;if(!i(e))return;t.report({node:e,message:a})}}}});
1
+ import{attributeName as o,hasAncestor as a,problem as n}from"../../../lib/ast.js";import{CLASS_ATTRIBUTE as f,CLASS_BINDING as u}from"./shared.js";const d="ml|mr|pl|pr|left|right|scroll-ml|scroll-mr|scroll-pl|scroll-pr",m="border-l|border-r|rounded-l|rounded-r|rounded-tl|rounded-tr|rounded-bl|rounded-br",c="text-left|text-right|float-left|float-right|clear-left|clear-right",p=String.raw`(?:(?:${d})-\S+|(?:${m})(?:-\S+)?|${c})`,s=String.raw`[\s"'\`{}]`,e=new RegExp(String.raw`(?:^|${s})(?:[^\s:]+:)*!?-?${p}(?:$|${s})`),l="Swap this for its logical twin so the layout mirrors in RTL: `ms`/`me` for `ml`/`mr`, `text-start` for `text-left`. Insets, borders and corners follow.",i=(t)=>a(t,(r)=>{if(r.type==="JSXAttribute")return f.test(o(r));if(r.type!=="VariableDeclarator")return!1;return r.id.type==="Identifier"&&u.test(r.id.name)});export const useLogicalClasses=n("Require a logical Tailwind utility over its physical left/right twin, so a right-to-left layout mirrors. Reads class props and bindings named after classes, through variant prefixes and the `!` and `-` modifiers.",{createOnce(t){return{before(){return e.test(t.sourceCode.text)},Literal(r){if(typeof r.value!=="string")return;if(!e.test(r.value))return;if(!i(r))return;t.report({node:r,message:l})},TemplateElement(r){if(!e.test(r.value.raw))return;if(!i(r))return;t.report({node:r,message:l})}}}});
@@ -1 +1 @@
1
- import{gate as u,importedNames as m,problem as f}from"../../../lib/ast.js";import{ROUTER_MODULE as h}from"./shared.js";const d=["search","location","state"],l={search:"Drop this assertion and let the route's `validateSearch` schema type the result. An `as` here claims a shape the URL never has to honour; widen the schema, or parse the value, if the type is wrong.",routerState:"Drop this assertion and read validated search through `useSearch({ from: ... })`. `router.state.location.search` is whatever the URL held, so an `as` here is a claim about a string nobody checked."},S=(t)=>{let e=t;while(e.type==="TSAsExpression")e=e.expression;return e},c=(t)=>t?.type==="CallExpression"&&t.callee.type==="Identifier"?t.callee.name:"",y=(t)=>{if(t.type!=="MemberExpression")return"";const{property:e}=t;if(e.type==="Identifier")return t.computed?"":e.name;return e.type==="Literal"?String(e.value):""},g=(t)=>{let e=t;for(const s of d){if(e.type!=="MemberExpression"||y(e)!==s)return null;e=e.object}return e};export const noSearchCasts=f("Disallow an `as` assertion on a `useSearch()` result or on `router.state.location.search`. The route's `validateSearch` schema is what supplies the type.",{createOnce(t){const e=new Set,s=new Set,n=new Set,i=[],p=(r)=>r.type==="Identifier"?n.has(r.name):s.has(c(r));return{before(){e.clear();s.clear();n.clear();i.length=0;return u(t,h)},ImportDeclaration(r){for(const{imported:a,local:o}of m(r,h))if(a==="useSearch")e.add(o);else if(a==="useRouter")s.add(o)},VariableDeclarator(r){if(r.id.type!=="Identifier")return;if(s.has(c(r.init)))n.add(r.id.name)},TSAsExpression(r){if(r.parent.type==="TSAsExpression")return;i.push({node:r,inner:S(r.expression)})},"Program:exit"(){for(const{node:r,inner:a}of i){if(e.has(c(a))){t.report({node:r,message:l.search});continue}const o=g(a);if(o&&p(o))t.report({node:r,message:l.routerState})}}}}});
1
+ import{gate as u,importedNames as m,problem as f}from"../../../lib/ast.js";import{ROUTER_MODULE as p}from"./shared.js";const S=["search","location","state"],l={search:"Drop this assertion and let the route's `validateSearch` schema type the result; widen the schema, or parse the value, if the type is wrong.",routerState:"Drop this assertion and read validated search through `useSearch({ from: ... })` — `router.state.location.search` is whatever the URL held."},d=(t)=>{let e=t;while(e.type==="TSAsExpression")e=e.expression;return e},c=(t)=>t?.type==="CallExpression"&&t.callee.type==="Identifier"?t.callee.name:"",y=(t)=>{if(t.type!=="MemberExpression")return"";const{property:e}=t;if(e.type==="Identifier")return t.computed?"":e.name;return e.type==="Literal"?String(e.value):""},g=(t)=>{let e=t;for(const s of S){if(e.type!=="MemberExpression"||y(e)!==s)return null;e=e.object}return e};export const noSearchCasts=f("Disallow an `as` assertion on a `useSearch()` result or on `router.state.location.search`. The route's `validateSearch` schema is what supplies the type.",{createOnce(t){const e=new Set,s=new Set,o=new Set,i=[],h=(r)=>r.type==="Identifier"?o.has(r.name):s.has(c(r));return{before(){e.clear();s.clear();o.clear();i.length=0;return u(t,p)},ImportDeclaration(r){for(const{imported:a,local:n}of m(r,p))if(a==="useSearch")e.add(n);else if(a==="useRouter")s.add(n)},VariableDeclarator(r){if(r.id.type!=="Identifier")return;if(s.has(c(r.init)))o.add(r.id.name)},TSAsExpression(r){if(r.parent.type==="TSAsExpression")return;i.push({node:r,inner:d(r.expression)})},"Program:exit"(){for(const{node:r,inner:a}of i){if(e.has(c(a))){t.report({node:r,message:l.search});continue}const n=g(a);if(n&&h(n))t.report({node:r,message:l.routerState})}}}}});
@@ -1 +1 @@
1
- import{gate as h,importedNames as m,problem as p,propertyValue as i}from"../../../lib/ast.js";import{ROUTER_MODULE as l}from"./shared.js";const d=new Set(["useLocation","useRouterState"]),c={whole:"This hook subscribes to the whole router value, so the component re-renders on every navigation. Pass a `select` that returns what it reads, e.g. `{ select: location => location.pathname }`.",noSelect:"This hook subscribes to the whole router value, so the component re-renders on every navigation. Add a `select` returning the smallest value it reads.",search:"`useSearch({ strict: false })` subscribes to every route-search change. Add a `select` returning the keys this component reads."},u=(t)=>i(t,"select")!==null,f=(t)=>{const s=i(t,"strict");return s?.type==="Literal"&&s.value===!1};export const requireSelector=p("Require a `select` on `useLocation`, `useRouterState` and non-strict `useSearch`, so a component reads the smallest router value it needs instead of re-rendering on every navigation.",{createOnce(t){const s=new Set,a=new Set,n=[];return{before(){s.clear();a.clear();n.length=0;return h(t,l)},ImportDeclaration(e){for(const{imported:o,local:r}of m(e,l))if(d.has(o))s.add(r);else if(o==="useSearch")a.add(r)},CallExpression(e){if(e.callee.type==="Identifier")n.push({node:e,name:e.callee.name,options:e.arguments[0]})},"Program:exit"(){for(const{node:e,name:o,options:r}of n){if(s.has(o)){if(!r)t.report({node:e,message:c.whole});else if(!u(r))t.report({node:e,message:c.noSelect});continue}if(!a.has(o)||!f(r))continue;if(!u(r))t.report({node:e,message:c.search})}}}}});
1
+ import{gate as h,importedNames as m,problem as p,propertyValue as i}from"../../../lib/ast.js";import{ROUTER_MODULE as l}from"./shared.js";const d=new Set(["useLocation","useRouterState"]),c={whole:"Pass a `select` that returns what this component reads, e.g. `{ select: location => location.pathname }` — the whole router value re-renders on every navigation.",noSelect:"Add a `select` returning the smallest value this component reads — the whole router value re-renders on every navigation.",search:"Add a `select` returning the keys this component reads — `{ strict: false }` subscribes to every route-search change."},u=(t)=>i(t,"select")!==null,f=(t)=>{const s=i(t,"strict");return s?.type==="Literal"&&s.value===!1};export const requireSelector=p("Require a `select` on `useLocation`, `useRouterState` and non-strict `useSearch`, so a component reads the smallest router value it needs instead of re-rendering on every navigation.",{createOnce(t){const s=new Set,a=new Set,n=[];return{before(){s.clear();a.clear();n.length=0;return h(t,l)},ImportDeclaration(e){for(const{imported:o,local:r}of m(e,l))if(d.has(o))s.add(r);else if(o==="useSearch")a.add(r)},CallExpression(e){if(e.callee.type==="Identifier")n.push({node:e,name:e.callee.name,options:e.arguments[0]})},"Program:exit"(){for(const{node:e,name:o,options:r}of n){if(s.has(o)){if(!r)t.report({node:e,message:c.whole});else if(!u(r))t.report({node:e,message:c.noSelect});continue}if(!a.has(o)||!f(r))continue;if(!u(r))t.report({node:e,message:c.search})}}}}});
@@ -1 +1 @@
1
- import{problem as u}from"../../../lib/ast.js";const c={zustandBare:"Pass a selector to this store hook, for example `useSettingsStore(state => state.theme)`, so it re-renders only on that slice. Use `.getState()` instead for an imperative read.",zustandUndefined:"Replace `undefined` with a selector such as `state => state.theme` — it still subscribes to the whole store otherwise. Use `.getState()` instead for an imperative read."},f=/^(?:@\/stores\/|(?:\.\.?\/)+stores\/)[^"']*-store$/,i=/^use[A-Za-z0-9_$]*Store$/,d=(s)=>{if(s.type!=="ImportSpecifier")return null;const o=s.imported.type==="Identifier"?s.imported.name:"",t=s.local.name;return i.test(o)||i.test(t)?t:null};export const requireSelector=u("Require a selector on a store hook, rather than no arguments or `undefined` in its place.",{createOnce(s){const o=new Set,t=[];return{before(){o.clear();t.length=0},ImportDeclaration(e){if(!f.test(e.source.value))return;for(const n of e.specifiers){const r=d(n);if(r!==null)o.add(r)}},CallExpression(e){const{callee:n}=e;if(n.type!=="Identifier")return;const{name:r}=n;if(!o.has(r)&&!i.test(r))return;const a=e.arguments;if(a.length===0){t.push({node:e,name:r,bare:!0});return}const[l]=a;if(a.length===1&&l?.type==="Identifier"&&l.name==="undefined")t.push({node:e,name:r,bare:!1})},"Program:exit"(){for(const e of t){if(!o.has(e.name))continue;s.report({node:e.node,message:e.bare?c.zustandBare:c.zustandUndefined})}}}}});
1
+ import{problem as u}from"../../../lib/ast.js";const l={zustandBare:"Pass a selector to this store hook, e.g. `useSettingsStore(state => state.theme)`. Use `.getState()` for an imperative read.",zustandUndefined:"Replace `undefined` with a selector such as `state => state.theme`. Use `.getState()` for an imperative read."},f=/^(?:@\/stores\/|(?:\.\.?\/)+stores\/)[^"']*-store$/,i=/^use[A-Za-z0-9_$]*Store$/,d=(s)=>{if(s.type!=="ImportSpecifier")return null;const n=s.imported.type==="Identifier"?s.imported.name:"",t=s.local.name;return i.test(n)||i.test(t)?t:null};export const requireSelector=u("Require a selector on a store hook, rather than no arguments or `undefined` in its place.",{createOnce(s){const n=new Set,t=[];return{before(){n.clear();t.length=0},ImportDeclaration(e){if(!f.test(e.source.value))return;for(const o of e.specifiers){const r=d(o);if(r!==null)n.add(r)}},CallExpression(e){const{callee:o}=e;if(o.type!=="Identifier")return;const{name:r}=o;if(!n.has(r)&&!i.test(r))return;const a=e.arguments;if(a.length===0){t.push({node:e,name:r,bare:!0});return}const[c]=a;if(a.length===1&&c?.type==="Identifier"&&c.name==="undefined")t.push({node:e,name:r,bare:!1})},"Program:exit"(){for(const e of t){if(!n.has(e.name))continue;s.report({node:e.node,message:e.bare?l.zustandBare:l.zustandUndefined})}}}}});
@@ -1 +1 @@
1
- import{TEST_FILES as s}from"../core/index.js";import{mergeConfigs as r}from"../lib/merge.js";import{composeModules as a}from"../lib/module.js";import{coreRegistry as o,reactNativeRegistry as i,reactRegistry as n}from"../lib/registry.js";import c from"../react/index.js";export const banGroups=[{packages:["react-native-gesture-handler"],restrictedImports:{paths:[{name:"react-native",importNames:["Pressable","TouchableOpacity","TouchableHighlight","TouchableWithoutFeedback","TouchableNativeFeedback"],message:"Import `Pressable` from `react-native-gesture-handler` so it cooperates with the gesture system instead of competing with it. Exception: inside a SwiftUI/Compose host view touches never reach `GestureHandlerRootView` — keep React Native's `Pressable` there and suppress this rule with the host named in the reason."}]}},{packages:["expo-router"],restrictedImports:{patterns:[{group:["@react-navigation/stack","@react-navigation/bottom-tabs"],message:"Use expo-router's `Stack` / `NativeTabs` instead of a raw react-navigation navigator."}]}},{packages:["expo-font"],restrictedImports:{paths:[{name:"expo-font",importNames:["useFonts","loadAsync"],message:"Declare the fonts in the `expo-font` config plugin so they load at build time, instead of `useFonts`/`loadAsync` at runtime."}]}},{packages:["react-native-quick-crypto"],restrictedImports:{paths:[{name:"crypto-js",message:"Use `react-native-quick-crypto` instead — `crypto-js` is pure JavaScript and runs orders of magnitude slower."}]}}];const p={"eslint/no-empty":"error","eslint/no-empty-function":"error"},m={"@ashstack/effects/no-event-handler":"off"},reactNative=(t={})=>{const e=a([...o,...n,...i],t,banGroups);return r(c(t),{jsPlugins:e.jsPlugins,globals:{__DEV__:"readonly"},rules:{...p,...m,"no-restricted-imports":["error",e.restricted],...e.rules},ignorePatterns:["**/.expo/**","**/android/**","**/ios/**"],overrides:[{files:s,rules:{"@ashstack/react-native/no-dynamic-import":"off","@ashstack/react-native/hoist-stateless-function":"off"}}]})};export{reactNative};export default reactNative;
1
+ import{TEST_FILES as a}from"../core/index.js";import{mergeConfigs as r}from"../lib/merge.js";import{composeModules as s}from"../lib/module.js";import{coreRegistry as o,reactNativeRegistry as i,reactRegistry as n}from"../lib/registry.js";import c from"../react/index.js";export const banGroups=[{packages:["react-native-gesture-handler"],restrictedImports:{paths:[{name:"react-native",importNames:["Pressable","TouchableOpacity","TouchableHighlight","TouchableWithoutFeedback","TouchableNativeFeedback"],message:"Import `Pressable` from `react-native-gesture-handler`. Inside a SwiftUI/Compose host view, keep React Native's `Pressable` and suppress this rule naming the host."}]}},{packages:["expo-router"],restrictedImports:{patterns:[{group:["@react-navigation/stack","@react-navigation/bottom-tabs"],message:"Use expo-router's `Stack` / `NativeTabs` instead of a raw react-navigation navigator."}]}},{packages:["expo-font"],restrictedImports:{paths:[{name:"expo-font",importNames:["useFonts","loadAsync"],message:"Declare the fonts in the `expo-font` config plugin so they load at build time, instead of `useFonts`/`loadAsync` at runtime."}]}},{packages:["react-native-quick-crypto"],restrictedImports:{paths:[{name:"crypto-js",message:"Use `react-native-quick-crypto` instead — `crypto-js` is pure JavaScript and runs orders of magnitude slower."}]}}];const p={"eslint/no-empty":"error","eslint/no-empty-function":"error"},m={"@ashstack/effects/no-event-handler":"off"},l=(e)=>e?{}:{"@ashstack/react-native/no-manual-memo":"off"},reactNative=(e={})=>{const t=s([...o,...n,...i],e,banGroups);return r(c(e),{jsPlugins:t.jsPlugins,globals:{__DEV__:"readonly"},rules:{...p,...m,"no-restricted-imports":["error",t.restricted],...t.rules,...l(e.reactCompiler??!0)},ignorePatterns:["**/.expo/**","**/android/**","**/ios/**"],overrides:[{files:a,rules:{"@ashstack/react-native/no-dynamic-import":"off","@ashstack/react-native/hoist-stateless-function":"off"}}]})};export{reactNative};export default reactNative;
@@ -1 +1 @@
1
- import{defineModule as e}from"../../../lib/module.js";export default e({meta:{name:"@ashstack/keyboard"},url:import.meta.url,packages:["react-native-keyboard-controller"],option:"keyboard",docsWhen:"auto-enabled when `react-native-keyboard-controller` is a dependency",rules:{},restrictedImports:{paths:[{name:"react-native",importNames:["KeyboardAvoidingView"],message:"Import `KeyboardAvoidingView` from `react-native-keyboard-controller` — same props, and it tracks the keyboard per frame instead of waiting for Android's post-animation `keyboardDidShow`. For a scrolling form use its `KeyboardAwareScrollView`; to just shift a view, use `rt.insets.ime`."}]}});
1
+ import{defineModule as e}from"../../../lib/module.js";export default e({meta:{name:"@ashstack/keyboard"},url:import.meta.url,packages:["react-native-keyboard-controller"],option:"keyboard",docsWhen:"auto-enabled when `react-native-keyboard-controller` is a dependency",rules:{},restrictedImports:{paths:[{name:"react-native",importNames:["KeyboardAvoidingView"],message:"Import `KeyboardAvoidingView` from `react-native-keyboard-controller` — same props, tracked per frame. For a scrolling form, its `KeyboardAwareScrollView`."}]}});
@@ -1 +1 @@
1
- import{attributeName as u,gate as m,problem as h}from"../../../lib/ast.js";import{isStyleSheetCreate as x,propertyName as c,stylesObjectOf as b}from"../../stylesheet.js";import{expressionOf as g,isListElement as d}from"./shared.js";const S="Move `flex` onto `style` and keep `contentContainerStyle` for padding inside the content. On the content container it sizes the content to the viewport, so the list measures as zero height and renders a blank screen with no error.",f=(e)=>e?.type==="ObjectExpression"&&e.properties.some((t)=>t.type==="Property"&&!t.computed&&c(t)==="flex"),w=(e)=>{if(e?.type!=="MemberExpression"||e.object.type!=="Identifier")return null;const t=e.object.name;if(!e.computed&&e.property.type==="Identifier")return{sheet:t,key:e.property.name};if(e.computed&&e.property.type==="Literal"&&typeof e.property.value==="string")return{sheet:t,key:e.property.value};return null},C=(e)=>{if(!e)return[];if(e.type!=="ArrayExpression")return[e];return e.elements.filter((t)=>t!==null)};export const noFlexInContentContainer=h("Disallow `flex` in a Legend List's `contentContainerStyle`, where it sizes the scrolled content to the viewport and the list ends up measuring zero height.",{createOnce(e){let t=new Map,i=[];const y=(r)=>{if(f(r))return!0;const n=w(r);return n!==null&&(t.get(n.sheet)?.has(n.key)??!1)};return{before(){t=new Map;i=[];return m(e,"contentContainerStyle")},VariableDeclarator(r){const{id:n,init:o}=r;if(n.type!=="Identifier"||o?.type!=="CallExpression"||!x(o))return;const p=b(o.arguments[0]);if(!p)return;const a=new Set;for(const s of p.properties){if(s.type!=="Property"||s.computed)continue;const l=c(s);if(l!==""&&f(s.value))a.add(l)}t.set(n.name,a)},JSXAttribute(r){if(u(r)!=="contentContainerStyle")return;if(!d(r.parent.parent))return;const n=C(g(r));if(n.length>0)i.push({node:r,parts:n})},"Program:exit"(){for(const{node:r,parts:n}of i)if(n.some((o)=>y(o)))e.report({node:r,message:S})}}}});
1
+ import{attributeName as u,gate as m,problem as h}from"../../../lib/ast.js";import{isStyleSheetCreate as x,propertyName as c,stylesObjectOf as b}from"../../stylesheet.js";import{expressionOf as g,isListElement as S}from"./shared.js";const d="Move `flex` onto `style` and keep `contentContainerStyle` for padding — on the content container the list measures as zero height and renders blank.",f=(e)=>e?.type==="ObjectExpression"&&e.properties.some((t)=>t.type==="Property"&&!t.computed&&c(t)==="flex"),C=(e)=>{if(e?.type!=="MemberExpression"||e.object.type!=="Identifier")return null;const t=e.object.name;if(!e.computed&&e.property.type==="Identifier")return{sheet:t,key:e.property.name};if(e.computed&&e.property.type==="Literal"&&typeof e.property.value==="string")return{sheet:t,key:e.property.value};return null},E=(e)=>{if(!e)return[];if(e.type!=="ArrayExpression")return[e];return e.elements.filter((t)=>t!==null)};export const noFlexInContentContainer=h("Disallow `flex` in a Legend List's `contentContainerStyle`, where it sizes the scrolled content to the viewport and the list ends up measuring zero height.",{createOnce(e){let t=new Map,i=[];const y=(r)=>{if(f(r))return!0;const n=C(r);return n!==null&&(t.get(n.sheet)?.has(n.key)??!1)};return{before(){t=new Map;i=[];return m(e,"contentContainerStyle")},VariableDeclarator(r){const{id:n,init:s}=r;if(n.type!=="Identifier"||s?.type!=="CallExpression"||!x(s))return;const p=b(s.arguments[0]);if(!p)return;const a=new Set;for(const o of p.properties){if(o.type!=="Property"||o.computed)continue;const l=c(o);if(l!==""&&f(o.value))a.add(l)}t.set(n.name,a)},JSXAttribute(r){if(u(r)!=="contentContainerStyle")return;if(!S(r.parent.parent))return;const n=E(g(r));if(n.length>0)i.push({node:r,parts:n})},"Program:exit"(){for(const{node:r,parts:n}of i)if(n.some((s)=>y(s)))e.report({node:r,message:d})}}}});
@@ -1 +1 @@
1
- import{attributeName as s,gate as i,problem as o,subtreeHas as p}from"../../../lib/ast.js";import{expressionOf as m,isListElement as f}from"./shared.js";export const noIndexKeyExtractor=o("Disallow a `keyExtractor` that uses its index parameter. Cached sizes and recycled row state hang off the key, so a prepend points every measurement at the wrong item.",{createOnce:(r)=>({before:()=>i(r,"keyExtractor"),JSXAttribute(t){if(s(t)!=="keyExtractor")return;if(!f(t.parent.parent))return;const e=m(t);if(e?.type!=="ArrowFunctionExpression"&&e?.type!=="FunctionExpression")return;const n=e.params[1];if(n?.type!=="Identifier")return;if(!p(e.body,(a)=>a.type==="Identifier"&&a.name===n.name))return;r.report({node:e,message:"Return a stable per-item id from this `keyExtractor` instead of the index. Legend List hangs cached sizes and recycled row state off the key, so one prepend re-points every measurement at the wrong item."})}})});
1
+ import{attributeName as i,gate as s,problem as o,subtreeHas as p}from"../../../lib/ast.js";import{expressionOf as f,isListElement as m}from"./shared.js";export const noIndexKeyExtractor=o("Disallow a `keyExtractor` that uses its index parameter. Cached sizes and recycled row state hang off the key, so a prepend points every measurement at the wrong item.",{createOnce:(r)=>({before:()=>s(r,"keyExtractor"),JSXAttribute(t){if(i(t)!=="keyExtractor")return;if(!m(t.parent.parent))return;const e=f(t);if(e?.type!=="ArrowFunctionExpression"&&e?.type!=="FunctionExpression")return;const a=e.params[1];if(a?.type!=="Identifier")return;if(!p(e.body,(n)=>n.type==="Identifier"&&n.name===a.name))return;r.report({node:e,message:"Return a stable per-item id from this `keyExtractor` instead of the index — Legend List hangs cached sizes and recycled row state off the key."})}})});
@@ -1 +1 @@
1
- import{attributeName as s,gate as o,problem as a}from"../../../lib/ast.js";import{literalKind as d}from"./shared.js";export const noInlineRenderItemProps=a("Disallow inline object and array literals on props nested inside `renderItem`. A row whose props take a new identity every render can never be skipped.",{createOnce(n){let e=0;return{before(){e=0;return o(n,"renderItem")},JSXAttribute(r){if(s(r)==="renderItem"){e++;return}if(e===0)return;const{value:t}=r;if(t?.type!=="JSXExpressionContainer")return;const i=d(t.expression);if(i===null)return;n.report({node:t,message:`Pass \`item\` or its primitive fields and build this ${i} inside the row, or hoist it to module scope when it is static. A fresh ${i} each render means the row can never be skipped, so typing anywhere re-renders every visible row.`})},"JSXAttribute:exit"(r){if(s(r)==="renderItem")e--}}}});
1
+ import{attributeName as o,gate as s,problem as a}from"../../../lib/ast.js";import{literalKind as d}from"./shared.js";export const noInlineRenderItemProps=a("Disallow inline object and array literals on props nested inside `renderItem`. A row whose props take a new identity every render can never be skipped.",{createOnce(n){let e=0;return{before(){e=0;return s(n,"renderItem")},JSXAttribute(r){if(o(r)==="renderItem"){e++;return}if(e===0)return;const{value:t}=r;if(t?.type!=="JSXExpressionContainer")return;const i=d(t.expression);if(i===null)return;n.report({node:t,message:`Pass \`item\` or its primitive fields and build this ${i} inside the row, or hoist it to module scope when static — a fresh ${i} each render blocks row skipping.`})},"JSXAttribute:exit"(r){if(o(r)==="renderItem")e--}}}});
@@ -1 +1 @@
1
- import{gate as r,problem as n}from"../../../lib/ast.js";import{attributeNamed as i,isListElement as o,LIST as a}from"./shared.js";const s=(t)=>t.type==="JSXElement"&&t.children.some((e)=>e.type==="JSXElement"||e.type==="JSXFragment"||e.type==="JSXText"&&e.value.trim()!==""||e.type==="JSXExpressionContainer"&&e.expression.type!=="JSXEmptyExpression");export const noMixedChildren=n("Disallow passing both `data` and real children to a Legend List. The combination is unsupported and one of the two is dropped without a warning.",{createOnce:(t)=>({before:()=>r(t,a),JSXElement(e){if(!o(e))return;if(!i(e,"data"))return;if(!s(e))return;t.report({node:e.openingElement,message:"Keep either `data` with `renderItem` or children mode here, and remove the other. Passing both fails silently, with no guarantee about which one is ignored."})}})});
1
+ import{gate as r,problem as n}from"../../../lib/ast.js";import{attributeNamed as i,isListElement as o,LIST as a}from"./shared.js";const s=(t)=>t.type==="JSXElement"&&t.children.some((e)=>e.type==="JSXElement"||e.type==="JSXFragment"||e.type==="JSXText"&&e.value.trim()!==""||e.type==="JSXExpressionContainer"&&e.expression.type!=="JSXEmptyExpression");export const noMixedChildren=n("Disallow passing both `data` and real children to a Legend List. The combination is unsupported and one of the two is dropped without a warning.",{createOnce:(t)=>({before:()=>r(t,a),JSXElement(e){if(!o(e))return;if(!i(e,"data"))return;if(!s(e))return;t.report({node:e.openingElement,message:"Keep either `data` with `renderItem` or children mode here, and remove the other — passing both fails silently."})}})});
@@ -1 +1 @@
1
- import{gate as a,problem as i}from"../../../lib/ast.js";import{attributeNamed as r,isListElement as n,LIST as o}from"./shared.js";export const noRemountKey=i("Disallow `key` on a Legend List, which remounts on any key change and loses its measurements and scroll position. Pass `dataKey` instead.",{createOnce:(e)=>({before:()=>a(e,o),JSXElement(t){if(!n(t))return;const s=r(t,"key");if(!s)return;e.report({node:s,message:"Pass `dataKey` instead of `key` here: it re-initialises the list for a different dataset from the inside, without the remount that discards every measurement, cached size and scroll position."})}})});
1
+ import{gate as a,problem as i}from"../../../lib/ast.js";import{attributeNamed as n,isListElement as o,LIST as r}from"./shared.js";export const noRemountKey=i("Disallow `key` on a Legend List, which remounts on any key change and loses its measurements and scroll position. Pass `dataKey` instead.",{createOnce:(e)=>({before:()=>a(e,r),JSXElement(t){if(!o(t))return;const s=n(t,"key");if(!s)return;e.report({node:s,message:"Pass `dataKey` instead of `key` here — it re-initialises the list without the remount that discards every measurement and the scroll position."})}})});
@@ -1 +1 @@
1
- import{componentName as i,gate as l,problem as s}from"../../../lib/ast.js";export const noScrollviewMap=s("A `ScrollView` mounts every child up front, so a mapped collection does not belong in its children.",{createOnce:(n)=>({before:()=>l(n,"ScrollView"),JSXElement(t){if(i(t.openingElement.name)!=="ScrollView")return;for(const e of t.children){if(e.type!=="JSXExpressionContainer")continue;const r=e.expression;if(r.type!=="CallExpression")continue;const{callee:o}=r;if(o.type!=="MemberExpression")continue;if(o.property.type!=="Identifier"||o.property.name!=="map")continue;n.report({node:e,message:"Render this collection with `LegendList` from '@legendapp/list/react-native', passing the array as `data` and the mapped body as `renderItem`. A ScrollView mounts every child up front, so 200 rows cost 200 mounts to show ten."});return}}})});
1
+ import{componentName as i,gate as l,problem as p}from"../../../lib/ast.js";export const noScrollviewMap=p("A `ScrollView` mounts every child up front, so a mapped collection does not belong in its children.",{createOnce:(o)=>({before:()=>l(o,"ScrollView"),JSXElement(r){if(i(r.openingElement.name)!=="ScrollView")return;for(const e of r.children){if(e.type!=="JSXExpressionContainer")continue;const t=e.expression;if(t.type!=="CallExpression")continue;const{callee:n}=t;if(n.type!=="MemberExpression")continue;if(n.property.type!=="Identifier"||n.property.name!=="map")continue;o.report({node:e,message:"Render this collection with `LegendList` from '@legendapp/list/react-native', passing the array as `data` and the mapped body as `renderItem` — a ScrollView mounts every child up front."});return}}})});
@@ -1 +1 @@
1
- import{attributeName as i,gate as o,problem as r}from"../../../lib/ast.js";import{attributesOf as a,isListElement as s,LIST as l}from"./shared.js";const m=new Set(["masonry","optimizeItemArrangement","inverted","onBlankArea","disableAutoLayout","CellRendererComponent"]);export const noUnsupportedProps=r("Disallow FlashList and FlatList props that Legend List v3 does not have. It ignores them rather than rejecting them, so the feature looks broken.",{createOnce:(e)=>({before:()=>o(e,l),JSXElement(n){if(!s(n))return;for(const t of a(n)){if(t.type!=="JSXAttribute")continue;if(!m.has(i(t)))continue;e.report({node:t,message:"Remove this prop: Legend List v3 has no such prop and ignores it silently rather than rejecting it. Build inverted chat lists from `maintainScrollAtEnd` / `initialScrollAtEnd` / `maintainVisibleContentPosition` instead."})}}})});
1
+ import{attributeName as o,gate as i,problem as r}from"../../../lib/ast.js";import{attributesOf as s,isListElement as a,LIST as l}from"./shared.js";const m=new Set(["masonry","optimizeItemArrangement","inverted","onBlankArea","disableAutoLayout","CellRendererComponent"]);export const noUnsupportedProps=r("Disallow FlashList and FlatList props that Legend List v3 does not have. It ignores them rather than rejecting them, so the feature looks broken.",{createOnce:(e)=>({before:()=>i(e,l),JSXElement(n){if(!a(n))return;for(const t of s(n)){if(t.type!=="JSXAttribute")continue;if(!m.has(o(t)))continue;e.report({node:t,message:"Remove this prop — Legend List v3 ignores it silently. Inverted chat lists use `maintainScrollAtEnd` / `initialScrollAtEnd` / `maintainVisibleContentPosition`."})}}})});
@@ -1 +1 @@
1
- import{gate as i,problem as s}from"../../../lib/ast.js";import{attributeNamed as r,hasSpread as a,isListElement as o,LIST as n}from"./shared.js";export const requiredProps=s("Require `keyExtractor` and an explicit `recycleItems` on a Legend List. Without a key extractor the list keys its rows by index.",{createOnce:(t)=>({before:()=>i(t,n),JSXElement(e){if(!o(e)||a(e))return;if(!r(e,"keyExtractor"))t.report({node:e.openingElement,message:"Add `keyExtractor={item => item.id}` returning a stable per-item identity. The index fallback pins cached measurements and recycled row state to a position, so a prepend leaves rows showing the previous row's data."});if(!r(e,"recycleItems"))t.report({node:e.openingElement,message:"Add `recycleItems={true}` — recycling is where most of the list's native speed comes from. If a row genuinely cannot be recycled, pass `recycleItems={false}` and state which part of the row required it."})}})});
1
+ import{gate as i,problem as s}from"../../../lib/ast.js";import{attributeNamed as r,hasSpread as a,isListElement as o,LIST as n}from"./shared.js";export const requiredProps=s("Require `keyExtractor` and an explicit `recycleItems` on a Legend List. Without a key extractor the list keys its rows by index.",{createOnce:(t)=>({before:()=>i(t,n),JSXElement(e){if(!o(e)||a(e))return;if(!r(e,"keyExtractor"))t.report({node:e.openingElement,message:"Add `keyExtractor={item => item.id}` returning a stable per-item identity — the index fallback pins cached measurements to a position, so a prepend shows the wrong data."});if(!r(e,"recycleItems"))t.report({node:e.openingElement,message:"Add `recycleItems={true}` — recycling is where most of the list's native speed comes from. If a row genuinely cannot recycle, pass `recycleItems={false}` with the reason."})}})});
@@ -1 +1 @@
1
- import{gate as p,problem as m,subtreeHas as y}from"../../../lib/ast.js";import{attributeNamed as a,expressionOf as d,hasSpread as l,isListElement as c,LIST as f}from"./shared.js";const u=(i)=>y(i,(t)=>t.type==="MemberExpression"&&t.property.type==="Identifier"&&t.property.name==="type");export const typedItemsNeedItemType=m("Require `getItemType` when a row branches on `item.type`. Without it every layout shares one recycling pool and one size average.",{createOnce(i){let t,o;return{before(){t=new Map;o=[];return p(i,f)},FunctionDeclaration(e){if(e.id?.type==="Identifier")t.set(e.id.name,e)},VariableDeclarator(e){const{id:r,init:n}=e;if(r.type!=="Identifier"||n===null)return;t.set(r.name,n)},JSXElement(e){if(!c(e)||l(e))return;if(a(e,"getItemType"))return;const r=a(e,"renderItem");if(!r)return;o.push({node:e,renderItem:r})},"Program:exit":()=>{for(const{node:e,renderItem:r}of o){const n=d(r),s=n?.type==="Identifier"?t.get(n.name):n;if(s===void 0||s===null||!u(s))continue;i.report({node:e.openingElement,message:"Add `getItemType={item => item.type}` to match the branch this row makes on `item.type`. It gives each layout its own recycling pool and its own measured-size average, instead of handing a header's view to a photo row."})}}}}});
1
+ import{gate as p,problem as m,subtreeHas as y}from"../../../lib/ast.js";import{attributeNamed as a,expressionOf as l,hasSpread as d,isListElement as c,LIST as f}from"./shared.js";const u=(i)=>y(i,(t)=>t.type==="MemberExpression"&&t.property.type==="Identifier"&&t.property.name==="type");export const typedItemsNeedItemType=m("Require `getItemType` when a row branches on `item.type`. Without it every layout shares one recycling pool and one size average.",{createOnce(i){let t,o;return{before(){t=new Map;o=[];return p(i,f)},FunctionDeclaration(e){if(e.id?.type==="Identifier")t.set(e.id.name,e)},VariableDeclarator(e){const{id:r,init:n}=e;if(r.type!=="Identifier"||n===null)return;t.set(r.name,n)},JSXElement(e){if(!c(e)||d(e))return;if(a(e,"getItemType"))return;const r=a(e,"renderItem");if(!r)return;o.push({node:e,renderItem:r})},"Program:exit":()=>{for(const{node:e,renderItem:r}of o){const n=l(r),s=n?.type==="Identifier"?t.get(n.name):n;if(s===void 0||s===null||!u(s))continue;i.report({node:e.openingElement,message:"Add `getItemType={item => item.type}` to match the branch this row makes, so each layout gets its own recycling pool and measured-size average."})}}}}});
@@ -1 +1 @@
1
- import{gate as i,problem as l}from"../../../lib/ast.js";import{factoryCalled as s,OBS as n}from"./shared.js";export const naming=l("A variable initialized from `observable()` or `useObservable()` needs a trailing `$`. The other rules in this module key off that suffix.",{createOnce(a){return{before(){return i(a,"observable","useObservable")},VariableDeclarator(o){const{id:e,init:t}=o;if(!s(t)||e.type!=="Identifier")return;const{name:r}=e;if(n.test(r))return;a.report({node:e,message:r.includes("$")?"Give this observable a trailing `$` and no other. The suffix is how a reader tells an observable from a plain value, and every other observable rule keys off it.":`Rename this to \`${r}$\`. The trailing \`$\` is how a reader tells an observable from a plain value, and every other observable rule keys off it.`})}}}});
1
+ import{gate as o,problem as n}from"../../../lib/ast.js";import{factoryCalled as s,OBS as l}from"./shared.js";export const naming=n("A variable initialized from `observable()` or `useObservable()` needs a trailing `$`. The other rules in this module key off that suffix.",{createOnce(t){return{before(){return o(t,"observable","useObservable")},VariableDeclarator(a){const{id:e,init:i}=a;if(!s(i)||e.type!=="Identifier")return;const{name:r}=e;if(l.test(r))return;t.report({node:e,message:r.includes("$")?"Give this observable a single trailing `$` and no other.":`Rename this to \`${r}$\` — every observable rule keys off the trailing \`$\`.`})}}}});
@@ -1 +1 @@
1
- import{gate as a,problem as g}from"../../../lib/ast.js";import{isObservableRef as o,textOf as n}from"./shared.js";export const noAssignment=g("Write an observable with `.set(...)` or `.assign({...})`. Assigning or incrementing it with an operator is a silent no-op.",{createOnce(r){return{before(){return a(r,"$")},AssignmentExpression(e){const{left:s}=e;if(!o(s))return;const t=n(r,s);r.report({node:e,message:`Write through \`${t}.set(...)\`, or \`.assign({...})\` to merge several fields. Assigning to an observable with \`${e.operator}\` replaces the observable in place of its value, so nothing downstream ever updates.`})},UpdateExpression(e){const{argument:s,operator:t}=e;if(!o(s))return;const i=n(r,s);r.report({node:e,message:`Use \`${i}.set(v => v ${t[0]} 1)\` — \`${t}\` on an observable is a silent no-op.`})}}}});
1
+ import{gate as a,problem as g}from"../../../lib/ast.js";import{isObservableRef as o,textOf as n}from"./shared.js";export const noAssignment=g("Write an observable with `.set(...)` or `.assign({...})`. Assigning or incrementing it with an operator is a silent no-op.",{createOnce(r){return{before(){return a(r,"$")},AssignmentExpression(e){const{left:s}=e;if(!o(s))return;const t=n(r,s);r.report({node:e,message:`Write through \`${t}.set(...)\`, or \`.assign({...})\` to merge several fields — \`${e.operator}\` replaces the observable itself, so nothing downstream updates.`})},UpdateExpression(e){const{argument:s,operator:t}=e;if(!o(s))return;const i=n(r,s);r.report({node:e,message:`Use \`${i}.set(v => v ${t[0]} 1)\` — \`${t}\` on an observable is a silent no-op.`})}}}});
@@ -1 +1 @@
1
- import{gate as o,problem as t}from"../../../lib/ast.js";import{factoryCalled as n,isObservableRef as i}from"./shared.js";export const noNestedObservable=t("Never pass an observable to `observable()` or `useObservable()`. The wrapper is a second node, and reads and writes on it never reach the original.",{createOnce(r){return{before(){return o(r,"observable","useObservable")},CallExpression(s){const a=n(s);if(!a)return;const e=s.arguments[0];if(!e||!i(e))return;r.report({node:e,message:`Use the existing observable reference directly instead of passing it to \`${a}()\`; wrapping it creates a second node whose reads and writes never reach the original.`})}}}});
1
+ import{gate as o,problem as t}from"../../../lib/ast.js";import{factoryCalled as n,isObservableRef as i}from"./shared.js";export const noNestedObservable=t("Never pass an observable to `observable()` or `useObservable()`. The wrapper is a second node, and reads and writes on it never reach the original.",{createOnce(r){return{before(){return o(r,"observable","useObservable")},CallExpression(s){const a=n(s);if(!a)return;const e=s.arguments[0];if(!e||!i(e))return;r.report({node:e,message:`Use the existing observable reference directly instead of passing it to \`${a}()\` — wrapping it creates a second node the original never sees.`})}}}});
@@ -1 +1 @@
1
- import{FUNCTION_TYPES as s,crossesFunctionBefore as i,findInSubtree as l,gate as a,problem as c}from"../../../lib/ast.js";const p=(e)=>{if(e.type==="ObjectExpression")return"object";if(e.type==="ArrayExpression")return"array";return null},y=(e)=>{const{body:r}=e.type==="ArrowFunctionExpression"||e.type==="FunctionExpression"?e:{body:null};if(r===null)return null;if(r.type!=="BlockStatement")return r;const t=l(r,(n)=>n.type==="ReturnStatement");if(t?.type!=="ReturnStatement"||i(t,r,s))return null;return t.argument};export const noObjectSelector=c("A `useValue` selector that builds a new object or array returns a fresh identity every run. The component then re-renders on every store change.",{createOnce(e){return{before(){return a(e,"useValue")},CallExpression(r){const{callee:t}=r;if(t.type!=="Identifier"||t.name!=="useValue")return;const n=r.arguments[0];if(n?.type!=="ArrowFunctionExpression"&&n?.type!=="FunctionExpression")return;const o=y(n);if(o===null)return;const u=p(o);if(u===null)return;e.report({node:o,message:`Return the primitive that decides the render from this selector, or call \`useValue\` once per field. A new ${u} each run has a new identity, so the component re-renders on every store change.`})}}}});
1
+ import{FUNCTION_TYPES as s,crossesFunctionBefore as i,findInSubtree as l,gate as a,problem as c}from"../../../lib/ast.js";const p=(e)=>{if(e.type==="ObjectExpression")return"object";if(e.type==="ArrayExpression")return"array";return null},y=(e)=>{const{body:r}=e.type==="ArrowFunctionExpression"||e.type==="FunctionExpression"?e:{body:null};if(r===null)return null;if(r.type!=="BlockStatement")return r;const t=l(r,(n)=>n.type==="ReturnStatement");if(t?.type!=="ReturnStatement"||i(t,r,s))return null;return t.argument};export const noObjectSelector=c("A `useValue` selector that builds a new object or array returns a fresh identity every run. The component then re-renders on every store change.",{createOnce(e){return{before(){return a(e,"useValue")},CallExpression(r){const{callee:t}=r;if(t.type!=="Identifier"||t.name!=="useValue")return;const n=r.arguments[0];if(n?.type!=="ArrowFunctionExpression"&&n?.type!=="FunctionExpression")return;const o=y(n);if(o===null)return;const u=p(o);if(u===null)return;e.report({node:o,message:`Return the primitive that decides the render from this selector, or call \`useValue\` once per field — a new ${u} each run re-renders on every store change.`})}}}});
@@ -1 +1 @@
1
- import{COMPONENT_OR_HOOK as s,gate as b,isFunction as l,problem as c}from"../../../lib/ast.js";export const noObservableInComponent=c("Calling `observable()` inside a component or hook makes a new observable on every render. Use `useObservable()` or a module-level store instead.",{createOnce(n){let r=0;const i=(e)=>{if(e!==void 0&&s.test(e))r++},o=(e)=>{if(e!==void 0&&s.test(e))r--},t=(e)=>e?.type==="Identifier"?e.name:void 0;return{before(){r=0;return b(n,"observable")},FunctionDeclaration(e){i(t(e.id))},"FunctionDeclaration:exit"(e){o(t(e.id))},VariableDeclarator(e){if(l(e.init))i(t(e.id))},"VariableDeclarator:exit"(e){if(l(e.init))o(t(e.id))},CallExpression(e){if(r===0)return;const{callee:a}=e;if(a.type!=="Identifier"||a.name!=="observable")return;n.report({node:e,message:"Use `useObservable()` for component-lifetime state, or move this observable into a store in `src/stores` and import it. `observable()` inside a component makes a new observable every render, so nothing that read the previous one is listening."})}}}});
1
+ import{COMPONENT_OR_HOOK as s,gate as b,isFunction as l,problem as c}from"../../../lib/ast.js";export const noObservableInComponent=c("Calling `observable()` inside a component or hook makes a new observable on every render. Use `useObservable()` or a module-level store instead.",{createOnce(n){let r=0;const o=(e)=>{if(e!==void 0&&s.test(e))r++},i=(e)=>{if(e!==void 0&&s.test(e))r--},t=(e)=>e?.type==="Identifier"?e.name:void 0;return{before(){r=0;return b(n,"observable")},FunctionDeclaration(e){o(t(e.id))},"FunctionDeclaration:exit"(e){i(t(e.id))},VariableDeclarator(e){if(l(e.init))o(t(e.id))},"VariableDeclarator:exit"(e){if(l(e.init))i(t(e.id))},CallExpression(e){if(r===0)return;const{callee:a}=e;if(a.type!=="Identifier"||a.name!=="observable")return;n.report({node:e,message:"Use `useObservable()` for component-lifetime state, or move this observable into a store in `src/stores` — `observable()` here makes a new one every render."})}}}});
@@ -1 +1 @@
1
- import{gate as l,isFunction as u,problem as p}from"../../../lib/ast.js";import{isObservableRef as f,textOf as k}from"./shared.js";const b=new Set(["useValue","observe","useObserve","useObserveEffect","when","whenReady"]),h=(e)=>{const{callee:r}=e;if(r.type!=="Identifier")return null;if(!b.has(r.name))return null;const n=e.arguments[0];return n&&u(n)?n:null},g=(e)=>{if(e.type!=="MemberExpression"||e.computed)return null;if(e.property.type!=="Identifier"||e.property.name!=="peek")return null;const{object:r}=e;return f(r)?r:null};export const noPeekInSelector=p("`peek()` never subscribes, so a selector or tracking callback that uses it never re-runs. Call `get()` there and keep `peek()` for handlers.",{createOnce(e){let r=new WeakSet,n=0;const s=(t)=>{if(r.has(t))n++},o=(t)=>{if(r.has(t))n--};return{before(){r=new WeakSet;n=0;return l(e,".peek()")},CallExpression(t){const i=h(t);if(i)r.add(i);if(n===0)return;const a=g(t.callee);if(!a)return;const c=k(e,a);e.report({node:t,message:`Use \`${c}.get()\` inside this selector and keep \`peek()\` for handlers and async work. \`peek()\` never subscribes, so nothing re-runs when the value changes and the first read is the only one.`})},ArrowFunctionExpression:s,"ArrowFunctionExpression:exit":o,FunctionExpression:s,"FunctionExpression:exit":o}}});
1
+ import{gate as u,isFunction as l,problem as p}from"../../../lib/ast.js";import{isObservableRef as f,textOf as k}from"./shared.js";const b=new Set(["useValue","observe","useObserve","useObserveEffect","when","whenReady"]),g=(e)=>{const{callee:r}=e;if(r.type!=="Identifier")return null;if(!b.has(r.name))return null;const n=e.arguments[0];return n&&l(n)?n:null},h=(e)=>{if(e.type!=="MemberExpression"||e.computed)return null;if(e.property.type!=="Identifier"||e.property.name!=="peek")return null;const{object:r}=e;return f(r)?r:null};export const noPeekInSelector=p("`peek()` never subscribes, so a selector or tracking callback that uses it never re-runs. Call `get()` there and keep `peek()` for handlers.",{createOnce(e){let r=new WeakSet,n=0;const s=(t)=>{if(r.has(t))n++},o=(t)=>{if(r.has(t))n--};return{before(){r=new WeakSet;n=0;return u(e,".peek()")},CallExpression(t){const i=g(t);if(i)r.add(i);if(n===0)return;const a=h(t.callee);if(!a)return;const c=k(e,a);e.report({node:t,message:`Use \`${c}.get()\` inside this selector and keep \`peek()\` for handlers and async work — \`peek()\` never subscribes, so nothing re-runs.`})},ArrowFunctionExpression:s,"ArrowFunctionExpression:exit":o,FunctionExpression:s,"FunctionExpression:exit":o}}});
@@ -1 +1 @@
1
- import{gate as s,problem as a}from"../../../lib/ast.js";import{isObservableRef as o}from"./shared.js";const n=new Set(["get","peek"]),i=(r)=>{if(r?.type!=="CallExpression")return!1;const{callee:e}=r;if(e.type!=="MemberExpression"||e.computed)return!1;if(e.property.type!=="Identifier"||!n.has(e.property.name))return!1;return o(e.object)};export const noReactMirror=a("Seeding `useState` from an observable's `get()` or `peek()` gives the value two owners. Read it with `useValue(...)` where it renders.",{createOnce(r){return{before(){return s(r,"useState")},CallExpression(e){const{callee:t}=e;if(t.type!=="Identifier"||t.name!=="useState")return;if(!i(e.arguments[0]))return;r.report({node:e,message:"Drop this `useState` mirror and read the observable with `useValue(...)` where it renders, so the observable stays the single owner of the value."})}}}});
1
+ import{gate as s,problem as a}from"../../../lib/ast.js";import{isObservableRef as o}from"./shared.js";const n=new Set(["get","peek"]),i=(r)=>{if(r?.type!=="CallExpression")return!1;const{callee:e}=r;if(e.type!=="MemberExpression"||e.computed)return!1;if(e.property.type!=="Identifier"||!n.has(e.property.name))return!1;return o(e.object)};export const noReactMirror=a("Seeding `useState` from an observable's `get()` or `peek()` gives the value two owners. Read it with `useValue(...)` where it renders.",{createOnce(r){return{before(){return s(r,"useState")},CallExpression(e){const{callee:t}=e;if(t.type!=="Identifier"||t.name!=="useState")return;if(!i(e.arguments[0]))return;r.report({node:e,message:"Drop this `useState` mirror and read the observable with `useValue(...)` where it renders, so it stays the single owner of the value."})}}}});
@@ -1 +1 @@
1
- import{gate as u,problem as l}from"../../../lib/ast.js";import{isObservableRef as x,textOf as d}from"./shared.js";export const noUntrackedGetInJsx=l("A `get()` placed directly in a JSX expression container is a plain read. The value renders once and never updates.",{createOnce(r){let t=0,e=[];const i=()=>{t++},o=()=>{t--},p=()=>e.length>0&&e[e.length-1]===t;return{before(){t=0;e=[];return u(r,".get()")},FunctionDeclaration:i,"FunctionDeclaration:exit":o,FunctionExpression:i,"FunctionExpression:exit":o,ArrowFunctionExpression:i,"ArrowFunctionExpression:exit":o,JSXExpressionContainer(){e.push(t)},"JSXExpressionContainer:exit"(){e.pop()},CallExpression(s){if(!p())return;const{callee:n}=s;if(n.type!=="MemberExpression"||n.computed)return;if(n.property.type!=="Identifier"||n.property.name!=="get")return;const{object:a}=n;if(!x(a))return;const c=d(r,a);r.report({node:s,message:`Read it with \`useValue(${c})\` at the top of the component, or wrap this fragment in \`<Memo>\` so the read happens inside a tracking context. A \`get()\` here is a plain read, so it renders the first value and never updates.`})}}}});
1
+ import{gate as u,problem as l}from"../../../lib/ast.js";import{isObservableRef as x,textOf as f}from"./shared.js";export const noUntrackedGetInJsx=l("A `get()` placed directly in a JSX expression container is a plain read. The value renders once and never updates.",{createOnce(r){let t=0,e=[];const o=()=>{t++},i=()=>{t--},p=()=>e.length>0&&e[e.length-1]===t;return{before(){t=0;e=[];return u(r,".get()")},FunctionDeclaration:o,"FunctionDeclaration:exit":i,FunctionExpression:o,"FunctionExpression:exit":i,ArrowFunctionExpression:o,"ArrowFunctionExpression:exit":i,JSXExpressionContainer(){e.push(t)},"JSXExpressionContainer:exit"(){e.pop()},CallExpression(s){if(!p())return;const{callee:n}=s;if(n.type!=="MemberExpression"||n.computed)return;if(n.property.type!=="Identifier"||n.property.name!=="get")return;const{object:a}=n;if(!x(a))return;const c=f(r,a);r.report({node:s,message:`Read it with \`useValue(${c})\` at the top of the component, or wrap this fragment in \`<Memo>\` — a \`get()\` here renders the first value and never updates.`})}}}});
@@ -1 +1 @@
1
- import{ancestors as u,COMPONENT_OR_HOOK as a,isFunction as p,problem as l}from"../../../lib/ast.js";const f=/^[A-Z]/,c=(n)=>{if(n.type==="FunctionDeclaration")return n.id?.name??null;const{parent:e}=n;if(e===null||e.type!=="VariableDeclarator"||e.id.type!=="Identifier")return null;return e.id.name},d=(n)=>{for(const e of u(n)){if(!p(e))continue;const t=c(e);if(t!==null&&a.test(t))return e}return null},m=(n,e)=>{for(let t=n;t!==null&&t!==void 0;t=t.upper)if(t===e)return!0;return!1},h=(n,e)=>{for(const t of n.through)if(m(t.resolved?.scope,e))return!0;return!1};export const hoistStatelessFunction=l("Require module scope for a non-component function that reads nothing from the component around it. Out there it is created once, keeps a stable identity, and can be tested without rendering.",{createOnce(n){const e=(t)=>{const o=c(t);if(o===null||f.test(o))return;const r=d(t);if(r===null)return;const i=n.sourceCode.getScope(t),s=n.sourceCode.getScope(r);if(h(i,s))return;n.report({node:t,message:`Move \`${o}\` to module scope: it reads nothing from the component, so out there it is created once and keeps a stable identity without memoising. If it was meant to read a prop or a piece of state, wire that read up instead.`})};return{FunctionDeclaration:e,FunctionExpression:e,ArrowFunctionExpression:e}}});
1
+ import{ancestors as u,COMPONENT_OR_HOOK as a,isFunction as p,problem as l}from"../../../lib/ast.js";const f=/^[A-Z]/,c=(t)=>{if(t.type==="FunctionDeclaration")return t.id?.name??null;const{parent:e}=t;if(e===null||e.type!=="VariableDeclarator"||e.id.type!=="Identifier")return null;return e.id.name},m=(t)=>{for(const e of u(t)){if(!p(e))continue;const n=c(e);if(n!==null&&a.test(n))return e}return null},d=(t,e)=>{for(let n=t;n!==null&&n!==void 0;n=n.upper)if(n===e)return!0;return!1},h=(t,e)=>{for(const n of t.through)if(d(n.resolved?.scope,e))return!0;return!1};export const hoistStatelessFunction=l("Require module scope for a non-component function that reads nothing from the component around it. Out there it is created once, keeps a stable identity, and can be tested without rendering.",{createOnce(t){const e=(n)=>{const o=c(n);if(o===null||f.test(o))return;const r=m(n);if(r===null)return;const s=t.sourceCode.getScope(n),i=t.sourceCode.getScope(r);if(h(s,i))return;t.report({node:n,message:`Move \`${o}\` to module scope — it reads nothing from the component. If it was meant to read a prop or a piece of state, wire that read up instead.`})};return{FunctionDeclaration:e,FunctionExpression:e,ArrowFunctionExpression:e}}});
@@ -1 +1 @@
1
- import{problem as r}from"../../../lib/ast.js";const o="Move the condition into a Unistyles dynamic style function: `card: (active: boolean) => ({ ... })`, then `style={styles.card(active)}`. A conditional entry can evaluate to a falsy hole, which shifts the array indices and breaks the Unistyles C++ proxy.";export const noConditionalStyleArray=r("Disallow conditional and logical entries inside a JSX `style` array. A falsy entry leaves a hole that breaks the Unistyles C++ proxy.",{createOnce(i){return{JSXAttribute(e){if(e.name.type!=="JSXIdentifier"||e.name.name!=="style")return;const{value:n}=e;if(n?.type!=="JSXExpressionContainer")return;const a=n.expression;if(a.type!=="ArrayExpression")return;for(const t of a.elements){if(t?.type!=="ConditionalExpression"&&t?.type!=="LogicalExpression")continue;i.report({node:t,message:o})}}}}});
1
+ import{problem as a}from"../../../lib/ast.js";const s="Move the condition into a Unistyles dynamic style: `card: (active: boolean) => ({ ... })`, then `style={styles.card(active)}` — a falsy entry breaks the C++ proxy.";export const noConditionalStyleArray=a("Disallow conditional and logical entries inside a JSX `style` array. A falsy entry leaves a hole that breaks the Unistyles C++ proxy.",{createOnce(o){return{JSXAttribute(e){if(e.name.type!=="JSXIdentifier"||e.name.name!=="style")return;const{value:n}=e;if(n?.type!=="JSXExpressionContainer")return;const r=n.expression;if(r.type!=="ArrayExpression")return;for(const t of r.elements){if(t?.type!=="ConditionalExpression"&&t?.type!=="LogicalExpression")continue;o.report({node:t,message:s})}}}}});
@@ -1 +1 @@
1
- import{calleeName as i,gate as r,hasAncestor as n,problem as a}from"../../../lib/ast.js";const s=new Set(["lazy","dynamic"]),m="Turn this into a static `import` at the top of the file. Metro inlines `import()` into the same bundle, so it buys no laziness and only hides the module from the typechecker; wrapping it in `React.lazy(() => import(...))` or `dynamic(() => import(...))` stays allowed.";export const noDynamicImport=a("Disallow dynamic `import()` outside a `React.lazy` or `dynamic` wrapper. Metro inlines it into the same bundle, so nothing is deferred and the module is hidden from the typechecker.",{createOnce(e){return{before(){return r(e,"import(")},ImportExpression(t){if(n(t,(o)=>o.type==="CallExpression"&&s.has(i(o))))return;e.report({node:t,message:m})}}}});
1
+ import{calleeName as r,gate as i,hasAncestor as n,problem as s}from"../../../lib/ast.js";const a=new Set(["lazy","dynamic"]),m="Turn this into a static `import` — Metro inlines `import()` into the same bundle, so it buys no laziness. `React.lazy(() => import(...))` stays allowed.";export const noDynamicImport=s("Disallow dynamic `import()` outside a `React.lazy` or `dynamic` wrapper. Metro inlines it into the same bundle, so nothing is deferred and the module is hidden from the typechecker.",{createOnce(e){return{before(){return i(e,"import(")},ImportExpression(t){if(n(t,(o)=>o.type==="CallExpression"&&a.has(r(o))))return;e.report({node:t,message:m})}}}});
@@ -1 +1 @@
1
- import{gate as o,problem as t}from"../../../lib/ast.js";const n=new Set(["keyboardWillShow","keyboardWillHide","keyboardWillChangeFrame"]),a="Drive this from `rt.insets.ime`, which updates per frame on both platforms, or from a `react-native-keyboard-controller` component. `keyboardWill*` events are iOS-only, so on Android the listener registers and never fires.",i=(e)=>e.type==="Literal"&&typeof e.value==="string"&&n.has(e.value);export const noKeyboardWillEvents=t("Disallow the `keyboardWill*` event names. They are iOS-only, so on Android the listener registers and never fires.",{createOnce(e){return{before(){return o(e,"keyboardWill")},Literal(r){if(!i(r))return;e.report({node:r,message:a})}}}});
1
+ import{gate as o,problem as t}from"../../../lib/ast.js";const i=new Set(["keyboardWillShow","keyboardWillHide","keyboardWillChangeFrame"]),n="Drive this from `rt.insets.ime`, or a `react-native-keyboard-controller` component — `keyboardWill*` is iOS-only, so on Android the listener never fires.",l=(e)=>e.type==="Literal"&&typeof e.value==="string"&&i.has(e.value);export const noKeyboardWillEvents=t("Disallow the `keyboardWill*` event names. They are iOS-only, so on Android the listener registers and never fires.",{createOnce(e){return{before(){return o(e,"keyboardWill")},Literal(r){if(!l(r))return;e.report({node:r,message:n})}}}});
@@ -1 +1 @@
1
- import{problem as i}from"../../../lib/ast.js";const s='Compare explicitly (`list.length > 0 &&`), coerce with `!!`, or use a ternary ending in `null`. A falsy number or string leaks into the tree: `0` renders a bare zero, which crashes React Native with "Text strings must be rendered within a <Text> component".',p=new Set(["length","size"]),o=new Set(["+","-","*","/","%","**"]),a=(e)=>{if(e===void 0)return!1;if(e.type==="MemberExpression")return e.property.type==="Identifier"&&p.has(e.property.name);if(e.type==="Literal")return typeof e.value==="number"||typeof e.value==="string";if(e.type==="BinaryExpression")return o.has(e.operator);if(e.type==="TemplateLiteral")return!0;if(e.type==="LogicalExpression"&&e.operator==="&&")return a(e.right);return!1};export const noLeakedRender=i('Disallow a `&&` guard in JSX on a value this rule can see is a number or a string — a `.length`, a `.size`, arithmetic, a literal. The falsy left operand leaks into the output, and a bare `0` crashes React Native with "Text strings must be rendered within a <Text> component".',{createOnce(e){return{JSXExpressionContainer(r){const n=r.parent.type;if(n!=="JSXElement"&&n!=="JSXFragment")return;const t=r.expression;if(t.type!=="LogicalExpression"||t.operator!=="&&")return;if(!a(t.left))return;e.report({node:t,message:s})}}}});
1
+ import{problem as i}from"../../../lib/ast.js";const s="Compare explicitly (`list.length > 0 &&`), coerce with `!!`, or use a ternary ending in `null` — a `0` leaks into the tree and crashes React Native.",p=new Set(["length","size"]),o=new Set(["+","-","*","/","%","**"]),n=(e)=>{if(e===void 0)return!1;if(e.type==="MemberExpression")return e.property.type==="Identifier"&&p.has(e.property.name);if(e.type==="Literal")return typeof e.value==="number"||typeof e.value==="string";if(e.type==="BinaryExpression")return o.has(e.operator);if(e.type==="TemplateLiteral")return!0;if(e.type==="LogicalExpression"&&e.operator==="&&")return n(e.right);return!1};export const noLeakedRender=i('Disallow a `&&` guard in JSX on a value this rule can see is a number or a string — a `.length`, a `.size`, arithmetic, a literal. The falsy left operand leaks into the output, and a bare `0` crashes React Native with "Text strings must be rendered within a <Text> component".',{createOnce(e){return{JSXExpressionContainer(r){const a=r.parent.type;if(a!=="JSXElement"&&a!=="JSXFragment")return;const t=r.expression;if(t.type!=="LogicalExpression"||t.operator!=="&&")return;if(!n(t.left))return;e.report({node:t,message:s})}}}});
@@ -1 +1 @@
1
- import{gate as m,problem as c}from"../../../lib/ast.js";const l=new Set(["useMemo","useCallback","memo"]),u=["babel-plugin-react-compiler","react-compiler-runtime","react-compiler-marker"],h=/^why:/i,p=10,f=(e)=>e.type==="Identifier"&&l.has(e.name)?e.name:null,d=(e)=>{if(e.type!=="MemberExpression"||e.computed)return null;if(e.object.type!=="Identifier"||e.object.name!=="React")return null;const t=e.property.name;return l.has(t)?t:null},b=(e)=>{if(e.type!=="CallExpression")return null;const{callee:t}=e;if(t.type==="Identifier")return f(t);if(t.type==="MemberExpression")return d(t);return null},g=(e)=>{const t=[0];for(let o=0;o<e.length;o++)if(e.charCodeAt(o)===p)t.push(o+1);return t},y=(e,t)=>{let o=0,n=e.length-1;while(o<n){const r=Math.floor((o+n+1)/2);if((e[r]??0)<=t)o=r;else n=r-1}return o},w=(e,t)=>{const o=new Set;for(const n of e){if(!h.test(n.value.trim()))continue;for(let r=t(n.start);r<=t(n.end);r++)o.add(r)}return o};export const noManualMemo=c("Require a `// why:` line above every kept `useMemo`, `useCallback` and `memo`. The React Compiler memoises on a best-effort basis, not a guarantee, so a memo is allowed where the cost is real: something rendered per list row, or a computation measured as heavy. The `// why:` line names which of the two applies. `@ashstack/core/no-comments` keeps that line and never counts it against its `budget`, so both rules run together.",{meta:{packages:u},createOnce(e){let t=[];return{before(){t=[];return m(e,"useMemo","useCallback","memo")},CallExpression(o){const n=b(o);if(n!==null)t.push({node:o,name:n})},"Program:exit"(){if(t.length===0)return;const o=g(e.sourceCode.getText()),n=(s)=>y(o,s),r=w(e.sourceCode.getAllComments(),n);for(const{node:s,name:a}of t){const i=n(s.start);if(r.has(i)||r.has(i-1))continue;e.report({node:s,message:`The React Compiler memoises on a best-effort basis, not a guarantee, so a \`${a}\` is allowed where you know the cost is real: something rendered per row in a list, or a computation you measured as heavy. Add a \`// why:\` line directly above it naming which of the two applies. If neither does, delete the \`${a}\`.`})}}}}});
1
+ import{gate as m,problem as u}from"../../../lib/ast.js";const i=new Set(["useMemo","useCallback","memo"]),c=/^why:/i,h=10,p=(e)=>e.type==="Identifier"&&i.has(e.name)?e.name:null,f=(e)=>{if(e.type!=="MemberExpression"||e.computed)return null;if(e.object.type!=="Identifier"||e.object.name!=="React")return null;const t=e.property.name;return i.has(t)?t:null},d=(e)=>{if(e.type!=="CallExpression")return null;const{callee:t}=e;if(t.type==="Identifier")return p(t);if(t.type==="MemberExpression")return f(t);return null},y=(e)=>{const t=[0];for(let n=0;n<e.length;n++)if(e.charCodeAt(n)===h)t.push(n+1);return t},b=(e,t)=>{let n=0,o=e.length-1;while(n<o){const s=Math.floor((n+o+1)/2);if((e[s]??0)<=t)n=s;else o=s-1}return n},g=(e,t)=>{const n=new Set;for(const o of e){if(!c.test(o.value.trim()))continue;for(let s=t(o.start);s<=t(o.end);s++)n.add(s)}return n};export const noManualMemo=u("Require a `// why:` line above every kept `useMemo`, `useCallback` and `memo`. The React Compiler memoises on a best-effort basis, not a guarantee, so a memo is allowed where the cost is real: something rendered per list row, or a computation measured as heavy. The `// why:` line names which of the two applies. `@ashstack/core/no-comments` keeps that line and never counts it against its `budget`, so both rules run together. Assumes the compiler is on — pass `reactCompiler: false` to the entry and this rule turns off, since without it a hand-written memo is the only memo there is.",{createOnce(e){let t=[];return{before(){t=[];return m(e,"useMemo","useCallback","memo")},CallExpression(n){const o=d(n);if(o!==null)t.push({node:n,name:o})},"Program:exit"(){if(t.length===0)return;const n=y(e.sourceCode.getText()),o=(r)=>b(n,r),s=g(e.sourceCode.getAllComments(),o);for(const{node:r,name:l}of t){const a=o(r.start);if(s.has(a)||s.has(a-1))continue;e.report({node:r,message:`Add a \`// why:\` line above this \`${l}\` naming the real cost — a list row, or a computation you measured as heavy. If neither applies, delete it.`})}}}}});
@@ -1 +1 @@
1
- import{problem as h,tagPath as s}from"../../../lib/ast.js";const c=new Set(["style"]),m=new Set(["View","Animated.View"]),p=(t)=>t.filter((e)=>e.type!=="JSXText"||e.value.trim()!==""),l=(t)=>t.every((e)=>e.type==="JSXAttribute"&&e.name.type==="JSXIdentifier"&&c.has(e.name.name));export const noRedundantViewNesting=h("Disallow a View or Animated.View that wraps an identical view when neither carries anything but a style. Every extra host view is a real node in the native tree.",{createOnce(t){return{JSXElement(e){const n=e.openingElement,r=s(n.name);if(!m.has(r))return;if(!l(n.attributes))return;const i=p(e.children);if(i.length!==1)return;const a=i[0];if(a?.type!=="JSXElement")return;const o=a.openingElement;if(s(o.name)!==r)return;if(!l(o.attributes))return;t.report({node:n.name,message:`Merge these two <${r}> style objects into the inner one and delete the outer wrapper — each carries nothing but a style, and every extra host view is a real node in the native tree.`})}}}});
1
+ import{problem as h,tagPath as s}from"../../../lib/ast.js";const c=new Set(["style"]),m=new Set(["View","Animated.View"]),p=(t)=>t.filter((e)=>e.type!=="JSXText"||e.value.trim()!==""),l=(t)=>t.every((e)=>e.type==="JSXAttribute"&&e.name.type==="JSXIdentifier"&&c.has(e.name.name));export const noRedundantViewNesting=h("Disallow a View or Animated.View that wraps an identical view when neither carries anything but a style. Every extra host view is a real node in the native tree.",{createOnce(t){return{JSXElement(e){const n=e.openingElement,r=s(n.name);if(!m.has(r))return;if(!l(n.attributes))return;const i=p(e.children);if(i.length!==1)return;const a=i[0];if(a?.type!=="JSXElement")return;const o=a.openingElement;if(s(o.name)!==r)return;if(!l(o.attributes))return;t.report({node:n.name,message:`Merge these two <${r}> style objects into the inner one and delete the outer wrapper — every extra host view is a real node in the native tree.`})}}}});
@@ -1 +1 @@
1
- import{componentName as s,gate as c,importedSpecifiers as d,problem as p}from"../../../lib/ast.js";const m="Render this network image with `TurboImage` (react-native-turbo-image), passing `resize` and `cachePolicy`; keep react-native `<Image>` for local `require(...)` assets. RN `<Image>` has no disk cache and no decode sizing, so it re-downloads and decodes at full size on every cold start.",f=(e)=>{if(e.type==="Identifier")return e.name;return e.type==="Literal"&&typeof e.value==="string"?e.value:void 0},u=(e)=>{if(e.type!=="JSXAttribute")return!1;const{value:t}=e,r=t?.type==="JSXExpressionContainer"?t.expression:void 0;if(r?.type!=="ObjectExpression")return!1;return r.properties.some((n)=>n.type==="Property"&&f(n.key)==="uri")};export const noRnImageNetworkSource=p("Disallow a react-native `<Image>` with a network `{ uri }` source. It has no disk cache and no decode sizing, so the image re-downloads at full size on every cold start.",{createOnce(e){const t=new Set,r=[];return{before(){t.clear();r.length=0;return c(e,"source")},ImportDeclaration(n){for(const o of d(n,"react-native")){if(o.type!=="ImportSpecifier")continue;const{imported:a}=o;if(a.type==="Identifier"&&a.name==="Image")t.add(o.local.name)}},JSXOpeningElement(n){const o=s(n.name);if(o==="")return;const a=n.attributes.find((i)=>i.type==="JSXAttribute"&&i.name.type==="JSXIdentifier"&&i.name.name==="source");if(!a||!u(a))return;r.push({node:n.name,tag:o})},"Program:exit"(){for(const n of r){if(!t.has(n.tag))continue;e.report({node:n.node,message:m})}}}}});
1
+ import{componentName as s,gate as c,importedSpecifiers as p,problem as m}from"../../../lib/ast.js";const f="Render this network image with `TurboImage` (react-native-turbo-image), passing `resize` and `cachePolicy`; keep react-native `<Image>` for local `require(...)` assets.",u=(e)=>{if(e.type==="Identifier")return e.name;return e.type==="Literal"&&typeof e.value==="string"?e.value:void 0},d=(e)=>{if(e.type!=="JSXAttribute")return!1;const{value:n}=e,r=n?.type==="JSXExpressionContainer"?n.expression:void 0;if(r?.type!=="ObjectExpression")return!1;return r.properties.some((t)=>t.type==="Property"&&u(t.key)==="uri")};export const noRnImageNetworkSource=m("Disallow a react-native `<Image>` with a network `{ uri }` source. It has no disk cache and no decode sizing, so the image re-downloads at full size on every cold start.",{createOnce(e){const n=new Set,r=[];return{before(){n.clear();r.length=0;return c(e,"source")},ImportDeclaration(t){for(const i of p(t,"react-native")){if(i.type!=="ImportSpecifier")continue;const{imported:o}=i;if(o.type==="Identifier"&&o.name==="Image")n.add(i.local.name)}},JSXOpeningElement(t){const i=s(t.name);if(i==="")return;const o=t.attributes.find((a)=>a.type==="JSXAttribute"&&a.name.type==="JSXIdentifier"&&a.name.name==="source");if(!o||!d(o))return;r.push({node:t.name,tag:i})},"Program:exit"(){for(const t of r){if(!n.has(t.tag))continue;e.report({node:t.node,message:f})}}}}});
@@ -1 +1 @@
1
- import{gate as i,problem as s}from"../../../lib/ast.js";const a={namespaceImport:'Import the react-native APIs by name instead: `import { View, Platform } from "react-native"`. A namespace import defeats Metro platform shaking, so dead `Platform.OS` branches ship in both bundles.',platformReExport:"Drop this re-export and have each consumer import `Platform` straight from react-native; re-exporting it defeats Metro platform shaking."};export const noRnNamespaceImport=s("Disallow a namespace import of react-native and a re-export of its `Platform`. Both defeat Metro's platform shaking, so dead `Platform.OS` branches ship in both bundles.",{createOnce(t){return{before(){return i(t,"react-native")},ImportDeclaration(e){if(e.source.value!=="react-native")return;for(const r of e.specifiers){if(r.type!=="ImportNamespaceSpecifier")continue;t.report({node:r,message:a.namespaceImport})}},ExportAllDeclaration(e){if(e.source.value!=="react-native")return;t.report({node:e,message:a.namespaceImport})},ExportNamedDeclaration(e){if(e.source?.value!=="react-native")return;for(const r of e.specifiers){const{local:o}=r;if(o.type!=="Identifier"||o.name!=="Platform")continue;t.report({node:r,message:a.platformReExport})}}}}});
1
+ import{gate as i,problem as p}from"../../../lib/ast.js";const a={namespaceImport:'Import the react-native APIs by name instead: `import { View, Platform } from "react-native"` — a namespace import defeats Metro platform shaking.',platformReExport:"Drop this re-export and have each consumer import `Platform` straight from react-native; re-exporting it defeats Metro platform shaking."};export const noRnNamespaceImport=p("Disallow a namespace import of react-native and a re-export of its `Platform`. Both defeat Metro's platform shaking, so dead `Platform.OS` branches ship in both bundles.",{createOnce(t){return{before(){return i(t,"react-native")},ImportDeclaration(e){if(e.source.value!=="react-native")return;for(const r of e.specifiers){if(r.type!=="ImportNamespaceSpecifier")continue;t.report({node:r,message:a.namespaceImport})}},ExportAllDeclaration(e){if(e.source.value!=="react-native")return;t.report({node:e,message:a.namespaceImport})},ExportNamedDeclaration(e){if(e.source?.value!=="react-native")return;for(const r of e.specifiers){const{local:o}=r;if(o.type!=="Identifier"||o.name!=="Platform")continue;t.report({node:r,message:a.platformReExport})}}}}});
@@ -1 +1 @@
1
- import{attributeName as a,gate as i,problem as l,subtreeHas as c}from"../../../lib/ast.js";const o=new Set(["onScroll","onScrollBeginDrag","onScrollEndDrag","onMomentumScrollEnd"]),S=/^set[A-Z]/,p="Use `useAnimatedScrollHandler` with a shared value when this drives an animation, or a ref when nothing renders from it. Scroll fires every frame, so a state setter here re-renders the screen every frame.";export const noScrollPositionState=l("Disallow a React state setter inside a scroll handler prop. Scroll fires every frame, and so would the re-render.",{createOnce(n){return{before(){return i(n,...o)},JSXAttribute(e){if(!o.has(a(e)))return;const{value:s}=e,r=s?.type==="JSXExpressionContainer"?s.expression:void 0;if(r?.type!=="ArrowFunctionExpression"&&r?.type!=="FunctionExpression")return;if(c(r.body,(t)=>t.type==="CallExpression"&&t.callee.type==="Identifier"&&S.test(t.callee.name)))n.report({node:e,message:p})}}}});
1
+ import{attributeName as a,gate as i,problem as l,subtreeHas as c}from"../../../lib/ast.js";const o=new Set(["onScroll","onScrollBeginDrag","onScrollEndDrag","onMomentumScrollEnd"]),S=/^set[A-Z]/,p="Use `useAnimatedScrollHandler` with a shared value when this drives an animation, or a ref when nothing renders from it — a state setter here re-renders every frame.";export const noScrollPositionState=l("Disallow a React state setter inside a scroll handler prop. Scroll fires every frame, and so would the re-render.",{createOnce(n){return{before(){return i(n,...o)},JSXAttribute(e){if(!o.has(a(e)))return;const{value:s}=e,r=s?.type==="JSXExpressionContainer"?s.expression:void 0;if(r?.type!=="ArrowFunctionExpression"&&r?.type!=="FunctionExpression")return;if(c(r.body,(t)=>t.type==="CallExpression"&&t.callee.type==="Identifier"&&S.test(t.callee.name)))n.report({node:e,message:p})}}}});
@@ -1 +1 @@
1
- import{componentName as a,problem as l,tagIdentifier as h}from"../../../lib/ast.js";const m=new Set(["Pressable","PressableScale","TouchableOpacity","TouchableHighlight"]),i={touchable:"Add an `accessibilityLabel` (plus an `accessibilityHint` when the outcome is not obvious), or render a visible `<Text>` child, so this icon-only touchable has an accessible name.",expoButton:"Add a `label`, or an `accessibilityLabel(...)` modifier, so this icon-only Expo UI `<Button>` has an accessible name."},o=(t)=>t.type==="JSXAttribute"&&t.name.type==="JSXIdentifier"?t.name.name:void 0,u=(t)=>t.some((n)=>{const e=o(n);return e==="accessibilityLabel"||e==="accessibilityHint"}),b=(t)=>{const n={hasIcon:!1,hasText:!1,hasExpression:!1};for(const e of t){if(e.type==="JSXExpressionContainer")n.hasExpression=!0;if(e.type==="JSXText"&&e.value.trim()!=="")n.hasText=!0;if(e.type!=="JSXElement")continue;const s=h(e.openingElement.name);if(s==="Text")n.hasText=!0;if(s.endsWith("Icon"))n.hasIcon=!0}return n};export const noUnlabeledIconPressable=l("Require an accessible name on an icon-only touchable or an icon-only Expo UI `<Button>`. Without a label, hint or visible text, a screen reader cannot reach the control.",{createOnce(t){return{JSXElement(n){const e=n.openingElement;if(!m.has(a(e.name)))return;if(u(e.attributes))return;const{hasIcon:s,hasText:c,hasExpression:r}=b(n.children);if(!s||c||r)return;t.report({node:e.name,message:i.touchable})},JSXOpeningElement(n){if(!n.selfClosing)return;if(a(n.name)!=="Button")return;const e=new Set(n.attributes.map(o).filter(Boolean));if(!e.has("systemImage"))return;if(e.has("label")||e.has("accessibilityLabel")||e.has("accessibilityHint")||e.has("modifiers"))return;t.report({node:n.name,message:i.expoButton})}}}});
1
+ import{componentName as a,problem as r,tagIdentifier as h}from"../../../lib/ast.js";const m=new Set(["Pressable","PressableScale","TouchableOpacity","TouchableHighlight"]),i={touchable:"Add an `accessibilityLabel` (plus an `accessibilityHint` when the outcome is not obvious), or a visible `<Text>` child, so this icon-only touchable has an accessible name.",expoButton:"Add a `label`, or an `accessibilityLabel(...)` modifier, so this icon-only Expo UI `<Button>` has an accessible name."},o=(t)=>t.type==="JSXAttribute"&&t.name.type==="JSXIdentifier"?t.name.name:void 0,u=(t)=>t.some((n)=>{const e=o(n);return e==="accessibilityLabel"||e==="accessibilityHint"}),b=(t)=>{const n={hasIcon:!1,hasText:!1,hasExpression:!1};for(const e of t){if(e.type==="JSXExpressionContainer")n.hasExpression=!0;if(e.type==="JSXText"&&e.value.trim()!=="")n.hasText=!0;if(e.type!=="JSXElement")continue;const s=h(e.openingElement.name);if(s==="Text")n.hasText=!0;if(s.endsWith("Icon"))n.hasIcon=!0}return n};export const noUnlabeledIconPressable=r("Require an accessible name on an icon-only touchable or an icon-only Expo UI `<Button>`. Without a label, hint or visible text, a screen reader cannot reach the control.",{createOnce(t){return{JSXElement(n){const e=n.openingElement;if(!m.has(a(e.name)))return;if(u(e.attributes))return;const{hasIcon:s,hasText:c,hasExpression:l}=b(n.children);if(!s||c||l)return;t.report({node:e.name,message:i.touchable})},JSXOpeningElement(n){if(!n.selfClosing)return;if(a(n.name)!=="Button")return;const e=new Set(n.attributes.map(o).filter(Boolean));if(!e.has("systemImage"))return;if(e.has("label")||e.has("accessibilityLabel")||e.has("accessibilityHint")||e.has("modifiers"))return;t.report({node:n.name,message:i.expoButton})}}}});
@@ -1 +1 @@
1
- import{calleeName as i,gate as c,hasAncestor as f,isMemberCall as m,isWithin as h,problem as d,receiverName as l,subtreeHas as g}from"../../../lib/ast.js";const u={hotBridgeUnguarded:"Guard this `scheduleOnRN` by comparing the current and previous prepared results, so high-frequency input does not bridge to RN every frame.",hotBridgeNoPrevious:"Take the previous prepared result as the callback's second parameter and guard `scheduleOnRN` on it differing from the current one, so high-frequency input does not bridge to RN every frame."},p=new Set(["useAnimatedReaction"]),y=new Set(["!==","!="]),o=(e)=>e?.type==="Identifier"?e.name:null,b=(e,t,r)=>{if(e.type!=="BinaryExpression"||!y.has(e.operator))return!1;const s=o(e.left),a=o(e.right);return s===t&&a===r||s===r&&a===t},E=(e,t,r)=>{if(e.type!=="UnaryExpression"||e.operator!=="!")return!1;const{argument:s}=e;if(s.type!=="CallExpression"||i(s)!=="is")return!1;const a=s.arguments;return a.some((n)=>o(n)===t)&&a.some((n)=>o(n)===r)},N=(e,t,r)=>{if(!t||!r)return!1;return f(e,(s)=>{if(s.type!=="IfStatement")return!1;const{test:a}=s;return b(a,t,r)||E(a,t,r)})},v=(e,t)=>{if(e.type!=="CallExpression"||e.callee.type!=="MemberExpression")return!1;const r=l(e);if(!r)return!1;if(t.type!=="CallExpression")return!1;const[s,a]=t.arguments;if(!s||!a||!h(e,a))return!1;return g(s,(n)=>m(n,"get")&&l(n)===r)},R=(e)=>{if(e.type!=="CallExpression")return null;const t=e.arguments[1];if(t?.type!=="FunctionDeclaration"&&t?.type!=="FunctionExpression"&&t?.type!=="ArrowFunctionExpression")return null;return t.params},O=(e,t)=>{const r=R(t);if(!r)return null;const s=o(r[1]);if(!s)return u.hotBridgeNoPrevious;return N(e,o(r[0]),s)?null:u.hotBridgeUnguarded};export const animatedReactionSafety=d("A `useAnimatedReaction` result callback loops forever if it writes a shared value the prepare callback reads. Guard `scheduleOnRN` there on the current result differing from the previous one.",{createOnce(e){let t=[];return{before(){t=[];return c(e,"useAnimatedReaction")},CallExpression(r){const s=i(r);if(p.has(s)){t.push(r);return}const a=t.at(-1);if(!a)return;if(s==="set"||s==="modify"){if(!v(r,a))return;e.report({node:r,message:`Have this result callback ${s==="set"?"write":"modify"} a shared value its prepare callback does not read, or gate the call on a comparison; feeding its own input loops forever.`});return}if(s!=="scheduleOnRN")return;const n=O(r,a);if(n!==null)e.report({node:r,message:n})},"CallExpression:exit"(r){if(p.has(i(r)))t.pop()}}}});
1
+ import{calleeName as o,gate as c,hasAncestor as f,isMemberCall as m,isWithin as h,problem as d,receiverName as l,subtreeHas as g}from"../../../lib/ast.js";const u={hotBridgeUnguarded:"Guard this `scheduleOnRN` on the current and previous prepared results differing, so high-frequency input does not bridge every frame.",hotBridgeNoPrevious:"Take the previous prepared result as the callback's second parameter and guard `scheduleOnRN` on it differing from the current one."},p=new Set(["useAnimatedReaction"]),y=new Set(["!==","!="]),i=(e)=>e?.type==="Identifier"?e.name:null,E=(e,t,r)=>{if(e.type!=="BinaryExpression"||!y.has(e.operator))return!1;const s=i(e.left),a=i(e.right);return s===t&&a===r||s===r&&a===t},b=(e,t,r)=>{if(e.type!=="UnaryExpression"||e.operator!=="!")return!1;const{argument:s}=e;if(s.type!=="CallExpression"||o(s)!=="is")return!1;const a=s.arguments;return a.some((n)=>i(n)===t)&&a.some((n)=>i(n)===r)},v=(e,t,r)=>{if(!t||!r)return!1;return f(e,(s)=>{if(s.type!=="IfStatement")return!1;const{test:a}=s;return E(a,t,r)||b(a,t,r)})},N=(e,t)=>{if(e.type!=="CallExpression"||e.callee.type!=="MemberExpression")return!1;const r=l(e);if(!r)return!1;if(t.type!=="CallExpression")return!1;const[s,a]=t.arguments;if(!s||!a||!h(e,a))return!1;return g(s,(n)=>m(n,"get")&&l(n)===r)},O=(e)=>{if(e.type!=="CallExpression")return null;const t=e.arguments[1];if(t?.type!=="FunctionDeclaration"&&t?.type!=="FunctionExpression"&&t?.type!=="ArrowFunctionExpression")return null;return t.params},x=(e,t)=>{const r=O(t);if(!r)return null;const s=i(r[1]);if(!s)return u.hotBridgeNoPrevious;return v(e,i(r[0]),s)?null:u.hotBridgeUnguarded};export const animatedReactionSafety=d("A `useAnimatedReaction` result callback loops forever if it writes a shared value the prepare callback reads. Guard `scheduleOnRN` there on the current result differing from the previous one.",{createOnce(e){let t=[];return{before(){t=[];return c(e,"useAnimatedReaction")},CallExpression(r){const s=o(r);if(p.has(s)){t.push(r);return}const a=t.at(-1);if(!a)return;if(s==="set"||s==="modify"){if(!N(r,a))return;e.report({node:r,message:`Have this result callback ${s==="set"?"write":"modify"} a shared value its prepare callback does not read, or gate the call on a comparison — feeding its own input loops forever.`});return}if(s!=="scheduleOnRN")return;const n=x(r,a);if(n!==null)e.report({node:r,message:n})},"CallExpression:exit"(r){if(p.has(o(r)))t.pop()}}}});
@@ -1 +1 @@
1
- import{attributeName as m,calleeName as l,gate as p,problem as f,tagPath as c}from"../../../lib/ast.js";import{ANIMATED_STYLE_HOOKS as d}from"./shared.js";const u="Render this with the matching `Animated.*` component so the animated style takes effect. On a plain element the style is applied once at mount and never updates, and nothing errors.",y=new Set(["createAnimatedComponent","withUnistyles","withAnimated"]),A=(e)=>{if(e.type==="JSXElement")return e.openingElement.name;if(e.type==="JSXOpeningElement")return e.name;return null},h=(e)=>{if(e?.type!=="JSXExpressionContainer")return[];const{expression:t}=e;if(t.type==="Identifier")return[t.name];if(t.type!=="ArrayExpression")return[];return t.elements.flatMap((i)=>i?.type==="Identifier"?[i.name]:[])};export const animatedStyleNeedsAnimatedComponent=f("An animated style only takes effect on an `Animated.*` component. A plain element applies it once at mount and then never updates.",{createOnce(e){let t=new Set,i=new Set,o=[];return{before(){t=new Set;i=new Set;o=[];return p(e,"useAnimatedStyle","useAnimatedProps")},VariableDeclarator(a){const{id:n,init:r}=a;if(n.type!=="Identifier")return;if(r?.type!=="CallExpression")return;const s=l(r);if(d.has(s))t.add(n.name);else if(y.has(s))i.add(n.name)},"Program:exit"(){if(t.size===0)return;for(const{node:a,tag:n,referenced:r}of o){if(i.has(n))continue;if(!r.some((s)=>t.has(s)))continue;e.report({node:a,message:u})}},JSXAttribute(a){if(m(a)!=="style")return;const n=c(A(a.parent));if(n===""||n.startsWith("Animated"))return;const r=h(a.value);if(r.length===0)return;o.push({node:a,tag:n,referenced:r})}}}});
1
+ import{attributeName as o,calleeName as l,gate as p,problem as f,tagPath as c}from"../../../lib/ast.js";import{ANIMATED_STYLE_HOOKS as u}from"./shared.js";const d="Render this with the matching `Animated.*` component — on a plain element the style applies once at mount and never updates.",A=new Set(["createAnimatedComponent","withUnistyles","withAnimated"]),y=(e)=>{if(e.type==="JSXElement")return e.openingElement.name;if(e.type==="JSXOpeningElement")return e.name;return null},S=(e)=>{if(e?.type!=="JSXExpressionContainer")return[];const{expression:t}=e;if(t.type==="Identifier")return[t.name];if(t.type!=="ArrayExpression")return[];return t.elements.flatMap((i)=>i?.type==="Identifier"?[i.name]:[])};export const animatedStyleNeedsAnimatedComponent=f("An animated style only takes effect on an `Animated.*` component. A plain element applies it once at mount and then never updates.",{createOnce(e){let t=new Set,i=new Set,m=[];return{before(){t=new Set;i=new Set;m=[];return p(e,"useAnimatedStyle","useAnimatedProps")},VariableDeclarator(a){const{id:n,init:r}=a;if(n.type!=="Identifier")return;if(r?.type!=="CallExpression")return;const s=l(r);if(u.has(s))t.add(n.name);else if(A.has(s))i.add(n.name)},"Program:exit"(){if(t.size===0)return;for(const{node:a,tag:n,referenced:r}of m){if(i.has(n))continue;if(!r.some((s)=>t.has(s)))continue;e.report({node:a,message:d})}},JSXAttribute(a){if(o(a)!=="style")return;const n=c(y(a.parent));if(n===""||n.startsWith("Animated"))return;const r=S(a.value);if(r.length===0)return;m.push({node:a,tag:n,referenced:r})}}}});
@@ -1 +1 @@
1
- import{calleeName as s,problem as n}from"../../../lib/ast.js";import{ANIMATED_STYLE_HOOKS as i}from"./shared.js";const o="Schedule this RN side effect from an animation completion callback or `useAnimatedReaction` instead; `useAnimatedStyle`/`useAnimatedProps` updaters must stay pure.";export const animatedUpdaterPurity=n("An updater passed to `useAnimatedStyle` or `useAnimatedProps` must stay pure, so it may not write a shared value or call `scheduleOnRN`.",{createOnce(a){let r=0;return{before(){r=0},CallExpression(e){const t=s(e);if(i.has(t)){r+=1;return}if(r===0)return;if(t==="set"||t==="modify"){if(e.callee.type!=="MemberExpression")return;a.report({node:e,message:`Move this \`.${t}()\` write out to an event handler, an effect, a derived value, or \`useAnimatedReaction\`; \`useAnimatedStyle\`/\`useAnimatedProps\` updaters must stay pure.`});return}if(t!=="scheduleOnRN")return;a.report({node:e,message:o})},"CallExpression:exit"(e){if(i.has(s(e)))r-=1}}}});
1
+ import{calleeName as s,problem as n}from"../../../lib/ast.js";import{ANIMATED_STYLE_HOOKS as i}from"./shared.js";const o="Schedule this RN side effect from an animation completion callback or `useAnimatedReaction` — updaters must stay pure.";export const animatedUpdaterPurity=n("An updater passed to `useAnimatedStyle` or `useAnimatedProps` must stay pure, so it may not write a shared value or call `scheduleOnRN`.",{createOnce(a){let r=0;return{before(){r=0},CallExpression(e){const t=s(e);if(i.has(t)){r+=1;return}if(r===0)return;if(t==="set"||t==="modify"){if(e.callee.type!=="MemberExpression")return;a.report({node:e,message:`Move this \`.${t}()\` write out to an event handler, an effect, or \`useAnimatedReaction\` — updaters must stay pure.`});return}if(t!=="scheduleOnRN")return;a.report({node:e,message:o})},"CallExpression:exit"(e){if(i.has(s(e)))r-=1}}}});
@@ -1 +1 @@
1
- import{calleeName as i,gate as o,problem as s}from"../../../lib/ast.js";import{ANIMATED_STYLE_HOOKS as n}from"./shared.js";const p="Animate `transform` and `opacity` instead: a panel that grows is `scaleY` with `transformOrigin`, a thing that slides is `translateY`. This property recalculates layout on every frame.",m=new Set(["width","height","minWidth","minHeight","maxWidth","maxHeight","top","left","right","bottom","start","end","flex","flexBasis","padding","paddingTop","paddingBottom","paddingLeft","paddingRight","paddingStart","paddingEnd","paddingHorizontal","paddingVertical","margin","marginTop","marginBottom","marginLeft","marginRight","marginStart","marginEnd","marginHorizontal","marginVertical","gap","rowGap","columnGap"]),l=(a)=>{if(a.type!=="Property")return null;const{key:t}=a;if(t.type==="Identifier")return t.name;if(t.type==="Literal")return String(t.value);return null};export const gpuPropertiesOnly=s("Animate `transform` and `opacity` in `useAnimatedStyle` and `useAnimatedProps`. Layout properties such as `width` or `margin` recalculate layout every frame.",{createOnce(a){let t=0;return{before(){t=0;return o(a,"useAnimatedStyle","useAnimatedProps")},CallExpression(e){if(n.has(i(e)))t+=1},"CallExpression:exit"(e){if(n.has(i(e)))t-=1},Property(e){if(t===0)return;if(e.parent.type!=="ObjectExpression")return;const r=l(e);if(r!==null&&m.has(r))a.report({node:e,message:p})}}}});
1
+ import{calleeName as i,gate as o,problem as s}from"../../../lib/ast.js";import{ANIMATED_STYLE_HOOKS as n}from"./shared.js";const p="Animate `transform` and `opacity` instead — a panel that grows is `scaleY` with `transformOrigin`, a slide is `translateY`. This property relayouts every frame.",m=new Set(["width","height","minWidth","minHeight","maxWidth","maxHeight","top","left","right","bottom","start","end","flex","flexBasis","padding","paddingTop","paddingBottom","paddingLeft","paddingRight","paddingStart","paddingEnd","paddingHorizontal","paddingVertical","margin","marginTop","marginBottom","marginLeft","marginRight","marginStart","marginEnd","marginHorizontal","marginVertical","gap","rowGap","columnGap"]),l=(a)=>{if(a.type!=="Property")return null;const{key:t}=a;if(t.type==="Identifier")return t.name;if(t.type==="Literal")return String(t.value);return null};export const gpuPropertiesOnly=s("Animate `transform` and `opacity` in `useAnimatedStyle` and `useAnimatedProps`. Layout properties such as `width` or `margin` recalculate layout every frame.",{createOnce(a){let t=0;return{before(){t=0;return o(a,"useAnimatedStyle","useAnimatedProps")},CallExpression(e){if(n.has(i(e)))t+=1},"CallExpression:exit"(e){if(n.has(i(e)))t-=1},Property(e){if(t===0)return;if(e.parent.type!=="ObjectExpression")return;const r=l(e);if(r!==null&&m.has(r))a.report({node:e,message:p})}}}});
@@ -1 +1 @@
1
- import{gate as p,problem as l}from"../../../lib/ast.js";const u="Pass `Extrapolation.CLAMP` as the fourth argument to `interpolate`. Without it the output keeps extrapolating past the input range, so a scroll offset of 400 against `[0, 100]` runs well past where it should stop.",f="Change this fourth argument to `Extrapolation.CLAMP`. `EXTEND` and `IDENTITY` both keep extrapolating past the input range, so a scroll offset of 400 against `[0, 100]` runs well past where it should stop.",s=new Set(["EXTEND","IDENTITY","extend","identity"]),o=(e)=>{if(e.type==="Literal")return typeof e.value==="string"&&s.has(e.value);if(e.type==="Identifier")return s.has(e.name);if(e.type==="MemberExpression")return e.property.type==="Identifier"&&s.has(e.property.name);if(e.type==="ObjectExpression")return e.properties.some((t)=>t.type==="Property"&&o(t.value));return!1};export const interpolateNeedsClamp=l("Give `interpolate()` a fourth argument that clamps. Without one, or with `EXTEND` or `IDENTITY`, the output keeps going past the ends of the input range.",{createOnce(e){return{before(){return p(e,"interpolate")},CallExpression(t){const{callee:n,arguments:r}=t;if(n.type!=="Identifier"||n.name!=="interpolate")return;if(r.length<3||r.length>4)return;if(r.some((i)=>i.type==="SpreadElement"))return;const a=r[3];if(a===void 0)e.report({node:t,message:u});else if(o(a))e.report({node:a,message:f})}}}});
1
+ import{gate as o,problem as u}from"../../../lib/ast.js";const l="Pass `Extrapolation.CLAMP` as the fourth argument to `interpolate`, or the output keeps extrapolating past the input range.",f="Change this fourth argument to `Extrapolation.CLAMP` — `EXTEND` and `IDENTITY` keep extrapolating past the input range.",n=new Set(["EXTEND","IDENTITY","extend","identity"]),i=(e)=>{if(e.type==="Literal")return typeof e.value==="string"&&n.has(e.value);if(e.type==="Identifier")return n.has(e.name);if(e.type==="MemberExpression")return e.property.type==="Identifier"&&n.has(e.property.name);if(e.type==="ObjectExpression")return e.properties.some((t)=>t.type==="Property"&&i(t.value));return!1};export const interpolateNeedsClamp=u("Give `interpolate()` a fourth argument that clamps. Without one, or with `EXTEND` or `IDENTITY`, the output keeps going past the ends of the input range.",{createOnce(e){return{before(){return o(e,"interpolate")},CallExpression(t){const{callee:p,arguments:r}=t;if(p.type!=="Identifier"||p.name!=="interpolate")return;if(r.length<3||r.length>4)return;if(r.some((s)=>s.type==="SpreadElement"))return;const a=r[3];if(a===void 0)e.report({node:t,message:l});else if(i(a))e.report({node:a,message:f})}}}});
@@ -1 +1 @@
1
- import{calleeName as f,enclosingCall as m,gate as p,importedSpecifiers as c,isFunction as d,problem as h,subtreeHas as y}from"../../../lib/ast.js";const S="Keep this gate in shared or native state, or apply it through an imperative ref. A continuously evaluated worklet that schedules React state can land a Fabric commit or a Skia re-recording on an animation frame.",O=new Set(["useAnimatedReaction","useFrameCallback","useDerivedValue","useAnimatedStyle","useAnimatedProps","useAnimatedScrollHandler"]),u=(t,a)=>t.type==="ImportSpecifier"&&t.imported.type==="Identifier"&&t.imported.name===a,R=(t)=>{if(t.type==="Identifier")return[t.name];if(!d(t))return[];const a=[];y(t,(n)=>{if(n.type==="CallExpression"&&n.callee.type==="Identifier")a.push(n.callee.name);return!1});return a};export const noReactStateFromContinuousWorklet=h("A worklet that runs every frame must not send a React state setter through `scheduleOnRN`. That puts a Fabric commit on an animation frame.",{createOnce(t){const a=new Set,n=[];let i=!1,o=!1;return{before(){a.clear();n.length=0;i=!1;o=!1;return p(t,"scheduleOnRN")},ImportDeclaration(e){for(const r of c(e,"react"))if(u(r,"useState"))i=!0;for(const r of c(e,"react-native-worklets"))if(u(r,"scheduleOnRN"))o=!0},VariableDeclarator(e){const{id:r,init:s}=e;if(r.type!=="ArrayPattern")return;if(s?.type!=="CallExpression")return;if(s.callee.type!=="Identifier"||s.callee.name!=="useState")return;const l=r.elements[1];if(l?.type==="Identifier")a.add(l.name)},CallExpression(e){if(f(e)!=="scheduleOnRN")return;const r=e.arguments[0];if(r===void 0)return;if(!m(e,O))return;for(const s of R(r))n.push({node:e,name:s})},"Program:exit"(){if(!i||!o)return;for(const e of n){if(!a.has(e.name))continue;t.report({node:e.node,message:S})}}}}});
1
+ import{calleeName as f,enclosingCall as m,gate as p,importedSpecifiers as c,isFunction as d,problem as h,subtreeHas as y}from"../../../lib/ast.js";const S="Keep this gate in shared or native state, or apply it through an imperative ref — a continuous worklet lands a Fabric commit on every frame.",O=new Set(["useAnimatedReaction","useFrameCallback","useDerivedValue","useAnimatedStyle","useAnimatedProps","useAnimatedScrollHandler"]),u=(t,n)=>t.type==="ImportSpecifier"&&t.imported.type==="Identifier"&&t.imported.name===n,N=(t)=>{if(t.type==="Identifier")return[t.name];if(!d(t))return[];const n=[];y(t,(a)=>{if(a.type==="CallExpression"&&a.callee.type==="Identifier")n.push(a.callee.name);return!1});return n};export const noReactStateFromContinuousWorklet=h("A worklet that runs every frame must not send a React state setter through `scheduleOnRN`. That puts a Fabric commit on an animation frame.",{createOnce(t){const n=new Set,a=[];let i=!1,o=!1;return{before(){n.clear();a.length=0;i=!1;o=!1;return p(t,"scheduleOnRN")},ImportDeclaration(e){for(const r of c(e,"react"))if(u(r,"useState"))i=!0;for(const r of c(e,"react-native-worklets"))if(u(r,"scheduleOnRN"))o=!0},VariableDeclarator(e){const{id:r,init:s}=e;if(r.type!=="ArrayPattern")return;if(s?.type!=="CallExpression")return;if(s.callee.type!=="Identifier"||s.callee.name!=="useState")return;const l=r.elements[1];if(l?.type==="Identifier")n.add(l.name)},CallExpression(e){if(f(e)!=="scheduleOnRN")return;const r=e.arguments[0];if(r===void 0)return;if(!m(e,O))return;for(const s of N(r))a.push({node:e,name:s})},"Program:exit"(){if(!i||!o)return;for(const e of a){if(!n.has(e.name))continue;t.report({node:e.node,message:S})}}}}});
@@ -1 +1 @@
1
- import{gate as n,problem as i}from"../../../lib/ast.js";const s="Wrap this in a lazy initializer: `useSharedValue(() => compute())`, since the eager call runs on every React render.";export const preferLazySharedValueInitializer=i("Pass a computed `useSharedValue` initial value as a function. An eager call runs on every render while only the first result is kept.",{createOnce(a){return{before(){return n(a,"useSharedValue")},CallExpression(r){const{callee:t}=r;if(t.type!=="Identifier"||t.name!=="useSharedValue")return;const e=r.arguments[0];if(!e)return;if(!(e.type==="CallExpression"&&e.callee.type==="Identifier"||e.type==="NewExpression"))return;a.report({node:r,message:s})}}}});
1
+ import{gate as i,problem as n}from"../../../lib/ast.js";const s="Wrap this in a lazy initializer: `useSharedValue(() => compute())`.";export const preferLazySharedValueInitializer=n("Pass a computed `useSharedValue` initial value as a function. An eager call runs on every render while only the first result is kept.",{createOnce(a){return{before(){return i(a,"useSharedValue")},CallExpression(r){const{callee:t}=r;if(t.type!=="Identifier"||t.name!=="useSharedValue")return;const e=r.arguments[0];if(!e)return;if(!(e.type==="CallExpression"&&e.callee.type==="Identifier"||e.type==="NewExpression"))return;a.report({node:r,message:s})}}}});
@@ -1 +1 @@
1
- import{calleeName as a,gate as r,isFunction as t,problem as i}from"../../../lib/ast.js";const o="Pass a function declared in RN Runtime scope to `scheduleOnRN`; an inline callback has ambiguous runtime ownership and can be created on the wrong runtime.";export const scheduleOnRnScope=i("`scheduleOnRN` takes a function declared in RN Runtime scope. An inline callback can end up created on the wrong runtime.",{createOnce(n){return{before(){return r(n,"scheduleOnRN")},CallExpression(c){if(a(c)!=="scheduleOnRN")return;const[e]=c.arguments;if(e===void 0)return;if(t(e)&&e.type!=="FunctionDeclaration")n.report({node:e,message:o})}}}});
1
+ import{calleeName as r,gate as t,isFunction as a,problem as i}from"../../../lib/ast.js";const o="Pass a function declared in RN Runtime scope to `scheduleOnRN`; an inline callback can be created on the wrong runtime.";export const scheduleOnRnScope=i("`scheduleOnRN` takes a function declared in RN Runtime scope. An inline callback can end up created on the wrong runtime.",{createOnce(n){return{before(){return t(n,"scheduleOnRN")},CallExpression(c){if(r(c)!=="scheduleOnRN")return;const[e]=c.arguments;if(e===void 0)return;if(a(e)&&e.type!=="FunctionDeclaration")n.report({node:e,message:o})}}}});
@@ -1 +1 @@
1
- import{closestAncestor as p,crossesFunctionBefore as f,FUNCTION_TYPES as c,isMemberCall as u,problem as m,receiverName as d,subtreeHas as h}from"../../../lib/ast.js";import{isProducerCall as g}from"./shared.js";const n={renderRead:"Read this inside `useAnimatedStyle`/`useAnimatedProps`, or mirror non-visual state through an explicit callback. A `.get()` while JSX is evaluated is untracked, so the rendered value never updates.",renderWrite:"Move this write into an event handler, an effect, or an animation callback — render must stay pure.",destructure:"Keep the SharedValue object itself and read or write it with `.get()` / `.set(...)`; destructuring detaches the value from Reanimated reactivity.",nestedProperty:"Assign a new value with `.set(...)`, or use `.modify()` for a large object; mutating a property returned by `.get()` bypasses shared-value reactivity.",nestedCollection:"Assign a new collection with `.set(...)`, or mutate inside `.modify()`; mutating the collection returned by `.get()` bypasses shared-value reactivity."},y=new Set(["push","pop","shift","unshift","splice","sort","reverse","copyWithin","fill","set","add","delete","clear"]),b=new Set(["JSXExpressionContainer","JSXAttribute"]),v=(r)=>{if(r.type!=="CallExpression"||r.callee.type!=="MemberExpression")return!1;const{object:s,property:a}=r.callee;return a.type==="Identifier"&&y.has(a.name)&&u(s,"get")},S=(r)=>{const s=p(r,b);if(!s)return!1;if(f(r,s,c))return!1;return!h(s,(a)=>c.has(a.type))};export const sharedValueUsage=m("Destructuring a shared value or mutating what its `get()` returned detaches it from Reanimated reactivity. Reading or writing one while JSX evaluates also makes render impure.",{createOnce(r){const s=new Set,a=[];return{before(){s.clear();a.length=0},VariableDeclarator(e){const{id:t,init:i}=e;if(t.type==="Identifier"&&g(i)){s.add(t.name);return}if(t.type!=="ObjectPattern")return;if(i?.type!=="CallExpression")return;if(i.callee.type!=="Identifier"||i.callee.name!=="useSharedValue")return;if(t.properties.some((o)=>{if(o.type!=="Property")return!1;const{key:l}=o;return l.type==="Identifier"&&l.name==="value"}))r.report({node:e,message:n.destructure})},AssignmentExpression(e){if(e.left.type!=="MemberExpression")return;if(!u(e.left.object,"get"))return;const t=d(e.left.object);if(!t)return;a.push({node:e,shared:t,message:n.nestedProperty})},CallExpression(e){if(v(e)){r.report({node:e,message:n.nestedCollection});return}const t=u(e,"get")&&e.arguments.length===0;if(!t&&!u(e,"set"))return;const i=d(e);if(!i)return;if(!S(e))return;a.push({node:e,shared:i,message:t?n.renderRead:n.renderWrite})},"Program:exit"(){for(const{node:e,shared:t,message:i}of a)if(s.has(t))r.report({node:e,message:i})}}}});
1
+ import{closestAncestor as p,crossesFunctionBefore as d,FUNCTION_TYPES as c,isMemberCall as u,problem as m,receiverName as f,subtreeHas as h}from"../../../lib/ast.js";import{isProducerCall as g}from"./shared.js";const i={renderRead:"Read this inside `useAnimatedStyle`/`useAnimatedProps`, or mirror non-visual state through a callback — a `.get()` during render is untracked, so the value never updates.",renderWrite:"Move this write into an event handler, an effect, or an animation callback — render must stay pure.",destructure:"Keep the SharedValue object itself and use `.get()` / `.set(...)` — destructuring detaches the value from reactivity.",nestedProperty:"Assign a new value with `.set(...)`, or `.modify()` for a large object — mutating what `.get()` returns bypasses reactivity.",nestedCollection:"Assign a new collection with `.set(...)`, or mutate inside `.modify()` — mutating what `.get()` returns bypasses reactivity."},y=new Set(["push","pop","shift","unshift","splice","sort","reverse","copyWithin","fill","set","add","delete","clear"]),b=new Set(["JSXExpressionContainer","JSXAttribute"]),v=(r)=>{if(r.type!=="CallExpression"||r.callee.type!=="MemberExpression")return!1;const{object:s,property:a}=r.callee;return a.type==="Identifier"&&y.has(a.name)&&u(s,"get")},S=(r)=>{const s=p(r,b);if(!s)return!1;if(d(r,s,c))return!1;return!h(s,(a)=>c.has(a.type))};export const sharedValueUsage=m("Destructuring a shared value or mutating what its `get()` returned detaches it from Reanimated reactivity. Reading or writing one while JSX evaluates also makes render impure.",{createOnce(r){const s=new Set,a=[];return{before(){s.clear();a.length=0},VariableDeclarator(e){const{id:t,init:n}=e;if(t.type==="Identifier"&&g(n)){s.add(t.name);return}if(t.type!=="ObjectPattern")return;if(n?.type!=="CallExpression")return;if(n.callee.type!=="Identifier"||n.callee.name!=="useSharedValue")return;if(t.properties.some((o)=>{if(o.type!=="Property")return!1;const{key:l}=o;return l.type==="Identifier"&&l.name==="value"}))r.report({node:e,message:i.destructure})},AssignmentExpression(e){if(e.left.type!=="MemberExpression")return;if(!u(e.left.object,"get"))return;const t=f(e.left.object);if(!t)return;a.push({node:e,shared:t,message:i.nestedProperty})},CallExpression(e){if(v(e)){r.report({node:e,message:i.nestedCollection});return}const t=u(e,"get")&&e.arguments.length===0;if(!t&&!u(e,"set"))return;const n=f(e);if(!n)return;if(!S(e))return;a.push({node:e,shared:n,message:t?i.renderRead:i.renderWrite})},"Program:exit"(){for(const{node:e,shared:t,message:n}of a)if(s.has(t))r.report({node:e,message:n})}}}});
@@ -1 +1 @@
1
- import{attributeName as p,gate as f,importedSpecifiers as m,problem as l,tagPath as u}from"../../../lib/ast.js";const d="Add an explicit `opaque` prop to this `<Canvas>`: `opaque={Platform.OS === 'android'}` for a fullscreen animated canvas, or `opaque={false}` when it needs transparency, view transforms, or ordinary stacking.";export const canvasOpaque=l("Require an explicit `opaque` prop on a Skia `<Canvas>`. A fullscreen animated canvas wants it on; anything that needs transparency or a view transform wants it off.",{createOnce(o){const n=new Set,r=new Set,s=[],i=(a)=>{const e=u(a),t=e.lastIndexOf(".");if(t===-1)return n.has(e);return r.has(e.slice(0,t))&&e.slice(t+1)==="Canvas"};return{before(){n.clear();r.clear();s.length=0;return f(o,"react-native-skia")},ImportDeclaration(a){for(const e of m(a,"@shopify/react-native-skia")){if(e.type==="ImportSpecifier"){const{imported:t,local:c}=e;if(t.type==="Identifier"&&t.name==="Canvas")n.add(c.name);continue}if(e.type==="ImportNamespaceSpecifier"||e.type==="ImportDefaultSpecifier")r.add(e.local.name)}},JSXOpeningElement(a){if(!i(a.name))return;if(!a.attributes.some((t)=>t.type==="JSXAttribute"&&p(t)==="opaque"))s.push(a)},"Program:exit"(){for(const a of s)o.report({node:a,message:d})}}}});
1
+ import{attributeName as c,gate as f,importedSpecifiers as m,problem as l,tagPath as u}from"../../../lib/ast.js";const d="Add an explicit `opaque` prop to this `<Canvas>`: `opaque={Platform.OS === 'android'}` when fullscreen and animated, `opaque={false}` when it needs transparency or transforms.";export const canvasOpaque=l("Require an explicit `opaque` prop on a Skia `<Canvas>`. A fullscreen animated canvas wants it on; anything that needs transparency or a view transform wants it off.",{createOnce(o){const n=new Set,r=new Set,s=[],i=(a)=>{const e=u(a),t=e.lastIndexOf(".");if(t===-1)return n.has(e);return r.has(e.slice(0,t))&&e.slice(t+1)==="Canvas"};return{before(){n.clear();r.clear();s.length=0;return f(o,"react-native-skia")},ImportDeclaration(a){for(const e of m(a,"@shopify/react-native-skia")){if(e.type==="ImportSpecifier"){const{imported:t,local:p}=e;if(t.type==="Identifier"&&t.name==="Canvas")n.add(p.name);continue}if(e.type==="ImportNamespaceSpecifier"||e.type==="ImportDefaultSpecifier")r.add(e.local.name)}},JSXOpeningElement(a){if(!i(a.name))return;if(!a.attributes.some((t)=>t.type==="JSXAttribute"&&c(t)==="opaque"))s.push(a)},"Program:exit"(){for(const a of s)o.report({node:a,message:d})}}}});
@@ -1 +1 @@
1
- import{defineModule as e}from"../../../lib/module.js";import{canvasOpaque as a}from"./canvas-opaque.js";export default e({meta:{name:"@ashstack/skia"},url:import.meta.url,packages:["@shopify/react-native-skia"],option:"skia",docsWhen:"auto-enabled when `@shopify/react-native-skia` is a dependency",rules:{"canvas-opaque":a},restrictedImports:{paths:[{name:"@shopify/react-native-skia",importNames:["usePathValue","usePathInterpolation"],message:"Keep one stable `SkPath` buffer and mutate it inside `useDerivedValue`, with consumers reading the driving shared value. These legacy path-value hooks self-dirty Reanimated mappers and re-record idle canvases."}]}});
1
+ import{defineModule as e}from"../../../lib/module.js";import{canvasOpaque as a}from"./canvas-opaque.js";export default e({meta:{name:"@ashstack/skia"},url:import.meta.url,packages:["@shopify/react-native-skia"],option:"skia",docsWhen:"auto-enabled when `@shopify/react-native-skia` is a dependency",rules:{"canvas-opaque":a},restrictedImports:{paths:[{name:"@shopify/react-native-skia",importNames:["usePathValue","usePathInterpolation"],message:"Keep one stable `SkPath` buffer and mutate it inside `useDerivedValue`, with consumers reading the driving shared value."}]}});
@@ -1 +1 @@
1
- import{attributeName as s,componentName as a,gate as i,problem as n}from"../../../lib/ast.js";const m="Add `resize` to this TurboImage, set slightly BELOW the rendered width in points, so the native decoder downsamples before the bitmap reaches memory. Rounding down costs nothing visible; a value above the rendered width keeps the full-resolution decode (a 4000px photo in a 100pt avatar holds roughly 48MB of pixels).";export const requireResize=n("Require `resize` on a TurboImage so the native decoder downsamples before the bitmap reaches memory. A full-resolution decode wastes tens of megabytes and stalls the first frame.",{createOnce(r){return{before(){return i(r,"TurboImage")},JSXOpeningElement(t){if(a(t.name)!=="TurboImage")return;const{attributes:o}=t;if(o.some((e)=>e.type==="JSXSpreadAttribute"))return;if(o.some((e)=>e.type==="JSXAttribute"&&s(e)==="resize"))return;r.report({node:t.name,message:m})}}}});
1
+ import{attributeName as o,componentName as a,gate as i,problem as m}from"../../../lib/ast.js";const n="Add `resize` to this TurboImage, set slightly BELOW the rendered width in points, so the native decoder downsamples before the bitmap reaches memory.";export const requireResize=m("Require `resize` on a TurboImage so the native decoder downsamples before the bitmap reaches memory. A full-resolution decode wastes tens of megabytes and stalls the first frame.",{createOnce(r){return{before(){return i(r,"TurboImage")},JSXOpeningElement(t){if(a(t.name)!=="TurboImage")return;const{attributes:s}=t;if(s.some((e)=>e.type==="JSXSpreadAttribute"))return;if(s.some((e)=>e.type==="JSXAttribute"&&o(e)==="resize"))return;r.report({node:t.name,message:n})}}}});
@@ -1 +1 @@
1
- import{calleeName as n,problem as m}from"../../../lib/ast.js";import{readsTheme as h}from"./shared.js";const d=new Set(["useAnimatedStyle","useDerivedValue","useAnimatedProps"]),l="Use `useAnimatedTheme()` and read its shared value inside the Reanimated worklet so theme changes reach the UI thread.";export const animatedTheme=m("A Reanimated worklet hook cannot see `useUnistyles()` theme changes on the UI thread. Read the shared value from `useAnimatedTheme()` there instead.",{createOnce(i){let t=!1;const a=[];return{before(){t=!1;a.length=0},VariableDeclarator(e){const{id:s,init:o}=e;if(s.type==="ObjectPattern"&&n(o)==="useUnistyles"&&s.properties.some((r)=>r.type==="Property"&&r.key.type==="Identifier"&&r.key.name==="theme"))t=!0},CallExpression(e){if(!d.has(n(e)))return;if(!h(e))return;a.push(e)},"Program:exit"(){if(!t)return;for(const e of a)i.report({node:e,message:l})}}}});
1
+ import{calleeName as n,problem as m}from"../../../lib/ast.js";import{readsTheme as h}from"./shared.js";const d=new Set(["useAnimatedStyle","useDerivedValue","useAnimatedProps"]),l="Use `useAnimatedTheme()` and read its shared value inside the worklet, so theme changes reach the UI thread.";export const animatedTheme=m("A Reanimated worklet hook cannot see `useUnistyles()` theme changes on the UI thread. Read the shared value from `useAnimatedTheme()` there instead.",{createOnce(i){let t=!1;const a=[];return{before(){t=!1;a.length=0},VariableDeclarator(e){const{id:s,init:o}=e;if(s.type==="ObjectPattern"&&n(o)==="useUnistyles"&&s.properties.some((r)=>r.type==="Property"&&r.key.type==="Identifier"&&r.key.name==="theme"))t=!0},CallExpression(e){if(!d.has(n(e)))return;if(!h(e))return;a.push(e)},"Program:exit"(){if(!t)return;for(const e of a)i.report({node:e,message:l})}}}});
@@ -1 +1 @@
1
- import{calleeName as f,findInSubtree as u,gate as b,problem as d,subtreeHas as y,tagIdentifier as S}from"../../../lib/ast.js";import{isStyleSheetCreate as w,memberPath as l,propertyName as C,stylesObjectOf as g}from"./shared.js";const h={contentContainerRuntime:"Wrap this component with `withUnistyles`, or pass `contentContainerStyle` a plain hook-fed style. On a raw component `contentContainerStyle` never subscribes to Unistyles updates.",contentContainerTheme:"Wrap this component with `withUnistyles`, or pass `contentContainerStyle` a plain hook-fed style. On a raw component `contentContainerStyle` never subscribes to theme changes."};export const contentContainer=d("A raw component never subscribes its `contentContainerStyle` to theme or `rt` updates. Wrap the component with `withUnistyles` when the style depends on either.",{createOnce(p){const a=new Map,c=new Set,m=[];return{before(){a.clear();c.clear();m.length=0;return b(p,"contentContainerStyle")},VariableDeclarator(e){const{id:r,init:n}=e;if(r.type!=="Identifier")return;if(f(n)==="withUnistyles"){c.add(r.name);return}if(n?.type!=="CallExpression"||!w(n))return;const s=g(n.arguments[0]);if(!s)return;const i=new Map;for(const o of s.properties){if(o.type!=="Property")continue;i.set(C(o),o.value)}a.set(r.name,i)},JSXAttribute(e){if(e.name.type!=="JSXIdentifier"||e.name.name!=="contentContainerStyle")return;m.push(e)},"Program:exit"(){for(const e of m){const r=u(e.value,(t)=>t.type==="MemberExpression"&&t.object.type==="Identifier"&&a.has(t.object.name));if(r?.type!=="MemberExpression")continue;const n=e.parent;if(n.type==="JSXOpeningElement"&&c.has(S(n.name)))continue;const{object:s,property:i}=r;if(s.type!=="Identifier")continue;const o=a.get(s.name)?.get(i.type==="Identifier"?i.name:"");if(!o)continue;if(y(o,(t)=>t.type==="MemberExpression"&&l(t).startsWith("rt."))){p.report({node:e,message:h.contentContainerRuntime});continue}if(y(o,(t)=>t.type==="MemberExpression"&&l(t).startsWith("theme.")))p.report({node:e,message:h.contentContainerTheme})}}}}});
1
+ import{calleeName as u,findInSubtree as l,gate as b,problem as d,subtreeHas as y,tagIdentifier as w}from"../../../lib/ast.js";import{isStyleSheetCreate as S,memberPath as h,propertyName as C,stylesObjectOf as g}from"./shared.js";const f={contentContainerRuntime:"Wrap this component with `withUnistyles`, or pass `contentContainerStyle` a hook-fed style — on a raw component it never subscribes to Unistyles updates.",contentContainerTheme:"Wrap this component with `withUnistyles`, or pass `contentContainerStyle` a hook-fed style — on a raw component it never subscribes to theme changes."};export const contentContainer=d("A raw component never subscribes its `contentContainerStyle` to theme or `rt` updates. Wrap the component with `withUnistyles` when the style depends on either.",{createOnce(p){const a=new Map,c=new Set,m=[];return{before(){a.clear();c.clear();m.length=0;return b(p,"contentContainerStyle")},VariableDeclarator(e){const{id:r,init:n}=e;if(r.type!=="Identifier")return;if(u(n)==="withUnistyles"){c.add(r.name);return}if(n?.type!=="CallExpression"||!S(n))return;const s=g(n.arguments[0]);if(!s)return;const i=new Map;for(const o of s.properties){if(o.type!=="Property")continue;i.set(C(o),o.value)}a.set(r.name,i)},JSXAttribute(e){if(e.name.type!=="JSXIdentifier"||e.name.name!=="contentContainerStyle")return;m.push(e)},"Program:exit"(){for(const e of m){const r=l(e.value,(t)=>t.type==="MemberExpression"&&t.object.type==="Identifier"&&a.has(t.object.name));if(r?.type!=="MemberExpression")continue;const n=e.parent;if(n.type==="JSXOpeningElement"&&c.has(w(n.name)))continue;const{object:s,property:i}=r;if(s.type!=="Identifier")continue;const o=a.get(s.name)?.get(i.type==="Identifier"?i.name:"");if(!o)continue;if(y(o,(t)=>t.type==="MemberExpression"&&h(t).startsWith("rt."))){p.report({node:e,message:f.contentContainerRuntime});continue}if(y(o,(t)=>t.type==="MemberExpression"&&h(t).startsWith("theme.")))p.report({node:e,message:f.contentContainerTheme})}}}}});
@@ -1 +1 @@
1
- import{calleeName as h,subtreeHas as p}from"../../../lib/ast.js";import{inCreate as m,isStyleSheetCreate as u,memberPath as l,propertyName as c}from"./shared.js";const S=new Set(["paddingLeft","paddingRight","marginLeft","marginRight"]),f=new Set(["shadowColor","shadowOffset","shadowOpacity","shadowRadius","elevation"]),i={logicalSpacing:"Use `paddingStart`/`paddingEnd` or `marginStart`/`marginEnd` here so the directional spacing mirrors in RTL.",rtlInSheet:"Use `rt.rtl` for RTL-dependent styles so Unistyles tracks the dependency natively.",screenDimensions:"Use reactive `rt.screen` inside `StyleSheet.create` instead of a `Dimensions.get` snapshot.",pixelRatio:"Use reactive `rt.pixelRatio` inside `StyleSheet.create`.",fontScale:"Use reactive `rt.fontScale` inside `StyleSheet.create`.",statusBar:"Use reactive `rt.statusBar.height` inside `StyleSheet.create`.",colorScheme:"Use the Unistyles theme or reactive `rt.colorScheme` inside `StyleSheet.create`.",fullRuntime:"Use the injected mini runtime (`rt`) inside `StyleSheet.create` instead of `UnistylesRuntime`, so the style recalculates on its own.",fullRuntimeDestructure:"Destructure from the injected mini runtime (`rt`) inside `StyleSheet.create`, not from `UnistylesRuntime`.",themeScreen:"Use reactive `rt.screen`, or Unistyles breakpoints, instead of the theme's module-initialization screen snapshot.",borderCurve:'Add `borderCurve` (normally `"continuous"`) alongside `borderRadius`, or the corner renders as a circular arc rather than the squircle native iOS controls use.',legacyShadow:'Use `boxShadow` with CSS syntax, e.g. `boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)"`, which renders on both platforms on the New Architecture, instead of the legacy `shadow*`/`elevation` props.',asConst:"Drop `as const` here — the typed `StyleSheet.create` already narrows the literal. If TS still widens the value to `string`, fix the `StyleSheet` import instead."};export const inSheet=m("Inside `StyleSheet.create`, require `rt` and theme values rather than Dimensions, PixelRatio, Appearance, I18nManager, StatusBar or `UnistylesRuntime`. It also covers logical spacing, `boxShadow`, `borderCurve` and a redundant `as const`.",(s,a)=>({CallExpression(e){if(!a()||u(e))return;const{callee:r}=e,t=r.type==="MemberExpression"?l(r):h(e);if(t==="Dimensions.get")s.report({node:e,message:i.screenDimensions});else if(t==="PixelRatio.get")s.report({node:e,message:i.pixelRatio});else if(t==="PixelRatio.getFontScale")s.report({node:e,message:i.fontScale});else if(t==="Appearance.getColorScheme")s.report({node:e,message:i.colorScheme});else if(t==="useColorScheme")s.report({node:e,message:i.colorScheme})},MemberExpression(e){if(!a())return;const{parent:r}=e;if(r.type==="MemberExpression"&&r.object===e)return;const t=l(e);if(t==="I18nManager.isRTL"){s.report({node:e,message:i.rtlInSheet});return}if(t==="StatusBar.currentHeight"){s.report({node:e,message:i.statusBar});return}if(t.startsWith("UnistylesRuntime.")){s.report({node:e,message:i.fullRuntime});return}if(t.startsWith("theme.screen."))s.report({node:e,message:i.themeScreen})},VariableDeclarator(e){if(!a())return;const{id:r,init:t}=e;if(r.type!=="ObjectPattern")return;if(t?.type==="Identifier"&&t.name==="UnistylesRuntime")s.report({node:e,message:i.fullRuntimeDestructure})},TSAsExpression(e){if(!a())return;const{typeAnnotation:r}=e;if(r.type!=="TSTypeReference")return;const{typeName:t}=r;if(t.type!=="Identifier"||t.name!=="const")return;s.report({node:e,message:i.asConst})},ObjectExpression(e){if(!a())return;let r=null,t=!1;for(const n of e.properties){if(n.type!=="Property")continue;const o=c(n);if(o==="borderRadius")r=n;else if(o==="borderCurve")t=!0}if(r&&!t)s.report({node:r,message:i.borderCurve})},Property(e){if(!a())return;const r=c(e);if(f.has(r)){s.report({node:e,message:i.legacyShadow});return}if(!S.has(r))return;if(!p(e,(n)=>{if(n.type!=="MemberExpression")return!1;const o=l(n);return o==="rt.insets.left"||o==="rt.insets.right"}))s.report({node:e,message:i.logicalSpacing})}}));
1
+ import{calleeName as p,subtreeHas as m}from"../../../lib/ast.js";import{inCreate as h,isStyleSheetCreate as u,memberPath as l,propertyName as c}from"./shared.js";const S=new Set(["paddingLeft","paddingRight","marginLeft","marginRight"]),f=new Set(["shadowColor","shadowOffset","shadowOpacity","shadowRadius","elevation"]),i={logicalSpacing:"Use `paddingStart`/`paddingEnd` or `marginStart`/`marginEnd` here, so the spacing mirrors in RTL.",rtlInSheet:"Use `rt.rtl` for RTL-dependent styles so Unistyles tracks the dependency natively.",screenDimensions:"Use reactive `rt.screen` inside `StyleSheet.create` instead of a `Dimensions.get` snapshot.",pixelRatio:"Use reactive `rt.pixelRatio` inside `StyleSheet.create`.",fontScale:"Use reactive `rt.fontScale` inside `StyleSheet.create`.",statusBar:"Use reactive `rt.statusBar.height` inside `StyleSheet.create`.",colorScheme:"Use the Unistyles theme or reactive `rt.colorScheme` inside `StyleSheet.create`.",fullRuntime:"Use the injected mini runtime (`rt`) inside `StyleSheet.create` instead of `UnistylesRuntime`, so the style recalculates itself.",fullRuntimeDestructure:"Destructure from the injected mini runtime (`rt`) inside `StyleSheet.create`, not from `UnistylesRuntime`.",themeScreen:"Use reactive `rt.screen`, or Unistyles breakpoints — `theme.screen` is a module-initialization snapshot.",borderCurve:'Add `borderCurve` (normally `"continuous"`) alongside `borderRadius`, or the corner renders as a circular arc instead of a squircle.',legacyShadow:'Use `boxShadow` with CSS syntax, e.g. `boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)"`, instead of the legacy `shadow*`/`elevation` props.',asConst:"Drop `as const` here — the typed `StyleSheet.create` already narrows the literal. If TS still widens it to `string`, fix the `StyleSheet` import instead."};export const inSheet=h("Inside `StyleSheet.create`, require `rt` and theme values rather than Dimensions, PixelRatio, Appearance, I18nManager, StatusBar or `UnistylesRuntime`. It also covers logical spacing, `boxShadow`, `borderCurve` and a redundant `as const`.",(s,a)=>({CallExpression(e){if(!a()||u(e))return;const{callee:r}=e,t=r.type==="MemberExpression"?l(r):p(e);if(t==="Dimensions.get")s.report({node:e,message:i.screenDimensions});else if(t==="PixelRatio.get")s.report({node:e,message:i.pixelRatio});else if(t==="PixelRatio.getFontScale")s.report({node:e,message:i.fontScale});else if(t==="Appearance.getColorScheme")s.report({node:e,message:i.colorScheme});else if(t==="useColorScheme")s.report({node:e,message:i.colorScheme})},MemberExpression(e){if(!a())return;const{parent:r}=e;if(r.type==="MemberExpression"&&r.object===e)return;const t=l(e);if(t==="I18nManager.isRTL"){s.report({node:e,message:i.rtlInSheet});return}if(t==="StatusBar.currentHeight"){s.report({node:e,message:i.statusBar});return}if(t.startsWith("UnistylesRuntime.")){s.report({node:e,message:i.fullRuntime});return}if(t.startsWith("theme.screen."))s.report({node:e,message:i.themeScreen})},VariableDeclarator(e){if(!a())return;const{id:r,init:t}=e;if(r.type!=="ObjectPattern")return;if(t?.type==="Identifier"&&t.name==="UnistylesRuntime")s.report({node:e,message:i.fullRuntimeDestructure})},TSAsExpression(e){if(!a())return;const{typeAnnotation:r}=e;if(r.type!=="TSTypeReference")return;const{typeName:t}=r;if(t.type!=="Identifier"||t.name!=="const")return;s.report({node:e,message:i.asConst})},ObjectExpression(e){if(!a())return;let r=null,t=!1;for(const n of e.properties){if(n.type!=="Property")continue;const o=c(n);if(o==="borderRadius")r=n;else if(o==="borderCurve")t=!0}if(r&&!t)s.report({node:r,message:i.borderCurve})},Property(e){if(!a())return;const r=c(e);if(f.has(r)){s.report({node:e,message:i.legacyShadow});return}if(!S.has(r))return;if(!m(e,(n)=>{if(n.type!=="MemberExpression")return!1;const o=l(n);return o==="rt.insets.left"||o==="rt.insets.right"}))s.report({node:e,message:i.logicalSpacing})}}));
@@ -1 +1 @@
1
- import{defineModule as e}from"../../../lib/module.js";import{animatedTheme as t}from"./animated-theme.js";import{contentContainer as n}from"./content-container.js";import{inSheet as a}from"./in-sheet.js";import{insets as r}from"./insets.js";import{noHardcodedColor as s}from"./no-hardcoded-color.js";import{noHardcodedSpacing as o}from"./no-hardcoded-spacing.js";import{noMargin as i}from"./no-margin.js";import{noStyleSpread as m}from"./no-style-spread.js";import{noUnusedStyles as l}from"./no-unused-styles.js";import{rtlStyleCall as c}from"./rtl-style-call.js";import{themeScreenComponent as d}from"./theme-screen-component.js";import{themeStyleAttr as p}from"./theme-style-attr.js";export default e({meta:{name:"@ashstack/unistyles"},rules:{"animated-theme":t,"content-container":n,"in-sheet":a,insets:r,"no-hardcoded-color":s,"no-hardcoded-spacing":o,"no-margin":i,"no-style-spread":m,"no-unused-styles":l,"rtl-style-call":c,"theme-screen-component":d,"theme-style-attr":p},url:import.meta.url,packages:["react-native-unistyles"],option:"unistyles",docsWhen:"auto-enabled when `react-native-unistyles` is a dependency",restrictedImports:{paths:[{name:"react-native",importNames:["StyleSheet"],message:"Import `StyleSheet` from `react-native-unistyles` instead of `react-native`."},{name:"react-native",importNames:["useWindowDimensions","Dimensions"],message:"Read screen size from `rt.screen` inside `StyleSheet.create`, or `UnistylesRuntime.screen` outside it, instead of the `Dimensions` APIs."},{name:"react-native",importNames:["SafeAreaView"],message:'Set `contentInsetAdjustmentBehavior="automatic"` on the scroll view, or apply `rt.insets` from `react-native-unistyles`, instead of `SafeAreaView`.'},{name:"react-native-safe-area-context",importNames:["useSafeAreaInsets","useSafeAreaFrame","SafeAreaView"],message:"Read safe-area values from `rt.insets`, or `UnistylesRuntime.insets` outside a stylesheet, in `react-native-unistyles`."}]}});
1
+ import{defineModule as e}from"../../../lib/module.js";import{animatedTheme as t}from"./animated-theme.js";import{contentContainer as n}from"./content-container.js";import{inSheet as a}from"./in-sheet.js";import{insets as r}from"./insets.js";import{noHardcodedColor as s}from"./no-hardcoded-color.js";import{noHardcodedSpacing as o}from"./no-hardcoded-spacing.js";import{noMargin as i}from"./no-margin.js";import{noParamlessDynamicFunction as m}from"./no-paramless-dynamic-function.js";import{noStyleSpread as c}from"./no-style-spread.js";import{noUnusedStyles as l}from"./no-unused-styles.js";import{rtlStyleCall as d}from"./rtl-style-call.js";import{themeScreenComponent as p}from"./theme-screen-component.js";import{themeStyleAttr as f}from"./theme-style-attr.js";export default e({meta:{name:"@ashstack/unistyles"},rules:{"animated-theme":t,"content-container":n,"in-sheet":a,insets:r,"no-hardcoded-color":s,"no-hardcoded-spacing":o,"no-margin":i,"no-paramless-dynamic-function":m,"no-style-spread":c,"no-unused-styles":l,"rtl-style-call":d,"theme-screen-component":p,"theme-style-attr":f},url:import.meta.url,packages:["react-native-unistyles"],option:"unistyles",docsWhen:"auto-enabled when `react-native-unistyles` is a dependency",restrictedImports:{paths:[{name:"react-native",importNames:["StyleSheet"],message:"Import `StyleSheet` from `react-native-unistyles` instead of `react-native`."},{name:"react-native",importNames:["useWindowDimensions","Dimensions"],message:"Read screen size from `rt.screen` inside `StyleSheet.create`, or `UnistylesRuntime.screen` outside it, instead of the `Dimensions` APIs."},{name:"react-native",importNames:["SafeAreaView"],message:'Set `contentInsetAdjustmentBehavior="automatic"` on the scroll view, or apply `rt.insets` from `react-native-unistyles`, instead of `SafeAreaView`.'},{name:"react-native-safe-area-context",importNames:["useSafeAreaInsets","useSafeAreaFrame","SafeAreaView"],message:"Read safe-area values from `rt.insets`, or `UnistylesRuntime.insets` outside a stylesheet, in `react-native-unistyles`."}]}});
@@ -1 +1 @@
1
- import{calleeName as y,closestAncestor as S,gate as b,problem as d,subtreeHas as u}from"../../../lib/ast.js";import{isStyleSheetCreate as g}from"./shared.js";const m=/(?:inset|safearea|top|bottom|left|right)/i,p={insetsStyleCall:"Read safe-area values from `rt.insets` inside `StyleSheet.create` instead of passing `useSafeAreaInsets()` into a style function.",insetsAttribute:"Resolve these safe-area values through `rt.insets` in `StyleSheet.create` instead of a hook-fed inline JSX style object."},i=(t)=>{if(t.type==="Identifier")return[t.name];if(t.type==="AssignmentPattern")return i(t.left);if(t.type==="RestElement")return i(t.argument);if(t.type==="ArrayPattern")return t.elements.flatMap((s)=>s?i(s):[]);if(t.type!=="ObjectPattern")return[];return t.properties.flatMap((s)=>s.type==="Property"?i(s.value):i(s.argument))};export const insets=d("Disallow passing `useSafeAreaInsets()` values into a dynamic style function or an inline JSX style object. Read `rt.insets` inside `StyleSheet.create` instead.",{createOnce(t){const s=new Set,o=new Set,f=[],l=[];return{before(){s.clear();o.clear();f.length=0;l.length=0;return b(t,"useSafeAreaInsets")},VariableDeclarator(r){const{id:e,init:n}=r;if(e.type==="Identifier"&&g(n)){o.add(e.name);return}if(y(n)!=="useSafeAreaInsets")return;for(const a of i(e))s.add(a)},CallExpression(r){const{callee:e}=r;if(e.type!=="MemberExpression")return;const n=e.object;if(n.type!=="Identifier")return;const a=S(r,new Set(["JSXAttribute"]));if(a?.type==="JSXAttribute"&&a.name.type==="JSXIdentifier"&&a.name.name==="contentContainerStyle")return;f.push({node:r,receiver:n.name})},JSXAttribute(r){if(r.name.type!=="JSXIdentifier"||r.name.name!=="style")return;l.push(r)},"Program:exit"(){const r=(e)=>u(e,(n)=>n.type==="Identifier"&&s.has(n.name));for(const e of f){if(!o.has(e.receiver))continue;const n=t.sourceCode.getText(e.node);if(!m.test(n))continue;if(!r(e.node))continue;t.report({node:e.node,message:p.insetsStyleCall})}for(const e of l){const n=t.sourceCode.getText(e);if(!m.test(n))continue;if(!u(e,(c)=>c.type==="MemberExpression"&&c.object.type==="Identifier"&&s.has(c.object.name)))continue;t.report({node:e,message:p.insetsAttribute})}}}}});
1
+ import{calleeName as y,closestAncestor as S,gate as b,problem as d,subtreeHas as u}from"../../../lib/ast.js";import{isStyleSheetCreate as g}from"./shared.js";const m=/(?:inset|safearea|top|bottom|left|right)/i,p={insetsStyleCall:"Read safe-area values from `rt.insets` inside `StyleSheet.create` instead of passing `useSafeAreaInsets()` into a style function.",insetsAttribute:"Resolve these safe-area values through `rt.insets` in `StyleSheet.create` instead of a hook-fed inline style object."},i=(t)=>{if(t.type==="Identifier")return[t.name];if(t.type==="AssignmentPattern")return i(t.left);if(t.type==="RestElement")return i(t.argument);if(t.type==="ArrayPattern")return t.elements.flatMap((s)=>s?i(s):[]);if(t.type!=="ObjectPattern")return[];return t.properties.flatMap((s)=>s.type==="Property"?i(s.value):i(s.argument))};export const insets=d("Disallow passing `useSafeAreaInsets()` values into a dynamic style function or an inline JSX style object. Read `rt.insets` inside `StyleSheet.create` instead.",{createOnce(t){const s=new Set,o=new Set,f=[],l=[];return{before(){s.clear();o.clear();f.length=0;l.length=0;return b(t,"useSafeAreaInsets")},VariableDeclarator(r){const{id:e,init:n}=r;if(e.type==="Identifier"&&g(n)){o.add(e.name);return}if(y(n)!=="useSafeAreaInsets")return;for(const a of i(e))s.add(a)},CallExpression(r){const{callee:e}=r;if(e.type!=="MemberExpression")return;const n=e.object;if(n.type!=="Identifier")return;const a=S(r,new Set(["JSXAttribute"]));if(a?.type==="JSXAttribute"&&a.name.type==="JSXIdentifier"&&a.name.name==="contentContainerStyle")return;f.push({node:r,receiver:n.name})},JSXAttribute(r){if(r.name.type!=="JSXIdentifier"||r.name.name!=="style")return;l.push(r)},"Program:exit"(){const r=(e)=>u(e,(n)=>n.type==="Identifier"&&s.has(n.name));for(const e of f){if(!o.has(e.receiver))continue;const n=t.sourceCode.getText(e.node);if(!m.test(n))continue;if(!r(e.node))continue;t.report({node:e.node,message:p.insetsStyleCall})}for(const e of l){const n=t.sourceCode.getText(e);if(!m.test(n))continue;if(!u(e,(c)=>c.type==="MemberExpression"&&c.object.type==="Identifier"&&s.has(c.object.name)))continue;t.report({node:e,message:p.insetsAttribute})}}}}});
@@ -1 +1 @@
1
- import{ancestors as o}from"../../../lib/ast.js";import{inCreate as s,propertyName as a}from"./shared.js";const n=/^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/,i=/^(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\(/i,l="Use a `theme.colors` token for this value — a raw color bypasses dark mode and never shifts with the theme. If no token fits, add one to the theme.",c=/^colors?$|Colors?$/,f=(t)=>{for(const e of o(t)){if(e.type==="ArrayExpression")continue;if(e.type!=="Property")return!1;return c.test(a(e))}return!1};export const noHardcodedColor=s("Disallow hex and CSS-function colors inside `StyleSheet.create`. A raw color skips dark mode and never changes with the theme.",(t,e)=>({Literal(r){if(!e())return;if(typeof r.value!=="string")return;if(!n.test(r.value)&&!i.test(r.value))return;if(!f(r))return;t.report({node:r,message:l})}}));
1
+ import{ancestors as o}from"../../../lib/ast.js";import{inCreate as s,propertyName as n}from"./shared.js";const a=/^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/,i=/^(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\(/i,l="Use a `theme.colors` token for this value; add one to the theme if none fits.",c=/^colors?$|Colors?$/,f=(t)=>{for(const e of o(t)){if(e.type==="ArrayExpression")continue;if(e.type!=="Property")return!1;return c.test(n(e))}return!1};export const noHardcodedColor=s("Disallow hex and CSS-function colors inside `StyleSheet.create`. A raw color skips dark mode and never changes with the theme.",(t,e)=>({Literal(r){if(!e())return;if(typeof r.value!=="string")return;if(!a.test(r.value)&&!i.test(r.value))return;if(!f(r))return;t.report({node:r,message:l})}}));
@@ -1 +1 @@
1
- import{findInSubtree as m,hasAncestor as g,subtreeHas as l}from"../../../lib/ast.js";import{inCreate as d,memberPath as n,propertyName as h}from"./shared.js";const f=new Set(["padding","paddingTop","paddingBottom","paddingHorizontal","paddingVertical","paddingStart","paddingEnd","margin","marginTop","marginBottom","marginHorizontal","marginVertical","marginStart","marginEnd","gap","rowGap","columnGap","borderRadius","borderTopLeftRadius","borderTopRightRadius","borderBottomLeftRadius","borderBottomRightRadius","fontSize","lineHeight"]),r=/^-?[1-9][0-9]*(?:\.[0-9]+)?$/,c="Use `theme.spacing[...]` or `theme.sizing.scale(...)` for this value, so a rhythm change stays one edit. If no token fits, add one to the theme.",p=(i)=>g(i,(t)=>{if(t.type==="MemberExpression"){const e=n(t);if(e.startsWith("theme.spacing")||e.startsWith("theme.sizing"))return!0}if(t.type==="CallExpression"){const{callee:e}=t,s=e.type==="MemberExpression"?n(e):"";if(s==="theme.spacing.scale"||s==="theme.sizing.scale"||s==="theme.scale")return!0}return!1}),u=(i)=>l(i,(t)=>{if(t.type==="MemberExpression"){const e=n(t);return e.startsWith("theme.spacing")||e.startsWith("theme.sizing")}if(t.type==="CallExpression"&&t.callee.type==="MemberExpression"){const e=n(t.callee);return e==="theme.spacing.scale"||e==="theme.sizing.scale"||e==="theme.scale"}return!1});export const noHardcodedSpacing=d("Require `theme.spacing` or `theme.sizing.scale` for spacing, radius and type values inside `StyleSheet.create`, instead of raw numbers.",(i,t)=>({Property(e){if(!t())return;const s=h(e);if(!f.has(s))return;if(u(e.value))return;const o=m(e.value,(a)=>a.type==="Literal"&&typeof a.value==="number"&&r.test(String(a.value))&&!p(a)||a.type==="UnaryExpression"&&a.operator==="-"&&a.argument.type==="Literal"&&typeof a.argument.value==="number"&&r.test(`-${a.argument.value}`)&&!p(a));if(o)i.report({node:o,message:c})}}));
1
+ import{findInSubtree as m,hasAncestor as l,subtreeHas as g}from"../../../lib/ast.js";import{inCreate as d,memberPath as n,propertyName as h}from"./shared.js";const f=new Set(["padding","paddingTop","paddingBottom","paddingHorizontal","paddingVertical","paddingStart","paddingEnd","margin","marginTop","marginBottom","marginHorizontal","marginVertical","marginStart","marginEnd","gap","rowGap","columnGap","borderRadius","borderTopLeftRadius","borderTopRightRadius","borderBottomLeftRadius","borderBottomRightRadius","fontSize","lineHeight"]),r=/^-?[1-9][0-9]*(?:\.[0-9]+)?$/,c="Use `theme.spacing[...]` or `theme.sizing.scale(...)` for this value; add a token to the theme if none fits.",p=(i)=>l(i,(t)=>{if(t.type==="MemberExpression"){const e=n(t);if(e.startsWith("theme.spacing")||e.startsWith("theme.sizing"))return!0}if(t.type==="CallExpression"){const{callee:e}=t,s=e.type==="MemberExpression"?n(e):"";if(s==="theme.spacing.scale"||s==="theme.sizing.scale"||s==="theme.scale")return!0}return!1}),u=(i)=>g(i,(t)=>{if(t.type==="MemberExpression"){const e=n(t);return e.startsWith("theme.spacing")||e.startsWith("theme.sizing")}if(t.type==="CallExpression"&&t.callee.type==="MemberExpression"){const e=n(t.callee);return e==="theme.spacing.scale"||e==="theme.sizing.scale"||e==="theme.scale"}return!1});export const noHardcodedSpacing=d("Require `theme.spacing` or `theme.sizing.scale` for spacing, radius and type values inside `StyleSheet.create`, instead of raw numbers.",(i,t)=>({Property(e){if(!t())return;const s=h(e);if(!f.has(s))return;if(u(e.value))return;const o=m(e.value,(a)=>a.type==="Literal"&&typeof a.value==="number"&&r.test(String(a.value))&&!p(a)||a.type==="UnaryExpression"&&a.operator==="-"&&a.argument.type==="Literal"&&typeof a.argument.value==="number"&&r.test(`-${a.argument.value}`)&&!p(a));if(o)i.report({node:o,message:c})}}));
@@ -1 +1 @@
1
- import{subtreeHas as n}from"../../../lib/ast.js";import{inCreate as r,propertyName as i}from"./shared.js";const s=/^margin(?:$|Top$|Bottom$|Left$|Right$|Start$|End$|Horizontal$|Vertical$)/,p=(e)=>e.type==="UnaryExpression"&&e.operator==="-"||e.type==="Literal"&&typeof e.value==="number"&&e.value<=0,l="Use `gap` on the parent or `padding` on this element instead of `margin`. Margin escapes the child's own box, so it leaves stray space behind when the first or last child is removed; a negative margin stays allowed for overlap and half-size centering, and `0` for a reset.";export const noMargin=r("Disallow non-negative `margin` inside `StyleSheet.create`; `gap` on the parent or `padding` on the element spaces children without leaving a hole when one is removed.",(e,o)=>({Property(t){if(!o())return;const a=i(t);if(a===""||!s.test(a))return;if(n(t.value,p))return;e.report({node:t,message:l})}}));
1
+ import{subtreeHas as o}from"../../../lib/ast.js";import{inCreate as r,propertyName as i}from"./shared.js";const s=/^margin(?:$|Top$|Bottom$|Left$|Right$|Start$|End$|Horizontal$|Vertical$)/,p=(e)=>e.type==="UnaryExpression"&&e.operator==="-"||e.type==="Literal"&&typeof e.value==="number"&&e.value<=0,l="Use `gap` on the parent or `padding` on this element instead of `margin` — margin escapes the child's box and leaves stray space behind. A negative margin and `0` stay allowed.";export const noMargin=r("Disallow non-negative `margin` inside `StyleSheet.create`; `gap` on the parent or `padding` on the element spaces children without leaving a hole when one is removed.",(e,n)=>({Property(t){if(!n())return;const a=i(t);if(a===""||!s.test(a))return;if(o(t.value,p))return;e.report({node:t,message:l})}}));
@@ -0,0 +1 @@
1
+ import{gate as a,problem as o}from"../../../lib/ast.js";import{CREATE_MARKER as l,isStyleSheetCreate as c,stylesObjectOf as u}from"./shared.js";const i="Drop the `() =>` and keep the object — `theme` and `rt` reach a static style already.",p=(e)=>{if(e?.type!=="ArrowFunctionExpression"&&e?.type!=="FunctionExpression")return null;return e.params.length===0?e:null},y=(e)=>{if(e.type!=="ArrowFunctionExpression")return null;return e.body.type==="ObjectExpression"?e.body:null},m=(e,r)=>{if(r.type!=="Property")return;const t=p(r.value);if(t===null)return;const s=y(t);e.report({node:t,message:i,suggest:s===null?[]:[{desc:"Unwrap into a static style object",fix:(n)=>n.replaceText(t,e.sourceCode.getText(s))}]})};export const noParamlessDynamicFunction=o("Disallow a style written as a function that takes no arguments. It returns the same object on every render, and `theme` and `rt` reach a static style anyway, so the function only adds a call at each use site. The suggestion turns `() => ({ ... })` back into the object.",{meta:{hasSuggestions:!0},createOnce(e){return{before:()=>a(e,l),CallExpression(r){if(!c(r))return;const t=u(r.arguments[0]);if(t===null)return;for(const s of t.properties)m(e,s)}}}});
@@ -1 +1 @@
1
- import{gate as o,problem as i}from"../../../lib/ast.js";import{isStyleSheetCreate as p}from"./shared.js";const l="Compose styles with an array — `[styles.a, styles.b]` — instead of spreading. A spread reads the object once and breaks the Unistyles C++ proxy, so the style silently stops reacting to the theme.",c=(r)=>{let e=r;while(e?.type==="MemberExpression")e=e.object;return e?.type==="Identifier"?e.name:""};export const noStyleSpread=i("Disallow spreading a stylesheet style into another object. The spread reads through the Unistyles C++ proxy once, so the result stops reacting to the theme.",{createOnce(r){const e=new Set,a=[];return{before(){e.clear();a.length=0;return o(r,"...")},VariableDeclarator(t){const{id:s,init:n}=t;if(s.type==="Identifier"&&p(n))e.add(s.name)},SpreadElement(t){if(t.parent.type!=="ObjectExpression")return;const s=c(t.argument);if(s!=="")a.push({node:t,base:s})},"Program:exit"(){for(const t of a){if(!e.has(t.base)&&!/[Ss]tyles$/.test(t.base))continue;r.report({node:t.node,message:l})}}}}});
1
+ import{gate as o,problem as i}from"../../../lib/ast.js";import{isStyleSheetCreate as p}from"./shared.js";const l="Compose styles with an array — `[styles.a, styles.b]` — instead of spreading; a spread breaks the Unistyles C++ proxy, so the style stops reacting to the theme.",c=(r)=>{let e=r;while(e?.type==="MemberExpression")e=e.object;return e?.type==="Identifier"?e.name:""};export const noStyleSpread=i("Disallow spreading a stylesheet style into another object. The spread reads through the Unistyles C++ proxy once, so the result stops reacting to the theme.",{createOnce(r){const e=new Set,a=[];return{before(){e.clear();a.length=0;return o(r,"...")},VariableDeclarator(t){const{id:s,init:n}=t;if(s.type==="Identifier"&&p(n))e.add(s.name)},SpreadElement(t){if(t.parent.type!=="ObjectExpression")return;const s=c(t.argument);if(s!=="")a.push({node:t,base:s})},"Program:exit"(){for(const t of a){if(!e.has(t.base)&&!/[Ss]tyles$/.test(t.base))continue;r.report({node:t.node,message:l})}}}}});
@@ -1 +1 @@
1
- import{gate as d,problem as u}from"../../../lib/ast.js";import{CREATE_MARKER as y,isStyleSheetCreate as m,propertyName as h,stylesObjectOf as k}from"./shared.js";const p="*",E=(i)=>{const a=new Map;for(const n of i.properties){if(n.type!=="Property"||n.computed)return null;const o=h(n);if(o==="")return null;a.set(o,n.key)}return a};export const noUnusedStyles=u("Disallow stylesheet keys that nothing in the file reads. A computed key, a computed read, or a sheet that leaves the module skips the whole file.",{createOnce(i){let a=new Map,n=new Set,o=!1;const c=(e,t)=>{n.add(`${e}.${t}`)},s=(e)=>{c(e,p)};return{before(){a=new Map;n=new Set;o=!1;return d(i,y)},CallExpression(e){if(!m(e))return;const r=(e.parent.type==="VariableDeclarator"?e.parent:null)?.id;if(r?.type!=="Identifier"){o=!0;return}const l=k(e.arguments[0]),f=l===null?null:E(l);if(f===null){o=!0;return}a.set(r.name,f)},MemberExpression(e){const{object:t,property:r}=e;if(t.type!=="Identifier")return;if(e.computed){s(t.name);return}if(r.type==="Identifier")c(t.name,r.name)},ExportNamedDeclaration(e){const{declaration:t}=e;if(t?.type==="VariableDeclaration"){for(const r of t.declarations)if(r.id.type==="Identifier")s(r.id.name)}for(const r of e.specifiers)if(r.local.type==="Identifier")s(r.local.name)},ExportDefaultDeclaration(e){const{declaration:t}=e;if(t.type==="Identifier")s(t.name)},"Program:exit":()=>{if(o)return;for(const[e,t]of a){if(n.has(`${e}.${p}`))continue;for(const[r,l]of t){if(n.has(`${e}.${r}`))continue;i.report({node:l,message:`Delete \`${e}.${r}\` — nothing reads it, so it is dead weight that keeps a token alive nothing renders.`})}}}}}});
1
+ import{gate as d,problem as u}from"../../../lib/ast.js";import{CREATE_MARKER as y,isStyleSheetCreate as m,propertyName as h,stylesObjectOf as E}from"./shared.js";const p="*",k=(s)=>{const a=new Map;for(const n of s.properties){if(n.type!=="Property"||n.computed)return null;const o=h(n);if(o==="")return null;a.set(o,n.key)}return a};export const noUnusedStyles=u("Disallow stylesheet keys that nothing in the file reads. A computed key, a computed read, or a sheet that leaves the module skips the whole file.",{createOnce(s){let a=new Map,n=new Set,o=!1;const c=(e,t)=>{n.add(`${e}.${t}`)},i=(e)=>{c(e,p)};return{before(){a=new Map;n=new Set;o=!1;return d(s,y)},CallExpression(e){if(!m(e))return;const r=(e.parent.type==="VariableDeclarator"?e.parent:null)?.id;if(r?.type!=="Identifier"){o=!0;return}const l=E(e.arguments[0]),f=l===null?null:k(l);if(f===null){o=!0;return}a.set(r.name,f)},MemberExpression(e){const{object:t,property:r}=e;if(t.type!=="Identifier")return;if(e.computed){i(t.name);return}if(r.type==="Identifier")c(t.name,r.name)},ExportNamedDeclaration(e){const{declaration:t}=e;if(t?.type==="VariableDeclaration"){for(const r of t.declarations)if(r.id.type==="Identifier")i(r.id.name)}for(const r of e.specifiers)if(r.local.type==="Identifier")i(r.local.name)},ExportDefaultDeclaration(e){const{declaration:t}=e;if(t.type==="Identifier")i(t.name)},"Program:exit":()=>{if(o)return;for(const[e,t]of a){if(n.has(`${e}.${p}`))continue;for(const[r,l]of t){if(n.has(`${e}.${r}`))continue;s.report({node:l,message:`Delete \`${e}.${r}\` — nothing reads it.`})}}}}}});
@@ -1 +1 @@
1
- import{hasAncestor as r,problem as n}from"../../../lib/ast.js";import{isStyleSheetCreate as i,memberPath as o,themeConsumingComponent as m}from"./shared.js";const a="theme.screen.",c="Read current screen values from `useUnistyles().rt.screen` or `useWindowDimensions()` instead of the theme's module-initialization snapshot.";export const themeScreenComponent=n("`theme.screen.*` is a snapshot taken at module initialization, so a component that reads it never sees the current size. `useUnistyles().rt.screen` and `useWindowDimensions` do.",{createOnce(s){return{MemberExpression(e){if(!o(e).startsWith(a))return;const{parent:t}=e;if(t.type==="MemberExpression"&&t.object===e)return;if(r(e,i))return;if(!m(e))return;s.report({node:e,message:c})}}}});
1
+ import{hasAncestor as r,problem as n}from"../../../lib/ast.js";import{isStyleSheetCreate as i,memberPath as o,themeConsumingComponent as m}from"./shared.js";const a="theme.screen.",c="Read current screen values from `useUnistyles().rt.screen` or `useWindowDimensions()` — `theme.screen` is a module-initialization snapshot.";export const themeScreenComponent=n("`theme.screen.*` is a snapshot taken at module initialization, so a component that reads it never sees the current size. `useUnistyles().rt.screen` and `useWindowDimensions` do.",{createOnce(s){return{MemberExpression(e){if(!o(e).startsWith(a))return;const{parent:t}=e;if(t.type==="MemberExpression"&&t.object===e)return;if(r(e,i))return;if(!m(e))return;s.report({node:e,message:c})}}}});
@@ -1 +1 @@
1
- import{problem as n}from"../../../lib/ast.js";import{readsTheme as r,themeConsumingComponent as i}from"./shared.js";const s="Resolve theme-dependent style values inside `StyleSheet.create` instead of reading `theme` in a JSX `style` prop or passing it into a dynamic style function.";export const themeStyleAttr=n("Theme-dependent values belong in `StyleSheet.create`, not in a JSX `style` prop that reads a `useUnistyles()` theme.",{createOnce(t){return{JSXAttribute(e){if(e.name.type!=="JSXIdentifier"||e.name.name!=="style")return;if(!r(e))return;if(!i(e))return;t.report({node:e,message:s})}}}});
1
+ import{problem as r}from"../../../lib/ast.js";import{readsTheme as n,themeConsumingComponent as s}from"./shared.js";const m="Resolve theme-dependent style values inside `StyleSheet.create` instead of reading `theme` in a JSX `style` prop.";export const themeStyleAttr=r("Theme-dependent values belong in `StyleSheet.create`, not in a JSX `style` prop that reads a `useUnistyles()` theme.",{createOnce(t){return{JSXAttribute(e){if(e.name.type!=="JSXIdentifier"||e.name.name!=="style")return;if(!n(e))return;if(!s(e))return;t.report({node:e,message:m})}}}});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ashstack/lint",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "description": "ashStack shared oxlint configuration and custom rules. Entries: core, react, react-native, with per-library rule groups auto-detected from your dependencies. Consume from oxlint.config.mts.",
5
5
  "type": "module",
6
6
  "license": "MIT",