@ashstack/lint 0.6.0 → 0.8.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 (83) hide show
  1. package/README.md +1 -1
  2. package/dist/core/index.js +1 -1
  3. package/dist/core/rules/core/components-tsx-only.js +1 -1
  4. package/dist/core/rules/core/hoist-intl.js +1 -1
  5. package/dist/core/rules/core/index.js +1 -1
  6. package/dist/core/rules/core/max-lines.js +1 -0
  7. package/dist/core/rules/core/no-comments.js +1 -1
  8. package/dist/core/rules/core/no-packed-condition.js +1 -1
  9. package/dist/core/rules/core/use-design-system.js +1 -1
  10. package/dist/core/rules/zod/prefer-enum.js +1 -1
  11. package/dist/lib/registry.js +1 -1
  12. package/dist/lib/rule-types/core.d.ts +7 -1
  13. package/dist/lib/rule-types/react-native.d.ts +7 -1
  14. package/dist/react/index.d.ts +8 -0
  15. package/dist/react/index.js +1 -1
  16. package/dist/react/rules/query/next-page-param-undefined.js +1 -1
  17. package/dist/react/rules/query/no-deprecated-filters.js +1 -1
  18. package/dist/react/rules/query/no-fetch-in-query-fn.js +1 -1
  19. package/dist/react/rules/query/no-inline-keys.js +1 -1
  20. package/dist/react/rules/query/require-destructured-hooks.js +1 -1
  21. package/dist/react/rules/react/no-svg-without-title.js +1 -1
  22. package/dist/react/rules/react/no-unlabeled-icon-button.js +1 -1
  23. package/dist/react/rules/tailwind/prefer-cn.js +1 -1
  24. package/dist/react/rules/tailwind/use-logical-classes.js +1 -1
  25. package/dist/react/rules/tanstack-router/no-search-casts.js +1 -1
  26. package/dist/react/rules/tanstack-router/require-selector.js +1 -1
  27. package/dist/react/rules/zustand/require-selector.js +1 -1
  28. package/dist/react-native/index.js +1 -1
  29. package/dist/react-native/rules/keyboard/index.js +1 -1
  30. package/dist/react-native/rules/legend-list/no-flex-in-content-container.js +1 -1
  31. package/dist/react-native/rules/legend-list/no-index-key-extractor.js +1 -1
  32. package/dist/react-native/rules/legend-list/no-inline-render-item-props.js +1 -1
  33. package/dist/react-native/rules/legend-list/no-mixed-children.js +1 -1
  34. package/dist/react-native/rules/legend-list/no-remount-key.js +1 -1
  35. package/dist/react-native/rules/legend-list/no-scrollview-map.js +1 -1
  36. package/dist/react-native/rules/legend-list/no-unsupported-props.js +1 -1
  37. package/dist/react-native/rules/legend-list/required-props.js +1 -1
  38. package/dist/react-native/rules/legend-list/typed-items-need-item-type.js +1 -1
  39. package/dist/react-native/rules/legend-state/naming.js +1 -1
  40. package/dist/react-native/rules/legend-state/no-assignment.js +1 -1
  41. package/dist/react-native/rules/legend-state/no-nested-observable.js +1 -1
  42. package/dist/react-native/rules/legend-state/no-object-selector.js +1 -1
  43. package/dist/react-native/rules/legend-state/no-observable-in-component.js +1 -1
  44. package/dist/react-native/rules/legend-state/no-peek-in-selector.js +1 -1
  45. package/dist/react-native/rules/legend-state/no-react-mirror.js +1 -1
  46. package/dist/react-native/rules/legend-state/no-untracked-get-in-jsx.js +1 -1
  47. package/dist/react-native/rules/react-native/hoist-stateless-function.js +1 -1
  48. package/dist/react-native/rules/react-native/no-conditional-style-array.js +1 -1
  49. package/dist/react-native/rules/react-native/no-dynamic-import.js +1 -1
  50. package/dist/react-native/rules/react-native/no-keyboard-will-events.js +1 -1
  51. package/dist/react-native/rules/react-native/no-leaked-render.js +1 -1
  52. package/dist/react-native/rules/react-native/no-manual-memo.js +1 -1
  53. package/dist/react-native/rules/react-native/no-redundant-view-nesting.js +1 -1
  54. package/dist/react-native/rules/react-native/no-rn-image-network-source.js +1 -1
  55. package/dist/react-native/rules/react-native/no-rn-namespace-import.js +1 -1
  56. package/dist/react-native/rules/react-native/no-scroll-position-state.js +1 -1
  57. package/dist/react-native/rules/react-native/no-unlabeled-icon-pressable.js +1 -1
  58. package/dist/react-native/rules/reanimated/animated-reaction-safety.js +1 -1
  59. package/dist/react-native/rules/reanimated/animated-style-needs-animated-component.js +1 -1
  60. package/dist/react-native/rules/reanimated/animated-updater-purity.js +1 -1
  61. package/dist/react-native/rules/reanimated/gpu-properties-only.js +1 -1
  62. package/dist/react-native/rules/reanimated/interpolate-needs-clamp.js +1 -1
  63. package/dist/react-native/rules/reanimated/no-react-state-from-continuous-worklet.js +1 -1
  64. package/dist/react-native/rules/reanimated/prefer-lazy-shared-value-initializer.js +1 -1
  65. package/dist/react-native/rules/reanimated/schedule-on-rn-scope.js +1 -1
  66. package/dist/react-native/rules/reanimated/shared-value-usage.js +1 -1
  67. package/dist/react-native/rules/skia/canvas-opaque.js +1 -1
  68. package/dist/react-native/rules/skia/index.js +1 -1
  69. package/dist/react-native/rules/turbo-image/require-resize.js +1 -1
  70. package/dist/react-native/rules/unistyles/animated-theme.js +1 -1
  71. package/dist/react-native/rules/unistyles/content-container.js +1 -1
  72. package/dist/react-native/rules/unistyles/in-sheet.js +1 -1
  73. package/dist/react-native/rules/unistyles/index.js +1 -1
  74. package/dist/react-native/rules/unistyles/insets.js +1 -1
  75. package/dist/react-native/rules/unistyles/no-hardcoded-color.js +1 -1
  76. package/dist/react-native/rules/unistyles/no-hardcoded-spacing.js +1 -1
  77. package/dist/react-native/rules/unistyles/no-margin.js +1 -1
  78. package/dist/react-native/rules/unistyles/no-paramless-dynamic-function.js +1 -0
  79. package/dist/react-native/rules/unistyles/no-style-spread.js +1 -1
  80. package/dist/react-native/rules/unistyles/no-unused-styles.js +1 -1
  81. package/dist/react-native/rules/unistyles/theme-screen-component.js +1 -1
  82. package/dist/react-native/rules/unistyles/theme-style-attr.js +1 -1
  83. package/package.json +1 -1
@@ -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 u}from"../../../lib/ast.js";const l=new Set(["useMemo","useCallback","memo"]),c=/^why:/i,h=10,p=(e)=>e.type==="Identifier"&&l.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 l.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 o=0;o<e.length;o++)if(e.charCodeAt(o)===h)t.push(o+1);return t},w=(e,t)=>{let o=0,n=e.length-1;while(o<n){const s=Math.floor((o+n+1)/2);if((e[s]??0)<=t)o=s;else n=s-1}return o},b=(e,t)=>{const o=new Set;for(const n of e){if(!c.test(n.value.trim()))continue;for(let s=t(n.start);s<=t(n.end);s++)o.add(s)}return o};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(o){const n=d(o);if(n!==null)t.push({node:o,name:n})},"Program:exit"(){if(t.length===0)return;const o=y(e.sourceCode.getText()),n=(r)=>w(o,r),s=b(e.sourceCode.getAllComments(),n);for(const{node:r,name:a}of t){const i=n(r.start);if(s.has(i)||s.has(i-1))continue;e.report({node:r,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.6.0",
3
+ "version": "0.8.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",