@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.
- package/README.md +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/rules/core/components-tsx-only.js +1 -1
- package/dist/core/rules/core/hoist-intl.js +1 -1
- package/dist/core/rules/core/index.js +1 -1
- package/dist/core/rules/core/max-lines.js +1 -0
- package/dist/core/rules/core/no-comments.js +1 -1
- package/dist/core/rules/core/no-packed-condition.js +1 -1
- package/dist/core/rules/core/use-design-system.js +1 -1
- package/dist/core/rules/zod/prefer-enum.js +1 -1
- package/dist/lib/registry.js +1 -1
- package/dist/lib/rule-types/core.d.ts +7 -1
- package/dist/lib/rule-types/react-native.d.ts +7 -1
- package/dist/react/index.d.ts +8 -0
- package/dist/react/index.js +1 -1
- package/dist/react/rules/query/next-page-param-undefined.js +1 -1
- package/dist/react/rules/query/no-deprecated-filters.js +1 -1
- package/dist/react/rules/query/no-fetch-in-query-fn.js +1 -1
- package/dist/react/rules/query/no-inline-keys.js +1 -1
- package/dist/react/rules/query/require-destructured-hooks.js +1 -1
- package/dist/react/rules/react/no-svg-without-title.js +1 -1
- package/dist/react/rules/react/no-unlabeled-icon-button.js +1 -1
- package/dist/react/rules/tailwind/prefer-cn.js +1 -1
- package/dist/react/rules/tailwind/use-logical-classes.js +1 -1
- package/dist/react/rules/tanstack-router/no-search-casts.js +1 -1
- package/dist/react/rules/tanstack-router/require-selector.js +1 -1
- package/dist/react/rules/zustand/require-selector.js +1 -1
- package/dist/react-native/index.js +1 -1
- package/dist/react-native/rules/keyboard/index.js +1 -1
- package/dist/react-native/rules/legend-list/no-flex-in-content-container.js +1 -1
- package/dist/react-native/rules/legend-list/no-index-key-extractor.js +1 -1
- package/dist/react-native/rules/legend-list/no-inline-render-item-props.js +1 -1
- package/dist/react-native/rules/legend-list/no-mixed-children.js +1 -1
- package/dist/react-native/rules/legend-list/no-remount-key.js +1 -1
- package/dist/react-native/rules/legend-list/no-scrollview-map.js +1 -1
- package/dist/react-native/rules/legend-list/no-unsupported-props.js +1 -1
- package/dist/react-native/rules/legend-list/required-props.js +1 -1
- package/dist/react-native/rules/legend-list/typed-items-need-item-type.js +1 -1
- package/dist/react-native/rules/legend-state/naming.js +1 -1
- package/dist/react-native/rules/legend-state/no-assignment.js +1 -1
- package/dist/react-native/rules/legend-state/no-nested-observable.js +1 -1
- package/dist/react-native/rules/legend-state/no-object-selector.js +1 -1
- package/dist/react-native/rules/legend-state/no-observable-in-component.js +1 -1
- package/dist/react-native/rules/legend-state/no-peek-in-selector.js +1 -1
- package/dist/react-native/rules/legend-state/no-react-mirror.js +1 -1
- package/dist/react-native/rules/legend-state/no-untracked-get-in-jsx.js +1 -1
- package/dist/react-native/rules/react-native/hoist-stateless-function.js +1 -1
- package/dist/react-native/rules/react-native/no-conditional-style-array.js +1 -1
- package/dist/react-native/rules/react-native/no-dynamic-import.js +1 -1
- package/dist/react-native/rules/react-native/no-keyboard-will-events.js +1 -1
- package/dist/react-native/rules/react-native/no-leaked-render.js +1 -1
- package/dist/react-native/rules/react-native/no-manual-memo.js +1 -1
- package/dist/react-native/rules/react-native/no-redundant-view-nesting.js +1 -1
- package/dist/react-native/rules/react-native/no-rn-image-network-source.js +1 -1
- package/dist/react-native/rules/react-native/no-rn-namespace-import.js +1 -1
- package/dist/react-native/rules/react-native/no-scroll-position-state.js +1 -1
- package/dist/react-native/rules/react-native/no-unlabeled-icon-pressable.js +1 -1
- package/dist/react-native/rules/reanimated/animated-reaction-safety.js +1 -1
- package/dist/react-native/rules/reanimated/animated-style-needs-animated-component.js +1 -1
- package/dist/react-native/rules/reanimated/animated-updater-purity.js +1 -1
- package/dist/react-native/rules/reanimated/gpu-properties-only.js +1 -1
- package/dist/react-native/rules/reanimated/interpolate-needs-clamp.js +1 -1
- package/dist/react-native/rules/reanimated/no-react-state-from-continuous-worklet.js +1 -1
- package/dist/react-native/rules/reanimated/prefer-lazy-shared-value-initializer.js +1 -1
- package/dist/react-native/rules/reanimated/schedule-on-rn-scope.js +1 -1
- package/dist/react-native/rules/reanimated/shared-value-usage.js +1 -1
- package/dist/react-native/rules/skia/canvas-opaque.js +1 -1
- package/dist/react-native/rules/skia/index.js +1 -1
- package/dist/react-native/rules/turbo-image/require-resize.js +1 -1
- package/dist/react-native/rules/unistyles/animated-theme.js +1 -1
- package/dist/react-native/rules/unistyles/content-container.js +1 -1
- package/dist/react-native/rules/unistyles/in-sheet.js +1 -1
- package/dist/react-native/rules/unistyles/index.js +1 -1
- package/dist/react-native/rules/unistyles/insets.js +1 -1
- package/dist/react-native/rules/unistyles/no-hardcoded-color.js +1 -1
- package/dist/react-native/rules/unistyles/no-hardcoded-spacing.js +1 -1
- package/dist/react-native/rules/unistyles/no-margin.js +1 -1
- package/dist/react-native/rules/unistyles/no-paramless-dynamic-function.js +1 -0
- package/dist/react-native/rules/unistyles/no-style-spread.js +1 -1
- package/dist/react-native/rules/unistyles/no-unused-styles.js +1 -1
- package/dist/react-native/rules/unistyles/theme-screen-component.js +1 -1
- package/dist/react-native/rules/unistyles/theme-style-attr.js +1 -1
- 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
|
|
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
|
|
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
|
|
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}()
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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=(
|
|
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
|
|
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
|
|
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
|
|
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=
|
|
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
|
|
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 —
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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{
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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()`
|
|
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
|
|
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.
|
|
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",
|