@ashstack/lint 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +1 -1
  2. package/dist/core/index.js +1 -1
  3. package/dist/core/rules/core/components-tsx-only.js +1 -1
  4. package/dist/core/rules/core/hoist-intl.js +1 -1
  5. package/dist/core/rules/core/index.js +1 -1
  6. package/dist/core/rules/core/max-lines.js +1 -0
  7. package/dist/core/rules/core/no-comments.js +1 -1
  8. package/dist/core/rules/core/no-packed-condition.js +1 -1
  9. package/dist/core/rules/core/use-design-system.js +1 -1
  10. package/dist/core/rules/zod/prefer-enum.js +1 -1
  11. package/dist/lib/registry.js +1 -1
  12. package/dist/lib/rule-types/core.d.ts +7 -1
  13. package/dist/lib/rule-types/react-native.d.ts +7 -1
  14. package/dist/react/index.d.ts +8 -0
  15. package/dist/react/index.js +1 -1
  16. package/dist/react/rules/query/next-page-param-undefined.js +1 -1
  17. package/dist/react/rules/query/no-deprecated-filters.js +1 -1
  18. package/dist/react/rules/query/no-fetch-in-query-fn.js +1 -1
  19. package/dist/react/rules/query/no-inline-keys.js +1 -1
  20. package/dist/react/rules/query/require-destructured-hooks.js +1 -1
  21. package/dist/react/rules/react/no-svg-without-title.js +1 -1
  22. package/dist/react/rules/react/no-unlabeled-icon-button.js +1 -1
  23. package/dist/react/rules/tailwind/prefer-cn.js +1 -1
  24. package/dist/react/rules/tailwind/use-logical-classes.js +1 -1
  25. package/dist/react/rules/tanstack-router/no-search-casts.js +1 -1
  26. package/dist/react/rules/tanstack-router/require-selector.js +1 -1
  27. package/dist/react/rules/zustand/require-selector.js +1 -1
  28. package/dist/react-native/index.js +1 -1
  29. package/dist/react-native/rules/keyboard/index.js +1 -1
  30. package/dist/react-native/rules/legend-list/no-flex-in-content-container.js +1 -1
  31. package/dist/react-native/rules/legend-list/no-index-key-extractor.js +1 -1
  32. package/dist/react-native/rules/legend-list/no-inline-render-item-props.js +1 -1
  33. package/dist/react-native/rules/legend-list/no-mixed-children.js +1 -1
  34. package/dist/react-native/rules/legend-list/no-remount-key.js +1 -1
  35. package/dist/react-native/rules/legend-list/no-scrollview-map.js +1 -1
  36. package/dist/react-native/rules/legend-list/no-unsupported-props.js +1 -1
  37. package/dist/react-native/rules/legend-list/required-props.js +1 -1
  38. package/dist/react-native/rules/legend-list/typed-items-need-item-type.js +1 -1
  39. package/dist/react-native/rules/legend-state/naming.js +1 -1
  40. package/dist/react-native/rules/legend-state/no-assignment.js +1 -1
  41. package/dist/react-native/rules/legend-state/no-nested-observable.js +1 -1
  42. package/dist/react-native/rules/legend-state/no-object-selector.js +1 -1
  43. package/dist/react-native/rules/legend-state/no-observable-in-component.js +1 -1
  44. package/dist/react-native/rules/legend-state/no-peek-in-selector.js +1 -1
  45. package/dist/react-native/rules/legend-state/no-react-mirror.js +1 -1
  46. package/dist/react-native/rules/legend-state/no-untracked-get-in-jsx.js +1 -1
  47. package/dist/react-native/rules/react-native/hoist-stateless-function.js +1 -1
  48. package/dist/react-native/rules/react-native/no-conditional-style-array.js +1 -1
  49. package/dist/react-native/rules/react-native/no-dynamic-import.js +1 -1
  50. package/dist/react-native/rules/react-native/no-keyboard-will-events.js +1 -1
  51. package/dist/react-native/rules/react-native/no-leaked-render.js +1 -1
  52. package/dist/react-native/rules/react-native/no-manual-memo.js +1 -1
  53. package/dist/react-native/rules/react-native/no-redundant-view-nesting.js +1 -1
  54. package/dist/react-native/rules/react-native/no-rn-image-network-source.js +1 -1
  55. package/dist/react-native/rules/react-native/no-rn-namespace-import.js +1 -1
  56. package/dist/react-native/rules/react-native/no-scroll-position-state.js +1 -1
  57. package/dist/react-native/rules/react-native/no-unlabeled-icon-pressable.js +1 -1
  58. package/dist/react-native/rules/reanimated/animated-reaction-safety.js +1 -1
  59. package/dist/react-native/rules/reanimated/animated-style-needs-animated-component.js +1 -1
  60. package/dist/react-native/rules/reanimated/animated-updater-purity.js +1 -1
  61. package/dist/react-native/rules/reanimated/gpu-properties-only.js +1 -1
  62. package/dist/react-native/rules/reanimated/interpolate-needs-clamp.js +1 -1
  63. package/dist/react-native/rules/reanimated/no-react-state-from-continuous-worklet.js +1 -1
  64. package/dist/react-native/rules/reanimated/prefer-lazy-shared-value-initializer.js +1 -1
  65. package/dist/react-native/rules/reanimated/schedule-on-rn-scope.js +1 -1
  66. package/dist/react-native/rules/reanimated/shared-value-usage.js +1 -1
  67. package/dist/react-native/rules/skia/canvas-opaque.js +1 -1
  68. package/dist/react-native/rules/skia/index.js +1 -1
  69. package/dist/react-native/rules/turbo-image/require-resize.js +1 -1
  70. package/dist/react-native/rules/unistyles/animated-theme.js +1 -1
  71. package/dist/react-native/rules/unistyles/content-container.js +1 -1
  72. package/dist/react-native/rules/unistyles/in-sheet.js +1 -1
  73. package/dist/react-native/rules/unistyles/index.js +1 -1
  74. package/dist/react-native/rules/unistyles/insets.js +1 -1
  75. package/dist/react-native/rules/unistyles/no-hardcoded-color.js +1 -1
  76. package/dist/react-native/rules/unistyles/no-hardcoded-spacing.js +1 -1
  77. package/dist/react-native/rules/unistyles/no-margin.js +1 -1
  78. package/dist/react-native/rules/unistyles/no-paramless-dynamic-function.js +1 -0
  79. package/dist/react-native/rules/unistyles/no-style-spread.js +1 -1
  80. package/dist/react-native/rules/unistyles/no-unused-styles.js +1 -1
  81. package/dist/react-native/rules/unistyles/theme-screen-component.js +1 -1
  82. package/dist/react-native/rules/unistyles/theme-style-attr.js +1 -1
  83. package/package.json +1 -1
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, 257 on React Native, 74 of them custom-built<!-- /rule-counts -->. Three entries, each containing the one before it: `core()`, `react()`, `react-native()`.
3
+ Strict shared [oxlint](https://oxc.rs) config: <!-- rule-counts -->118 rules on plain TypeScript, 201 with React, 258 on React Native, 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
 
@@ -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":["error",{max:300,skipBlankLines:!0,skipComments:!0}],"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:{"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
+ 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` (helpers, pure logic), `src/hooks` (a hook), or `src/api/<feature>/` (data access). `components/` holds only files that render JSX, plus a barrel.",l="src/components",n=(r)=>r.every((e)=>e.type==="ImportDeclaration"||e.type==="ExportAllDeclaration"||e.type==="ExportNamedDeclaration"&&e.declaration==null);export const componentsTsxOnly={meta:{type:"problem",docs:{description:"Require every file under the components directory to render JSX or be a re-export barrel. `dir` says which directory, defaulting to `src/components`."},schema:[{type:"object",properties:{dir:{type:"string",minLength:1}},additionalProperties:!1}],defaultOff:!0},createOnce(r){let e=!1;return{before(){e=!1;const{dir:o=l}=t(r,{});return r.filename.replaceAll("\\","/").includes(`/${o}/`)},JSXElement(){e=!0},JSXFragment(){e=!0},"Program:exit"(o){if(e||n(o.body))return;r.report({node:o,message:s})}}}};
1
+ import{optionsOf as t}from"../../../lib/ast.js";const s="Move this file to `src/utils`, `src/hooks`, or `src/api/<feature>/` — `components/` holds only files that render JSX.",n="src/components",i=(r)=>r.every((e)=>e.type==="ImportDeclaration"||e.type==="ExportAllDeclaration"||e.type==="ExportNamedDeclaration"&&e.declaration==null);export const componentsTsxOnly={meta:{type:"problem",docs:{description:"Require every file under the components directory to render JSX or be a re-export barrel. `dir` says which directory, defaulting to `src/components`."},schema:[{type:"object",properties:{dir:{type:"string",minLength:1}},additionalProperties:!1}],defaultOff:!0},createOnce(r){let e=!1;return{before(){e=!1;const{dir:o=n}=t(r,{});return r.filename.replaceAll("\\","/").includes(`/${o}/`)},JSXElement(){e=!0},JSXFragment(){e=!0},"Program:exit"(o){if(e||i(o.body))return;r.report({node:o,message:s})}}}};
@@ -1 +1 @@
1
- import{ancestors as o,calleeName as a,hasAncestor as i,isFunction as l,problem as c,subtreeHas as p}from"../../../lib/ast.js";const u="Move this `Intl` formatter to module scope when the locale and options are static, or wrap it in `useMemo` keyed on the locale — constructing one per render is expensive.",m=new Set(["useMemo","useCallback"]),f=(t)=>p(t,(e)=>e.type==="JSXElement"||e.type==="JSXFragment"),h=(t)=>{for(const e of o(t))if(l(e)&&f(e))return!0;return!1};export const hoistIntl=c("Disallow `new Intl.*` inside a function that renders JSX, unless the call already sits in `useMemo` or `useCallback`.",{createOnce(t){return{NewExpression(e){const{callee:n}=e;if(n.type!=="MemberExpression")return;const{object:s}=n;if(s.type!=="Identifier"||s.name!=="Intl")return;if(i(e,(r)=>r.type==="CallExpression"&&m.has(a(r))))return;if(!h(e))return;t.report({node:e,message:u})}}}});
1
+ import{ancestors as o,calleeName as a,hasAncestor as l,isFunction as i,problem as c,subtreeHas as u}from"../../../lib/ast.js";const p="Move this `Intl` formatter to module scope when the locale and options are static, or wrap it in `useMemo` keyed on the locale.",m=new Set(["useMemo","useCallback"]),f=(t)=>u(t,(e)=>e.type==="JSXElement"||e.type==="JSXFragment"),h=(t)=>{for(const e of o(t))if(i(e)&&f(e))return!0;return!1};export const hoistIntl=c("Disallow `new Intl.*` inside a function that renders JSX, unless the call already sits in `useMemo` or `useCallback`.",{createOnce(t){return{NewExpression(e){const{callee:s}=e;if(s.type!=="MemberExpression")return;const{object:n}=s;if(n.type!=="Identifier"||n.name!=="Intl")return;if(l(e,(r)=>r.type==="CallExpression"&&m.has(a(r))))return;if(!h(e))return;t.report({node:e,message:p})}}}});
@@ -1 +1 @@
1
- import{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{noComments as t}from"./no-comments.js";import{noNamingConvention as m}from"./no-naming-convention.js";import{noPackedCondition as r}from"./no-packed-condition.js";import{useDesignSystem as i}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":t,"no-naming-convention":m,"no-packed-condition":r,"use-design-system":i,"components-tsx-only":n,"hoist-intl":e}});
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 b}from"../../../lib/ast.js";const v=["@ts-expect-error","@ts-ignore","@ts-nocheck","@ts-check","@jsx","@jsxRuntime","@jsxImportSource","prettier-ignore","eslint-disable","eslint-enable","oxlint-disable","oxlint-enable","oxfmt-ignore","react-doctor-disable","@type","<reference","#__PURE__","@__PURE__","v8 ignore","c8 ignore","istanbul ignore","@vitest-environment","EXPECT_PASS","EXPECT_FAIL"],E=/^(?<kind>what|why):\s*(?<fact>[\s\S]+)$/i,m=new Set(["Block","MultiLine"]),k=new Set(["Shebang","Hashbang"]),u=10,f=120,C=2,D="Make the code say this, then delete the line — nearly every comment here is one rename or one refactor away from unnecessary. Reach for the move that fits: Rename the value, function or type to the plain word this sentence uses; Extract Function for the block it summarises, named after the claim it makes; Guard Clause for the nesting it walks the reader through; split the clever expression it decodes into named steps. You are done when the sentence's content is legible in an identifier, a signature or the control flow.",A=" The `// what:` hatch carries one kind of fact: one that outlives this code — a platform bug, an upstream contract, a number measured on a device. A fact about what this code does fails that test, and the refactor is still owed.",_=" This project runs with the `// what:` hatch turned off, so the refactor is the only way out. The one line left is `// why:`, which a kept `memo` requires.",x=(e)=>D+(e?A:_),H="Attach this block to the declaration it documents, or Rename and Extract Function until the code reads without it. A `/** */` block earns its place as documentation of the symbol directly beneath it.",d={block:"Rewrite this as a single `// what: <fact>` line comment. If the fact needs a paragraph, name the pieces in code until it fits on one line.",shortFact:`Write the fact after \`what:\` (at least ${u} characters), or delete the comment.`,tooLong:`Trim this \`what:\` line under ${f} characters, moving what is left into names in the code.`,stacked:"Keep one `what:` line here — the single irreducible fact — and refactor what the others explain into named values and functions."},M=(e,t)=>`Delete \`what:\` comments from this file until at most ${e} remain (it has ${t}): move the logic each one annotates into a function whose name carries what the comment says.`,P=new Set(["ExportNamedDeclaration","ExportDefaultDeclaration","ExportAllDeclaration","FunctionDeclaration","VariableDeclaration","ClassDeclaration","MethodDefinition","PropertyDefinition","Property","TSInterfaceDeclaration","TSTypeAliasDeclaration","TSEnumDeclaration","TSEnumMember","TSModuleDeclaration","TSDeclareFunction","TSPropertySignature","TSMethodSignature","TSIndexSignature","TSCallSignatureDeclaration","TSConstructSignatureDeclaration","TSAbstractMethodDefinition","TSAbstractPropertyDefinition","AccessorProperty","ImportDeclaration"]),O=(e)=>(m.has(e.type)?e.value.replace(/^[*\s]+/,""):e.value).trim(),R=(e)=>v.some((t)=>e.startsWith(t)),F=(e)=>m.has(e.type)&&e.value.startsWith("*"),I=(e)=>{const t=[];for(const o of e.sourceCode.getAllComments()){if(k.has(o.type))continue;const l=O(o);if(R(l))continue;t.push({comment:o,body:l})}return t},L=(e,t)=>{let o=t;while(/\s/.test(e.charAt(o)))o+=1;return o},p=(e,t)=>{const o=E.exec(e.body)?.groups;if(!o?.kind||!o.fact)return[];const l=o.kind.toLowerCase()==="why"?"why":"what";if(l==="what"&&!t)return[];return[{...e,kind:l,fact:o.fact}]},N=(e,t,o)=>{if(m.has(e.type))return d.block;if(o.trim().length<u)return d.shortFact;if(t.length>f)return d.tooLong;return null},j=(e,t,o)=>e.slice(t.end,o.start).trim()==="",w=(e)=>e.escapeHatch!==!1;export const noComments={meta:{type:"problem",docs:{description:'Disallow every comment that is neither a `// what: <fact>` line, a `// why:` marker, nor a tooling directive. The message names the refactoring that removes it: Rename, Extract Function, Guard Clause. Surviving `// what:` lines are held to one short line each, at most `budget` per file (default 2); `escapeHatch: false` removes that hatch, so no discretionary prose survives. A `// why:` line is the marker `@ashstack/react-native/no-manual-memo` requires above a kept `memo`: held to the same one-line shape, never counted against `budget`, and kept even with `escapeHatch: false`, so the two rules run together. With `jsdoc: "allow"`, a `/** */` block documenting the declaration directly beneath it is kept, while a floating one still reports.'},schema:[{type:"object",properties:{jsdoc:{enum:["allow","report"]},escapeHatch:{type:"boolean"},budget:{type:"integer",minimum:0}},additionalProperties:!1}]},createOnce(e){const t=new Set,o=(n)=>{t.add(n.start)},l={};for(const n of P)l[n]=o;const h=(n,r)=>{e.report({node:n,message:r})},g=(n,r)=>{const{text:s}=e.sourceCode,c=w(r);for(const a of n){const{comment:i}=a;if(p(a,c).length>0)continue;if(r.jsdoc!=="allow"||!F(i))h(i,x(c));else if(!t.has(L(s,i.end)))h(i,H)}},S=(n,r)=>{const s=[];for(const c of n)for(const a of p(c,r)){const i=N(a.comment,a.body,a.fact);if(i===null)s.push(a);else h(a.comment,i)}return s},T=(n)=>{const r=e.sourceCode.getText(),s=n.map((c)=>c.comment);for(const[c,a]of s.entries()){const i=s[c-1];if(i&&j(r,i,a))h(a,d.stacked)}},y=(n,r)=>{const s=r.budget??C,c=n.filter((i)=>i.kind==="what"),a=c[s];if(a)h(a.comment,M(s,c.length))};return{...l,before(){t.clear();return!0},"Program:exit"(){const n=b(e,{}),r=I(e);g(r,n);const s=S(r,w(n));T(s);y(s,n)}}}};
1
+ import{optionsOf as y}from"../../../lib/ast.js";const b=["@ts-expect-error","@ts-ignore","@ts-nocheck","@ts-check","@jsx","@jsxRuntime","@jsxImportSource","prettier-ignore","eslint-disable","eslint-enable","oxlint-disable","oxlint-enable","oxfmt-ignore","react-doctor-disable","@type","<reference","#__PURE__","@__PURE__","v8 ignore","c8 ignore","istanbul ignore","@vitest-environment","EXPECT_PASS","EXPECT_FAIL"],D=/^(?<kind>what|why):\s*(?<fact>[\s\S]+)$/i,m=new Set(["Block","MultiLine"]),C=new Set(["Shebang","Hashbang"]),u=10,f=120,v=2,k="Make the code say this, then delete the line — rename, Extract Function, Guard Clause, or named steps. Done when the sentence reads off a name or the control flow.",A=" `// what:` fits only a fact that outlives the code — a platform bug, an upstream contract, a measured number; what the code does is not one, and still owes the refactor.",_=" This project runs with the `// what:` hatch turned off. The one line left is `// why:`, which a kept `memo` requires.",x=(e)=>k+(e?A:_),H="Attach this block to the declaration it documents, or Rename and Extract Function until the code reads without it.",d={block:"Rewrite this as a single `// what: <fact>` line comment; if the fact needs a paragraph, name the pieces in code instead.",shortFact:`Write the fact after \`what:\` (at least ${u} characters), or delete the comment.`,tooLong:`Trim this \`what:\` line under ${f} characters, moving what is left into names in the code.`,stacked:"Keep one `what:` line here and refactor what the others explain into names."},M=(e,t)=>`Delete \`what:\` comments from this file until at most ${e} remain (it has ${t}): move each one's logic into a function named for it.`,P=new Set(["ExportNamedDeclaration","ExportDefaultDeclaration","ExportAllDeclaration","FunctionDeclaration","VariableDeclaration","ClassDeclaration","MethodDefinition","PropertyDefinition","Property","TSInterfaceDeclaration","TSTypeAliasDeclaration","TSEnumDeclaration","TSEnumMember","TSModuleDeclaration","TSDeclareFunction","TSPropertySignature","TSMethodSignature","TSIndexSignature","TSCallSignatureDeclaration","TSConstructSignatureDeclaration","TSAbstractMethodDefinition","TSAbstractPropertyDefinition","AccessorProperty","ImportDeclaration"]),O=(e)=>(m.has(e.type)?e.value.replace(/^[*\s]+/,""):e.value).trim(),F=(e)=>b.some((t)=>e.startsWith(t)),R=(e)=>m.has(e.type)&&e.value.startsWith("*"),I=(e)=>{const t=[];for(const o of e.sourceCode.getAllComments()){if(C.has(o.type))continue;const l=O(o);if(F(l))continue;t.push({comment:o,body:l})}return t},L=(e,t)=>{let o=t;while(/\s/.test(e.charAt(o)))o+=1;return o},p=(e,t)=>{const o=D.exec(e.body)?.groups;if(!o?.kind||!o.fact)return[];const l=o.kind.toLowerCase()==="why"?"why":"what";if(l==="what"&&!t)return[];return[{...e,kind:l,fact:o.fact}]},N=(e,t,o)=>{if(m.has(e.type))return d.block;if(o.trim().length<u)return d.shortFact;if(t.length>f)return d.tooLong;return null},j=(e,t,o)=>e.slice(t.end,o.start).trim()==="",w=(e)=>e.escapeHatch!==!1;export const noComments={meta:{type:"problem",docs:{description:'Disallow every comment that is neither a `// what: <fact>` line, a `// why:` marker, nor a tooling directive. The message names the refactoring that removes it: Rename, Extract Function, Guard Clause. Surviving `// what:` lines are held to one short line each, at most `budget` per file (default 2); `escapeHatch: false` removes that hatch, so no discretionary prose survives. A `// why:` line is the marker `@ashstack/react-native/no-manual-memo` requires above a kept `memo`: held to the same one-line shape, never counted against `budget`, and kept even with `escapeHatch: false`, so the two rules run together. With `jsdoc: "allow"`, a `/** */` block documenting the declaration directly beneath it is kept, while a floating one still reports.'},schema:[{type:"object",properties:{jsdoc:{enum:["allow","report"]},escapeHatch:{type:"boolean"},budget:{type:"integer",minimum:0}},additionalProperties:!1}]},createOnce(e){const t=new Set,o=(n)=>{t.add(n.start)},l={};for(const n of P)l[n]=o;const h=(n,r)=>{e.report({node:n,message:r})},g=(n,r)=>{const{text:s}=e.sourceCode,i=w(r);for(const a of n){const{comment:c}=a;if(p(a,i).length>0)continue;if(r.jsdoc!=="allow"||!R(c))h(c,x(i));else if(!t.has(L(s,c.end)))h(c,H)}},S=(n,r)=>{const s=[];for(const i of n)for(const a of p(i,r)){const c=N(a.comment,a.body,a.fact);if(c===null)s.push(a);else h(a.comment,c)}return s},T=(n)=>{const r=e.sourceCode.getText(),s=n.map((i)=>i.comment);for(const[i,a]of s.entries()){const c=s[i-1];if(c&&j(r,c,a))h(a,d.stacked)}},E=(n,r)=>{const s=r.budget??v,i=n.filter((c)=>c.kind==="what"),a=i[s];if(a)h(a.comment,M(s,i.length))};return{...l,before(){t.clear();return!0},"Program:exit"(){const n=y(e,{}),r=I(e);g(r,n);const s=S(r,w(n));T(s);E(s,n)}}}};
@@ -1 +1 @@
1
- import{gate as s}from"../../../lib/ast.js";const p=new Set(["===","!==","==","!=","<","<=",">",">=","in","instanceof"]),a=5,n=(t)=>{if(!t)return 0;if(t.type==="LogicalExpression")return 1+n(t.left)+n(t.right);if(t.type==="BinaryExpression")return(p.has(t.operator)?1:0)+n(t.left)+n(t.right);if(t.type==="UnaryExpression")return t.operator==="!"?n(t.argument):0;if(t.type==="ConditionalExpression")return 1+n(t.test)+n(t.consequent)+n(t.alternate);return 0};export const noPackedCondition={meta:{type:"problem",docs:{description:"Require a condition to be split into named booleans once it holds too many boolean operators and comparisons. The option says how many, defaulting to 5."},schema:[{type:"integer",minimum:1}]},createOnce(t){let r=a;const o=(e)=>{if(!e)return;const i=n(e);if(i<=r)return;t.report({node:e,message:`This condition holds ${i} boolean operators and comparisons, past the ${r} a reader takes in at once. Extract each part into an explaining variable, named so it reads on its own, like \`hasValue\` or \`priceChanged\`. Stop when the condition is just those names joined by \`&&\`/\`||\`.`})};return{before(){r=typeof t.options[0]==="number"?t.options[0]:a;return s(t,"&&","||","??")},IfStatement(e){o(e.test)},ConditionalExpression(e){o(e.test)},WhileStatement(e){o(e.test)},DoWhileStatement(e){o(e.test)},ForStatement(e){o(e.test)}}}};
1
+ import{gate as s}from"../../../lib/ast.js";const p=new Set(["===","!==","==","!=","<","<=",">",">=","in","instanceof"]),o=5,r=(t)=>{if(!t)return 0;if(t.type==="LogicalExpression")return 1+r(t.left)+r(t.right);if(t.type==="BinaryExpression")return(p.has(t.operator)?1:0)+r(t.left)+r(t.right);if(t.type==="UnaryExpression")return t.operator==="!"?r(t.argument):0;if(t.type==="ConditionalExpression")return 1+r(t.test)+r(t.consequent)+r(t.alternate);return 0};export const noPackedCondition={meta:{type:"problem",docs:{description:"Require a condition to be split into named booleans once it holds too many boolean operators and comparisons. The option says how many, defaulting to 5."},schema:[{type:"integer",minimum:1}]},createOnce(t){let a=o;const n=(e)=>{if(!e)return;const i=r(e);if(i<=a)return;t.report({node:e,message:`Extract each part into a named variable like \`hasValue\`, until this condition reads as those names joined by \`&&\`/\`||\` — it packs ${i} operators, past the ${a} a reader takes in.`})};return{before(){a=typeof t.options[0]==="number"?t.options[0]:o;return s(t,"&&","||","??")},IfStatement(e){n(e.test)},ConditionalExpression(e){n(e.test)},WhileStatement(e){n(e.test)},DoWhileStatement(e){n(e.test)},ForStatement(e){n(e.test)}}}};
@@ -1 +1 @@
1
- import{readdirSync as l}from"node:fs";import{isAbsolute as d,join as y}from"node:path";import{optionsOf as u}from"../../../lib/ast.js";const g="src/components/ui",h="@/components/ui",b=/\.(?:ios|android|native|web)$/,S={Pressable:["TouchableOpacity","TouchableHighlight","TouchableWithoutFeedback","TouchableNativeFeedback"]},m={anyOf:[{type:"string"},{type:"array",items:{type:"string"}}]},A={anyOf:[...m.anyOf,{type:"object",properties:{replaces:m,from:{type:"string"},path:{type:"string"},reason:{type:"string"}},required:["replaces"],additionalProperties:!1}]},E=(e)=>e.split("-").filter(Boolean).map((t)=>t.charAt(0).toUpperCase()+t.slice(1)).join(""),w=(e)=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),p=(e,t,r,o)=>{const n=e.get(t)??new Map;n.set(r,o);e.set(t,n)},I=(e,t,r)=>{let o;try{o=l(d(t)?t:y(process.cwd(),t))}catch{return}for(const n of o){if(!n.endsWith(".tsx"))continue;const s=n.slice(0,-4).replace(b,"");if(s==="index")continue;const i=E(s),a=`${r}/${s}`;p(e,"react-native",i,{name:i,from:a});for(const c of S[i]??[])p(e,"react-native",c,{name:i,from:a})}},O=(e,t,r)=>{for(const[o,n]of Object.entries(r)){const s=typeof n==="string"||Array.isArray(n)?{replaces:n}:n,i={name:o,from:s.path??`${t}/${w(o)}`,reason:s.reason};for(const a of[s.replaces].flat())p(e,s.from??"react-native",a,i)}},f=new Map,T=(e,t)=>!!e&&t.some((r)=>e.includes(r)),_=(e)=>{const t=e.dir??g,r=e.alias??h,o=e.exempt??[t],n=JSON.stringify([t,r,e.use]);let s=f.get(n);if(s===void 0){s=new Map;I(s,t,r);if(e.use)O(s,r,e.use);f.set(n,s)}return{banned:s,exempt:o}};export const useDesignSystem={meta:{type:"problem",docs:{description:"Disallow importing a raw primitive your design system already wraps. Wrapped components come from scanning the design-system directory, plus the explicit `use` map for names, paths and source modules the scan cannot infer. Files under the design-system directory (or `exempt`) are skipped."},schema:[{type:"object",properties:{dir:{type:"string",minLength:1},alias:{type:"string",minLength:1},use:{type:"object",additionalProperties:A},exempt:{type:"array",items:{type:"string"}}},additionalProperties:!1}],defaultOff:!0},createOnce(e){let t=new Map;return{before(){const r=u(e,{}),o=_(r);t=o.banned;if(t.size===0)return!1;const{filename:n}=e;if(T(n,o.exempt))return!1;return!0},ImportDeclaration(r){const o=r.source.value,n=t.get(o);if(n===void 0)return;for(const s of r.specifiers){if(s.type!=="ImportSpecifier")continue;const{imported:i}=s;if(i.type!=="Identifier")continue;const a=n.get(i.name);if(a===void 0)continue;const c=a.reason?` ${a.reason}`:"";e.report({node:s,message:`Import \`${a.name}\` from "${a.from}" instead of \`${i.name}\` from ${o}. The wrapper is where the theme tokens and app behavior live.${c}`})}}}}};
1
+ import{readdirSync as l}from"node:fs";import{isAbsolute as d,join as y}from"node:path";import{optionsOf as u}from"../../../lib/ast.js";const g="src/components/ui",b="@/components/ui",h=/\.(?:ios|android|native|web)$/,S={Pressable:["TouchableOpacity","TouchableHighlight","TouchableWithoutFeedback","TouchableNativeFeedback"]},m={anyOf:[{type:"string"},{type:"array",items:{type:"string"}}]},A={anyOf:[...m.anyOf,{type:"object",properties:{replaces:m,from:{type:"string"},path:{type:"string"},reason:{type:"string"}},required:["replaces"],additionalProperties:!1}]},E=(e)=>e.split("-").filter(Boolean).map((t)=>t.charAt(0).toUpperCase()+t.slice(1)).join(""),I=(e)=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),p=(e,t,r,o)=>{const n=e.get(t)??new Map;n.set(r,o);e.set(t,n)},O=(e,t,r)=>{let o;try{o=l(d(t)?t:y(process.cwd(),t))}catch{return}for(const n of o){if(!n.endsWith(".tsx"))continue;const s=n.slice(0,-4).replace(h,"");if(s==="index")continue;const i=E(s),a=`${r}/${s}`;p(e,"react-native",i,{name:i,from:a});for(const c of S[i]??[])p(e,"react-native",c,{name:i,from:a})}},w=(e,t,r)=>{for(const[o,n]of Object.entries(r)){const s=typeof n==="string"||Array.isArray(n)?{replaces:n}:n,i={name:o,from:s.path??`${t}/${I(o)}`,reason:s.reason};for(const a of[s.replaces].flat())p(e,s.from??"react-native",a,i)}},f=new Map,_=(e,t)=>!!e&&t.some((r)=>e.includes(r)),M=(e)=>{const t=e.dir??g,r=e.alias??b,o=e.exempt??[t],n=JSON.stringify([t,r,e.use]);let s=f.get(n);if(s===void 0){s=new Map;O(s,t,r);if(e.use)w(s,r,e.use);f.set(n,s)}return{banned:s,exempt:o}};export const useDesignSystem={meta:{type:"problem",docs:{description:"Disallow importing a raw primitive your design system already wraps. Wrapped components come from scanning the design-system directory, plus the explicit `use` map for names, paths and source modules the scan cannot infer. Files under the design-system directory (or `exempt`) are skipped."},schema:[{type:"object",properties:{dir:{type:"string",minLength:1},alias:{type:"string",minLength:1},use:{type:"object",additionalProperties:A},exempt:{type:"array",items:{type:"string"}}},additionalProperties:!1}],defaultOff:!0},createOnce(e){let t=new Map;return{before(){const r=u(e,{}),o=M(r);t=o.banned;if(t.size===0)return!1;const{filename:n}=e;if(_(n,o.exempt))return!1;return!0},ImportDeclaration(r){const o=r.source.value,n=t.get(o);if(n===void 0)return;for(const s of r.specifiers){if(s.type!=="ImportSpecifier")continue;const{imported:i}=s;if(i.type!=="Identifier")continue;const a=n.get(i.name);if(a===void 0)continue;const c=a.reason?` ${a.reason}`:"";e.report({node:s,message:`Import \`${a.name}\` from "${a.from}" instead of \`${i.name}\` from ${o}.${c}`})}}}}};
@@ -1 +1 @@
1
- import{problem as l}from"../../../lib/ast.js";const a={nativeEnum:"Call `z.enum()` here — it takes the same native enum object and params, and `z.nativeEnum()` is deprecated in Zod 4.",literalUnion:"Replace this union with `z.enum([...])` listing the same string values, so invalid input reports one issue and the options stay reusable."},i=(t,e)=>{const{callee:n}=t;if(n.type!=="MemberExpression")return!1;const{object:r,property:s}=n;if(r.type!=="Identifier"||r.name!=="z")return!1;return s.type==="Identifier"&&s.name===e},o=(t)=>{if(t?.type!=="CallExpression"||!i(t,"literal"))return!1;const e=t.arguments;if(e.length!==1)return!1;const[n]=e;return n?.type==="Literal"&&typeof n.value==="string"};export const preferEnum=l("Disallow `z.nativeEnum()` and any `z.union()` whose members are all `z.literal()` strings.",{createOnce(t){return{CallExpression(e){if(i(e,"nativeEnum")){t.report({node:e,message:a.nativeEnum});return}if(!i(e,"union"))return;const[n]=e.arguments;if(n?.type!=="ArrayExpression")return;const{elements:r}=n;if(r.length===0||!r.every((s)=>o(s)))return;t.report({node:e,message:a.literalUnion})}}}});
1
+ import{problem as l}from"../../../lib/ast.js";const a={nativeEnum:"Call `z.enum()` here — same arguments, and `z.nativeEnum()` is deprecated in Zod 4.",literalUnion:"Replace this union with `z.enum([...])` listing the same string values."},i=(r,e)=>{const{callee:t}=r;if(t.type!=="MemberExpression")return!1;const{object:n,property:s}=t;if(n.type!=="Identifier"||n.name!=="z")return!1;return s.type==="Identifier"&&s.name===e},u=(r)=>{if(r?.type!=="CallExpression"||!i(r,"literal"))return!1;const e=r.arguments;if(e.length!==1)return!1;const[t]=e;return t?.type==="Literal"&&typeof t.value==="string"};export const preferEnum=l("Disallow `z.nativeEnum()` and any `z.union()` whose members are all `z.literal()` strings.",{createOnce(r){return{CallExpression(e){if(i(e,"nativeEnum")){r.report({node:e,message:a.nativeEnum});return}if(!i(e,"union"))return;const[t]=e.arguments;if(t?.type!=="ArrayExpression")return;const{elements:n}=t;if(n.length===0||!n.every((s)=>u(s)))return;r.report({node:e,message:a.literalUnion})}}}});
@@ -1 +1 @@
1
- const e=(t)=>new URL(t,import.meta.url).href;export const coreRegistry=[{meta:{name:"@ashstack/core"},url:e("../core/rules/core/index.js"),rules:{"no-comments":{},"no-naming-convention":{},"no-packed-condition":{},"use-design-system":{defaultOff:!0},"components-tsx-only":{defaultOff:!0},"hoist-intl":{}}},{meta:{name:"@ashstack/zod"},url:e("../core/rules/zod/index.js"),option:"zod",packages:["zod"],rules:{"prefer-enum":{}}}],reactRegistry=[{meta:{name:"@ashstack/react"},url:e("../react/rules/react/index.js"),rules:{"no-unlabeled-icon-button":{},"no-svg-without-title":{}}},{meta:{name:"@ashstack/query"},url:e("../react/rules/query/index.js"),option:"query",packages:["@tanstack/react-query"],rules:{"no-inline-keys":{},"no-deprecated-filters":{},"require-destructured-hooks":{},"no-fetch-in-query-fn":{},"next-page-param-undefined":{}}},{meta:{name:"@ashstack/zustand"},url:e("../react/rules/zustand/index.js"),option:"zustand",packages:["zustand"],rules:{"require-selector":{}}},{meta:{name:"@ashstack/i18n"},url:e("../react/rules/i18n/index.js"),option:"i18n",packages:["i18next","react-i18next","@lingui/core","react-intl","use-intl","next-intl","expo-localization"],rules:{"no-bare-text":{},"no-bare-attrs":{},"no-bare-toast":{}}},{meta:{name:"@ashstack/tailwind"},url:e("../react/rules/tailwind/index.js"),option:"tailwind",packages:["tailwindcss"],rules:{"prefer-cn":{},"use-logical-classes":{}}},{meta:{name:"@ashstack/tanstack-router"},url:e("../react/rules/tanstack-router/index.js"),option:"tanstackRouter",packages:["@tanstack/react-router"],rules:{"require-selector":{},"no-search-casts":{}}}],reactNativeRegistry=[{meta:{name:"@ashstack/react-native"},url:e("../react-native/rules/react-native/index.js"),restrictedImports:{paths:[{name:"react",importNames:["forwardRef"],message:"Accept `ref` as a regular prop and delete the `forwardRef` wrapper — React 19 passes `ref` like any other prop."},{name:"react",importNames:["useContext"],message:"Call `use(Context)` instead of `useContext(Context)`; React 19 replaces the hook with it."}],patterns:[{group:["expo-linear-gradient","react-native-linear-gradient"],message:"Apply the gradient through the `experimental_backgroundImage` style property instead of a gradient component."}]},rules:{"no-keyboard-will-events":{},"no-scroll-position-state":{},"no-conditional-style-array":{},"no-leaked-render":{},"no-rn-image-network-source":{},"no-redundant-view-nesting":{},"no-rn-namespace-import":{},"no-unlabeled-icon-pressable":{},"hoist-stateless-function":{},"no-manual-memo":{},"no-dynamic-import":{}}},{meta:{name:"@ashstack/unistyles"},url:e("../react-native/rules/unistyles/index.js"),option:"unistyles",packages:["react-native-unistyles"],restrictedImports:{paths:[{name:"react-native",importNames:["StyleSheet"],message:"Import `StyleSheet` from `react-native-unistyles` instead of `react-native`."},{name:"react-native",importNames:["useWindowDimensions","Dimensions"],message:"Read screen size from `rt.screen` inside `StyleSheet.create`, or `UnistylesRuntime.screen` outside it, instead of the `Dimensions` APIs."},{name:"react-native",importNames:["SafeAreaView"],message:'Set `contentInsetAdjustmentBehavior="automatic"` on the scroll view, or apply `rt.insets` from `react-native-unistyles`, instead of `SafeAreaView`.'},{name:"react-native-safe-area-context",importNames:["useSafeAreaInsets","useSafeAreaFrame","SafeAreaView"],message:"Read safe-area values from `rt.insets`, or `UnistylesRuntime.insets` outside a stylesheet, in `react-native-unistyles`."}]},rules:{"animated-theme":{},"content-container":{},"in-sheet":{},insets:{},"no-hardcoded-color":{},"no-hardcoded-spacing":{},"no-margin":{},"no-style-spread":{},"no-unused-styles":{},"rtl-style-call":{},"theme-screen-component":{},"theme-style-attr":{}}},{meta:{name:"@ashstack/legend-list"},url:e("../react-native/rules/legend-list/index.js"),option:"legendList",packages:["@legendapp/list"],restrictedImports:{paths:[{name:"react-native",importNames:["FlatList","SectionList","VirtualizedList"],message:"Render this list with `LegendList` from `@legendapp/list/react-native`; a `ScrollView` plus `map` is fine for fewer than 20 static items."},{name:"@shopify/flash-list",message:"Import `LegendList` from `@legendapp/list/react-native` — Legend List replaced FlashList in this stack."}],patterns:[{group:["@legendapp/list"],message:"Import from the platform entrypoint `@legendapp/list/react-native`, not the package root."}]},rules:{"required-props":{},"no-index-key-extractor":{},"no-remount-key":{},"no-inline-render-item-props":{},"no-mixed-children":{},"no-flex-in-content-container":{},"typed-items-need-item-type":{},"no-scrollview-map":{},"no-unsupported-props":{}}},{meta:{name:"@ashstack/legend-state"},url:e("../react-native/rules/legend-state/index.js"),option:"legendState",packages:["@legendapp/state"],restrictedImports:{paths:[{name:"@legendapp/state/react",importNames:["use$","useSelector"],message:"Use `useValue` from `@legendapp/state/react`; `use$` and `useSelector` are deprecated."}]},rules:{"no-assignment":{},naming:{},"no-nested-observable":{},"no-react-mirror":{},"no-untracked-get-in-jsx":{},"no-peek-in-selector":{},"no-object-selector":{},"no-observable-in-component":{}}},{meta:{name:"@ashstack/reanimated"},url:e("../react-native/rules/reanimated/index.js"),option:"reanimated",packages:["react-native-reanimated"],restrictedImports:{paths:[{name:"react-native",importNames:["Animated"],message:"Use `react-native-reanimated` instead of React Native's `Animated` API."},{name:"react-native-reanimated",importNames:["runOnJS","runOnUI"],message:"Use `scheduleOnRN` and `scheduleOnUI` from `react-native-worklets`; `runOnJS` and `runOnUI` are deprecated."},{name:"react-native-worklets",importNames:["runOnJS","runOnUI"],message:"Use `scheduleOnRN` and `scheduleOnUI` from `react-native-worklets`; `runOnJS` and `runOnUI` are deprecated."}]},rules:{"animated-reaction-safety":{},"animated-style-needs-animated-component":{},"animated-updater-purity":{},"gpu-properties-only":{},"hoist-layout-animation-builder":{},"interpolate-needs-clamp":{},"no-shared-value-dot-value":{},"no-react-state-from-continuous-worklet":{},"prefer-lazy-shared-value-initializer":{},"schedule-on-rn-scope":{},"shared-value-usage":{}}},{meta:{name:"@ashstack/turbo-image"},url:e("../react-native/rules/turbo-image/index.js"),option:"turboImage",packages:["react-native-turbo-image"],rules:{"require-resize":{},"require-cache-policy":{}}},{meta:{name:"@ashstack/skia"},url:e("../react-native/rules/skia/index.js"),option:"skia",packages:["@shopify/react-native-skia"],restrictedImports:{paths:[{name:"@shopify/react-native-skia",importNames:["usePathValue","usePathInterpolation"],message:"Keep one stable `SkPath` buffer and mutate it inside `useDerivedValue`, with consumers reading the driving shared value. These legacy path-value hooks self-dirty Reanimated mappers and re-record idle canvases."}]},rules:{"canvas-opaque":{}}},{meta:{name:"@ashstack/keyboard"},url:e("../react-native/rules/keyboard/index.js"),option:"keyboard",packages:["react-native-keyboard-controller"],restrictedImports:{paths:[{name:"react-native",importNames:["KeyboardAvoidingView"],message:"Import `KeyboardAvoidingView` from `react-native-keyboard-controller` — same props, and it tracks the keyboard per frame instead of waiting for Android's post-animation `keyboardDidShow`. For a scrolling form use its `KeyboardAwareScrollView`; to just shift a view, use `rt.insets.ime`."}]},rules:{}}];
1
+ const e=(t)=>new URL(t,import.meta.url).href;export const coreRegistry=[{meta:{name:"@ashstack/core"},url:e("../core/rules/core/index.js"),rules:{"no-comments":{},"no-naming-convention":{},"no-packed-condition":{},"use-design-system":{defaultOff:!0},"components-tsx-only":{defaultOff:!0},"hoist-intl":{},"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
  *
@@ -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
@@ -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"],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
+ 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 n,findInSubtree as o,FUNCTION_TYPES as u,gate as l,problem as s,propertyKeyName as i}from"../../../lib/ast.js";const g="Return `undefined` to signal there are no more pages. `null` is a valid page param, so returning it reads as a real next cursor and the list keeps fetching forever.",p=(e)=>e.type==="ReturnStatement"&&e.argument?.type==="Literal"&&e.argument.value===null,m=(e)=>e.type==="ArrowFunctionExpression"&&e.body.type==="Literal"&&e.body.value===null?e.body:null;export const nextPageParamUndefined=s("Disallow `return null` in the body of a `getNextPageParam`.",{createOnce(e){return{before(){return l(e,"getNextPageParam")},Property(t){if(i(t)!=="getNextPageParam")return;const{value:r}=t,a=m(r)??o(r,p);if(!a||n(a,r,u))return;e.report({node:r,message:g})}}}});
1
+ import{crossesFunctionBefore as a,findInSubtree as o,FUNCTION_TYPES as u,gate as l,problem as s,propertyKeyName as i}from"../../../lib/ast.js";const g="Return `undefined` to signal there are no more pages — `null` reads as a real next cursor, so the list keeps fetching forever.",p=(e)=>e.type==="ReturnStatement"&&e.argument?.type==="Literal"&&e.argument.value===null,m=(e)=>e.type==="ArrowFunctionExpression"&&e.body.type==="Literal"&&e.body.value===null?e.body:null;export const nextPageParamUndefined=s("Disallow `return null` in the body of a `getNextPageParam`.",{createOnce(e){return{before(){return l(e,"getNextPageParam")},Property(t){if(i(t)!=="getNextPageParam")return;const{value:r}=t,n=m(r)??o(r,p);if(!n||a(n,r,u))return;e.report({node:r,message:g})}}}});
@@ -1 +1 @@
1
- const n="Pass a filter object here: `.invalidateQueries({ queryKey: someKeys.scope(...) })`. TanStack Query v5 removed the positional `(queryKey)` form.",o=new Set(["invalidateQueries","removeQueries","refetchQueries","cancelQueries","resetQueries"]),a=(e)=>{if(e.type!=="CallExpression")return!1;const{callee:r}=e;if(r.type!=="MemberExpression"||r.computed)return!1;const{property:t}=r;return t.type==="Identifier"&&o.has(t.name)},l=(e)=>{if(e.type!=="CallExpression"||e.arguments.length!==1)return;return e.arguments[0]},u=(e)=>e.type==="Literal"&&typeof e.value==="string",c=(e,r)=>{if(r.type==="ArrayExpression")return`{ queryKey: ${e.sourceCode.getText(r)} }`;if(u(r))return`{ queryKey: [${e.sourceCode.getText(r)}] }`;return null};export const noDeprecatedFilters={meta:{type:"problem",docs:{description:"Disallow the positional key argument that TanStack Query v5 removed from `invalidateQueries` and its sibling methods. The suggestion rewrites it to the filter-object form."},hasSuggestions:!0},createOnce(e){return{CallExpression(r){if(!a(r))return;const t=l(r);if(t===void 0)return;const s=c(e,t);if(s===null)return;e.report({node:t,message:n,suggest:[{desc:"Wrap in a filter object",fix:(i)=>i.replaceText(t,s)}]})}}}};
1
+ const n="Pass a filter object here: `.invalidateQueries({ queryKey: someKeys.scope(...) })`; v5 removed the positional form.",o=new Set(["invalidateQueries","removeQueries","refetchQueries","cancelQueries","resetQueries"]),l=(e)=>{if(e.type!=="CallExpression")return!1;const{callee:t}=e;if(t.type!=="MemberExpression"||t.computed)return!1;const{property:r}=t;return r.type==="Identifier"&&o.has(r.name)},a=(e)=>{if(e.type!=="CallExpression"||e.arguments.length!==1)return;return e.arguments[0]},u=(e)=>e.type==="Literal"&&typeof e.value==="string",c=(e,t)=>{if(t.type==="ArrayExpression")return`{ queryKey: ${e.sourceCode.getText(t)} }`;if(u(t))return`{ queryKey: [${e.sourceCode.getText(t)}] }`;return null};export const noDeprecatedFilters={meta:{type:"problem",docs:{description:"Disallow the positional key argument that TanStack Query v5 removed from `invalidateQueries` and its sibling methods. The suggestion rewrites it to the filter-object form."},hasSuggestions:!0},createOnce(e){return{CallExpression(t){if(!l(t))return;const r=a(t);if(r===void 0)return;const s=c(e,r);if(s===null)return;e.report({node:r,message:n,suggest:[{desc:"Wrap in a filter object",fix:(i)=>i.replaceText(r,s)}]})}}}};
@@ -1 +1 @@
1
- import{gate as r,problem as a,propertyKeyName as n,subtreeHas as s}from"../../../lib/ast.js";const o="Call a typed function from the feature's `*.requests.ts` module here instead of `fetch`. That client is what sends the auth header, applies the timeout, retries a 429 or 5xx, and throws the app's error type.",i=new Set(["queryFn","mutationFn"]),u=(e)=>e.type==="CallExpression"&&e.callee.type==="Identifier"&&e.callee.name==="fetch";export const noFetchInQueryFn=a("Disallow a bare `fetch(` inside a `queryFn` or `mutationFn`.",{createOnce(e){return{before(){return r(e,"queryFn","mutationFn")},Property(t){if(!i.has(n(t)))return;if(!s(t.value,u))return;e.report({node:t.value,message:o})}}}});
1
+ import{gate as t,problem as a,propertyKeyName as n,subtreeHas as s}from"../../../lib/ast.js";const o="Call a typed function from the feature's `*.requests.ts` module here instead of `fetch` — it carries auth, retries and the app's error type.",u=new Set(["queryFn","mutationFn"]),i=(e)=>e.type==="CallExpression"&&e.callee.type==="Identifier"&&e.callee.name==="fetch";export const noFetchInQueryFn=a("Disallow a bare `fetch(` inside a `queryFn` or `mutationFn`.",{createOnce(e){return{before(){return t(e,"queryFn","mutationFn")},Property(r){if(!u.has(n(r)))return;if(!s(r.value,i))return;e.report({node:r.value,message:o})}}}});
@@ -1 +1 @@
1
- import{problem as a,propertyKeyName as y}from"../../../lib/ast.js";const i={inlineQueryKey:"Define this key in the feature's `*.keys.ts` factory and reference it: `queryKey: someKeys.scope(...)`, so invalidation elsewhere matches the same key.",inlineGetQueryData:"Read through the feature's `*.keys.ts` factory: `getQueryData(someKeys.scope())`.",inlineSetQueryData:"Write through the feature's `*.keys.ts` factory: `setQueryData(someKeys.scope(), updater)`."},o=new Set(["useQuery","useSuspenseQuery","useInfiniteQuery","useSuspenseInfiniteQuery","useQueries"]),p=new Set(["invalidateQueries","removeQueries","refetchQueries","cancelQueries","resetQueries","fetchQuery","prefetchQuery","ensureQueryData","getQueriesData","setQueriesData"]),c=(t)=>{if(t.type!=="CallExpression")return"";const{callee:e}=t;if(e.type!=="MemberExpression"||e.computed)return"";const{property:r}=e;return r.type==="Identifier"?r.name:""},f=(t)=>{if(t.type!=="CallExpression")return;const[e]=t.arguments;return e?.type==="ArrayExpression"?e:void 0},l=(t,e)=>{const r=t.type==="CallExpression"?t.callee:void 0;return r?.type==="Identifier"&&o.has(r.name)||p.has(e)},Q=(t)=>{if(t.type!=="Property"||t.computed)return!1;const{key:e}=t;if(e.type==="Identifier")return e.name==="queryKey";return e.type==="Literal"&&e.value==="queryKey"},m=(t)=>{if(t.type!=="ObjectExpression")return[];const e=t.properties.find((r)=>r.type==="Property"&&y(r)==="queries");if(e?.type!=="Property"||e.value.type!=="ArrayExpression")return[t];return[t,...e.value.elements.filter((r)=>r!==null)]},h=(t)=>{if(t.type!=="CallExpression")return[];const[e]=t.arguments;if(e===void 0)return[];const r=[];for(const s of m(e)){if(s.type!=="ObjectExpression")continue;for(const n of s.properties){if(n.type!=="Property"||!Q(n))continue;const{value:u}=n;if(u.type==="ArrayExpression")r.push(u)}}return r};export const noInlineKeys=a("Disallow a query key written as an array literal at the call site instead of coming from a keys factory.",{createOnce(t){return{CallExpression(e){const r=c(e);if(r==="getQueryData"||r==="setQueryData"){const s=f(e);if(s===void 0)return;t.report({node:s,message:r==="getQueryData"?i.inlineGetQueryData:i.inlineSetQueryData});return}if(!l(e,r))return;for(const s of h(e))t.report({node:s,message:i.inlineQueryKey})}}}});
1
+ import{problem as a,propertyKeyName as y}from"../../../lib/ast.js";const i={inlineQueryKey:"Define this key in the feature's `*.keys.ts` factory and reference it: `queryKey: someKeys.scope(...)`.",inlineGetQueryData:"Read through the feature's `*.keys.ts` factory: `getQueryData(someKeys.scope())`.",inlineSetQueryData:"Write through the feature's `*.keys.ts` factory: `setQueryData(someKeys.scope(), updater)`."},o=new Set(["useQuery","useSuspenseQuery","useInfiniteQuery","useSuspenseInfiniteQuery","useQueries"]),p=new Set(["invalidateQueries","removeQueries","refetchQueries","cancelQueries","resetQueries","fetchQuery","prefetchQuery","ensureQueryData","getQueriesData","setQueriesData"]),f=(r)=>{if(r.type!=="CallExpression")return"";const{callee:e}=r;if(e.type!=="MemberExpression"||e.computed)return"";const{property:t}=e;return t.type==="Identifier"?t.name:""},c=(r)=>{if(r.type!=="CallExpression")return;const[e]=r.arguments;return e?.type==="ArrayExpression"?e:void 0},l=(r,e)=>{const t=r.type==="CallExpression"?r.callee:void 0;return t?.type==="Identifier"&&o.has(t.name)||p.has(e)},Q=(r)=>{if(r.type!=="Property"||r.computed)return!1;const{key:e}=r;if(e.type==="Identifier")return e.name==="queryKey";return e.type==="Literal"&&e.value==="queryKey"},m=(r)=>{if(r.type!=="ObjectExpression")return[];const e=r.properties.find((t)=>t.type==="Property"&&y(t)==="queries");if(e?.type!=="Property"||e.value.type!=="ArrayExpression")return[r];return[r,...e.value.elements.filter((t)=>t!==null)]},d=(r)=>{if(r.type!=="CallExpression")return[];const[e]=r.arguments;if(e===void 0)return[];const t=[];for(const s of m(e)){if(s.type!=="ObjectExpression")continue;for(const n of s.properties){if(n.type!=="Property"||!Q(n))continue;const{value:u}=n;if(u.type==="ArrayExpression")t.push(u)}}return t};export const noInlineKeys=a("Disallow a query key written as an array literal at the call site instead of coming from a keys factory.",{createOnce(r){return{CallExpression(e){const t=f(e);if(t==="getQueryData"||t==="setQueryData"){const s=c(e);if(s===void 0)return;r.report({node:s,message:t==="getQueryData"?i.inlineGetQueryData:i.inlineSetQueryData});return}if(!l(e,t))return;for(const s of d(e))r.report({node:s,message:i.inlineQueryKey})}}}});
@@ -1 +1 @@
1
- import{problem as s}from"../../../lib/ast.js";const c="Destructure this hook at the call site: `const { data } = useFooQuery()` rather than binding the whole result to one name.",u=/^@\/api\/.*\.(?:queries|mutations)$/;export const requireDestructuredHooks=s("Require destructuring the result of a hook imported from an `@/api/*.queries` or `@/api/*.mutations` module.",{createOnce(a){const o=new Set,r=[];return{before(){o.clear();r.length=0},ImportDeclaration(e){if(!u.test(e.source.value))return;for(const t of e.specifiers)if(t.type==="ImportSpecifier")o.add(t.local.name)},VariableDeclarator(e){const{id:t,init:n}=e;if(t.type!=="Identifier")return;if(n?.type!=="CallExpression")return;const{callee:i}=n;if(i.type!=="Identifier"||!/^use[A-Z]/.test(i.name))return;r.push({node:t,hook:i.name})},"Program:exit"(){for(const e of r){if(!o.has(e.hook))continue;a.report({node:e.node,message:c})}}}}});
1
+ import{problem as a}from"../../../lib/ast.js";const c="Destructure this hook at the call site: `const { data } = useFooQuery()`.",u=/^@\/api\/.*\.(?:queries|mutations)$/;export const requireDestructuredHooks=a("Require destructuring the result of a hook imported from an `@/api/*.queries` or `@/api/*.mutations` module.",{createOnce(n){const o=new Set,r=[];return{before(){o.clear();r.length=0},ImportDeclaration(e){if(!u.test(e.source.value))return;for(const t of e.specifiers)if(t.type==="ImportSpecifier")o.add(t.local.name)},VariableDeclarator(e){const{id:t,init:s}=e;if(t.type!=="Identifier")return;if(s?.type!=="CallExpression")return;const{callee:i}=s;if(i.type!=="Identifier"||!/^use[A-Z]/.test(i.name))return;r.push({node:t,hook:i.name})},"Program:exit"(){for(const e of r){if(!o.has(e.hook))continue;n.report({node:e.node,message:c})}}}}});
@@ -1 +1 @@
1
- import{attributeName as a,gate as s,problem as o}from"../../../lib/ast.js";import{childNames as l,namedBy as m,NAMING_ATTRIBUTES as c}from"./shared.js";const f=new Set(["presentation","none"]),p='Give this `<svg>` a `<title>` child with content, or an `aria-label`, so a screen reader can name it. If it carries no meaning of its own, mark it `aria-hidden="true"` or `role="presentation"` and let the text beside it do the naming.',u=(e)=>{if(e?.type==="Literal")return e.value!=="false";if(e?.type!=="JSXExpressionContainer")return!0;const{expression:t}=e;return!(t.type==="Literal"&&(t.value===!1||t.value==="false"))},d=(e)=>e?.type==="Literal"&&typeof e.value==="string"&&f.has(e.value),g=(e)=>{if(e.type!=="JSXAttribute")return!1;const t=a(e);if(t==="aria-hidden")return u(e.value);if(t==="role")return d(e.value);return m(e,c)};export const noSvgWithoutTitle=o("Require an accessible name on an inline `<svg>`: a `<title>` child with content, an `aria-label`, or a marker that it is decorative. Every child counts, and a self-closing `<svg />` reports.",{createOnce(e){const t=(r)=>{if(r.type!=="JSXElement")return!1;const{name:n}=r.openingElement;if(n.type!=="JSXIdentifier"||n.name!=="title")return!1;return r.children.some((i)=>l(i,e)||i.type==="JSXElement"||i.type==="JSXFragment")};return{before(){return s(e,"<svg")},JSXElement(r){const n=r.openingElement;if(n.name.type!=="JSXIdentifier"||n.name.name!=="svg")return;if(n.attributes.some(g))return;if(r.children.some(t))return;e.report({node:n.name,message:p})}}}});
1
+ import{attributeName as a,gate as s,problem as o}from"../../../lib/ast.js";import{childNames as l,namedBy as m,NAMING_ATTRIBUTES as f}from"./shared.js";const c=new Set(["presentation","none"]),p='Give this `<svg>` a `<title>` child with content, or an `aria-label`. If it carries no meaning of its own, mark it `aria-hidden="true"`.',u=(e)=>{if(e?.type==="Literal")return e.value!=="false";if(e?.type!=="JSXExpressionContainer")return!0;const{expression:t}=e;return!(t.type==="Literal"&&(t.value===!1||t.value==="false"))},g=(e)=>e?.type==="Literal"&&typeof e.value==="string"&&c.has(e.value),d=(e)=>{if(e.type!=="JSXAttribute")return!1;const t=a(e);if(t==="aria-hidden")return u(e.value);if(t==="role")return g(e.value);return m(e,f)};export const noSvgWithoutTitle=o("Require an accessible name on an inline `<svg>`: a `<title>` child with content, an `aria-label`, or a marker that it is decorative. Every child counts, and a self-closing `<svg />` reports.",{createOnce(e){const t=(r)=>{if(r.type!=="JSXElement")return!1;const{name:n}=r.openingElement;if(n.type!=="JSXIdentifier"||n.name!=="title")return!1;return r.children.some((i)=>l(i,e)||i.type==="JSXElement"||i.type==="JSXFragment")};return{before(){return s(e,"<svg")},JSXElement(r){const n=r.openingElement;if(n.name.type!=="JSXIdentifier"||n.name.name!=="svg")return;if(n.attributes.some(d))return;if(r.children.some(t))return;e.report({node:n.name,message:p})}}}});
@@ -1 +1 @@
1
- import{attributeName as o,findInSubtree as i,gate as s,problem as l,tagIdentifier as m}from"../../../lib/ast.js";import{attributeHasContent as u,childNames as c,namedBy as b,NAMING_ATTRIBUTES as p}from"./shared.js";const g=new Set(["button","Button"]),S="Add an `aria-label` (or `aria-labelledby`), or render visible text, so this icon-only button has an accessible name. An `<img alt>` or an `<svg><title>` inside it counts too.",d=(e)=>e.type==="JSXAttribute"&&o(e)==="alt"&&u(e.value),f=(e)=>e.type==="JSXOpeningElement"&&m(e.name)==="img"&&e.attributes.some(d);export const noUnlabeledIconButton=l("Require an accessible name on an icon-only `<button>` or `<Button>`. Without a label, visible text or image alt text, a screen reader cannot reach the control.",{createOnce(e){const r=(t)=>c(t,e)||f(t);return{before(){return s(e,"button","Button")},JSXElement(t){const a=t.openingElement;if(a.name.type!=="JSXIdentifier"||!g.has(a.name.name))return;if(a.attributes.some((n)=>b(n,p)))return;if(!t.children.some((n)=>n.type==="JSXElement"||n.type==="JSXFragment"))return;if(t.children.some((n)=>i(n,r)!==null))return;e.report({node:a.name,message:S})}}}});
1
+ import{attributeName as o,findInSubtree as i,gate as s,problem as l,tagIdentifier as m}from"../../../lib/ast.js";import{attributeHasContent as u,childNames as b,namedBy as c,NAMING_ATTRIBUTES as p}from"./shared.js";const S=new Set(["button","Button"]),f="Add an `aria-label` (or `aria-labelledby`), or render visible text, so this icon-only button has an accessible name.",g=(e)=>e.type==="JSXAttribute"&&o(e)==="alt"&&u(e.value),d=(e)=>e.type==="JSXOpeningElement"&&m(e.name)==="img"&&e.attributes.some(g);export const noUnlabeledIconButton=l("Require an accessible name on an icon-only `<button>` or `<Button>`. Without a label, visible text or image alt text, a screen reader cannot reach the control.",{createOnce(e){const r=(t)=>b(t,e)||d(t);return{before(){return s(e,"button","Button")},JSXElement(t){const a=t.openingElement;if(a.name.type!=="JSXIdentifier"||!S.has(a.name.name))return;if(a.attributes.some((n)=>c(n,p)))return;if(!t.children.some((n)=>n.type==="JSXElement"||n.type==="JSXFragment"))return;if(t.children.some((n)=>i(n,r)!==null))return;e.report({node:a.name,message:f})}}}});
@@ -1 +1 @@
1
- import{attributeName as f,importedNames as h,problem as m}from"../../../lib/ast.js";import{CLASS_ATTRIBUTE as d,CLASS_BINDING as S,CLASS_MARKER as y,COMPOSER_MODULES as b,KNOWN_COMPOSERS as c}from"./shared.js";const g=new Set(["join","concat"]),w=new Set(["ConditionalExpression","LogicalExpression"]),a={attribute:"Wrap this class value in `cn(...)`. It folds the conditional inputs into one string and lets a later Tailwind utility win over an earlier one that sets the same property.",precomputed:"Wrap this value in `cn(...)` where it reaches the class prop, or build it with `cn(...)` where it is declared. Conflicting Tailwind utilities survive into the DOM otherwise.",declaration:"Build this class value with `cn(...)` instead of joining it by hand, so conflicting Tailwind utilities resolve to the last one and falsy branches drop out."},E=(e,r)=>{if(e.type==="MemberExpression")return e.property.type==="Identifier"&&g.has(e.property.name);if(e.type!=="Identifier")return!1;return c.has(e.name)?e.name!=="cn":r.has(e.name)},p=(e,r)=>{if(!e)return!1;if(w.has(e.type))return!0;if(e.type==="TemplateLiteral")return e.expressions.length>0;if(e.type==="BinaryExpression")return e.operator==="+";return e.type==="CallExpression"&&E(e.callee,r)},C=(e)=>{if(e.type!=="JSXAttribute")return null;const{value:r}=e;return r?.type==="JSXExpressionContainer"?r.expression:null};export const preferCn=m("Require a dynamic class value to go through `cn(...)` before it reaches a `class`, `className` or `*ClassName` prop. Reports both where the value reaches the prop and where a variable named after classes is declared.",{createOnce(e){const r=new Set,s=new Map,o=[],u=()=>{const t=new Set;for(const[n,i]of s){if(!S.test(n))continue;if(!p(i,r))continue;t.add(n);e.report({node:i.parent??i,message:a.declaration})}return t},l=(t)=>{for(const n of o){const i=C(n);if(!i)continue;if(p(i,r)){e.report({node:n,message:a.attribute});continue}if(i.type!=="Identifier"||t.has(i.name))continue;if(!p(s.get(i.name),r))continue;e.report({node:n,message:a.precomputed})}};return{before(){r.clear();s.clear();o.length=0;return y.test(e.sourceCode.text)},ImportDeclaration(t){for(const n of b)for(const{local:i}of h(t,n))if(!c.has(i))r.add(i)},VariableDeclarator(t){if(t.id.type==="Identifier"&&t.init)s.set(t.id.name,t.init)},JSXAttribute(t){if(d.test(f(t)))o.push(t)},"Program:exit"(){l(u())}}}});
1
+ import{attributeName as f,importedNames as m,problem as h}from"../../../lib/ast.js";import{CLASS_ATTRIBUTE as d,CLASS_BINDING as S,CLASS_MARKER as y,COMPOSER_MODULES as b,KNOWN_COMPOSERS as c}from"./shared.js";const g=new Set(["join","concat"]),E=new Set(["ConditionalExpression","LogicalExpression"]),a={attribute:"Wrap this class value in `cn(...)`, so a later Tailwind utility wins over an earlier one setting the same property.",precomputed:"Wrap this value in `cn(...)` where it reaches the class prop, or build it with `cn(...)` where it is declared.",declaration:"Build this class value with `cn(...)` instead of joining it by hand, so conflicting utilities resolve and falsy branches drop out."},w=(e,r)=>{if(e.type==="MemberExpression")return e.property.type==="Identifier"&&g.has(e.property.name);if(e.type!=="Identifier")return!1;return c.has(e.name)?e.name!=="cn":r.has(e.name)},p=(e,r)=>{if(!e)return!1;if(E.has(e.type))return!0;if(e.type==="TemplateLiteral")return e.expressions.length>0;if(e.type==="BinaryExpression")return e.operator==="+";return e.type==="CallExpression"&&w(e.callee,r)},C=(e)=>{if(e.type!=="JSXAttribute")return null;const{value:r}=e;return r?.type==="JSXExpressionContainer"?r.expression:null};export const preferCn=h("Require a dynamic class value to go through `cn(...)` before it reaches a `class`, `className` or `*ClassName` prop. Reports both where the value reaches the prop and where a variable named after classes is declared.",{createOnce(e){const r=new Set,s=new Map,o=[],u=()=>{const t=new Set;for(const[n,i]of s){if(!S.test(n))continue;if(!p(i,r))continue;t.add(n);e.report({node:i.parent??i,message:a.declaration})}return t},l=(t)=>{for(const n of o){const i=C(n);if(!i)continue;if(p(i,r)){e.report({node:n,message:a.attribute});continue}if(i.type!=="Identifier"||t.has(i.name))continue;if(!p(s.get(i.name),r))continue;e.report({node:n,message:a.precomputed})}};return{before(){r.clear();s.clear();o.length=0;return y.test(e.sourceCode.text)},ImportDeclaration(t){for(const n of b)for(const{local:i}of m(t,n))if(!c.has(i))r.add(i)},VariableDeclarator(t){if(t.id.type==="Identifier"&&t.init)s.set(t.id.name,t.init)},JSXAttribute(t){if(d.test(f(t)))o.push(t)},"Program:exit"(){l(u())}}}});
@@ -1 +1 @@
1
- import{attributeName as l,hasAncestor as o,problem as n}from"../../../lib/ast.js";import{CLASS_ATTRIBUTE as d,CLASS_BINDING as f}from"./shared.js";const u="ml|mr|pl|pr|left|right|scroll-ml|scroll-mr|scroll-pl|scroll-pr",c="border-l|border-r|rounded-l|rounded-r|rounded-tl|rounded-tr|rounded-bl|rounded-br",m="text-left|text-right|float-left|float-right|clear-left|clear-right",p=String.raw`(?:(?:${u})-\S+|(?:${c})(?:-\S+)?|${m})`,s=String.raw`[\s"'\`{}]`,r=new RegExp(String.raw`(?:^|${s})(?:[^\s:]+:)*!?-?${p}(?:$|${s})`),a="Swap this for its logical twin so the layout mirrors in Arabic and Hebrew: `ms`/`me` instead of `ml`/`mr`, `text-start` instead of `text-left`. Padding, insets, borders and corners follow the same pattern. Keep the physical utility only where the coordinate really is the screen rather than the reading order.",i=(t)=>o(t,(e)=>{if(e.type==="JSXAttribute")return d.test(l(e));if(e.type!=="VariableDeclarator")return!1;return e.id.type==="Identifier"&&f.test(e.id.name)});export const useLogicalClasses=n("Require a logical Tailwind utility over its physical left/right twin, so a right-to-left layout mirrors. Reads class props and bindings named after classes, through variant prefixes and the `!` and `-` modifiers.",{createOnce(t){return{before(){return r.test(t.sourceCode.text)},Literal(e){if(typeof e.value!=="string")return;if(!r.test(e.value))return;if(!i(e))return;t.report({node:e,message:a})},TemplateElement(e){if(!r.test(e.value.raw))return;if(!i(e))return;t.report({node:e,message:a})}}}});
1
+ import{attributeName as o,hasAncestor as a,problem as n}from"../../../lib/ast.js";import{CLASS_ATTRIBUTE as f,CLASS_BINDING as u}from"./shared.js";const d="ml|mr|pl|pr|left|right|scroll-ml|scroll-mr|scroll-pl|scroll-pr",m="border-l|border-r|rounded-l|rounded-r|rounded-tl|rounded-tr|rounded-bl|rounded-br",c="text-left|text-right|float-left|float-right|clear-left|clear-right",p=String.raw`(?:(?:${d})-\S+|(?:${m})(?:-\S+)?|${c})`,s=String.raw`[\s"'\`{}]`,e=new RegExp(String.raw`(?:^|${s})(?:[^\s:]+:)*!?-?${p}(?:$|${s})`),l="Swap this for its logical twin so the layout mirrors in RTL: `ms`/`me` for `ml`/`mr`, `text-start` for `text-left`. Insets, borders and corners follow.",i=(t)=>a(t,(r)=>{if(r.type==="JSXAttribute")return f.test(o(r));if(r.type!=="VariableDeclarator")return!1;return r.id.type==="Identifier"&&u.test(r.id.name)});export const useLogicalClasses=n("Require a logical Tailwind utility over its physical left/right twin, so a right-to-left layout mirrors. Reads class props and bindings named after classes, through variant prefixes and the `!` and `-` modifiers.",{createOnce(t){return{before(){return e.test(t.sourceCode.text)},Literal(r){if(typeof r.value!=="string")return;if(!e.test(r.value))return;if(!i(r))return;t.report({node:r,message:l})},TemplateElement(r){if(!e.test(r.value.raw))return;if(!i(r))return;t.report({node:r,message:l})}}}});
@@ -1 +1 @@
1
- import{gate as u,importedNames as m,problem as f}from"../../../lib/ast.js";import{ROUTER_MODULE as h}from"./shared.js";const d=["search","location","state"],l={search:"Drop this assertion and let the route's `validateSearch` schema type the result. An `as` here claims a shape the URL never has to honour; widen the schema, or parse the value, if the type is wrong.",routerState:"Drop this assertion and read validated search through `useSearch({ from: ... })`. `router.state.location.search` is whatever the URL held, so an `as` here is a claim about a string nobody checked."},S=(t)=>{let e=t;while(e.type==="TSAsExpression")e=e.expression;return e},c=(t)=>t?.type==="CallExpression"&&t.callee.type==="Identifier"?t.callee.name:"",y=(t)=>{if(t.type!=="MemberExpression")return"";const{property:e}=t;if(e.type==="Identifier")return t.computed?"":e.name;return e.type==="Literal"?String(e.value):""},g=(t)=>{let e=t;for(const s of d){if(e.type!=="MemberExpression"||y(e)!==s)return null;e=e.object}return e};export const noSearchCasts=f("Disallow an `as` assertion on a `useSearch()` result or on `router.state.location.search`. The route's `validateSearch` schema is what supplies the type.",{createOnce(t){const e=new Set,s=new Set,n=new Set,i=[],p=(r)=>r.type==="Identifier"?n.has(r.name):s.has(c(r));return{before(){e.clear();s.clear();n.clear();i.length=0;return u(t,h)},ImportDeclaration(r){for(const{imported:a,local:o}of m(r,h))if(a==="useSearch")e.add(o);else if(a==="useRouter")s.add(o)},VariableDeclarator(r){if(r.id.type!=="Identifier")return;if(s.has(c(r.init)))n.add(r.id.name)},TSAsExpression(r){if(r.parent.type==="TSAsExpression")return;i.push({node:r,inner:S(r.expression)})},"Program:exit"(){for(const{node:r,inner:a}of i){if(e.has(c(a))){t.report({node:r,message:l.search});continue}const o=g(a);if(o&&p(o))t.report({node:r,message:l.routerState})}}}}});
1
+ import{gate as u,importedNames as m,problem as f}from"../../../lib/ast.js";import{ROUTER_MODULE as p}from"./shared.js";const S=["search","location","state"],l={search:"Drop this assertion and let the route's `validateSearch` schema type the result; widen the schema, or parse the value, if the type is wrong.",routerState:"Drop this assertion and read validated search through `useSearch({ from: ... })` — `router.state.location.search` is whatever the URL held."},d=(t)=>{let e=t;while(e.type==="TSAsExpression")e=e.expression;return e},c=(t)=>t?.type==="CallExpression"&&t.callee.type==="Identifier"?t.callee.name:"",y=(t)=>{if(t.type!=="MemberExpression")return"";const{property:e}=t;if(e.type==="Identifier")return t.computed?"":e.name;return e.type==="Literal"?String(e.value):""},g=(t)=>{let e=t;for(const s of S){if(e.type!=="MemberExpression"||y(e)!==s)return null;e=e.object}return e};export const noSearchCasts=f("Disallow an `as` assertion on a `useSearch()` result or on `router.state.location.search`. The route's `validateSearch` schema is what supplies the type.",{createOnce(t){const e=new Set,s=new Set,o=new Set,i=[],h=(r)=>r.type==="Identifier"?o.has(r.name):s.has(c(r));return{before(){e.clear();s.clear();o.clear();i.length=0;return u(t,p)},ImportDeclaration(r){for(const{imported:a,local:n}of m(r,p))if(a==="useSearch")e.add(n);else if(a==="useRouter")s.add(n)},VariableDeclarator(r){if(r.id.type!=="Identifier")return;if(s.has(c(r.init)))o.add(r.id.name)},TSAsExpression(r){if(r.parent.type==="TSAsExpression")return;i.push({node:r,inner:d(r.expression)})},"Program:exit"(){for(const{node:r,inner:a}of i){if(e.has(c(a))){t.report({node:r,message:l.search});continue}const n=g(a);if(n&&h(n))t.report({node:r,message:l.routerState})}}}}});
@@ -1 +1 @@
1
- import{gate as h,importedNames as m,problem as p,propertyValue as i}from"../../../lib/ast.js";import{ROUTER_MODULE as l}from"./shared.js";const d=new Set(["useLocation","useRouterState"]),c={whole:"This hook subscribes to the whole router value, so the component re-renders on every navigation. Pass a `select` that returns what it reads, e.g. `{ select: location => location.pathname }`.",noSelect:"This hook subscribes to the whole router value, so the component re-renders on every navigation. Add a `select` returning the smallest value it reads.",search:"`useSearch({ strict: false })` subscribes to every route-search change. Add a `select` returning the keys this component reads."},u=(t)=>i(t,"select")!==null,f=(t)=>{const s=i(t,"strict");return s?.type==="Literal"&&s.value===!1};export const requireSelector=p("Require a `select` on `useLocation`, `useRouterState` and non-strict `useSearch`, so a component reads the smallest router value it needs instead of re-rendering on every navigation.",{createOnce(t){const s=new Set,a=new Set,n=[];return{before(){s.clear();a.clear();n.length=0;return h(t,l)},ImportDeclaration(e){for(const{imported:o,local:r}of m(e,l))if(d.has(o))s.add(r);else if(o==="useSearch")a.add(r)},CallExpression(e){if(e.callee.type==="Identifier")n.push({node:e,name:e.callee.name,options:e.arguments[0]})},"Program:exit"(){for(const{node:e,name:o,options:r}of n){if(s.has(o)){if(!r)t.report({node:e,message:c.whole});else if(!u(r))t.report({node:e,message:c.noSelect});continue}if(!a.has(o)||!f(r))continue;if(!u(r))t.report({node:e,message:c.search})}}}}});
1
+ import{gate as h,importedNames as m,problem as p,propertyValue as i}from"../../../lib/ast.js";import{ROUTER_MODULE as l}from"./shared.js";const d=new Set(["useLocation","useRouterState"]),c={whole:"Pass a `select` that returns what this component reads, e.g. `{ select: location => location.pathname }` — the whole router value re-renders on every navigation.",noSelect:"Add a `select` returning the smallest value this component reads — the whole router value re-renders on every navigation.",search:"Add a `select` returning the keys this component reads — `{ strict: false }` subscribes to every route-search change."},u=(t)=>i(t,"select")!==null,f=(t)=>{const s=i(t,"strict");return s?.type==="Literal"&&s.value===!1};export const requireSelector=p("Require a `select` on `useLocation`, `useRouterState` and non-strict `useSearch`, so a component reads the smallest router value it needs instead of re-rendering on every navigation.",{createOnce(t){const s=new Set,a=new Set,n=[];return{before(){s.clear();a.clear();n.length=0;return h(t,l)},ImportDeclaration(e){for(const{imported:o,local:r}of m(e,l))if(d.has(o))s.add(r);else if(o==="useSearch")a.add(r)},CallExpression(e){if(e.callee.type==="Identifier")n.push({node:e,name:e.callee.name,options:e.arguments[0]})},"Program:exit"(){for(const{node:e,name:o,options:r}of n){if(s.has(o)){if(!r)t.report({node:e,message:c.whole});else if(!u(r))t.report({node:e,message:c.noSelect});continue}if(!a.has(o)||!f(r))continue;if(!u(r))t.report({node:e,message:c.search})}}}}});
@@ -1 +1 @@
1
- import{problem as u}from"../../../lib/ast.js";const c={zustandBare:"Pass a selector to this store hook, for example `useSettingsStore(state => state.theme)`, so it re-renders only on that slice. Use `.getState()` instead for an imperative read.",zustandUndefined:"Replace `undefined` with a selector such as `state => state.theme` — it still subscribes to the whole store otherwise. Use `.getState()` instead for an imperative read."},f=/^(?:@\/stores\/|(?:\.\.?\/)+stores\/)[^"']*-store$/,i=/^use[A-Za-z0-9_$]*Store$/,d=(s)=>{if(s.type!=="ImportSpecifier")return null;const o=s.imported.type==="Identifier"?s.imported.name:"",t=s.local.name;return i.test(o)||i.test(t)?t:null};export const requireSelector=u("Require a selector on a store hook, rather than no arguments or `undefined` in its place.",{createOnce(s){const o=new Set,t=[];return{before(){o.clear();t.length=0},ImportDeclaration(e){if(!f.test(e.source.value))return;for(const n of e.specifiers){const r=d(n);if(r!==null)o.add(r)}},CallExpression(e){const{callee:n}=e;if(n.type!=="Identifier")return;const{name:r}=n;if(!o.has(r)&&!i.test(r))return;const a=e.arguments;if(a.length===0){t.push({node:e,name:r,bare:!0});return}const[l]=a;if(a.length===1&&l?.type==="Identifier"&&l.name==="undefined")t.push({node:e,name:r,bare:!1})},"Program:exit"(){for(const e of t){if(!o.has(e.name))continue;s.report({node:e.node,message:e.bare?c.zustandBare:c.zustandUndefined})}}}}});
1
+ import{problem as u}from"../../../lib/ast.js";const l={zustandBare:"Pass a selector to this store hook, e.g. `useSettingsStore(state => state.theme)`. Use `.getState()` for an imperative read.",zustandUndefined:"Replace `undefined` with a selector such as `state => state.theme`. Use `.getState()` for an imperative read."},f=/^(?:@\/stores\/|(?:\.\.?\/)+stores\/)[^"']*-store$/,i=/^use[A-Za-z0-9_$]*Store$/,d=(s)=>{if(s.type!=="ImportSpecifier")return null;const n=s.imported.type==="Identifier"?s.imported.name:"",t=s.local.name;return i.test(n)||i.test(t)?t:null};export const requireSelector=u("Require a selector on a store hook, rather than no arguments or `undefined` in its place.",{createOnce(s){const n=new Set,t=[];return{before(){n.clear();t.length=0},ImportDeclaration(e){if(!f.test(e.source.value))return;for(const o of e.specifiers){const r=d(o);if(r!==null)n.add(r)}},CallExpression(e){const{callee:o}=e;if(o.type!=="Identifier")return;const{name:r}=o;if(!n.has(r)&&!i.test(r))return;const a=e.arguments;if(a.length===0){t.push({node:e,name:r,bare:!0});return}const[c]=a;if(a.length===1&&c?.type==="Identifier"&&c.name==="undefined")t.push({node:e,name:r,bare:!1})},"Program:exit"(){for(const e of t){if(!n.has(e.name))continue;s.report({node:e.node,message:e.bare?l.zustandBare:l.zustandUndefined})}}}}});
@@ -1 +1 @@
1
- import{TEST_FILES as r}from"../core/index.js";import{mergeConfigs as s}from"../lib/merge.js";import{composeModules as a}from"../lib/module.js";import{coreRegistry as o,reactNativeRegistry as i,reactRegistry as n}from"../lib/registry.js";import c from"../react/index.js";export const banGroups=[{packages:["react-native-gesture-handler"],restrictedImports:{paths:[{name:"react-native",importNames:["Pressable","TouchableOpacity","TouchableHighlight","TouchableWithoutFeedback","TouchableNativeFeedback"],message:"Import `Pressable` from `react-native-gesture-handler` so it cooperates with the gesture system instead of competing with it. Exception: inside a SwiftUI/Compose host view touches never reach `GestureHandlerRootView` — keep React Native's `Pressable` there and suppress this rule with the host named in the reason."}]}},{packages:["expo-router"],restrictedImports:{patterns:[{group:["@react-navigation/stack","@react-navigation/bottom-tabs"],message:"Use expo-router's `Stack` / `NativeTabs` instead of a raw react-navigation navigator."}]}},{packages:["expo-font"],restrictedImports:{paths:[{name:"expo-font",importNames:["useFonts","loadAsync"],message:"Declare the fonts in the `expo-font` config plugin so they load at build time, instead of `useFonts`/`loadAsync` at runtime."}]}},{packages:["react-native-quick-crypto"],restrictedImports:{paths:[{name:"crypto-js",message:"Use `react-native-quick-crypto` instead — `crypto-js` is pure JavaScript and runs orders of magnitude slower."}]}}];const p={"eslint/no-empty":"error","eslint/no-empty-function":"error"},m={"@ashstack/effects/no-event-handler":"off"},l=(e)=>e?{}:{"@ashstack/react-native/no-manual-memo":"off"},reactNative=(e={})=>{const t=a([...o,...n,...i],e,banGroups);return s(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:r,rules:{"@ashstack/react-native/no-dynamic-import":"off","@ashstack/react-native/hoist-stateless-function":"off"}}]})};export{reactNative};export default reactNative;
1
+ import{TEST_FILES as a}from"../core/index.js";import{mergeConfigs as r}from"../lib/merge.js";import{composeModules as s}from"../lib/module.js";import{coreRegistry as o,reactNativeRegistry as i,reactRegistry as n}from"../lib/registry.js";import c,{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, and it tracks the keyboard per frame instead of waiting for Android's post-animation `keyboardDidShow`. For a scrolling form use its `KeyboardAwareScrollView`; to just shift a view, use `rt.insets.ime`."}]}});
1
+ import{defineModule as e}from"../../../lib/module.js";export default e({meta:{name:"@ashstack/keyboard"},url:import.meta.url,packages:["react-native-keyboard-controller"],option:"keyboard",docsWhen:"auto-enabled when `react-native-keyboard-controller` is a dependency",rules:{},restrictedImports:{paths:[{name:"react-native",importNames:["KeyboardAvoidingView"],message:"Import `KeyboardAvoidingView` from `react-native-keyboard-controller` — same props, tracked per frame. For a scrolling form, its `KeyboardAwareScrollView`."}]}});
@@ -1 +1 @@
1
- import{attributeName as u,gate as m,problem as h}from"../../../lib/ast.js";import{isStyleSheetCreate as x,propertyName as c,stylesObjectOf as b}from"../../stylesheet.js";import{expressionOf as g,isListElement as d}from"./shared.js";const S="Move `flex` onto `style` and keep `contentContainerStyle` for padding inside the content. On the content container it sizes the content to the viewport, so the list measures as zero height and renders a blank screen with no error.",f=(e)=>e?.type==="ObjectExpression"&&e.properties.some((t)=>t.type==="Property"&&!t.computed&&c(t)==="flex"),w=(e)=>{if(e?.type!=="MemberExpression"||e.object.type!=="Identifier")return null;const t=e.object.name;if(!e.computed&&e.property.type==="Identifier")return{sheet:t,key:e.property.name};if(e.computed&&e.property.type==="Literal"&&typeof e.property.value==="string")return{sheet:t,key:e.property.value};return null},C=(e)=>{if(!e)return[];if(e.type!=="ArrayExpression")return[e];return e.elements.filter((t)=>t!==null)};export const noFlexInContentContainer=h("Disallow `flex` in a Legend List's `contentContainerStyle`, where it sizes the scrolled content to the viewport and the list ends up measuring zero height.",{createOnce(e){let t=new Map,i=[];const y=(r)=>{if(f(r))return!0;const n=w(r);return n!==null&&(t.get(n.sheet)?.has(n.key)??!1)};return{before(){t=new Map;i=[];return m(e,"contentContainerStyle")},VariableDeclarator(r){const{id:n,init:o}=r;if(n.type!=="Identifier"||o?.type!=="CallExpression"||!x(o))return;const p=b(o.arguments[0]);if(!p)return;const a=new Set;for(const s of p.properties){if(s.type!=="Property"||s.computed)continue;const l=c(s);if(l!==""&&f(s.value))a.add(l)}t.set(n.name,a)},JSXAttribute(r){if(u(r)!=="contentContainerStyle")return;if(!d(r.parent.parent))return;const n=C(g(r));if(n.length>0)i.push({node:r,parts:n})},"Program:exit"(){for(const{node:r,parts:n}of i)if(n.some((o)=>y(o)))e.report({node:r,message:S})}}}});
1
+ import{attributeName as u,gate as m,problem as h}from"../../../lib/ast.js";import{isStyleSheetCreate as x,propertyName as c,stylesObjectOf as b}from"../../stylesheet.js";import{expressionOf as g,isListElement as S}from"./shared.js";const d="Move `flex` onto `style` and keep `contentContainerStyle` for padding — on the content container the list measures as zero height and renders blank.",f=(e)=>e?.type==="ObjectExpression"&&e.properties.some((t)=>t.type==="Property"&&!t.computed&&c(t)==="flex"),C=(e)=>{if(e?.type!=="MemberExpression"||e.object.type!=="Identifier")return null;const t=e.object.name;if(!e.computed&&e.property.type==="Identifier")return{sheet:t,key:e.property.name};if(e.computed&&e.property.type==="Literal"&&typeof e.property.value==="string")return{sheet:t,key:e.property.value};return null},E=(e)=>{if(!e)return[];if(e.type!=="ArrayExpression")return[e];return e.elements.filter((t)=>t!==null)};export const noFlexInContentContainer=h("Disallow `flex` in a Legend List's `contentContainerStyle`, where it sizes the scrolled content to the viewport and the list ends up measuring zero height.",{createOnce(e){let t=new Map,i=[];const y=(r)=>{if(f(r))return!0;const n=C(r);return n!==null&&(t.get(n.sheet)?.has(n.key)??!1)};return{before(){t=new Map;i=[];return m(e,"contentContainerStyle")},VariableDeclarator(r){const{id:n,init:s}=r;if(n.type!=="Identifier"||s?.type!=="CallExpression"||!x(s))return;const p=b(s.arguments[0]);if(!p)return;const a=new Set;for(const o of p.properties){if(o.type!=="Property"||o.computed)continue;const l=c(o);if(l!==""&&f(o.value))a.add(l)}t.set(n.name,a)},JSXAttribute(r){if(u(r)!=="contentContainerStyle")return;if(!S(r.parent.parent))return;const n=E(g(r));if(n.length>0)i.push({node:r,parts:n})},"Program:exit"(){for(const{node:r,parts:n}of i)if(n.some((s)=>y(s)))e.report({node:r,message:d})}}}});
@@ -1 +1 @@
1
- import{attributeName as s,gate as i,problem as o,subtreeHas as p}from"../../../lib/ast.js";import{expressionOf as m,isListElement as f}from"./shared.js";export const noIndexKeyExtractor=o("Disallow a `keyExtractor` that uses its index parameter. Cached sizes and recycled row state hang off the key, so a prepend points every measurement at the wrong item.",{createOnce:(r)=>({before:()=>i(r,"keyExtractor"),JSXAttribute(t){if(s(t)!=="keyExtractor")return;if(!f(t.parent.parent))return;const e=m(t);if(e?.type!=="ArrowFunctionExpression"&&e?.type!=="FunctionExpression")return;const n=e.params[1];if(n?.type!=="Identifier")return;if(!p(e.body,(a)=>a.type==="Identifier"&&a.name===n.name))return;r.report({node:e,message:"Return a stable per-item id from this `keyExtractor` instead of the index. Legend List hangs cached sizes and recycled row state off the key, so one prepend re-points every measurement at the wrong item."})}})});
1
+ import{attributeName as i,gate as s,problem as o,subtreeHas as p}from"../../../lib/ast.js";import{expressionOf as f,isListElement as m}from"./shared.js";export const noIndexKeyExtractor=o("Disallow a `keyExtractor` that uses its index parameter. Cached sizes and recycled row state hang off the key, so a prepend points every measurement at the wrong item.",{createOnce:(r)=>({before:()=>s(r,"keyExtractor"),JSXAttribute(t){if(i(t)!=="keyExtractor")return;if(!m(t.parent.parent))return;const e=f(t);if(e?.type!=="ArrowFunctionExpression"&&e?.type!=="FunctionExpression")return;const a=e.params[1];if(a?.type!=="Identifier")return;if(!p(e.body,(n)=>n.type==="Identifier"&&n.name===a.name))return;r.report({node:e,message:"Return a stable per-item id from this `keyExtractor` instead of the index — Legend List hangs cached sizes and recycled row state off the key."})}})});
@@ -1 +1 @@
1
- import{attributeName as s,gate as o,problem as a}from"../../../lib/ast.js";import{literalKind as d}from"./shared.js";export const noInlineRenderItemProps=a("Disallow inline object and array literals on props nested inside `renderItem`. A row whose props take a new identity every render can never be skipped.",{createOnce(n){let e=0;return{before(){e=0;return o(n,"renderItem")},JSXAttribute(r){if(s(r)==="renderItem"){e++;return}if(e===0)return;const{value:t}=r;if(t?.type!=="JSXExpressionContainer")return;const i=d(t.expression);if(i===null)return;n.report({node:t,message:`Pass \`item\` or its primitive fields and build this ${i} inside the row, or hoist it to module scope when it is static. A fresh ${i} each render means the row can never be skipped, so typing anywhere re-renders every visible row.`})},"JSXAttribute:exit"(r){if(s(r)==="renderItem")e--}}}});
1
+ import{attributeName as o,gate as s,problem as a}from"../../../lib/ast.js";import{literalKind as d}from"./shared.js";export const noInlineRenderItemProps=a("Disallow inline object and array literals on props nested inside `renderItem`. A row whose props take a new identity every render can never be skipped.",{createOnce(n){let e=0;return{before(){e=0;return s(n,"renderItem")},JSXAttribute(r){if(o(r)==="renderItem"){e++;return}if(e===0)return;const{value:t}=r;if(t?.type!=="JSXExpressionContainer")return;const i=d(t.expression);if(i===null)return;n.report({node:t,message:`Pass \`item\` or its primitive fields and build this ${i} inside the row, or hoist it to module scope when static — a fresh ${i} each render blocks row skipping.`})},"JSXAttribute:exit"(r){if(o(r)==="renderItem")e--}}}});
@@ -1 +1 @@
1
- import{gate as r,problem as n}from"../../../lib/ast.js";import{attributeNamed as i,isListElement as o,LIST as a}from"./shared.js";const s=(t)=>t.type==="JSXElement"&&t.children.some((e)=>e.type==="JSXElement"||e.type==="JSXFragment"||e.type==="JSXText"&&e.value.trim()!==""||e.type==="JSXExpressionContainer"&&e.expression.type!=="JSXEmptyExpression");export const noMixedChildren=n("Disallow passing both `data` and real children to a Legend List. The combination is unsupported and one of the two is dropped without a warning.",{createOnce:(t)=>({before:()=>r(t,a),JSXElement(e){if(!o(e))return;if(!i(e,"data"))return;if(!s(e))return;t.report({node:e.openingElement,message:"Keep either `data` with `renderItem` or children mode here, and remove the other. Passing both fails silently, with no guarantee about which one is ignored."})}})});
1
+ import{gate as r,problem as n}from"../../../lib/ast.js";import{attributeNamed as i,isListElement as o,LIST as a}from"./shared.js";const s=(t)=>t.type==="JSXElement"&&t.children.some((e)=>e.type==="JSXElement"||e.type==="JSXFragment"||e.type==="JSXText"&&e.value.trim()!==""||e.type==="JSXExpressionContainer"&&e.expression.type!=="JSXEmptyExpression");export const noMixedChildren=n("Disallow passing both `data` and real children to a Legend List. The combination is unsupported and one of the two is dropped without a warning.",{createOnce:(t)=>({before:()=>r(t,a),JSXElement(e){if(!o(e))return;if(!i(e,"data"))return;if(!s(e))return;t.report({node:e.openingElement,message:"Keep either `data` with `renderItem` or children mode here, and remove the other — passing both fails silently."})}})});
@@ -1 +1 @@
1
- import{gate as a,problem as i}from"../../../lib/ast.js";import{attributeNamed as r,isListElement as n,LIST as o}from"./shared.js";export const noRemountKey=i("Disallow `key` on a Legend List, which remounts on any key change and loses its measurements and scroll position. Pass `dataKey` instead.",{createOnce:(e)=>({before:()=>a(e,o),JSXElement(t){if(!n(t))return;const s=r(t,"key");if(!s)return;e.report({node:s,message:"Pass `dataKey` instead of `key` here: it re-initialises the list for a different dataset from the inside, without the remount that discards every measurement, cached size and scroll position."})}})});
1
+ import{gate as a,problem as i}from"../../../lib/ast.js";import{attributeNamed as n,isListElement as o,LIST as r}from"./shared.js";export const noRemountKey=i("Disallow `key` on a Legend List, which remounts on any key change and loses its measurements and scroll position. Pass `dataKey` instead.",{createOnce:(e)=>({before:()=>a(e,r),JSXElement(t){if(!o(t))return;const s=n(t,"key");if(!s)return;e.report({node:s,message:"Pass `dataKey` instead of `key` here — it re-initialises the list without the remount that discards every measurement and the scroll position."})}})});
@@ -1 +1 @@
1
- import{componentName as i,gate as l,problem as s}from"../../../lib/ast.js";export const noScrollviewMap=s("A `ScrollView` mounts every child up front, so a mapped collection does not belong in its children.",{createOnce:(n)=>({before:()=>l(n,"ScrollView"),JSXElement(t){if(i(t.openingElement.name)!=="ScrollView")return;for(const e of t.children){if(e.type!=="JSXExpressionContainer")continue;const r=e.expression;if(r.type!=="CallExpression")continue;const{callee:o}=r;if(o.type!=="MemberExpression")continue;if(o.property.type!=="Identifier"||o.property.name!=="map")continue;n.report({node:e,message:"Render this collection with `LegendList` from '@legendapp/list/react-native', passing the array as `data` and the mapped body as `renderItem`. A ScrollView mounts every child up front, so 200 rows cost 200 mounts to show ten."});return}}})});
1
+ import{componentName as i,gate as l,problem as p}from"../../../lib/ast.js";export const noScrollviewMap=p("A `ScrollView` mounts every child up front, so a mapped collection does not belong in its children.",{createOnce:(o)=>({before:()=>l(o,"ScrollView"),JSXElement(r){if(i(r.openingElement.name)!=="ScrollView")return;for(const e of r.children){if(e.type!=="JSXExpressionContainer")continue;const t=e.expression;if(t.type!=="CallExpression")continue;const{callee:n}=t;if(n.type!=="MemberExpression")continue;if(n.property.type!=="Identifier"||n.property.name!=="map")continue;o.report({node:e,message:"Render this collection with `LegendList` from '@legendapp/list/react-native', passing the array as `data` and the mapped body as `renderItem` — a ScrollView mounts every child up front."});return}}})});
@@ -1 +1 @@
1
- import{attributeName as i,gate as o,problem as r}from"../../../lib/ast.js";import{attributesOf as a,isListElement as s,LIST as l}from"./shared.js";const m=new Set(["masonry","optimizeItemArrangement","inverted","onBlankArea","disableAutoLayout","CellRendererComponent"]);export const noUnsupportedProps=r("Disallow FlashList and FlatList props that Legend List v3 does not have. It ignores them rather than rejecting them, so the feature looks broken.",{createOnce:(e)=>({before:()=>o(e,l),JSXElement(n){if(!s(n))return;for(const t of a(n)){if(t.type!=="JSXAttribute")continue;if(!m.has(i(t)))continue;e.report({node:t,message:"Remove this prop: Legend List v3 has no such prop and ignores it silently rather than rejecting it. Build inverted chat lists from `maintainScrollAtEnd` / `initialScrollAtEnd` / `maintainVisibleContentPosition` instead."})}}})});
1
+ import{attributeName as o,gate as i,problem as r}from"../../../lib/ast.js";import{attributesOf as s,isListElement as a,LIST as l}from"./shared.js";const m=new Set(["masonry","optimizeItemArrangement","inverted","onBlankArea","disableAutoLayout","CellRendererComponent"]);export const noUnsupportedProps=r("Disallow FlashList and FlatList props that Legend List v3 does not have. It ignores them rather than rejecting them, so the feature looks broken.",{createOnce:(e)=>({before:()=>i(e,l),JSXElement(n){if(!a(n))return;for(const t of s(n)){if(t.type!=="JSXAttribute")continue;if(!m.has(o(t)))continue;e.report({node:t,message:"Remove this prop — Legend List v3 ignores it silently. Inverted chat lists use `maintainScrollAtEnd` / `initialScrollAtEnd` / `maintainVisibleContentPosition`."})}}})});
@@ -1 +1 @@
1
- import{gate as i,problem as s}from"../../../lib/ast.js";import{attributeNamed as r,hasSpread as a,isListElement as o,LIST as n}from"./shared.js";export const requiredProps=s("Require `keyExtractor` and an explicit `recycleItems` on a Legend List. Without a key extractor the list keys its rows by index.",{createOnce:(t)=>({before:()=>i(t,n),JSXElement(e){if(!o(e)||a(e))return;if(!r(e,"keyExtractor"))t.report({node:e.openingElement,message:"Add `keyExtractor={item => item.id}` returning a stable per-item identity. The index fallback pins cached measurements and recycled row state to a position, so a prepend leaves rows showing the previous row's data."});if(!r(e,"recycleItems"))t.report({node:e.openingElement,message:"Add `recycleItems={true}` — recycling is where most of the list's native speed comes from. If a row genuinely cannot be recycled, pass `recycleItems={false}` and state which part of the row required it."})}})});
1
+ import{gate as i,problem as s}from"../../../lib/ast.js";import{attributeNamed as r,hasSpread as a,isListElement as o,LIST as n}from"./shared.js";export const requiredProps=s("Require `keyExtractor` and an explicit `recycleItems` on a Legend List. Without a key extractor the list keys its rows by index.",{createOnce:(t)=>({before:()=>i(t,n),JSXElement(e){if(!o(e)||a(e))return;if(!r(e,"keyExtractor"))t.report({node:e.openingElement,message:"Add `keyExtractor={item => item.id}` returning a stable per-item identity — the index fallback pins cached measurements to a position, so a prepend shows the wrong data."});if(!r(e,"recycleItems"))t.report({node:e.openingElement,message:"Add `recycleItems={true}` — recycling is where most of the list's native speed comes from. If a row genuinely cannot recycle, pass `recycleItems={false}` with the reason."})}})});