@ashstack/lint 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/rules/core/components-tsx-only.js +1 -1
- package/dist/core/rules/core/hoist-intl.js +1 -1
- package/dist/core/rules/core/index.js +1 -1
- package/dist/core/rules/core/max-lines.js +1 -0
- package/dist/core/rules/core/no-comments.js +1 -1
- package/dist/core/rules/core/no-packed-condition.js +1 -1
- package/dist/core/rules/core/use-design-system.js +1 -1
- package/dist/core/rules/zod/prefer-enum.js +1 -1
- package/dist/lib/registry.js +1 -1
- package/dist/lib/rule-types/core.d.ts +7 -1
- package/dist/lib/rule-types/react-native.d.ts +7 -1
- package/dist/react/index.d.ts +8 -0
- package/dist/react/index.js +1 -1
- package/dist/react/rules/query/next-page-param-undefined.js +1 -1
- package/dist/react/rules/query/no-deprecated-filters.js +1 -1
- package/dist/react/rules/query/no-fetch-in-query-fn.js +1 -1
- package/dist/react/rules/query/no-inline-keys.js +1 -1
- package/dist/react/rules/query/require-destructured-hooks.js +1 -1
- package/dist/react/rules/react/no-svg-without-title.js +1 -1
- package/dist/react/rules/react/no-unlabeled-icon-button.js +1 -1
- package/dist/react/rules/tailwind/prefer-cn.js +1 -1
- package/dist/react/rules/tailwind/use-logical-classes.js +1 -1
- package/dist/react/rules/tanstack-router/no-search-casts.js +1 -1
- package/dist/react/rules/tanstack-router/require-selector.js +1 -1
- package/dist/react/rules/zustand/require-selector.js +1 -1
- package/dist/react-native/index.js +1 -1
- package/dist/react-native/rules/keyboard/index.js +1 -1
- package/dist/react-native/rules/legend-list/no-flex-in-content-container.js +1 -1
- package/dist/react-native/rules/legend-list/no-index-key-extractor.js +1 -1
- package/dist/react-native/rules/legend-list/no-inline-render-item-props.js +1 -1
- package/dist/react-native/rules/legend-list/no-mixed-children.js +1 -1
- package/dist/react-native/rules/legend-list/no-remount-key.js +1 -1
- package/dist/react-native/rules/legend-list/no-scrollview-map.js +1 -1
- package/dist/react-native/rules/legend-list/no-unsupported-props.js +1 -1
- package/dist/react-native/rules/legend-list/required-props.js +1 -1
- package/dist/react-native/rules/legend-list/typed-items-need-item-type.js +1 -1
- package/dist/react-native/rules/legend-state/naming.js +1 -1
- package/dist/react-native/rules/legend-state/no-assignment.js +1 -1
- package/dist/react-native/rules/legend-state/no-nested-observable.js +1 -1
- package/dist/react-native/rules/legend-state/no-object-selector.js +1 -1
- package/dist/react-native/rules/legend-state/no-observable-in-component.js +1 -1
- package/dist/react-native/rules/legend-state/no-peek-in-selector.js +1 -1
- package/dist/react-native/rules/legend-state/no-react-mirror.js +1 -1
- package/dist/react-native/rules/legend-state/no-untracked-get-in-jsx.js +1 -1
- package/dist/react-native/rules/react-native/hoist-stateless-function.js +1 -1
- package/dist/react-native/rules/react-native/no-conditional-style-array.js +1 -1
- package/dist/react-native/rules/react-native/no-dynamic-import.js +1 -1
- package/dist/react-native/rules/react-native/no-keyboard-will-events.js +1 -1
- package/dist/react-native/rules/react-native/no-leaked-render.js +1 -1
- package/dist/react-native/rules/react-native/no-manual-memo.js +1 -1
- package/dist/react-native/rules/react-native/no-redundant-view-nesting.js +1 -1
- package/dist/react-native/rules/react-native/no-rn-image-network-source.js +1 -1
- package/dist/react-native/rules/react-native/no-rn-namespace-import.js +1 -1
- package/dist/react-native/rules/react-native/no-scroll-position-state.js +1 -1
- package/dist/react-native/rules/react-native/no-unlabeled-icon-pressable.js +1 -1
- package/dist/react-native/rules/reanimated/animated-reaction-safety.js +1 -1
- package/dist/react-native/rules/reanimated/animated-style-needs-animated-component.js +1 -1
- package/dist/react-native/rules/reanimated/animated-updater-purity.js +1 -1
- package/dist/react-native/rules/reanimated/gpu-properties-only.js +1 -1
- package/dist/react-native/rules/reanimated/interpolate-needs-clamp.js +1 -1
- package/dist/react-native/rules/reanimated/no-react-state-from-continuous-worklet.js +1 -1
- package/dist/react-native/rules/reanimated/prefer-lazy-shared-value-initializer.js +1 -1
- package/dist/react-native/rules/reanimated/schedule-on-rn-scope.js +1 -1
- package/dist/react-native/rules/reanimated/shared-value-usage.js +1 -1
- package/dist/react-native/rules/skia/canvas-opaque.js +1 -1
- package/dist/react-native/rules/skia/index.js +1 -1
- package/dist/react-native/rules/turbo-image/require-resize.js +1 -1
- package/dist/react-native/rules/unistyles/animated-theme.js +1 -1
- package/dist/react-native/rules/unistyles/content-container.js +1 -1
- package/dist/react-native/rules/unistyles/in-sheet.js +1 -1
- package/dist/react-native/rules/unistyles/index.js +1 -1
- package/dist/react-native/rules/unistyles/insets.js +1 -1
- package/dist/react-native/rules/unistyles/no-hardcoded-color.js +1 -1
- package/dist/react-native/rules/unistyles/no-hardcoded-spacing.js +1 -1
- package/dist/react-native/rules/unistyles/no-margin.js +1 -1
- package/dist/react-native/rules/unistyles/no-paramless-dynamic-function.js +1 -0
- package/dist/react-native/rules/unistyles/no-style-spread.js +1 -1
- package/dist/react-native/rules/unistyles/no-unused-styles.js +1 -1
- package/dist/react-native/rules/unistyles/theme-screen-component.js +1 -1
- package/dist/react-native/rules/unistyles/theme-style-attr.js +1 -1
- package/package.json +1 -1
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, 201 with React,
|
|
3
|
+
Strict shared [oxlint](https://oxc.rs) config: <!-- rule-counts -->118 rules on plain TypeScript, 201 with React, 258 on React Native, 76 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
|
|
package/dist/core/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{composeModules as o}from"../lib/module.js";import{coreRegistry as n}from"../lib/registry.js";const s={"array-callback-return":["error",{allowImplicit:!0}],"constructor-super":"error","default-param-last":"error",eqeqeq:["error","always",{null:"ignore"}],"for-direction":"error","getter-return":"error","func-style":["error","expression"],"no-array-constructor":"error","no-async-promise-executor":"error","no-case-declarations":"error","no-class-assign":"error","no-compare-neg-zero":"error","no-cond-assign":"error","no-const-assign":"error","no-constant-condition":"error","no-control-regex":"error","no-debugger":"error","no-dupe-class-members":"error","no-dupe-else-if":"error","no-dupe-keys":"error","no-duplicate-case":"error","no-empty":"error","no-empty-character-class":"error","no-empty-pattern":"error","no-ex-assign":"error","no-extra-boolean-cast":"error","no-fallthrough":"error","no-func-assign":"error","no-global-assign":"error","no-import-assign":"error","no-irregular-whitespace":"error","no-loss-of-precision":"error","no-misleading-character-class":"error","no-negated-condition":"error","no-nested-ternary":"error","no-new-native-nonconstructor":"error","no-nonoctal-decimal-escape":"error","no-obj-calls":"error","no-param-reassign":"error","no-prototype-builtins":"error","no-redeclare":"off","no-regex-spaces":"error","no-self-assign":"error","no-setter-return":"error","no-shadow-restricted-names":"error","no-sparse-arrays":"error","no-this-before-super":"error","no-undef":"off","no-unreachable":"error","no-unsafe-finally":"error","no-unsafe-negation":"error","no-unsafe-optional-chaining":"error","no-unused-labels":"error","no-unused-private-class-members":"error","no-useless-catch":"error","no-useless-constructor":"error","no-useless-escape":"error","no-var":"error","no-with":"error","object-shorthand":["error","methods"],"prefer-rest-params":"error","require-yield":"error","use-isnan":"error","valid-typeof":"error","no-unused-vars":["error",{args:"after-used",argsIgnorePattern:"^_",caughtErrors:"all",caughtErrorsIgnorePattern:"^_",varsIgnorePattern:"^_",destructuredArrayIgnorePattern:"^_",ignoreRestSiblings:!0,fix:{imports:"safe-fix"}}],"no-use-before-define":"off",complexity:["error",{max:12}],"max-depth":["error",3],"max-params":["error",4],"max-lines
|
|
1
|
+
import{composeModules as o}from"../lib/module.js";import{coreRegistry as n}from"../lib/registry.js";const s={"array-callback-return":["error",{allowImplicit:!0}],"constructor-super":"error","default-param-last":"error",eqeqeq:["error","always",{null:"ignore"}],"for-direction":"error","getter-return":"error","func-style":["error","expression"],"no-array-constructor":"error","no-async-promise-executor":"error","no-case-declarations":"error","no-class-assign":"error","no-compare-neg-zero":"error","no-cond-assign":"error","no-const-assign":"error","no-constant-condition":"error","no-control-regex":"error","no-debugger":"error","no-dupe-class-members":"error","no-dupe-else-if":"error","no-dupe-keys":"error","no-duplicate-case":"error","no-empty":"error","no-empty-character-class":"error","no-empty-pattern":"error","no-ex-assign":"error","no-extra-boolean-cast":"error","no-fallthrough":"error","no-func-assign":"error","no-global-assign":"error","no-import-assign":"error","no-irregular-whitespace":"error","no-loss-of-precision":"error","no-misleading-character-class":"error","no-negated-condition":"error","no-nested-ternary":"error","no-new-native-nonconstructor":"error","no-nonoctal-decimal-escape":"error","no-obj-calls":"error","no-param-reassign":"error","no-prototype-builtins":"error","no-redeclare":"off","no-regex-spaces":"error","no-self-assign":"error","no-setter-return":"error","no-shadow-restricted-names":"error","no-sparse-arrays":"error","no-this-before-super":"error","no-undef":"off","no-unreachable":"error","no-unsafe-finally":"error","no-unsafe-negation":"error","no-unsafe-optional-chaining":"error","no-unused-labels":"error","no-unused-private-class-members":"error","no-useless-catch":"error","no-useless-constructor":"error","no-useless-escape":"error","no-var":"error","no-with":"error","object-shorthand":["error","methods"],"prefer-rest-params":"error","require-yield":"error","use-isnan":"error","valid-typeof":"error","no-unused-vars":["error",{args:"after-used",argsIgnorePattern:"^_",caughtErrors:"all",caughtErrorsIgnorePattern:"^_",varsIgnorePattern:"^_",destructuredArrayIgnorePattern:"^_",ignoreRestSiblings:!0,fix:{imports:"safe-fix"}}],"no-use-before-define":"off",complexity:["error",{max:12}],"max-depth":["error",3],"max-params":["error",4],"max-lines-per-function":["error",{max:250,skipBlankLines:!0,skipComments:!0}],"eslint/no-await-in-loop":"error","eslint/no-console":"off","eslint/no-constant-binary-expression":"off","eslint/no-control-regex":"error","eslint/no-else-return":"error","eslint/no-empty":"error","eslint/no-empty-function":"error","eslint/no-new":"off","eslint/no-self-compare":"error","eslint/no-shadow":"error","eslint/no-template-curly-in-string":"error","eslint/no-unassigned-vars":"off","eslint/no-underscore-dangle":"off","eslint/no-unmodified-loop-condition":"off","eslint/no-unused-expressions":"off","eslint/no-useless-concat":"off","eslint/preserve-caught-error":"off","eslint/prefer-arrow-callback":"error","eslint/prefer-const":"error","eslint/prefer-template":"error","typescript/consistent-type-imports":"error","typescript/dot-notation":"error","typescript/no-confusing-void-expression":"error","typescript/no-empty-object-type":["error",{allowInterfaces:"with-single-extends"}],"typescript/no-explicit-any":"error","typescript/no-extra-non-null-assertion":"error","typescript/no-extraneous-class":"error","typescript/no-floating-promises":"off","typescript/no-import-type-side-effects":"error","typescript/no-misused-new":"error","typescript/no-misused-promises":["error",{checksVoidReturn:!1}],"typescript/no-namespace":"error","typescript/no-non-null-asserted-optional-chain":"error","typescript/no-non-null-assertion":"error","typescript/no-unnecessary-boolean-literal-compare":"error","typescript/no-unnecessary-condition":"error","typescript/no-unnecessary-type-assertion":"error","typescript/no-unnecessary-type-constraint":"error","typescript/no-unsafe-declaration-merging":"error","typescript/no-unsafe-function-type":"error","typescript/no-unsafe-type-assertion":"error","typescript/no-wrapper-object-types":"error","typescript/prefer-as-const":"error","typescript/prefer-namespace-keyword":"error","typescript/prefer-nullish-coalescing":"error","typescript/require-await":"error","typescript/switch-exhaustiveness-check":["error",{considerDefaultExhaustiveForUnions:!0}],"import/no-commonjs":"off","import/no-cycle":"error","import/no-duplicates":"error","import/no-named-as-default":"off","import/no-named-as-default-member":"off","import/no-self-import":"off","import/no-unassigned-import":"off","unicorn/consistent-function-scoping":"off","unicorn/no-array-reverse":"off","unicorn/no-array-sort":"off","unicorn/no-empty-file":"off","unicorn/no-new-array":"off","unicorn/no-useless-fallback-in-spread":"off","unicorn/no-useless-spread":"off","unicorn/prefer-add-event-listener":"off","unicorn/prefer-array-flat-map":"error","unicorn/prefer-date-now":"error","unicorn/prefer-logical-operator-over-ternary":"error","unicorn/prefer-node-protocol":"error","unicorn/prefer-optional-catch-binding":"error","unicorn/require-module-specifiers":"off","unicorn/throw-new-error":"error","promise/always-return":"off","promise/no-callback-in-promise":"off","promise/no-multiple-resolved":"off","promise/no-promise-in-callback":"off","oxc/no-async-endpoint-handlers":"off"};export const TEST_FILES=["**/*.test.ts","**/*.test.tsx","**/*.spec.ts","**/*.spec.tsx","**/__tests__/**","**/tests/**"];const core=(e={})=>{const r=o(n,e);return{plugins:["eslint","typescript","import","unicorn","promise","oxc"],jsPlugins:r.jsPlugins,env:{builtin:!0,es2024:!0},categories:{correctness:"error",suspicious:"error",perf:"error",pedantic:"off",style:"off",restriction:"off",nursery:"off"},rules:{...s,...r.rules},overrides:[{files:["**/*.tsx"],rules:{"func-style":"off"}},{files:["**/*.d.ts"],rules:{"typescript/no-explicit-any":"off"}},{files:TEST_FILES,rules:{"@ashstack/core/max-lines":"off","max-lines-per-function":"off","typescript/await-thenable":"off","typescript/no-non-null-assertion":"off","typescript/no-unsafe-type-assertion":"off","typescript/require-await":"off","typescript/unbound-method":"off"}}]}};export{core};export default core;
|
|
@@ -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{defineModule as o}from"../../../lib/module.js";import{componentsTsxOnly as n}from"./components-tsx-only.js";import{hoistIntl as e}from"./hoist-intl.js";import{
|
|
1
|
+
import{defineModule as o}from"../../../lib/module.js";import{componentsTsxOnly as n}from"./components-tsx-only.js";import{hoistIntl as e}from"./hoist-intl.js";import{maxLines as t}from"./max-lines.js";import{noComments as m}from"./no-comments.js";import{noNamingConvention as i}from"./no-naming-convention.js";import{noPackedCondition as r}from"./no-packed-condition.js";import{useDesignSystem as s}from"./use-design-system.js";export default o({meta:{name:"@ashstack/core"},url:import.meta.url,docsWhen:"always on via `core()` and every entry above it (opt-in rules noted per rule)",rules:{"no-comments":m,"no-naming-convention":i,"no-packed-condition":r,"use-design-system":s,"components-tsx-only":n,"hoist-intl":e,"max-lines":t}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{isMemberCall as i,receiverName as c}from"../../../lib/ast.js";const h=300,d=new Set(["StyleSheet","stylex"]),p=10,m=32,u=(e)=>i(e,"create")&&d.has(c(e)??""),f=(e,l)=>{const t=new Uint8Array(e.length);for(const[s,o]of l)t.fill(1,s,o);let r=0,n=!1;for(let s=0;s<e.length;s++){const o=e.charCodeAt(s);if(o===p){if(n)r++;n=!1}else if(o>m&&t[s]===0)n=!0}return n?r+1:r};export const maxLines={meta:{type:"problem",docs:{description:"Cap the lines of code in one file, counting neither blank lines, comments, nor the style tables `StyleSheet.create` and `stylex.create` build. A stylesheet is data, and keeping it next to the component it styles is the point — it should not spend the budget that logic spends. The option says how many lines: 300 by default, which `core()` keeps, and 250 from `react()` down, where a file past it is several components rather than one long one. Replaces the built-in `max-lines`, which counts every line of all three."},schema:[{type:"integer",minimum:1}]},createOnce(e){let l=[];return{before(){l=[];return!0},CallExpression(t){if(u(t))l.push([t.start,t.end])},"Program:exit"(t){const r=t.body.at(-1);if(!r)return;const n=typeof e.options[0]==="number"?e.options[0]:h,s=e.sourceCode.getAllComments().map((a)=>[a.start,a.end]),o=f(e.sourceCode.getText(),[...l,...s]);if(o<=n)return;e.report({node:r,message:`Move part of this file elsewhere — ${o} counted lines, past ${n}. Blank lines, comments and style tables are already excluded.`})}}}};
|
|
@@ -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":{},"no-dynamic-import":{}}},{meta:{name:"@ashstack/unistyles"},url:e("../react-native/rules/unistyles/index.js"),option:"unistyles",packages:["react-native-unistyles"],restrictedImports:{paths:[{name:"react-native",importNames:["StyleSheet"],message:"Import `StyleSheet` from `react-native-unistyles` instead of `react-native`."},{name:"react-native",importNames:["useWindowDimensions","Dimensions"],message:"Read screen size from `rt.screen` inside `StyleSheet.create`, or `UnistylesRuntime.screen` outside it, instead of the `Dimensions` APIs."},{name:"react-native",importNames:["SafeAreaView"],message:'Set `contentInsetAdjustmentBehavior="automatic"` on the scroll view, or apply `rt.insets` from `react-native-unistyles`, instead of `SafeAreaView`.'},{name:"react-native-safe-area-context",importNames:["useSafeAreaInsets","useSafeAreaFrame","SafeAreaView"],message:"Read safe-area values from `rt.insets`, or `UnistylesRuntime.insets` outside a stylesheet, in `react-native-unistyles`."}]},rules:{"animated-theme":{},"content-container":{},"in-sheet":{},insets:{},"no-hardcoded-color":{},"no-hardcoded-spacing":{},"no-margin":{},"no-style-spread":{},"no-unused-styles":{},"rtl-style-call":{},"theme-screen-component":{},"theme-style-attr":{}}},{meta:{name:"@ashstack/legend-list"},url:e("../react-native/rules/legend-list/index.js"),option:"legendList",packages:["@legendapp/list"],restrictedImports:{paths:[{name:"react-native",importNames:["FlatList","SectionList","VirtualizedList"],message:"Render this list with `LegendList` from `@legendapp/list/react-native`; a `ScrollView` plus `map` is fine for fewer than 20 static items."},{name:"@shopify/flash-list",message:"Import `LegendList` from `@legendapp/list/react-native` — Legend List replaced FlashList in this stack."}],patterns:[{group:["@legendapp/list"],message:"Import from the platform entrypoint `@legendapp/list/react-native`, not the package root."}]},rules:{"required-props":{},"no-index-key-extractor":{},"no-remount-key":{},"no-inline-render-item-props":{},"no-mixed-children":{},"no-flex-in-content-container":{},"typed-items-need-item-type":{},"no-scrollview-map":{},"no-unsupported-props":{}}},{meta:{name:"@ashstack/legend-state"},url:e("../react-native/rules/legend-state/index.js"),option:"legendState",packages:["@legendapp/state"],restrictedImports:{paths:[{name:"@legendapp/state/react",importNames:["use$","useSelector"],message:"Use `useValue` from `@legendapp/state/react`; `use$` and `useSelector` are deprecated."}]},rules:{"no-assignment":{},naming:{},"no-nested-observable":{},"no-react-mirror":{},"no-untracked-get-in-jsx":{},"no-peek-in-selector":{},"no-object-selector":{},"no-observable-in-component":{}}},{meta:{name:"@ashstack/reanimated"},url:e("../react-native/rules/reanimated/index.js"),option:"reanimated",packages:["react-native-reanimated"],restrictedImports:{paths:[{name:"react-native",importNames:["Animated"],message:"Use `react-native-reanimated` instead of React Native's `Animated` API."},{name:"react-native-reanimated",importNames:["runOnJS","runOnUI"],message:"Use `scheduleOnRN` and `scheduleOnUI` from `react-native-worklets`; `runOnJS` and `runOnUI` are deprecated."},{name:"react-native-worklets",importNames:["runOnJS","runOnUI"],message:"Use `scheduleOnRN` and `scheduleOnUI` from `react-native-worklets`; `runOnJS` and `runOnUI` are deprecated."}]},rules:{"animated-reaction-safety":{},"animated-style-needs-animated-component":{},"animated-updater-purity":{},"gpu-properties-only":{},"hoist-layout-animation-builder":{},"interpolate-needs-clamp":{},"no-shared-value-dot-value":{},"no-react-state-from-continuous-worklet":{},"prefer-lazy-shared-value-initializer":{},"schedule-on-rn-scope":{},"shared-value-usage":{}}},{meta:{name:"@ashstack/turbo-image"},url:e("../react-native/rules/turbo-image/index.js"),option:"turboImage",packages:["react-native-turbo-image"],rules:{"require-resize":{},"require-cache-policy":{}}},{meta:{name:"@ashstack/skia"},url:e("../react-native/rules/skia/index.js"),option:"skia",packages:["@shopify/react-native-skia"],restrictedImports:{paths:[{name:"@shopify/react-native-skia",importNames:["usePathValue","usePathInterpolation"],message:"Keep one stable `SkPath` buffer and mutate it inside `useDerivedValue`, with consumers reading the driving shared value.
|
|
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":{},"max-lines":{}}},{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 `core()` adds on top of the entry below it. */
|
|
5
|
-
export type CoreRuleId = "@ashstack/core/no-comments" | "@ashstack/core/no-naming-convention" | "@ashstack/core/no-packed-condition" | "@ashstack/core/use-design-system" | "@ashstack/core/components-tsx-only" | "@ashstack/core/hoist-intl" | "@ashstack/zod/prefer-enum";
|
|
5
|
+
export type CoreRuleId = "@ashstack/core/no-comments" | "@ashstack/core/no-naming-convention" | "@ashstack/core/no-packed-condition" | "@ashstack/core/use-design-system" | "@ashstack/core/components-tsx-only" | "@ashstack/core/hoist-intl" | "@ashstack/core/max-lines" | "@ashstack/zod/prefer-enum";
|
|
6
6
|
declare module "oxlint" {
|
|
7
7
|
interface DummyRuleMap {
|
|
8
8
|
/**
|
|
@@ -65,6 +65,12 @@ declare module "oxlint" {
|
|
|
65
65
|
* @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackcorehoist-intl
|
|
66
66
|
*/
|
|
67
67
|
"@ashstack/core/hoist-intl"?: RuleSetting;
|
|
68
|
+
/**
|
|
69
|
+
* Cap the lines of code in one file, counting neither blank lines, comments, nor the style tables `StyleSheet.create` and `stylex.create` build. A stylesheet is data, and keeping it next to the component it styles is the point — it should not spend the budget that logic spends. The option says how many lines: 300 by default, which `core()` keeps, and 250 from `react()` down, where a file past it is several components rather than one long one. Replaces the built-in `max-lines`, which counts every line of all three.
|
|
70
|
+
*
|
|
71
|
+
* @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackcoremax-lines
|
|
72
|
+
*/
|
|
73
|
+
"@ashstack/core/max-lines"?: RuleSetting<[number]>;
|
|
68
74
|
/**
|
|
69
75
|
* Disallow `z.nativeEnum()` and any `z.union()` whose members are all `z.literal()` strings.
|
|
70
76
|
*
|
|
@@ -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
|
/**
|
|
@@ -113,6 +113,12 @@ declare module "oxlint" {
|
|
|
113
113
|
* @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackunistylesno-margin
|
|
114
114
|
*/
|
|
115
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;
|
|
116
122
|
/**
|
|
117
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.
|
|
118
124
|
*
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { OxlintConfig, ReactOptions } from "../lib/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Tighter than core's 300, for the same reason `max-lines-per-function` is:
|
|
4
|
+
* a module of exported functions grows honestly, a component file past this
|
|
5
|
+
* is several components. Styles are already out of the count —
|
|
6
|
+
* `@ashstack/core/max-lines` discounts `StyleSheet.create` and
|
|
7
|
+
* `stylex.create` — so every line left is one a reader has to follow.
|
|
8
|
+
*/
|
|
9
|
+
export declare const COMPONENT_MAX_LINES = 250;
|
|
2
10
|
/**
|
|
3
11
|
* React (web) entry — everything in core plus react, jsx-a11y, React Compiler
|
|
4
12
|
* diagnostics, you-might-not-need-an-effect, and the auto-detected library
|
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 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"]
|
|
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"];export const COMPONENT_MAX_LINES=250;const 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,"@ashstack/core/max-lines":["error",COMPONENT_MAX_LINES]},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,{COMPONENT_MAX_LINES as p}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 m={"eslint/no-empty":"error","eslint/no-empty-function":"error"},l={"@ashstack/effects/no-event-handler":"off"},u=(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:{...m,...l,"no-restricted-imports":["error",t.restricted],...t.rules,"@ashstack/core/max-lines":["error",p],...u(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."})}})});
|