@ashstack/lint 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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/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/react-native.d.ts +8 -4
- package/dist/lib/types.d.ts +6 -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
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @ashstack/lint
|
|
2
2
|
|
|
3
|
-
Strict shared [oxlint](https://oxc.rs) config: <!-- rule-counts -->118 rules on plain TypeScript,
|
|
3
|
+
Strict shared [oxlint](https://oxc.rs) config: <!-- rule-counts -->118 rules on plain TypeScript, 201 with React, 258 on React Native, 75 of them custom-built<!-- /rule-counts -->. Three entries, each containing the one before it: `core()`, `react()`, `react-native()`.
|
|
4
4
|
|
|
5
5
|
Library-specific rules ship only when you depend on that library, so nothing fires about code you don't have.
|
|
6
6
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{optionsOf as t}from"../../../lib/ast.js";const s="Move this file to `src/utils
|
|
1
|
+
import{optionsOf as t}from"../../../lib/ast.js";const s="Move this file to `src/utils`, `src/hooks`, or `src/api/<feature>/` — `components/` holds only files that render JSX.",n="src/components",i=(r)=>r.every((e)=>e.type==="ImportDeclaration"||e.type==="ExportAllDeclaration"||e.type==="ExportNamedDeclaration"&&e.declaration==null);export const componentsTsxOnly={meta:{type:"problem",docs:{description:"Require every file under the components directory to render JSX or be a re-export barrel. `dir` says which directory, defaulting to `src/components`."},schema:[{type:"object",properties:{dir:{type:"string",minLength:1}},additionalProperties:!1}],defaultOff:!0},createOnce(r){let e=!1;return{before(){e=!1;const{dir:o=n}=t(r,{});return r.filename.replaceAll("\\","/").includes(`/${o}/`)},JSXElement(){e=!0},JSXFragment(){e=!0},"Program:exit"(o){if(e||i(o.body))return;r.report({node:o,message:s})}}}};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ancestors as o,calleeName as a,hasAncestor as
|
|
1
|
+
import{ancestors as o,calleeName as a,hasAncestor as l,isFunction as i,problem as c,subtreeHas as u}from"../../../lib/ast.js";const p="Move this `Intl` formatter to module scope when the locale and options are static, or wrap it in `useMemo` keyed on the locale.",m=new Set(["useMemo","useCallback"]),f=(t)=>u(t,(e)=>e.type==="JSXElement"||e.type==="JSXFragment"),h=(t)=>{for(const e of o(t))if(i(e)&&f(e))return!0;return!1};export const hoistIntl=c("Disallow `new Intl.*` inside a function that renders JSX, unless the call already sits in `useMemo` or `useCallback`.",{createOnce(t){return{NewExpression(e){const{callee:s}=e;if(s.type!=="MemberExpression")return;const{object:n}=s;if(n.type!=="Identifier"||n.name!=="Intl")return;if(l(e,(r)=>r.type==="CallExpression"&&m.has(a(r))))return;if(!h(e))return;t.report({node:e,message:p})}}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{optionsOf as
|
|
1
|
+
import{optionsOf as y}from"../../../lib/ast.js";const b=["@ts-expect-error","@ts-ignore","@ts-nocheck","@ts-check","@jsx","@jsxRuntime","@jsxImportSource","prettier-ignore","eslint-disable","eslint-enable","oxlint-disable","oxlint-enable","oxfmt-ignore","react-doctor-disable","@type","<reference","#__PURE__","@__PURE__","v8 ignore","c8 ignore","istanbul ignore","@vitest-environment","EXPECT_PASS","EXPECT_FAIL"],D=/^(?<kind>what|why):\s*(?<fact>[\s\S]+)$/i,m=new Set(["Block","MultiLine"]),C=new Set(["Shebang","Hashbang"]),u=10,f=120,v=2,k="Make the code say this, then delete the line — rename, Extract Function, Guard Clause, or named steps. Done when the sentence reads off a name or the control flow.",A=" `// what:` fits only a fact that outlives the code — a platform bug, an upstream contract, a measured number; what the code does is not one, and still owes the refactor.",_=" This project runs with the `// what:` hatch turned off. The one line left is `// why:`, which a kept `memo` requires.",x=(e)=>k+(e?A:_),H="Attach this block to the declaration it documents, or Rename and Extract Function until the code reads without it.",d={block:"Rewrite this as a single `// what: <fact>` line comment; if the fact needs a paragraph, name the pieces in code instead.",shortFact:`Write the fact after \`what:\` (at least ${u} characters), or delete the comment.`,tooLong:`Trim this \`what:\` line under ${f} characters, moving what is left into names in the code.`,stacked:"Keep one `what:` line here and refactor what the others explain into names."},M=(e,t)=>`Delete \`what:\` comments from this file until at most ${e} remain (it has ${t}): move each one's logic into a function named for it.`,P=new Set(["ExportNamedDeclaration","ExportDefaultDeclaration","ExportAllDeclaration","FunctionDeclaration","VariableDeclaration","ClassDeclaration","MethodDefinition","PropertyDefinition","Property","TSInterfaceDeclaration","TSTypeAliasDeclaration","TSEnumDeclaration","TSEnumMember","TSModuleDeclaration","TSDeclareFunction","TSPropertySignature","TSMethodSignature","TSIndexSignature","TSCallSignatureDeclaration","TSConstructSignatureDeclaration","TSAbstractMethodDefinition","TSAbstractPropertyDefinition","AccessorProperty","ImportDeclaration"]),O=(e)=>(m.has(e.type)?e.value.replace(/^[*\s]+/,""):e.value).trim(),F=(e)=>b.some((t)=>e.startsWith(t)),R=(e)=>m.has(e.type)&&e.value.startsWith("*"),I=(e)=>{const t=[];for(const o of e.sourceCode.getAllComments()){if(C.has(o.type))continue;const l=O(o);if(F(l))continue;t.push({comment:o,body:l})}return t},L=(e,t)=>{let o=t;while(/\s/.test(e.charAt(o)))o+=1;return o},p=(e,t)=>{const o=D.exec(e.body)?.groups;if(!o?.kind||!o.fact)return[];const l=o.kind.toLowerCase()==="why"?"why":"what";if(l==="what"&&!t)return[];return[{...e,kind:l,fact:o.fact}]},N=(e,t,o)=>{if(m.has(e.type))return d.block;if(o.trim().length<u)return d.shortFact;if(t.length>f)return d.tooLong;return null},j=(e,t,o)=>e.slice(t.end,o.start).trim()==="",w=(e)=>e.escapeHatch!==!1;export const noComments={meta:{type:"problem",docs:{description:'Disallow every comment that is neither a `// what: <fact>` line, a `// why:` marker, nor a tooling directive. The message names the refactoring that removes it: Rename, Extract Function, Guard Clause. Surviving `// what:` lines are held to one short line each, at most `budget` per file (default 2); `escapeHatch: false` removes that hatch, so no discretionary prose survives. A `// why:` line is the marker `@ashstack/react-native/no-manual-memo` requires above a kept `memo`: held to the same one-line shape, never counted against `budget`, and kept even with `escapeHatch: false`, so the two rules run together. With `jsdoc: "allow"`, a `/** */` block documenting the declaration directly beneath it is kept, while a floating one still reports.'},schema:[{type:"object",properties:{jsdoc:{enum:["allow","report"]},escapeHatch:{type:"boolean"},budget:{type:"integer",minimum:0}},additionalProperties:!1}]},createOnce(e){const t=new Set,o=(n)=>{t.add(n.start)},l={};for(const n of P)l[n]=o;const h=(n,r)=>{e.report({node:n,message:r})},g=(n,r)=>{const{text:s}=e.sourceCode,i=w(r);for(const a of n){const{comment:c}=a;if(p(a,i).length>0)continue;if(r.jsdoc!=="allow"||!R(c))h(c,x(i));else if(!t.has(L(s,c.end)))h(c,H)}},S=(n,r)=>{const s=[];for(const i of n)for(const a of p(i,r)){const c=N(a.comment,a.body,a.fact);if(c===null)s.push(a);else h(a.comment,c)}return s},T=(n)=>{const r=e.sourceCode.getText(),s=n.map((i)=>i.comment);for(const[i,a]of s.entries()){const c=s[i-1];if(c&&j(r,c,a))h(a,d.stacked)}},E=(n,r)=>{const s=r.budget??v,i=n.filter((c)=>c.kind==="what"),a=i[s];if(a)h(a.comment,M(s,i.length))};return{...l,before(){t.clear();return!0},"Program:exit"(){const n=y(e,{}),r=I(e);g(r,n);const s=S(r,w(n));T(s);E(s,n)}}}};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{gate as s}from"../../../lib/ast.js";const p=new Set(["===","!==","==","!=","<","<=",">",">=","in","instanceof"]),
|
|
1
|
+
import{gate as s}from"../../../lib/ast.js";const p=new Set(["===","!==","==","!=","<","<=",">",">=","in","instanceof"]),o=5,r=(t)=>{if(!t)return 0;if(t.type==="LogicalExpression")return 1+r(t.left)+r(t.right);if(t.type==="BinaryExpression")return(p.has(t.operator)?1:0)+r(t.left)+r(t.right);if(t.type==="UnaryExpression")return t.operator==="!"?r(t.argument):0;if(t.type==="ConditionalExpression")return 1+r(t.test)+r(t.consequent)+r(t.alternate);return 0};export const noPackedCondition={meta:{type:"problem",docs:{description:"Require a condition to be split into named booleans once it holds too many boolean operators and comparisons. The option says how many, defaulting to 5."},schema:[{type:"integer",minimum:1}]},createOnce(t){let a=o;const n=(e)=>{if(!e)return;const i=r(e);if(i<=a)return;t.report({node:e,message:`Extract each part into a named variable like \`hasValue\`, until this condition reads as those names joined by \`&&\`/\`||\` — it packs ${i} operators, past the ${a} a reader takes in.`})};return{before(){a=typeof t.options[0]==="number"?t.options[0]:o;return s(t,"&&","||","??")},IfStatement(e){n(e.test)},ConditionalExpression(e){n(e.test)},WhileStatement(e){n(e.test)},DoWhileStatement(e){n(e.test)},ForStatement(e){n(e.test)}}}};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readdirSync as l}from"node:fs";import{isAbsolute as d,join as y}from"node:path";import{optionsOf as u}from"../../../lib/ast.js";const g="src/components/ui",
|
|
1
|
+
import{readdirSync as l}from"node:fs";import{isAbsolute as d,join as y}from"node:path";import{optionsOf as u}from"../../../lib/ast.js";const g="src/components/ui",b="@/components/ui",h=/\.(?:ios|android|native|web)$/,S={Pressable:["TouchableOpacity","TouchableHighlight","TouchableWithoutFeedback","TouchableNativeFeedback"]},m={anyOf:[{type:"string"},{type:"array",items:{type:"string"}}]},A={anyOf:[...m.anyOf,{type:"object",properties:{replaces:m,from:{type:"string"},path:{type:"string"},reason:{type:"string"}},required:["replaces"],additionalProperties:!1}]},E=(e)=>e.split("-").filter(Boolean).map((t)=>t.charAt(0).toUpperCase()+t.slice(1)).join(""),I=(e)=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),p=(e,t,r,o)=>{const n=e.get(t)??new Map;n.set(r,o);e.set(t,n)},O=(e,t,r)=>{let o;try{o=l(d(t)?t:y(process.cwd(),t))}catch{return}for(const n of o){if(!n.endsWith(".tsx"))continue;const s=n.slice(0,-4).replace(h,"");if(s==="index")continue;const i=E(s),a=`${r}/${s}`;p(e,"react-native",i,{name:i,from:a});for(const c of S[i]??[])p(e,"react-native",c,{name:i,from:a})}},w=(e,t,r)=>{for(const[o,n]of Object.entries(r)){const s=typeof n==="string"||Array.isArray(n)?{replaces:n}:n,i={name:o,from:s.path??`${t}/${I(o)}`,reason:s.reason};for(const a of[s.replaces].flat())p(e,s.from??"react-native",a,i)}},f=new Map,_=(e,t)=>!!e&&t.some((r)=>e.includes(r)),M=(e)=>{const t=e.dir??g,r=e.alias??b,o=e.exempt??[t],n=JSON.stringify([t,r,e.use]);let s=f.get(n);if(s===void 0){s=new Map;O(s,t,r);if(e.use)w(s,r,e.use);f.set(n,s)}return{banned:s,exempt:o}};export const useDesignSystem={meta:{type:"problem",docs:{description:"Disallow importing a raw primitive your design system already wraps. Wrapped components come from scanning the design-system directory, plus the explicit `use` map for names, paths and source modules the scan cannot infer. Files under the design-system directory (or `exempt`) are skipped."},schema:[{type:"object",properties:{dir:{type:"string",minLength:1},alias:{type:"string",minLength:1},use:{type:"object",additionalProperties:A},exempt:{type:"array",items:{type:"string"}}},additionalProperties:!1}],defaultOff:!0},createOnce(e){let t=new Map;return{before(){const r=u(e,{}),o=M(r);t=o.banned;if(t.size===0)return!1;const{filename:n}=e;if(_(n,o.exempt))return!1;return!0},ImportDeclaration(r){const o=r.source.value,n=t.get(o);if(n===void 0)return;for(const s of r.specifiers){if(s.type!=="ImportSpecifier")continue;const{imported:i}=s;if(i.type!=="Identifier")continue;const a=n.get(i.name);if(a===void 0)continue;const c=a.reason?` ${a.reason}`:"";e.report({node:s,message:`Import \`${a.name}\` from "${a.from}" instead of \`${i.name}\` from ${o}.${c}`})}}}}};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{problem as l}from"../../../lib/ast.js";const a={nativeEnum:"Call `z.enum()` here —
|
|
1
|
+
import{problem as l}from"../../../lib/ast.js";const a={nativeEnum:"Call `z.enum()` here — same arguments, and `z.nativeEnum()` is deprecated in Zod 4.",literalUnion:"Replace this union with `z.enum([...])` listing the same string values."},i=(r,e)=>{const{callee:t}=r;if(t.type!=="MemberExpression")return!1;const{object:n,property:s}=t;if(n.type!=="Identifier"||n.name!=="z")return!1;return s.type==="Identifier"&&s.name===e},u=(r)=>{if(r?.type!=="CallExpression"||!i(r,"literal"))return!1;const e=r.arguments;if(e.length!==1)return!1;const[t]=e;return t?.type==="Literal"&&typeof t.value==="string"};export const preferEnum=l("Disallow `z.nativeEnum()` and any `z.union()` whose members are all `z.literal()` strings.",{createOnce(r){return{CallExpression(e){if(i(e,"nativeEnum")){r.report({node:e,message:a.nativeEnum});return}if(!i(e,"union"))return;const[t]=e.arguments;if(t?.type!=="ArrayExpression")return;const{elements:n}=t;if(n.length===0||!n.every((s)=>u(s)))return;r.report({node:e,message:a.literalUnion})}}}});
|
package/dist/lib/registry.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const e=(t)=>new URL(t,import.meta.url).href;export const coreRegistry=[{meta:{name:"@ashstack/core"},url:e("../core/rules/core/index.js"),rules:{"no-comments":{},"no-naming-convention":{},"no-packed-condition":{},"use-design-system":{defaultOff:!0},"components-tsx-only":{defaultOff:!0},"hoist-intl":{}}},{meta:{name:"@ashstack/zod"},url:e("../core/rules/zod/index.js"),option:"zod",packages:["zod"],rules:{"prefer-enum":{}}}],reactRegistry=[{meta:{name:"@ashstack/react"},url:e("../react/rules/react/index.js"),rules:{"no-unlabeled-icon-button":{},"no-svg-without-title":{}}},{meta:{name:"@ashstack/query"},url:e("../react/rules/query/index.js"),option:"query",packages:["@tanstack/react-query"],rules:{"no-inline-keys":{},"no-deprecated-filters":{},"require-destructured-hooks":{},"no-fetch-in-query-fn":{},"next-page-param-undefined":{}}},{meta:{name:"@ashstack/zustand"},url:e("../react/rules/zustand/index.js"),option:"zustand",packages:["zustand"],rules:{"require-selector":{}}},{meta:{name:"@ashstack/i18n"},url:e("../react/rules/i18n/index.js"),option:"i18n",packages:["i18next","react-i18next","@lingui/core","react-intl","use-intl","next-intl","expo-localization"],rules:{"no-bare-text":{},"no-bare-attrs":{},"no-bare-toast":{}}},{meta:{name:"@ashstack/tailwind"},url:e("../react/rules/tailwind/index.js"),option:"tailwind",packages:["tailwindcss"],rules:{"prefer-cn":{},"use-logical-classes":{}}},{meta:{name:"@ashstack/tanstack-router"},url:e("../react/rules/tanstack-router/index.js"),option:"tanstackRouter",packages:["@tanstack/react-router"],rules:{"require-selector":{},"no-search-casts":{}}}],reactNativeRegistry=[{meta:{name:"@ashstack/react-native"},url:e("../react-native/rules/react-native/index.js"),restrictedImports:{paths:[{name:"react",importNames:["forwardRef"],message:"Accept `ref` as a regular prop and delete the `forwardRef` wrapper — React 19 passes `ref` like any other prop."},{name:"react",importNames:["useContext"],message:"Call `use(Context)` instead of `useContext(Context)`; React 19 replaces the hook with it."}],patterns:[{group:["expo-linear-gradient","react-native-linear-gradient"],message:"Apply the gradient through the `experimental_backgroundImage` style property instead of a gradient component."}]},rules:{"no-keyboard-will-events":{},"no-scroll-position-state":{},"no-conditional-style-array":{},"no-leaked-render":{},"no-rn-image-network-source":{},"no-redundant-view-nesting":{},"no-rn-namespace-import":{},"no-unlabeled-icon-pressable":{},"hoist-stateless-function":{},"no-manual-memo":{
|
|
1
|
+
const e=(t)=>new URL(t,import.meta.url).href;export const coreRegistry=[{meta:{name:"@ashstack/core"},url:e("../core/rules/core/index.js"),rules:{"no-comments":{},"no-naming-convention":{},"no-packed-condition":{},"use-design-system":{defaultOff:!0},"components-tsx-only":{defaultOff:!0},"hoist-intl":{}}},{meta:{name:"@ashstack/zod"},url:e("../core/rules/zod/index.js"),option:"zod",packages:["zod"],rules:{"prefer-enum":{}}}],reactRegistry=[{meta:{name:"@ashstack/react"},url:e("../react/rules/react/index.js"),rules:{"no-unlabeled-icon-button":{},"no-svg-without-title":{}}},{meta:{name:"@ashstack/query"},url:e("../react/rules/query/index.js"),option:"query",packages:["@tanstack/react-query"],rules:{"no-inline-keys":{},"no-deprecated-filters":{},"require-destructured-hooks":{},"no-fetch-in-query-fn":{},"next-page-param-undefined":{}}},{meta:{name:"@ashstack/zustand"},url:e("../react/rules/zustand/index.js"),option:"zustand",packages:["zustand"],rules:{"require-selector":{}}},{meta:{name:"@ashstack/i18n"},url:e("../react/rules/i18n/index.js"),option:"i18n",packages:["i18next","react-i18next","@lingui/core","react-intl","use-intl","next-intl","expo-localization"],rules:{"no-bare-text":{},"no-bare-attrs":{},"no-bare-toast":{}}},{meta:{name:"@ashstack/tailwind"},url:e("../react/rules/tailwind/index.js"),option:"tailwind",packages:["tailwindcss"],rules:{"prefer-cn":{},"use-logical-classes":{}}},{meta:{name:"@ashstack/tanstack-router"},url:e("../react/rules/tanstack-router/index.js"),option:"tanstackRouter",packages:["@tanstack/react-router"],rules:{"require-selector":{},"no-search-casts":{}}}],reactNativeRegistry=[{meta:{name:"@ashstack/react-native"},url:e("../react-native/rules/react-native/index.js"),restrictedImports:{paths:[{name:"react",importNames:["forwardRef"],message:"Accept `ref` as a regular prop and delete the `forwardRef` wrapper — React 19 passes `ref` like any other prop."},{name:"react",importNames:["useContext"],message:"Call `use(Context)` instead of `useContext(Context)`; React 19 replaces the hook with it."}],patterns:[{group:["expo-linear-gradient","react-native-linear-gradient"],message:"Apply the gradient through the `experimental_backgroundImage` style property instead of a gradient component."}]},rules:{"no-keyboard-will-events":{},"no-scroll-position-state":{},"no-conditional-style-array":{},"no-leaked-render":{},"no-rn-image-network-source":{},"no-redundant-view-nesting":{},"no-rn-namespace-import":{},"no-unlabeled-icon-pressable":{},"hoist-stateless-function":{},"no-manual-memo":{},"no-dynamic-import":{}}},{meta:{name:"@ashstack/unistyles"},url:e("../react-native/rules/unistyles/index.js"),option:"unistyles",packages:["react-native-unistyles"],restrictedImports:{paths:[{name:"react-native",importNames:["StyleSheet"],message:"Import `StyleSheet` from `react-native-unistyles` instead of `react-native`."},{name:"react-native",importNames:["useWindowDimensions","Dimensions"],message:"Read screen size from `rt.screen` inside `StyleSheet.create`, or `UnistylesRuntime.screen` outside it, instead of the `Dimensions` APIs."},{name:"react-native",importNames:["SafeAreaView"],message:'Set `contentInsetAdjustmentBehavior="automatic"` on the scroll view, or apply `rt.insets` from `react-native-unistyles`, instead of `SafeAreaView`.'},{name:"react-native-safe-area-context",importNames:["useSafeAreaInsets","useSafeAreaFrame","SafeAreaView"],message:"Read safe-area values from `rt.insets`, or `UnistylesRuntime.insets` outside a stylesheet, in `react-native-unistyles`."}]},rules:{"animated-theme":{},"content-container":{},"in-sheet":{},insets:{},"no-hardcoded-color":{},"no-hardcoded-spacing":{},"no-margin":{},"no-paramless-dynamic-function":{},"no-style-spread":{},"no-unused-styles":{},"rtl-style-call":{},"theme-screen-component":{},"theme-style-attr":{}}},{meta:{name:"@ashstack/legend-list"},url:e("../react-native/rules/legend-list/index.js"),option:"legendList",packages:["@legendapp/list"],restrictedImports:{paths:[{name:"react-native",importNames:["FlatList","SectionList","VirtualizedList"],message:"Render this list with `LegendList` from `@legendapp/list/react-native`; a `ScrollView` plus `map` is fine for fewer than 20 static items."},{name:"@shopify/flash-list",message:"Import `LegendList` from `@legendapp/list/react-native` — Legend List replaced FlashList in this stack."}],patterns:[{group:["@legendapp/list"],message:"Import from the platform entrypoint `@legendapp/list/react-native`, not the package root."}]},rules:{"required-props":{},"no-index-key-extractor":{},"no-remount-key":{},"no-inline-render-item-props":{},"no-mixed-children":{},"no-flex-in-content-container":{},"typed-items-need-item-type":{},"no-scrollview-map":{},"no-unsupported-props":{}}},{meta:{name:"@ashstack/legend-state"},url:e("../react-native/rules/legend-state/index.js"),option:"legendState",packages:["@legendapp/state"],restrictedImports:{paths:[{name:"@legendapp/state/react",importNames:["use$","useSelector"],message:"Use `useValue` from `@legendapp/state/react`; `use$` and `useSelector` are deprecated."}]},rules:{"no-assignment":{},naming:{},"no-nested-observable":{},"no-react-mirror":{},"no-untracked-get-in-jsx":{},"no-peek-in-selector":{},"no-object-selector":{},"no-observable-in-component":{}}},{meta:{name:"@ashstack/reanimated"},url:e("../react-native/rules/reanimated/index.js"),option:"reanimated",packages:["react-native-reanimated"],restrictedImports:{paths:[{name:"react-native",importNames:["Animated"],message:"Use `react-native-reanimated` instead of React Native's `Animated` API."},{name:"react-native-reanimated",importNames:["runOnJS","runOnUI"],message:"Use `scheduleOnRN` and `scheduleOnUI` from `react-native-worklets`; `runOnJS` and `runOnUI` are deprecated."},{name:"react-native-worklets",importNames:["runOnJS","runOnUI"],message:"Use `scheduleOnRN` and `scheduleOnUI` from `react-native-worklets`; `runOnJS` and `runOnUI` are deprecated."}]},rules:{"animated-reaction-safety":{},"animated-style-needs-animated-component":{},"animated-updater-purity":{},"gpu-properties-only":{},"hoist-layout-animation-builder":{},"interpolate-needs-clamp":{},"no-shared-value-dot-value":{},"no-react-state-from-continuous-worklet":{},"prefer-lazy-shared-value-initializer":{},"schedule-on-rn-scope":{},"shared-value-usage":{}}},{meta:{name:"@ashstack/turbo-image"},url:e("../react-native/rules/turbo-image/index.js"),option:"turboImage",packages:["react-native-turbo-image"],rules:{"require-resize":{},"require-cache-policy":{}}},{meta:{name:"@ashstack/skia"},url:e("../react-native/rules/skia/index.js"),option:"skia",packages:["@shopify/react-native-skia"],restrictedImports:{paths:[{name:"@shopify/react-native-skia",importNames:["usePathValue","usePathInterpolation"],message:"Keep one stable `SkPath` buffer and mutate it inside `useDerivedValue`, with consumers reading the driving shared value."}]},rules:{"canvas-opaque":{}}},{meta:{name:"@ashstack/keyboard"},url:e("../react-native/rules/keyboard/index.js"),option:"keyboard",packages:["react-native-keyboard-controller"],restrictedImports:{paths:[{name:"react-native",importNames:["KeyboardAvoidingView"],message:"Import `KeyboardAvoidingView` from `react-native-keyboard-controller` — same props, tracked per frame. For a scrolling form, its `KeyboardAwareScrollView`."}]},rules:{}}];
|
|
@@ -2,7 +2,7 @@ import type { AllowWarnDeny } from "oxlint";
|
|
|
2
2
|
/** A severity, or a severity followed by as many of the rule's options as you want to set. */
|
|
3
3
|
type RuleSetting<Options extends unknown[] = []> = AllowWarnDeny | [AllowWarnDeny, ...Partial<Options>];
|
|
4
4
|
/** Every rule id `react-native()` adds on top of the entry below it. */
|
|
5
|
-
export type ReactNativeRuleId = "@ashstack/react-native/no-keyboard-will-events" | "@ashstack/react-native/no-scroll-position-state" | "@ashstack/react-native/no-conditional-style-array" | "@ashstack/react-native/no-leaked-render" | "@ashstack/react-native/no-rn-image-network-source" | "@ashstack/react-native/no-redundant-view-nesting" | "@ashstack/react-native/no-rn-namespace-import" | "@ashstack/react-native/no-unlabeled-icon-pressable" | "@ashstack/react-native/hoist-stateless-function" | "@ashstack/react-native/no-manual-memo" | "@ashstack/react-native/no-dynamic-import" | "@ashstack/unistyles/animated-theme" | "@ashstack/unistyles/content-container" | "@ashstack/unistyles/in-sheet" | "@ashstack/unistyles/insets" | "@ashstack/unistyles/no-hardcoded-color" | "@ashstack/unistyles/no-hardcoded-spacing" | "@ashstack/unistyles/no-margin" | "@ashstack/unistyles/no-style-spread" | "@ashstack/unistyles/no-unused-styles" | "@ashstack/unistyles/rtl-style-call" | "@ashstack/unistyles/theme-screen-component" | "@ashstack/unistyles/theme-style-attr" | "@ashstack/legend-list/required-props" | "@ashstack/legend-list/no-index-key-extractor" | "@ashstack/legend-list/no-remount-key" | "@ashstack/legend-list/no-inline-render-item-props" | "@ashstack/legend-list/no-mixed-children" | "@ashstack/legend-list/no-flex-in-content-container" | "@ashstack/legend-list/typed-items-need-item-type" | "@ashstack/legend-list/no-scrollview-map" | "@ashstack/legend-list/no-unsupported-props" | "@ashstack/legend-state/no-assignment" | "@ashstack/legend-state/naming" | "@ashstack/legend-state/no-nested-observable" | "@ashstack/legend-state/no-react-mirror" | "@ashstack/legend-state/no-untracked-get-in-jsx" | "@ashstack/legend-state/no-peek-in-selector" | "@ashstack/legend-state/no-object-selector" | "@ashstack/legend-state/no-observable-in-component" | "@ashstack/reanimated/animated-reaction-safety" | "@ashstack/reanimated/animated-style-needs-animated-component" | "@ashstack/reanimated/animated-updater-purity" | "@ashstack/reanimated/gpu-properties-only" | "@ashstack/reanimated/hoist-layout-animation-builder" | "@ashstack/reanimated/interpolate-needs-clamp" | "@ashstack/reanimated/no-shared-value-dot-value" | "@ashstack/reanimated/no-react-state-from-continuous-worklet" | "@ashstack/reanimated/prefer-lazy-shared-value-initializer" | "@ashstack/reanimated/schedule-on-rn-scope" | "@ashstack/reanimated/shared-value-usage" | "@ashstack/turbo-image/require-resize" | "@ashstack/turbo-image/require-cache-policy" | "@ashstack/skia/canvas-opaque";
|
|
5
|
+
export type ReactNativeRuleId = "@ashstack/react-native/no-keyboard-will-events" | "@ashstack/react-native/no-scroll-position-state" | "@ashstack/react-native/no-conditional-style-array" | "@ashstack/react-native/no-leaked-render" | "@ashstack/react-native/no-rn-image-network-source" | "@ashstack/react-native/no-redundant-view-nesting" | "@ashstack/react-native/no-rn-namespace-import" | "@ashstack/react-native/no-unlabeled-icon-pressable" | "@ashstack/react-native/hoist-stateless-function" | "@ashstack/react-native/no-manual-memo" | "@ashstack/react-native/no-dynamic-import" | "@ashstack/unistyles/animated-theme" | "@ashstack/unistyles/content-container" | "@ashstack/unistyles/in-sheet" | "@ashstack/unistyles/insets" | "@ashstack/unistyles/no-hardcoded-color" | "@ashstack/unistyles/no-hardcoded-spacing" | "@ashstack/unistyles/no-margin" | "@ashstack/unistyles/no-paramless-dynamic-function" | "@ashstack/unistyles/no-style-spread" | "@ashstack/unistyles/no-unused-styles" | "@ashstack/unistyles/rtl-style-call" | "@ashstack/unistyles/theme-screen-component" | "@ashstack/unistyles/theme-style-attr" | "@ashstack/legend-list/required-props" | "@ashstack/legend-list/no-index-key-extractor" | "@ashstack/legend-list/no-remount-key" | "@ashstack/legend-list/no-inline-render-item-props" | "@ashstack/legend-list/no-mixed-children" | "@ashstack/legend-list/no-flex-in-content-container" | "@ashstack/legend-list/typed-items-need-item-type" | "@ashstack/legend-list/no-scrollview-map" | "@ashstack/legend-list/no-unsupported-props" | "@ashstack/legend-state/no-assignment" | "@ashstack/legend-state/naming" | "@ashstack/legend-state/no-nested-observable" | "@ashstack/legend-state/no-react-mirror" | "@ashstack/legend-state/no-untracked-get-in-jsx" | "@ashstack/legend-state/no-peek-in-selector" | "@ashstack/legend-state/no-object-selector" | "@ashstack/legend-state/no-observable-in-component" | "@ashstack/reanimated/animated-reaction-safety" | "@ashstack/reanimated/animated-style-needs-animated-component" | "@ashstack/reanimated/animated-updater-purity" | "@ashstack/reanimated/gpu-properties-only" | "@ashstack/reanimated/hoist-layout-animation-builder" | "@ashstack/reanimated/interpolate-needs-clamp" | "@ashstack/reanimated/no-shared-value-dot-value" | "@ashstack/reanimated/no-react-state-from-continuous-worklet" | "@ashstack/reanimated/prefer-lazy-shared-value-initializer" | "@ashstack/reanimated/schedule-on-rn-scope" | "@ashstack/reanimated/shared-value-usage" | "@ashstack/turbo-image/require-resize" | "@ashstack/turbo-image/require-cache-policy" | "@ashstack/skia/canvas-opaque";
|
|
6
6
|
declare module "oxlint" {
|
|
7
7
|
interface DummyRuleMap {
|
|
8
8
|
/**
|
|
@@ -60,9 +60,7 @@ declare module "oxlint" {
|
|
|
60
60
|
*/
|
|
61
61
|
"@ashstack/react-native/hoist-stateless-function"?: RuleSetting;
|
|
62
62
|
/**
|
|
63
|
-
* Require a `// why:` line above every kept `useMemo`, `useCallback` and `memo`. The React Compiler memoises on a best-effort basis, not a guarantee, so a memo is allowed where the cost is real: something rendered per list row, or a computation measured as heavy. The `// why:` line names which of the two applies. `@ashstack/core/no-comments` keeps that line and never counts it against its `budget`, so both rules run together.
|
|
64
|
-
*
|
|
65
|
-
* Enabled only when one of `babel-plugin-react-compiler`, `react-compiler-runtime`, `react-compiler-marker` is a dependency.
|
|
63
|
+
* Require a `// why:` line above every kept `useMemo`, `useCallback` and `memo`. The React Compiler memoises on a best-effort basis, not a guarantee, so a memo is allowed where the cost is real: something rendered per list row, or a computation measured as heavy. The `// why:` line names which of the two applies. `@ashstack/core/no-comments` keeps that line and never counts it against its `budget`, so both rules run together. Assumes the compiler is on — pass `reactCompiler: false` to the entry and this rule turns off, since without it a hand-written memo is the only memo there is.
|
|
66
64
|
*
|
|
67
65
|
* @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreact-nativeno-manual-memo
|
|
68
66
|
*/
|
|
@@ -115,6 +113,12 @@ declare module "oxlint" {
|
|
|
115
113
|
* @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackunistylesno-margin
|
|
116
114
|
*/
|
|
117
115
|
"@ashstack/unistyles/no-margin"?: RuleSetting;
|
|
116
|
+
/**
|
|
117
|
+
* Disallow a style written as a function that takes no arguments. It returns the same object on every render, and `theme` and `rt` reach a static style anyway, so the function only adds a call at each use site. The suggestion turns `() => ({ ... })` back into the object.
|
|
118
|
+
*
|
|
119
|
+
* @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackunistylesno-paramless-dynamic-function
|
|
120
|
+
*/
|
|
121
|
+
"@ashstack/unistyles/no-paramless-dynamic-function"?: RuleSetting;
|
|
118
122
|
/**
|
|
119
123
|
* Disallow spreading a stylesheet style into another object. The spread reads through the Unistyles C++ proxy once, so the result stops reacting to the theme.
|
|
120
124
|
*
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -102,6 +102,12 @@ export type CoreOptions = {
|
|
|
102
102
|
zod?: boolean;
|
|
103
103
|
};
|
|
104
104
|
export type ReactOptions = CoreOptions & {
|
|
105
|
+
/**
|
|
106
|
+
* Is the React Compiler on? Defaults to `true` — every other React setting here assumes it.
|
|
107
|
+
* Set `false` and the four [`react-perf/` inline-prop rules](https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#react)
|
|
108
|
+
* come back on, since without the compiler a fresh object, array, function or element in a prop really does re-render the child.
|
|
109
|
+
*/
|
|
110
|
+
reactCompiler?: boolean;
|
|
105
111
|
/** [`@ashstack/query/` rules](https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackquery) — detected from `@tanstack/react-query` */
|
|
106
112
|
query?: boolean;
|
|
107
113
|
/** [`@ashstack/zustand/` rules](https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackzustand) — detected from `zustand` */
|
package/dist/react/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import o from"../core/index.js";import{EFFECT_RULES as t,effectPlugin as a}from"../lib/effect-plugin.js";import{mergeConfigs as s}from"../lib/merge.js";import{composeModules as n}from"../lib/module.js";import{coreRegistry as c,reactRegistry as i}from"../lib/registry.js";const
|
|
1
|
+
import o from"../core/index.js";import{EFFECT_RULES as t,effectPlugin as a}from"../lib/effect-plugin.js";import{mergeConfigs as s}from"../lib/merge.js";import{composeModules as n}from"../lib/module.js";import{coreRegistry as c,reactRegistry as i}from"../lib/registry.js";const p={"eslint/no-empty":"off","eslint/no-empty-function":"off"},l={"react/error-boundaries":"error","react/globals":"error","react/immutability":"error","react/incompatible-library":"error","react/preserve-manual-memoization":"error","react/purity":"error","react/refs":"error","react/set-state-in-effect":"off","react/set-state-in-render":"error","react/static-components":"error","react/use-memo":"error","react/void-use-memo":"error","react/capitalized-calls":"error","react/exhaustive-effect-dependencies":"error","react/hooks":"error","react/memo-dependencies":"error","react/no-deriving-state-in-effects":"off","react/invariant":"error","react/rule-suppression":"error","react/syntax":"error","react/todo":"error","react/unsupported-syntax":"error"},f=(e)=>{const r=e?"off":"error";return{"react-perf/jsx-no-jsx-as-prop":r,"react-perf/jsx-no-new-array-as-prop":r,"react-perf/jsx-no-new-function-as-prop":r,"react-perf/jsx-no-new-object-as-prop":r,"react/jsx-no-constructed-context-values":r,"react/no-object-type-as-default-prop":r}},u=["**/routes/**","**/src/app/**","**/app/**/_layout.tsx","**/app/**/+*.tsx"],d={...p,"max-lines-per-function":["error",{max:120,skipBlankLines:!0,skipComments:!0}],"react/react-in-jsx-scope":"off","react/style-prop-object":"off","react/only-export-components":"off","react/rules-of-hooks":"error","react/exhaustive-deps":"error","react/button-has-type":"error","react/checked-requires-onchange-or-readonly":"error","react/iframe-missing-sandbox":"error","react/jsx-boolean-value":"error","react/jsx-key":"error","react/jsx-no-comment-textnodes":"error","react/jsx-no-duplicate-props":"error","react/jsx-no-target-blank":"error","react/jsx-no-useless-fragment":"error","react/no-array-index-key":"error","react/no-children-prop":"error","react/no-danger":"error","react/no-danger-with-children":"error","react/no-direct-mutation-state":"error","react/no-is-mounted":"error","react/no-render-return-value":"error","react/no-string-refs":"error","react/no-this-in-sfc":"error","react/no-unsafe":"error","react/no-unescaped-entities":"error","react/no-unstable-nested-components":["error",{allowAsProps:!0}],"react/require-render-return":"error","react/self-closing-comp":"error","react/void-dom-elements-no-children":"error",...l,"jsx-a11y/alt-text":"error","jsx-a11y/anchor-has-content":"error","jsx-a11y/anchor-is-valid":"error","jsx-a11y/click-events-have-key-events":"off","jsx-a11y/control-has-associated-label":"off","jsx-a11y/heading-has-content":"error","jsx-a11y/img-redundant-alt":"error","jsx-a11y/interactive-supports-focus":"error","jsx-a11y/label-has-associated-control":"error","jsx-a11y/no-aria-hidden-on-focusable":"error","jsx-a11y/no-autofocus":["error",{ignoreNonDOM:!0}],"jsx-a11y/no-noninteractive-element-interactions":"off","jsx-a11y/no-noninteractive-tabindex":"error","jsx-a11y/no-redundant-roles":"error","jsx-a11y/no-static-element-interactions":"error","jsx-a11y/prefer-tag-over-role":"off","jsx-a11y/role-has-required-aria-props":"error","jsx-a11y/role-supports-aria-props":"error","unicorn/filename-case":["error",{cases:{kebabCase:!0,pascalCase:!0}}]},react=(e={})=>{const r=n([...c,...i],e);return s(o(e),{plugins:["react","jsx-a11y","react-perf"],jsPlugins:[a,...r.jsPlugins],rules:{...d,...f(e.reactCompiler??!0),...t,...r.rules},overrides:[{files:u,rules:{"unicorn/filename-case":"off"}}]})};export{react};export default react;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{crossesFunctionBefore as
|
|
1
|
+
import{crossesFunctionBefore as a,findInSubtree as o,FUNCTION_TYPES as u,gate as l,problem as s,propertyKeyName as i}from"../../../lib/ast.js";const g="Return `undefined` to signal there are no more pages — `null` reads as a real next cursor, so the list keeps fetching forever.",p=(e)=>e.type==="ReturnStatement"&&e.argument?.type==="Literal"&&e.argument.value===null,m=(e)=>e.type==="ArrowFunctionExpression"&&e.body.type==="Literal"&&e.body.value===null?e.body:null;export const nextPageParamUndefined=s("Disallow `return null` in the body of a `getNextPageParam`.",{createOnce(e){return{before(){return l(e,"getNextPageParam")},Property(t){if(i(t)!=="getNextPageParam")return;const{value:r}=t,n=m(r)??o(r,p);if(!n||a(n,r,u))return;e.report({node:r,message:g})}}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const n="Pass a filter object here: `.invalidateQueries({ queryKey: someKeys.scope(...) })
|
|
1
|
+
const n="Pass a filter object here: `.invalidateQueries({ queryKey: someKeys.scope(...) })`; v5 removed the positional form.",o=new Set(["invalidateQueries","removeQueries","refetchQueries","cancelQueries","resetQueries"]),l=(e)=>{if(e.type!=="CallExpression")return!1;const{callee:t}=e;if(t.type!=="MemberExpression"||t.computed)return!1;const{property:r}=t;return r.type==="Identifier"&&o.has(r.name)},a=(e)=>{if(e.type!=="CallExpression"||e.arguments.length!==1)return;return e.arguments[0]},u=(e)=>e.type==="Literal"&&typeof e.value==="string",c=(e,t)=>{if(t.type==="ArrayExpression")return`{ queryKey: ${e.sourceCode.getText(t)} }`;if(u(t))return`{ queryKey: [${e.sourceCode.getText(t)}] }`;return null};export const noDeprecatedFilters={meta:{type:"problem",docs:{description:"Disallow the positional key argument that TanStack Query v5 removed from `invalidateQueries` and its sibling methods. The suggestion rewrites it to the filter-object form."},hasSuggestions:!0},createOnce(e){return{CallExpression(t){if(!l(t))return;const r=a(t);if(r===void 0)return;const s=c(e,r);if(s===null)return;e.report({node:r,message:n,suggest:[{desc:"Wrap in a filter object",fix:(i)=>i.replaceText(r,s)}]})}}}};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{gate as
|
|
1
|
+
import{gate as t,problem as a,propertyKeyName as n,subtreeHas as s}from"../../../lib/ast.js";const o="Call a typed function from the feature's `*.requests.ts` module here instead of `fetch` — it carries auth, retries and the app's error type.",u=new Set(["queryFn","mutationFn"]),i=(e)=>e.type==="CallExpression"&&e.callee.type==="Identifier"&&e.callee.name==="fetch";export const noFetchInQueryFn=a("Disallow a bare `fetch(` inside a `queryFn` or `mutationFn`.",{createOnce(e){return{before(){return t(e,"queryFn","mutationFn")},Property(r){if(!u.has(n(r)))return;if(!s(r.value,i))return;e.report({node:r.value,message:o})}}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{problem as a,propertyKeyName as y}from"../../../lib/ast.js";const i={inlineQueryKey:"Define this key in the feature's `*.keys.ts` factory and reference it: `queryKey: someKeys.scope(...)
|
|
1
|
+
import{problem as a,propertyKeyName as y}from"../../../lib/ast.js";const i={inlineQueryKey:"Define this key in the feature's `*.keys.ts` factory and reference it: `queryKey: someKeys.scope(...)`.",inlineGetQueryData:"Read through the feature's `*.keys.ts` factory: `getQueryData(someKeys.scope())`.",inlineSetQueryData:"Write through the feature's `*.keys.ts` factory: `setQueryData(someKeys.scope(), updater)`."},o=new Set(["useQuery","useSuspenseQuery","useInfiniteQuery","useSuspenseInfiniteQuery","useQueries"]),p=new Set(["invalidateQueries","removeQueries","refetchQueries","cancelQueries","resetQueries","fetchQuery","prefetchQuery","ensureQueryData","getQueriesData","setQueriesData"]),f=(r)=>{if(r.type!=="CallExpression")return"";const{callee:e}=r;if(e.type!=="MemberExpression"||e.computed)return"";const{property:t}=e;return t.type==="Identifier"?t.name:""},c=(r)=>{if(r.type!=="CallExpression")return;const[e]=r.arguments;return e?.type==="ArrayExpression"?e:void 0},l=(r,e)=>{const t=r.type==="CallExpression"?r.callee:void 0;return t?.type==="Identifier"&&o.has(t.name)||p.has(e)},Q=(r)=>{if(r.type!=="Property"||r.computed)return!1;const{key:e}=r;if(e.type==="Identifier")return e.name==="queryKey";return e.type==="Literal"&&e.value==="queryKey"},m=(r)=>{if(r.type!=="ObjectExpression")return[];const e=r.properties.find((t)=>t.type==="Property"&&y(t)==="queries");if(e?.type!=="Property"||e.value.type!=="ArrayExpression")return[r];return[r,...e.value.elements.filter((t)=>t!==null)]},d=(r)=>{if(r.type!=="CallExpression")return[];const[e]=r.arguments;if(e===void 0)return[];const t=[];for(const s of m(e)){if(s.type!=="ObjectExpression")continue;for(const n of s.properties){if(n.type!=="Property"||!Q(n))continue;const{value:u}=n;if(u.type==="ArrayExpression")t.push(u)}}return t};export const noInlineKeys=a("Disallow a query key written as an array literal at the call site instead of coming from a keys factory.",{createOnce(r){return{CallExpression(e){const t=f(e);if(t==="getQueryData"||t==="setQueryData"){const s=c(e);if(s===void 0)return;r.report({node:s,message:t==="getQueryData"?i.inlineGetQueryData:i.inlineSetQueryData});return}if(!l(e,t))return;for(const s of d(e))r.report({node:s,message:i.inlineQueryKey})}}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{problem as
|
|
1
|
+
import{problem as a}from"../../../lib/ast.js";const c="Destructure this hook at the call site: `const { data } = useFooQuery()`.",u=/^@\/api\/.*\.(?:queries|mutations)$/;export const requireDestructuredHooks=a("Require destructuring the result of a hook imported from an `@/api/*.queries` or `@/api/*.mutations` module.",{createOnce(n){const o=new Set,r=[];return{before(){o.clear();r.length=0},ImportDeclaration(e){if(!u.test(e.source.value))return;for(const t of e.specifiers)if(t.type==="ImportSpecifier")o.add(t.local.name)},VariableDeclarator(e){const{id:t,init:s}=e;if(t.type!=="Identifier")return;if(s?.type!=="CallExpression")return;const{callee:i}=s;if(i.type!=="Identifier"||!/^use[A-Z]/.test(i.name))return;r.push({node:t,hook:i.name})},"Program:exit"(){for(const e of r){if(!o.has(e.hook))continue;n.report({node:e.node,message:c})}}}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{attributeName as a,gate as s,problem as o}from"../../../lib/ast.js";import{childNames as l,namedBy as m,NAMING_ATTRIBUTES as
|
|
1
|
+
import{attributeName as a,gate as s,problem as o}from"../../../lib/ast.js";import{childNames as l,namedBy as m,NAMING_ATTRIBUTES as f}from"./shared.js";const c=new Set(["presentation","none"]),p='Give this `<svg>` a `<title>` child with content, or an `aria-label`. If it carries no meaning of its own, mark it `aria-hidden="true"`.',u=(e)=>{if(e?.type==="Literal")return e.value!=="false";if(e?.type!=="JSXExpressionContainer")return!0;const{expression:t}=e;return!(t.type==="Literal"&&(t.value===!1||t.value==="false"))},g=(e)=>e?.type==="Literal"&&typeof e.value==="string"&&c.has(e.value),d=(e)=>{if(e.type!=="JSXAttribute")return!1;const t=a(e);if(t==="aria-hidden")return u(e.value);if(t==="role")return g(e.value);return m(e,f)};export const noSvgWithoutTitle=o("Require an accessible name on an inline `<svg>`: a `<title>` child with content, an `aria-label`, or a marker that it is decorative. Every child counts, and a self-closing `<svg />` reports.",{createOnce(e){const t=(r)=>{if(r.type!=="JSXElement")return!1;const{name:n}=r.openingElement;if(n.type!=="JSXIdentifier"||n.name!=="title")return!1;return r.children.some((i)=>l(i,e)||i.type==="JSXElement"||i.type==="JSXFragment")};return{before(){return s(e,"<svg")},JSXElement(r){const n=r.openingElement;if(n.name.type!=="JSXIdentifier"||n.name.name!=="svg")return;if(n.attributes.some(d))return;if(r.children.some(t))return;e.report({node:n.name,message:p})}}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{attributeName as o,findInSubtree as i,gate as s,problem as l,tagIdentifier as m}from"../../../lib/ast.js";import{attributeHasContent as u,childNames as
|
|
1
|
+
import{attributeName as o,findInSubtree as i,gate as s,problem as l,tagIdentifier as m}from"../../../lib/ast.js";import{attributeHasContent as u,childNames as b,namedBy as c,NAMING_ATTRIBUTES as p}from"./shared.js";const S=new Set(["button","Button"]),f="Add an `aria-label` (or `aria-labelledby`), or render visible text, so this icon-only button has an accessible name.",g=(e)=>e.type==="JSXAttribute"&&o(e)==="alt"&&u(e.value),d=(e)=>e.type==="JSXOpeningElement"&&m(e.name)==="img"&&e.attributes.some(g);export const noUnlabeledIconButton=l("Require an accessible name on an icon-only `<button>` or `<Button>`. Without a label, visible text or image alt text, a screen reader cannot reach the control.",{createOnce(e){const r=(t)=>b(t,e)||d(t);return{before(){return s(e,"button","Button")},JSXElement(t){const a=t.openingElement;if(a.name.type!=="JSXIdentifier"||!S.has(a.name.name))return;if(a.attributes.some((n)=>c(n,p)))return;if(!t.children.some((n)=>n.type==="JSXElement"||n.type==="JSXFragment"))return;if(t.children.some((n)=>i(n,r)!==null))return;e.report({node:a.name,message:f})}}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{attributeName as f,importedNames as
|
|
1
|
+
import{attributeName as f,importedNames as m,problem as h}from"../../../lib/ast.js";import{CLASS_ATTRIBUTE as d,CLASS_BINDING as S,CLASS_MARKER as y,COMPOSER_MODULES as b,KNOWN_COMPOSERS as c}from"./shared.js";const g=new Set(["join","concat"]),E=new Set(["ConditionalExpression","LogicalExpression"]),a={attribute:"Wrap this class value in `cn(...)`, so a later Tailwind utility wins over an earlier one setting the same property.",precomputed:"Wrap this value in `cn(...)` where it reaches the class prop, or build it with `cn(...)` where it is declared.",declaration:"Build this class value with `cn(...)` instead of joining it by hand, so conflicting utilities resolve and falsy branches drop out."},w=(e,r)=>{if(e.type==="MemberExpression")return e.property.type==="Identifier"&&g.has(e.property.name);if(e.type!=="Identifier")return!1;return c.has(e.name)?e.name!=="cn":r.has(e.name)},p=(e,r)=>{if(!e)return!1;if(E.has(e.type))return!0;if(e.type==="TemplateLiteral")return e.expressions.length>0;if(e.type==="BinaryExpression")return e.operator==="+";return e.type==="CallExpression"&&w(e.callee,r)},C=(e)=>{if(e.type!=="JSXAttribute")return null;const{value:r}=e;return r?.type==="JSXExpressionContainer"?r.expression:null};export const preferCn=h("Require a dynamic class value to go through `cn(...)` before it reaches a `class`, `className` or `*ClassName` prop. Reports both where the value reaches the prop and where a variable named after classes is declared.",{createOnce(e){const r=new Set,s=new Map,o=[],u=()=>{const t=new Set;for(const[n,i]of s){if(!S.test(n))continue;if(!p(i,r))continue;t.add(n);e.report({node:i.parent??i,message:a.declaration})}return t},l=(t)=>{for(const n of o){const i=C(n);if(!i)continue;if(p(i,r)){e.report({node:n,message:a.attribute});continue}if(i.type!=="Identifier"||t.has(i.name))continue;if(!p(s.get(i.name),r))continue;e.report({node:n,message:a.precomputed})}};return{before(){r.clear();s.clear();o.length=0;return y.test(e.sourceCode.text)},ImportDeclaration(t){for(const n of b)for(const{local:i}of m(t,n))if(!c.has(i))r.add(i)},VariableDeclarator(t){if(t.id.type==="Identifier"&&t.init)s.set(t.id.name,t.init)},JSXAttribute(t){if(d.test(f(t)))o.push(t)},"Program:exit"(){l(u())}}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{attributeName as
|
|
1
|
+
import{attributeName as o,hasAncestor as a,problem as n}from"../../../lib/ast.js";import{CLASS_ATTRIBUTE as f,CLASS_BINDING as u}from"./shared.js";const d="ml|mr|pl|pr|left|right|scroll-ml|scroll-mr|scroll-pl|scroll-pr",m="border-l|border-r|rounded-l|rounded-r|rounded-tl|rounded-tr|rounded-bl|rounded-br",c="text-left|text-right|float-left|float-right|clear-left|clear-right",p=String.raw`(?:(?:${d})-\S+|(?:${m})(?:-\S+)?|${c})`,s=String.raw`[\s"'\`{}]`,e=new RegExp(String.raw`(?:^|${s})(?:[^\s:]+:)*!?-?${p}(?:$|${s})`),l="Swap this for its logical twin so the layout mirrors in RTL: `ms`/`me` for `ml`/`mr`, `text-start` for `text-left`. Insets, borders and corners follow.",i=(t)=>a(t,(r)=>{if(r.type==="JSXAttribute")return f.test(o(r));if(r.type!=="VariableDeclarator")return!1;return r.id.type==="Identifier"&&u.test(r.id.name)});export const useLogicalClasses=n("Require a logical Tailwind utility over its physical left/right twin, so a right-to-left layout mirrors. Reads class props and bindings named after classes, through variant prefixes and the `!` and `-` modifiers.",{createOnce(t){return{before(){return e.test(t.sourceCode.text)},Literal(r){if(typeof r.value!=="string")return;if(!e.test(r.value))return;if(!i(r))return;t.report({node:r,message:l})},TemplateElement(r){if(!e.test(r.value.raw))return;if(!i(r))return;t.report({node:r,message:l})}}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{gate as u,importedNames as m,problem as f}from"../../../lib/ast.js";import{ROUTER_MODULE as
|
|
1
|
+
import{gate as u,importedNames as m,problem as f}from"../../../lib/ast.js";import{ROUTER_MODULE as p}from"./shared.js";const S=["search","location","state"],l={search:"Drop this assertion and let the route's `validateSearch` schema type the result; widen the schema, or parse the value, if the type is wrong.",routerState:"Drop this assertion and read validated search through `useSearch({ from: ... })` — `router.state.location.search` is whatever the URL held."},d=(t)=>{let e=t;while(e.type==="TSAsExpression")e=e.expression;return e},c=(t)=>t?.type==="CallExpression"&&t.callee.type==="Identifier"?t.callee.name:"",y=(t)=>{if(t.type!=="MemberExpression")return"";const{property:e}=t;if(e.type==="Identifier")return t.computed?"":e.name;return e.type==="Literal"?String(e.value):""},g=(t)=>{let e=t;for(const s of S){if(e.type!=="MemberExpression"||y(e)!==s)return null;e=e.object}return e};export const noSearchCasts=f("Disallow an `as` assertion on a `useSearch()` result or on `router.state.location.search`. The route's `validateSearch` schema is what supplies the type.",{createOnce(t){const e=new Set,s=new Set,o=new Set,i=[],h=(r)=>r.type==="Identifier"?o.has(r.name):s.has(c(r));return{before(){e.clear();s.clear();o.clear();i.length=0;return u(t,p)},ImportDeclaration(r){for(const{imported:a,local:n}of m(r,p))if(a==="useSearch")e.add(n);else if(a==="useRouter")s.add(n)},VariableDeclarator(r){if(r.id.type!=="Identifier")return;if(s.has(c(r.init)))o.add(r.id.name)},TSAsExpression(r){if(r.parent.type==="TSAsExpression")return;i.push({node:r,inner:d(r.expression)})},"Program:exit"(){for(const{node:r,inner:a}of i){if(e.has(c(a))){t.report({node:r,message:l.search});continue}const n=g(a);if(n&&h(n))t.report({node:r,message:l.routerState})}}}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{gate as h,importedNames as m,problem as p,propertyValue as i}from"../../../lib/ast.js";import{ROUTER_MODULE as l}from"./shared.js";const d=new Set(["useLocation","useRouterState"]),c={whole:"
|
|
1
|
+
import{gate as h,importedNames as m,problem as p,propertyValue as i}from"../../../lib/ast.js";import{ROUTER_MODULE as l}from"./shared.js";const d=new Set(["useLocation","useRouterState"]),c={whole:"Pass a `select` that returns what this component reads, e.g. `{ select: location => location.pathname }` — the whole router value re-renders on every navigation.",noSelect:"Add a `select` returning the smallest value this component reads — the whole router value re-renders on every navigation.",search:"Add a `select` returning the keys this component reads — `{ strict: false }` subscribes to every route-search change."},u=(t)=>i(t,"select")!==null,f=(t)=>{const s=i(t,"strict");return s?.type==="Literal"&&s.value===!1};export const requireSelector=p("Require a `select` on `useLocation`, `useRouterState` and non-strict `useSearch`, so a component reads the smallest router value it needs instead of re-rendering on every navigation.",{createOnce(t){const s=new Set,a=new Set,n=[];return{before(){s.clear();a.clear();n.length=0;return h(t,l)},ImportDeclaration(e){for(const{imported:o,local:r}of m(e,l))if(d.has(o))s.add(r);else if(o==="useSearch")a.add(r)},CallExpression(e){if(e.callee.type==="Identifier")n.push({node:e,name:e.callee.name,options:e.arguments[0]})},"Program:exit"(){for(const{node:e,name:o,options:r}of n){if(s.has(o)){if(!r)t.report({node:e,message:c.whole});else if(!u(r))t.report({node:e,message:c.noSelect});continue}if(!a.has(o)||!f(r))continue;if(!u(r))t.report({node:e,message:c.search})}}}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{problem as u}from"../../../lib/ast.js";const
|
|
1
|
+
import{problem as u}from"../../../lib/ast.js";const l={zustandBare:"Pass a selector to this store hook, e.g. `useSettingsStore(state => state.theme)`. Use `.getState()` for an imperative read.",zustandUndefined:"Replace `undefined` with a selector such as `state => state.theme`. Use `.getState()` for an imperative read."},f=/^(?:@\/stores\/|(?:\.\.?\/)+stores\/)[^"']*-store$/,i=/^use[A-Za-z0-9_$]*Store$/,d=(s)=>{if(s.type!=="ImportSpecifier")return null;const n=s.imported.type==="Identifier"?s.imported.name:"",t=s.local.name;return i.test(n)||i.test(t)?t:null};export const requireSelector=u("Require a selector on a store hook, rather than no arguments or `undefined` in its place.",{createOnce(s){const n=new Set,t=[];return{before(){n.clear();t.length=0},ImportDeclaration(e){if(!f.test(e.source.value))return;for(const o of e.specifiers){const r=d(o);if(r!==null)n.add(r)}},CallExpression(e){const{callee:o}=e;if(o.type!=="Identifier")return;const{name:r}=o;if(!n.has(r)&&!i.test(r))return;const a=e.arguments;if(a.length===0){t.push({node:e,name:r,bare:!0});return}const[c]=a;if(a.length===1&&c?.type==="Identifier"&&c.name==="undefined")t.push({node:e,name:r,bare:!1})},"Program:exit"(){for(const e of t){if(!n.has(e.name))continue;s.report({node:e.node,message:e.bare?l.zustandBare:l.zustandUndefined})}}}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{TEST_FILES as
|
|
1
|
+
import{TEST_FILES as a}from"../core/index.js";import{mergeConfigs as r}from"../lib/merge.js";import{composeModules as s}from"../lib/module.js";import{coreRegistry as o,reactNativeRegistry as i,reactRegistry as n}from"../lib/registry.js";import c from"../react/index.js";export const banGroups=[{packages:["react-native-gesture-handler"],restrictedImports:{paths:[{name:"react-native",importNames:["Pressable","TouchableOpacity","TouchableHighlight","TouchableWithoutFeedback","TouchableNativeFeedback"],message:"Import `Pressable` from `react-native-gesture-handler`. Inside a SwiftUI/Compose host view, keep React Native's `Pressable` and suppress this rule naming the host."}]}},{packages:["expo-router"],restrictedImports:{patterns:[{group:["@react-navigation/stack","@react-navigation/bottom-tabs"],message:"Use expo-router's `Stack` / `NativeTabs` instead of a raw react-navigation navigator."}]}},{packages:["expo-font"],restrictedImports:{paths:[{name:"expo-font",importNames:["useFonts","loadAsync"],message:"Declare the fonts in the `expo-font` config plugin so they load at build time, instead of `useFonts`/`loadAsync` at runtime."}]}},{packages:["react-native-quick-crypto"],restrictedImports:{paths:[{name:"crypto-js",message:"Use `react-native-quick-crypto` instead — `crypto-js` is pure JavaScript and runs orders of magnitude slower."}]}}];const p={"eslint/no-empty":"error","eslint/no-empty-function":"error"},m={"@ashstack/effects/no-event-handler":"off"},l=(e)=>e?{}:{"@ashstack/react-native/no-manual-memo":"off"},reactNative=(e={})=>{const t=s([...o,...n,...i],e,banGroups);return r(c(e),{jsPlugins:t.jsPlugins,globals:{__DEV__:"readonly"},rules:{...p,...m,"no-restricted-imports":["error",t.restricted],...t.rules,...l(e.reactCompiler??!0)},ignorePatterns:["**/.expo/**","**/android/**","**/ios/**"],overrides:[{files:a,rules:{"@ashstack/react-native/no-dynamic-import":"off","@ashstack/react-native/hoist-stateless-function":"off"}}]})};export{reactNative};export default reactNative;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{defineModule as e}from"../../../lib/module.js";export default e({meta:{name:"@ashstack/keyboard"},url:import.meta.url,packages:["react-native-keyboard-controller"],option:"keyboard",docsWhen:"auto-enabled when `react-native-keyboard-controller` is a dependency",rules:{},restrictedImports:{paths:[{name:"react-native",importNames:["KeyboardAvoidingView"],message:"Import `KeyboardAvoidingView` from `react-native-keyboard-controller` — same props,
|
|
1
|
+
import{defineModule as e}from"../../../lib/module.js";export default e({meta:{name:"@ashstack/keyboard"},url:import.meta.url,packages:["react-native-keyboard-controller"],option:"keyboard",docsWhen:"auto-enabled when `react-native-keyboard-controller` is a dependency",rules:{},restrictedImports:{paths:[{name:"react-native",importNames:["KeyboardAvoidingView"],message:"Import `KeyboardAvoidingView` from `react-native-keyboard-controller` — same props, tracked per frame. For a scrolling form, its `KeyboardAwareScrollView`."}]}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{attributeName as u,gate as m,problem as h}from"../../../lib/ast.js";import{isStyleSheetCreate as x,propertyName as c,stylesObjectOf as b}from"../../stylesheet.js";import{expressionOf as g,isListElement as
|
|
1
|
+
import{attributeName as u,gate as m,problem as h}from"../../../lib/ast.js";import{isStyleSheetCreate as x,propertyName as c,stylesObjectOf as b}from"../../stylesheet.js";import{expressionOf as g,isListElement as S}from"./shared.js";const d="Move `flex` onto `style` and keep `contentContainerStyle` for padding — on the content container the list measures as zero height and renders blank.",f=(e)=>e?.type==="ObjectExpression"&&e.properties.some((t)=>t.type==="Property"&&!t.computed&&c(t)==="flex"),C=(e)=>{if(e?.type!=="MemberExpression"||e.object.type!=="Identifier")return null;const t=e.object.name;if(!e.computed&&e.property.type==="Identifier")return{sheet:t,key:e.property.name};if(e.computed&&e.property.type==="Literal"&&typeof e.property.value==="string")return{sheet:t,key:e.property.value};return null},E=(e)=>{if(!e)return[];if(e.type!=="ArrayExpression")return[e];return e.elements.filter((t)=>t!==null)};export const noFlexInContentContainer=h("Disallow `flex` in a Legend List's `contentContainerStyle`, where it sizes the scrolled content to the viewport and the list ends up measuring zero height.",{createOnce(e){let t=new Map,i=[];const y=(r)=>{if(f(r))return!0;const n=C(r);return n!==null&&(t.get(n.sheet)?.has(n.key)??!1)};return{before(){t=new Map;i=[];return m(e,"contentContainerStyle")},VariableDeclarator(r){const{id:n,init:s}=r;if(n.type!=="Identifier"||s?.type!=="CallExpression"||!x(s))return;const p=b(s.arguments[0]);if(!p)return;const a=new Set;for(const o of p.properties){if(o.type!=="Property"||o.computed)continue;const l=c(o);if(l!==""&&f(o.value))a.add(l)}t.set(n.name,a)},JSXAttribute(r){if(u(r)!=="contentContainerStyle")return;if(!S(r.parent.parent))return;const n=E(g(r));if(n.length>0)i.push({node:r,parts:n})},"Program:exit"(){for(const{node:r,parts:n}of i)if(n.some((s)=>y(s)))e.report({node:r,message:d})}}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{attributeName as
|
|
1
|
+
import{attributeName as i,gate as s,problem as o,subtreeHas as p}from"../../../lib/ast.js";import{expressionOf as f,isListElement as m}from"./shared.js";export const noIndexKeyExtractor=o("Disallow a `keyExtractor` that uses its index parameter. Cached sizes and recycled row state hang off the key, so a prepend points every measurement at the wrong item.",{createOnce:(r)=>({before:()=>s(r,"keyExtractor"),JSXAttribute(t){if(i(t)!=="keyExtractor")return;if(!m(t.parent.parent))return;const e=f(t);if(e?.type!=="ArrowFunctionExpression"&&e?.type!=="FunctionExpression")return;const a=e.params[1];if(a?.type!=="Identifier")return;if(!p(e.body,(n)=>n.type==="Identifier"&&n.name===a.name))return;r.report({node:e,message:"Return a stable per-item id from this `keyExtractor` instead of the index — Legend List hangs cached sizes and recycled row state off the key."})}})});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{attributeName as
|
|
1
|
+
import{attributeName as o,gate as s,problem as a}from"../../../lib/ast.js";import{literalKind as d}from"./shared.js";export const noInlineRenderItemProps=a("Disallow inline object and array literals on props nested inside `renderItem`. A row whose props take a new identity every render can never be skipped.",{createOnce(n){let e=0;return{before(){e=0;return s(n,"renderItem")},JSXAttribute(r){if(o(r)==="renderItem"){e++;return}if(e===0)return;const{value:t}=r;if(t?.type!=="JSXExpressionContainer")return;const i=d(t.expression);if(i===null)return;n.report({node:t,message:`Pass \`item\` or its primitive fields and build this ${i} inside the row, or hoist it to module scope when static — a fresh ${i} each render blocks row skipping.`})},"JSXAttribute:exit"(r){if(o(r)==="renderItem")e--}}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{gate as r,problem as n}from"../../../lib/ast.js";import{attributeNamed as i,isListElement as o,LIST as a}from"./shared.js";const s=(t)=>t.type==="JSXElement"&&t.children.some((e)=>e.type==="JSXElement"||e.type==="JSXFragment"||e.type==="JSXText"&&e.value.trim()!==""||e.type==="JSXExpressionContainer"&&e.expression.type!=="JSXEmptyExpression");export const noMixedChildren=n("Disallow passing both `data` and real children to a Legend List. The combination is unsupported and one of the two is dropped without a warning.",{createOnce:(t)=>({before:()=>r(t,a),JSXElement(e){if(!o(e))return;if(!i(e,"data"))return;if(!s(e))return;t.report({node:e.openingElement,message:"Keep either `data` with `renderItem` or children mode here, and remove the other
|
|
1
|
+
import{gate as r,problem as n}from"../../../lib/ast.js";import{attributeNamed as i,isListElement as o,LIST as a}from"./shared.js";const s=(t)=>t.type==="JSXElement"&&t.children.some((e)=>e.type==="JSXElement"||e.type==="JSXFragment"||e.type==="JSXText"&&e.value.trim()!==""||e.type==="JSXExpressionContainer"&&e.expression.type!=="JSXEmptyExpression");export const noMixedChildren=n("Disallow passing both `data` and real children to a Legend List. The combination is unsupported and one of the two is dropped without a warning.",{createOnce:(t)=>({before:()=>r(t,a),JSXElement(e){if(!o(e))return;if(!i(e,"data"))return;if(!s(e))return;t.report({node:e.openingElement,message:"Keep either `data` with `renderItem` or children mode here, and remove the other — passing both fails silently."})}})});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{gate as a,problem as i}from"../../../lib/ast.js";import{attributeNamed as
|
|
1
|
+
import{gate as a,problem as i}from"../../../lib/ast.js";import{attributeNamed as n,isListElement as o,LIST as r}from"./shared.js";export const noRemountKey=i("Disallow `key` on a Legend List, which remounts on any key change and loses its measurements and scroll position. Pass `dataKey` instead.",{createOnce:(e)=>({before:()=>a(e,r),JSXElement(t){if(!o(t))return;const s=n(t,"key");if(!s)return;e.report({node:s,message:"Pass `dataKey` instead of `key` here — it re-initialises the list without the remount that discards every measurement and the scroll position."})}})});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{componentName as i,gate as l,problem as
|
|
1
|
+
import{componentName as i,gate as l,problem as p}from"../../../lib/ast.js";export const noScrollviewMap=p("A `ScrollView` mounts every child up front, so a mapped collection does not belong in its children.",{createOnce:(o)=>({before:()=>l(o,"ScrollView"),JSXElement(r){if(i(r.openingElement.name)!=="ScrollView")return;for(const e of r.children){if(e.type!=="JSXExpressionContainer")continue;const t=e.expression;if(t.type!=="CallExpression")continue;const{callee:n}=t;if(n.type!=="MemberExpression")continue;if(n.property.type!=="Identifier"||n.property.name!=="map")continue;o.report({node:e,message:"Render this collection with `LegendList` from '@legendapp/list/react-native', passing the array as `data` and the mapped body as `renderItem` — a ScrollView mounts every child up front."});return}}})});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{attributeName as
|
|
1
|
+
import{attributeName as o,gate as i,problem as r}from"../../../lib/ast.js";import{attributesOf as s,isListElement as a,LIST as l}from"./shared.js";const m=new Set(["masonry","optimizeItemArrangement","inverted","onBlankArea","disableAutoLayout","CellRendererComponent"]);export const noUnsupportedProps=r("Disallow FlashList and FlatList props that Legend List v3 does not have. It ignores them rather than rejecting them, so the feature looks broken.",{createOnce:(e)=>({before:()=>i(e,l),JSXElement(n){if(!a(n))return;for(const t of s(n)){if(t.type!=="JSXAttribute")continue;if(!m.has(o(t)))continue;e.report({node:t,message:"Remove this prop — Legend List v3 ignores it silently. Inverted chat lists use `maintainScrollAtEnd` / `initialScrollAtEnd` / `maintainVisibleContentPosition`."})}}})});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{gate as i,problem as s}from"../../../lib/ast.js";import{attributeNamed as r,hasSpread as a,isListElement as o,LIST as n}from"./shared.js";export const requiredProps=s("Require `keyExtractor` and an explicit `recycleItems` on a Legend List. Without a key extractor the list keys its rows by index.",{createOnce:(t)=>({before:()=>i(t,n),JSXElement(e){if(!o(e)||a(e))return;if(!r(e,"keyExtractor"))t.report({node:e.openingElement,message:"Add `keyExtractor={item => item.id}` returning a stable per-item identity
|
|
1
|
+
import{gate as i,problem as s}from"../../../lib/ast.js";import{attributeNamed as r,hasSpread as a,isListElement as o,LIST as n}from"./shared.js";export const requiredProps=s("Require `keyExtractor` and an explicit `recycleItems` on a Legend List. Without a key extractor the list keys its rows by index.",{createOnce:(t)=>({before:()=>i(t,n),JSXElement(e){if(!o(e)||a(e))return;if(!r(e,"keyExtractor"))t.report({node:e.openingElement,message:"Add `keyExtractor={item => item.id}` returning a stable per-item identity — the index fallback pins cached measurements to a position, so a prepend shows the wrong data."});if(!r(e,"recycleItems"))t.report({node:e.openingElement,message:"Add `recycleItems={true}` — recycling is where most of the list's native speed comes from. If a row genuinely cannot recycle, pass `recycleItems={false}` with the reason."})}})});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{gate as p,problem as m,subtreeHas as y}from"../../../lib/ast.js";import{attributeNamed as a,expressionOf as
|
|
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
|
|
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.7.0",
|
|
4
4
|
"description": "ashStack shared oxlint configuration and custom rules. Entries: core, react, react-native, with per-library rule groups auto-detected from your dependencies. Consume from oxlint.config.mts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|