@ashstack/lint 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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, 258 on React Native, 75 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 -->117 rules on plain TypeScript, 201 with React, 258 on React Native, 77 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
 
@@ -89,7 +89,7 @@ Import bans follow detection only; disable one by rule name.
89
89
 
90
90
  ## Opt-in rules
91
91
 
92
- <!-- opt-in -->Off by default, since they need a team decision first: `@ashstack/core/use-design-system` and `@ashstack/core/components-tsx-only`.<!-- /opt-in -->
92
+ <!-- opt-in -->Off by default, since they need a team decision first: `@ashstack/react/prefer-design-system` and `@ashstack/react/components-tsx-only`.<!-- /opt-in -->
93
93
 
94
94
  ## Rule reference
95
95
 
@@ -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","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{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{maxComplexity as n}from"./max-complexity.js";import{maxLines as m}from"./max-lines.js";import{noComments as e}from"./no-comments.js";import{noNamingConvention as i}from"./no-naming-convention.js";import{noPackedCondition as t}from"./no-packed-condition.js";export default o({meta:{name:"@ashstack/core"},url:import.meta.url,docsWhen:"always on via `core()` and every entry above it",rules:{"no-comments":e,"no-naming-convention":i,"no-packed-condition":t,"max-lines":m,"max-complexity":n}});
@@ -0,0 +1 @@
1
+ import{ancestors as m,FUNCTION_TYPES as f,problem as h}from"../../../lib/ast.js";const p=15,S=new Set(["IfStatement","SwitchStatement","ForStatement","ForInStatement","ForOfStatement","WhileStatement","DoWhileStatement","CatchClause","ConditionalExpression"]),x=new Set(["&&","||"]),l=(t)=>{if(t.type!=="IfStatement")return!1;const{parent:o}=t;return o.type==="IfStatement"&&o.alternate===t},E=(t)=>t!==null&&t!==void 0&&t.type!=="IfStatement",u=(t)=>{let o=0;for(const e of m(t)){if(f.has(e.type))return o;if(S.has(e.type)&&!l(e))o+=1}return o};export const maxComplexity=h("Cap the cognitive complexity of one function: its branches, weighted by nesting depth. An `if`, `switch`, loop, `catch` or ternary costs a point plus one per enclosing structure. An `else` or a run of `&&`/`||` costs a flat point, `??` costs nothing. A nested function is scored on its own. Replaces the built-in `complexity`, which reads a 20-case `switch` as 20 decisions.",{meta:{schema:[{type:"integer",minimum:1,default:p,description:"Highest cognitive complexity one function may reach."}]},createOnce(t){let o=p,e=0;const r=[],c=()=>{r.push(e);e=0},a=(n)=>{const i=e;e=r.pop()??0;if(i<=o)return;t.report({node:n,message:`Extract the deepest branches into functions of their own — ${i} cognitive complexity, past ${o}. Depth is what costs: an \`if\` two structures in counts 3.`})},s=(n)=>{e+=1+u(n)};return{before(){o=typeof t.options[0]==="number"?t.options[0]:p;e=0;r.length=0;return!0},FunctionDeclaration:c,FunctionExpression:c,ArrowFunctionExpression:c,"FunctionDeclaration:exit":a,"FunctionExpression:exit":a,"ArrowFunctionExpression:exit":a,IfStatement(n){e+=l(n)?1:1+u(n);if(E(n.alternate))e+=1},SwitchStatement:s,ForStatement:s,ForInStatement:s,ForOfStatement:s,WhileStatement:s,DoWhileStatement:s,CatchClause:s,ConditionalExpression:s,LogicalExpression(n){if(!x.has(n.operator))return;const{parent:i}=n;if(i.type==="LogicalExpression"&&i.operator===n.operator)return;e+=1}}}});
@@ -0,0 +1 @@
1
+ import{isMemberCall as c,receiverName as d}from"../../../lib/ast.js";const i=300,m=new Set(["StyleSheet","stylex"]),f=10,u=32,h=(e)=>c(e,"create")&&m.has(d(e)??""),p=(e,r)=>{const t=new Uint8Array(e.length);for(const[s,o]of r)t.fill(1,s,o);let l=0,n=!1;for(let s=0;s<e.length;s++){const o=e.charCodeAt(s);if(o===f){if(n)l++;n=!1}else if(o>u&&t[s]===0)n=!0}return n?l+1:l};export const maxLines={meta:{type:"problem",docs:{description:"Cap the lines of code in one file. Blank lines, comments and the style tables `StyleSheet.create` and `stylex.create` build are not counted, so a colocated stylesheet costs the file nothing. Replaces the built-in `max-lines`, which counts all of them."},schema:[{type:"integer",minimum:1,default:i,description:"Highest number of code lines one file may hold."}]},createOnce(e){let r=[];return{before(){r=[];return!0},CallExpression(t){if(h(t))r.push([t.start,t.end])},"Program:exit"(t){const l=t.body.at(-1);if(!l)return;const n=typeof e.options[0]==="number"?e.options[0]:i,s=e.sourceCode.getAllComments().map((a)=>[a.start,a.end]),o=p(e.sourceCode.getText(),[...r,...s]);if(o<=n)return;e.report({node:l,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 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
+ import{optionsOf as v}from"../../../lib/ast.js";const E=["@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"]),k=new Set(["Shebang","Hashbang"]),u=10,f=120,p=2,A="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.",C=" `// 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 comment hatch closed: nothing but a tooling directive survives here. A `memo` worth keeping goes behind `// oxlint-disable-next-line @ashstack/react-native/no-manual-memo -- <the real cost>`, which is a directive and states the cost in the same line.",x=(e)=>A+(e?C:_),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)=>E.some((t)=>e.startsWith(t)),I=(e)=>m.has(e.type)&&e.value.startsWith("*"),R=(e)=>{const t=[];for(const o of e.sourceCode.getAllComments()){if(k.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},w=(e,t)=>{if(!t)return[];const o=D.exec(e.body)?.groups;if(!o?.kind||!o.fact)return[];const l=o.kind.toLowerCase()==="why"?"why":"what";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()==="",g=(e)=>e.escapeHatch!==!1;export const noComments={meta:{type:"problem",docs:{description:"Disallow every comment that is not a `// what:` fact, a `// why:` marker, or a tooling directive. The diagnostic names the refactor that removes it. A `// why:` line is what `@ashstack/react-native/no-manual-memo` requires above a kept `memo`, so no budget counts it; `escapeHatch: false` closes even that."},schema:[{type:"object",properties:{jsdoc:{enum:["allow","report"],default:"report",description:'With `"allow"`, a `/** */` block attached to the declaration below it is kept. A floating one still reports.'},escapeHatch:{type:"boolean",default:!0,description:"With `false`, nothing but a tooling directive survives: the `// what:` hatch, `// why:` markers and JSDoc all report, whatever `jsdoc` says."},budget:{type:"integer",minimum:0,default:p,description:"How many `// what:` lines one file may keep. `// why:` lines never count against it."}},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})},S=(n,r)=>{const{text:s}=e.sourceCode,i=g(r);for(const a of n){const{comment:c}=a;if(w(a,i).length>0)continue;if(!i||r.jsdoc!=="allow"||!I(c))h(c,x(i));else if(!t.has(L(s,c.end)))h(c,H)}},T=(n,r)=>{const s=[];for(const i of n)for(const a of w(i,r)){const c=N(a.comment,a.body,a.fact);if(c===null)s.push(a);else h(a.comment,c)}return s},b=(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)}},y=(n,r)=>{const s=r.budget??p,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=v(e,{}),r=R(e);S(r,n);const s=T(r,g(n));b(s);y(s,n)}}}};
@@ -1 +1 @@
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
+ 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."},schema:[{type:"integer",minimum:1,default:o,description:"Highest number of operators one condition may pack."}]},createOnce(t){let i=o;const n=(e)=>{if(!e)return;const a=r(e);if(a<=i)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 ${a} operators, past the ${i} a reader takes in.`})};return{before(){i=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
- const e=(t)=>new URL(t,import.meta.url).href;export const coreRegistry=[{meta:{name:"@ashstack/core"},url:e("../core/rules/core/index.js"),rules:{"no-comments":{},"no-naming-convention":{},"no-packed-condition":{},"use-design-system":{defaultOff:!0},"components-tsx-only":{defaultOff:!0},"hoist-intl":{}}},{meta:{name:"@ashstack/zod"},url:e("../core/rules/zod/index.js"),option:"zod",packages:["zod"],rules:{"prefer-enum":{}}}],reactRegistry=[{meta:{name:"@ashstack/react"},url:e("../react/rules/react/index.js"),rules:{"no-unlabeled-icon-button":{},"no-svg-without-title":{}}},{meta:{name:"@ashstack/query"},url:e("../react/rules/query/index.js"),option:"query",packages:["@tanstack/react-query"],rules:{"no-inline-keys":{},"no-deprecated-filters":{},"require-destructured-hooks":{},"no-fetch-in-query-fn":{},"next-page-param-undefined":{}}},{meta:{name:"@ashstack/zustand"},url:e("../react/rules/zustand/index.js"),option:"zustand",packages:["zustand"],rules:{"require-selector":{}}},{meta:{name:"@ashstack/i18n"},url:e("../react/rules/i18n/index.js"),option:"i18n",packages:["i18next","react-i18next","@lingui/core","react-intl","use-intl","next-intl","expo-localization"],rules:{"no-bare-text":{},"no-bare-attrs":{},"no-bare-toast":{}}},{meta:{name:"@ashstack/tailwind"},url:e("../react/rules/tailwind/index.js"),option:"tailwind",packages:["tailwindcss"],rules:{"prefer-cn":{},"use-logical-classes":{}}},{meta:{name:"@ashstack/tanstack-router"},url:e("../react/rules/tanstack-router/index.js"),option:"tanstackRouter",packages:["@tanstack/react-router"],rules:{"require-selector":{},"no-search-casts":{}}}],reactNativeRegistry=[{meta:{name:"@ashstack/react-native"},url:e("../react-native/rules/react-native/index.js"),restrictedImports:{paths:[{name:"react",importNames:["forwardRef"],message:"Accept `ref` as a regular prop and delete the `forwardRef` wrapper — React 19 passes `ref` like any other prop."},{name:"react",importNames:["useContext"],message:"Call `use(Context)` instead of `useContext(Context)`; React 19 replaces the hook with it."}],patterns:[{group:["expo-linear-gradient","react-native-linear-gradient"],message:"Apply the gradient through the `experimental_backgroundImage` style property instead of a gradient component."}]},rules:{"no-keyboard-will-events":{},"no-scroll-position-state":{},"no-conditional-style-array":{},"no-leaked-render":{},"no-rn-image-network-source":{},"no-redundant-view-nesting":{},"no-rn-namespace-import":{},"no-unlabeled-icon-pressable":{},"hoist-stateless-function":{},"no-manual-memo":{},"no-dynamic-import":{}}},{meta:{name:"@ashstack/unistyles"},url:e("../react-native/rules/unistyles/index.js"),option:"unistyles",packages:["react-native-unistyles"],restrictedImports:{paths:[{name:"react-native",importNames:["StyleSheet"],message:"Import `StyleSheet` from `react-native-unistyles` instead of `react-native`."},{name:"react-native",importNames:["useWindowDimensions","Dimensions"],message:"Read screen size from `rt.screen` inside `StyleSheet.create`, or `UnistylesRuntime.screen` outside it, instead of the `Dimensions` APIs."},{name:"react-native",importNames:["SafeAreaView"],message:'Set `contentInsetAdjustmentBehavior="automatic"` on the scroll view, or apply `rt.insets` from `react-native-unistyles`, instead of `SafeAreaView`.'},{name:"react-native-safe-area-context",importNames:["useSafeAreaInsets","useSafeAreaFrame","SafeAreaView"],message:"Read safe-area values from `rt.insets`, or `UnistylesRuntime.insets` outside a stylesheet, in `react-native-unistyles`."}]},rules:{"animated-theme":{},"content-container":{},"in-sheet":{},insets:{},"no-hardcoded-color":{},"no-hardcoded-spacing":{},"no-margin":{},"no-paramless-dynamic-function":{},"no-style-spread":{},"no-unused-styles":{},"rtl-style-call":{},"theme-screen-component":{},"theme-style-attr":{}}},{meta:{name:"@ashstack/legend-list"},url:e("../react-native/rules/legend-list/index.js"),option:"legendList",packages:["@legendapp/list"],restrictedImports:{paths:[{name:"react-native",importNames:["FlatList","SectionList","VirtualizedList"],message:"Render this list with `LegendList` from `@legendapp/list/react-native`; a `ScrollView` plus `map` is fine for fewer than 20 static items."},{name:"@shopify/flash-list",message:"Import `LegendList` from `@legendapp/list/react-native` — Legend List replaced FlashList in this stack."}],patterns:[{group:["@legendapp/list"],message:"Import from the platform entrypoint `@legendapp/list/react-native`, not the package root."}]},rules:{"required-props":{},"no-index-key-extractor":{},"no-remount-key":{},"no-inline-render-item-props":{},"no-mixed-children":{},"no-flex-in-content-container":{},"typed-items-need-item-type":{},"no-scrollview-map":{},"no-unsupported-props":{}}},{meta:{name:"@ashstack/legend-state"},url:e("../react-native/rules/legend-state/index.js"),option:"legendState",packages:["@legendapp/state"],restrictedImports:{paths:[{name:"@legendapp/state/react",importNames:["use$","useSelector"],message:"Use `useValue` from `@legendapp/state/react`; `use$` and `useSelector` are deprecated."}]},rules:{"no-assignment":{},naming:{},"no-nested-observable":{},"no-react-mirror":{},"no-untracked-get-in-jsx":{},"no-peek-in-selector":{},"no-object-selector":{},"no-observable-in-component":{}}},{meta:{name:"@ashstack/reanimated"},url:e("../react-native/rules/reanimated/index.js"),option:"reanimated",packages:["react-native-reanimated"],restrictedImports:{paths:[{name:"react-native",importNames:["Animated"],message:"Use `react-native-reanimated` instead of React Native's `Animated` API."},{name:"react-native-reanimated",importNames:["runOnJS","runOnUI"],message:"Use `scheduleOnRN` and `scheduleOnUI` from `react-native-worklets`; `runOnJS` and `runOnUI` are deprecated."},{name:"react-native-worklets",importNames:["runOnJS","runOnUI"],message:"Use `scheduleOnRN` and `scheduleOnUI` from `react-native-worklets`; `runOnJS` and `runOnUI` are deprecated."}]},rules:{"animated-reaction-safety":{},"animated-style-needs-animated-component":{},"animated-updater-purity":{},"gpu-properties-only":{},"hoist-layout-animation-builder":{},"interpolate-needs-clamp":{},"no-shared-value-dot-value":{},"no-react-state-from-continuous-worklet":{},"prefer-lazy-shared-value-initializer":{},"schedule-on-rn-scope":{},"shared-value-usage":{}}},{meta:{name:"@ashstack/turbo-image"},url:e("../react-native/rules/turbo-image/index.js"),option:"turboImage",packages:["react-native-turbo-image"],rules:{"require-resize":{},"require-cache-policy":{}}},{meta:{name:"@ashstack/skia"},url:e("../react-native/rules/skia/index.js"),option:"skia",packages:["@shopify/react-native-skia"],restrictedImports:{paths:[{name:"@shopify/react-native-skia",importNames:["usePathValue","usePathInterpolation"],message:"Keep one stable `SkPath` buffer and mutate it inside `useDerivedValue`, with consumers reading the driving shared value."}]},rules:{"canvas-opaque":{}}},{meta:{name:"@ashstack/keyboard"},url:e("../react-native/rules/keyboard/index.js"),option:"keyboard",packages:["react-native-keyboard-controller"],restrictedImports:{paths:[{name:"react-native",importNames:["KeyboardAvoidingView"],message:"Import `KeyboardAvoidingView` from `react-native-keyboard-controller` — same props, tracked per frame. For a scrolling form, its `KeyboardAwareScrollView`."}]},rules:{}}];
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":{},"max-lines":{},"max-complexity":{}}},{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":{},"hoist-intl":{},"prefer-design-system":{defaultOff:!0},"components-tsx-only":{defaultOff:!0}}},{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,11 +2,13 @@ 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/max-lines" | "@ashstack/core/max-complexity" | "@ashstack/zod/prefer-enum";
6
6
  declare module "oxlint" {
7
7
  interface DummyRuleMap {
8
8
  /**
9
- * 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.
9
+ * Disallow every comment that is not a `// what:` fact, a `// why:` marker, or a tooling directive. The diagnostic names the refactor that removes it. A `// why:` line is what `@ashstack/react-native/no-manual-memo` requires above a kept `memo`, so no budget counts it; `escapeHatch: false` closes even that.
10
+ *
11
+ * **Default: on.**
10
12
  *
11
13
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackcoreno-comments
12
14
  */
@@ -20,54 +22,40 @@ declare module "oxlint" {
20
22
  /**
21
23
  * Require a variable, object property, type member or enum member to use one of the casings allowed for its kind.
22
24
  *
25
+ * **Default: on.**
26
+ *
23
27
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackcoreno-naming-convention
24
28
  */
25
29
  "@ashstack/core/no-naming-convention"?: RuleSetting;
26
30
  /**
27
- * 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.
31
+ * Require a condition to be split into named booleans once it holds too many boolean operators and comparisons.
32
+ *
33
+ * **Default: on.**
28
34
  *
29
35
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackcoreno-packed-condition
30
36
  */
31
37
  "@ashstack/core/no-packed-condition"?: RuleSetting<[number]>;
32
38
  /**
33
- * 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.
39
+ * Cap the lines of code in one file. Blank lines, comments and the style tables `StyleSheet.create` and `stylex.create` build are not counted, so a colocated stylesheet costs the file nothing. Replaces the built-in `max-lines`, which counts all of them.
34
40
  *
35
- * Off by default — opt in per project.
41
+ * **Default: on.**
36
42
  *
37
- * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackcoreuse-design-system
43
+ * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackcoremax-lines
38
44
  */
39
- "@ashstack/core/use-design-system"?: RuleSetting<[
40
- {
41
- dir?: string;
42
- alias?: string;
43
- use?: Record<string, string | Array<string> | {
44
- replaces: string | Array<string>;
45
- from?: string;
46
- path?: string;
47
- reason?: string;
48
- }>;
49
- exempt?: Array<string>;
50
- }
51
- ]>;
45
+ "@ashstack/core/max-lines"?: RuleSetting<[number]>;
52
46
  /**
53
- * Require every file under the components directory to render JSX or be a re-export barrel. `dir` says which directory, defaulting to `src/components`.
47
+ * Cap the cognitive complexity of one function: its branches, weighted by nesting depth. An `if`, `switch`, loop, `catch` or ternary costs a point plus one per enclosing structure. An `else` or a run of `&&`/`||` costs a flat point, `??` costs nothing. A nested function is scored on its own. Replaces the built-in `complexity`, which reads a 20-case `switch` as 20 decisions.
54
48
  *
55
- * Off by default — opt in per project.
49
+ * **Default: on.**
56
50
  *
57
- * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackcorecomponents-tsx-only
51
+ * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackcoremax-complexity
58
52
  */
59
- "@ashstack/core/components-tsx-only"?: RuleSetting<[{
60
- dir?: string;
61
- }]>;
62
- /**
63
- * Disallow `new Intl.*` inside a function that renders JSX, unless the call already sits in `useMemo` or `useCallback`.
64
- *
65
- * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackcorehoist-intl
66
- */
67
- "@ashstack/core/hoist-intl"?: RuleSetting;
53
+ "@ashstack/core/max-complexity"?: RuleSetting<[number]>;
68
54
  /**
69
55
  * Disallow `z.nativeEnum()` and any `z.union()` whose members are all `z.literal()` strings.
70
56
  *
57
+ * **Default: on**, when `zod` is a dependency.
58
+ *
71
59
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackzodprefer-enum
72
60
  */
73
61
  "@ashstack/zod/prefer-enum"?: RuleSetting;
@@ -8,59 +8,79 @@ declare module "oxlint" {
8
8
  /**
9
9
  * Disallow the `keyboardWill*` event names. They are iOS-only, so on Android the listener registers and never fires.
10
10
  *
11
+ * **Default: on.**
12
+ *
11
13
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreact-nativeno-keyboard-will-events
12
14
  */
13
15
  "@ashstack/react-native/no-keyboard-will-events"?: RuleSetting;
14
16
  /**
15
17
  * Disallow a React state setter inside a scroll handler prop. Scroll fires every frame, and so would the re-render.
16
18
  *
19
+ * **Default: on.**
20
+ *
17
21
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreact-nativeno-scroll-position-state
18
22
  */
19
23
  "@ashstack/react-native/no-scroll-position-state"?: RuleSetting;
20
24
  /**
21
25
  * Disallow conditional and logical entries inside a JSX `style` array. A falsy entry leaves a hole that breaks the Unistyles C++ proxy.
22
26
  *
27
+ * **Default: on.**
28
+ *
23
29
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreact-nativeno-conditional-style-array
24
30
  */
25
31
  "@ashstack/react-native/no-conditional-style-array"?: RuleSetting;
26
32
  /**
27
33
  * Disallow a `&&` guard in JSX on a value this rule can see is a number or a string — a `.length`, a `.size`, arithmetic, a literal. The falsy left operand leaks into the output, and a bare `0` crashes React Native with "Text strings must be rendered within a <Text> component".
28
34
  *
35
+ * **Default: on.**
36
+ *
29
37
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreact-nativeno-leaked-render
30
38
  */
31
39
  "@ashstack/react-native/no-leaked-render"?: RuleSetting;
32
40
  /**
33
41
  * Disallow a react-native `<Image>` with a network `{ uri }` source. It has no disk cache and no decode sizing, so the image re-downloads at full size on every cold start.
34
42
  *
43
+ * **Default: on.**
44
+ *
35
45
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreact-nativeno-rn-image-network-source
36
46
  */
37
47
  "@ashstack/react-native/no-rn-image-network-source"?: RuleSetting;
38
48
  /**
39
49
  * Disallow a View or Animated.View that wraps an identical view when neither carries anything but a style. Every extra host view is a real node in the native tree.
40
50
  *
51
+ * **Default: on.**
52
+ *
41
53
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreact-nativeno-redundant-view-nesting
42
54
  */
43
55
  "@ashstack/react-native/no-redundant-view-nesting"?: RuleSetting;
44
56
  /**
45
57
  * Disallow a namespace import of react-native and a re-export of its `Platform`. Both defeat Metro's platform shaking, so dead `Platform.OS` branches ship in both bundles.
46
58
  *
59
+ * **Default: on.**
60
+ *
47
61
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreact-nativeno-rn-namespace-import
48
62
  */
49
63
  "@ashstack/react-native/no-rn-namespace-import"?: RuleSetting;
50
64
  /**
51
65
  * Require an accessible name on an icon-only touchable or an icon-only Expo UI `<Button>`. Without a label, hint or visible text, a screen reader cannot reach the control.
52
66
  *
67
+ * **Default: on.**
68
+ *
53
69
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreact-nativeno-unlabeled-icon-pressable
54
70
  */
55
71
  "@ashstack/react-native/no-unlabeled-icon-pressable"?: RuleSetting;
56
72
  /**
57
73
  * Require module scope for a non-component function that reads nothing from the component around it. Out there it is created once, keeps a stable identity, and can be tested without rendering.
58
74
  *
75
+ * **Default: on.**
76
+ *
59
77
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreact-nativehoist-stateless-function
60
78
  */
61
79
  "@ashstack/react-native/hoist-stateless-function"?: RuleSetting;
62
80
  /**
63
- * Require a `// why:` line above every kept `useMemo`, `useCallback` and `memo`. The React Compiler memoises on a best-effort basis, not a guarantee, so a memo is allowed where the cost is real: something rendered per list row, or a computation measured as heavy. The `// why:` line names which of the two applies. `@ashstack/core/no-comments` keeps that line and never counts it against its `budget`, so both rules run together. Assumes the compiler is on — pass `reactCompiler: false` to the entry and this rule turns off, since without it a hand-written memo is the only memo there is.
81
+ * Require a `// why:` line above every kept `useMemo`, `useCallback` and `memo`. The React Compiler memoises on a best-effort basis, so a memo is allowed where the cost is real: a list row, or a computation measured as heavy. Turns off with `reactCompiler: false`.
82
+ *
83
+ * **Default: on.**
64
84
  *
65
85
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreact-nativeno-manual-memo
66
86
  */
@@ -68,270 +88,360 @@ declare module "oxlint" {
68
88
  /**
69
89
  * Disallow dynamic `import()` outside a `React.lazy` or `dynamic` wrapper. Metro inlines it into the same bundle, so nothing is deferred and the module is hidden from the typechecker.
70
90
  *
91
+ * **Default: on.**
92
+ *
71
93
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreact-nativeno-dynamic-import
72
94
  */
73
95
  "@ashstack/react-native/no-dynamic-import"?: RuleSetting;
74
96
  /**
75
97
  * A Reanimated worklet hook cannot see `useUnistyles()` theme changes on the UI thread. Read the shared value from `useAnimatedTheme()` there instead.
76
98
  *
99
+ * **Default: on**, when `react-native-unistyles` is a dependency.
100
+ *
77
101
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackunistylesanimated-theme
78
102
  */
79
103
  "@ashstack/unistyles/animated-theme"?: RuleSetting;
80
104
  /**
81
105
  * A raw component never subscribes its `contentContainerStyle` to theme or `rt` updates. Wrap the component with `withUnistyles` when the style depends on either.
82
106
  *
107
+ * **Default: on**, when `react-native-unistyles` is a dependency.
108
+ *
83
109
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackunistylescontent-container
84
110
  */
85
111
  "@ashstack/unistyles/content-container"?: RuleSetting;
86
112
  /**
87
113
  * Inside `StyleSheet.create`, require `rt` and theme values rather than Dimensions, PixelRatio, Appearance, I18nManager, StatusBar or `UnistylesRuntime`. It also covers logical spacing, `boxShadow`, `borderCurve` and a redundant `as const`.
88
114
  *
115
+ * **Default: on**, when `react-native-unistyles` is a dependency.
116
+ *
89
117
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackunistylesin-sheet
90
118
  */
91
119
  "@ashstack/unistyles/in-sheet"?: RuleSetting;
92
120
  /**
93
121
  * Disallow passing `useSafeAreaInsets()` values into a dynamic style function or an inline JSX style object. Read `rt.insets` inside `StyleSheet.create` instead.
94
122
  *
123
+ * **Default: on**, when `react-native-unistyles` is a dependency.
124
+ *
95
125
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackunistylesinsets
96
126
  */
97
127
  "@ashstack/unistyles/insets"?: RuleSetting;
98
128
  /**
99
129
  * Disallow hex and CSS-function colors inside `StyleSheet.create`. A raw color skips dark mode and never changes with the theme.
100
130
  *
131
+ * **Default: on**, when `react-native-unistyles` is a dependency.
132
+ *
101
133
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackunistylesno-hardcoded-color
102
134
  */
103
135
  "@ashstack/unistyles/no-hardcoded-color"?: RuleSetting;
104
136
  /**
105
137
  * Require `theme.spacing` or `theme.sizing.scale` for spacing, radius and type values inside `StyleSheet.create`, instead of raw numbers.
106
138
  *
139
+ * **Default: on**, when `react-native-unistyles` is a dependency.
140
+ *
107
141
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackunistylesno-hardcoded-spacing
108
142
  */
109
143
  "@ashstack/unistyles/no-hardcoded-spacing"?: RuleSetting;
110
144
  /**
111
145
  * Disallow non-negative `margin` inside `StyleSheet.create`; `gap` on the parent or `padding` on the element spaces children without leaving a hole when one is removed.
112
146
  *
147
+ * **Default: on**, when `react-native-unistyles` is a dependency.
148
+ *
113
149
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackunistylesno-margin
114
150
  */
115
151
  "@ashstack/unistyles/no-margin"?: RuleSetting;
116
152
  /**
117
153
  * 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
154
  *
155
+ * **Default: on**, when `react-native-unistyles` is a dependency.
156
+ *
119
157
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackunistylesno-paramless-dynamic-function
120
158
  */
121
159
  "@ashstack/unistyles/no-paramless-dynamic-function"?: RuleSetting;
122
160
  /**
123
161
  * 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.
124
162
  *
163
+ * **Default: on**, when `react-native-unistyles` is a dependency.
164
+ *
125
165
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackunistylesno-style-spread
126
166
  */
127
167
  "@ashstack/unistyles/no-style-spread"?: RuleSetting;
128
168
  /**
129
169
  * Disallow stylesheet keys that nothing in the file reads. A computed key, a computed read, or a sheet that leaves the module skips the whole file.
130
170
  *
171
+ * **Default: on**, when `react-native-unistyles` is a dependency.
172
+ *
131
173
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackunistylesno-unused-styles
132
174
  */
133
175
  "@ashstack/unistyles/no-unused-styles"?: RuleSetting;
134
176
  /**
135
177
  * Disallow passing `I18nManager.isRTL` from JSX into a dynamic style function. Unistyles tracks the dependency itself once the style reads `rt.rtl`.
136
178
  *
179
+ * **Default: on**, when `react-native-unistyles` is a dependency.
180
+ *
137
181
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackunistylesrtl-style-call
138
182
  */
139
183
  "@ashstack/unistyles/rtl-style-call"?: RuleSetting;
140
184
  /**
141
185
  * `theme.screen.*` is a snapshot taken at module initialization, so a component that reads it never sees the current size. `useUnistyles().rt.screen` and `useWindowDimensions` do.
142
186
  *
187
+ * **Default: on**, when `react-native-unistyles` is a dependency.
188
+ *
143
189
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackunistylestheme-screen-component
144
190
  */
145
191
  "@ashstack/unistyles/theme-screen-component"?: RuleSetting;
146
192
  /**
147
193
  * Theme-dependent values belong in `StyleSheet.create`, not in a JSX `style` prop that reads a `useUnistyles()` theme.
148
194
  *
195
+ * **Default: on**, when `react-native-unistyles` is a dependency.
196
+ *
149
197
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackunistylestheme-style-attr
150
198
  */
151
199
  "@ashstack/unistyles/theme-style-attr"?: RuleSetting;
152
200
  /**
153
201
  * Require `keyExtractor` and an explicit `recycleItems` on a Legend List. Without a key extractor the list keys its rows by index.
154
202
  *
203
+ * **Default: on**, when `@legendapp/list` is a dependency.
204
+ *
155
205
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstacklegend-listrequired-props
156
206
  */
157
207
  "@ashstack/legend-list/required-props"?: RuleSetting;
158
208
  /**
159
209
  * 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.
160
210
  *
211
+ * **Default: on**, when `@legendapp/list` is a dependency.
212
+ *
161
213
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstacklegend-listno-index-key-extractor
162
214
  */
163
215
  "@ashstack/legend-list/no-index-key-extractor"?: RuleSetting;
164
216
  /**
165
217
  * Disallow `key` on a Legend List, which remounts on any key change and loses its measurements and scroll position. Pass `dataKey` instead.
166
218
  *
219
+ * **Default: on**, when `@legendapp/list` is a dependency.
220
+ *
167
221
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstacklegend-listno-remount-key
168
222
  */
169
223
  "@ashstack/legend-list/no-remount-key"?: RuleSetting;
170
224
  /**
171
225
  * 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.
172
226
  *
227
+ * **Default: on**, when `@legendapp/list` is a dependency.
228
+ *
173
229
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstacklegend-listno-inline-render-item-props
174
230
  */
175
231
  "@ashstack/legend-list/no-inline-render-item-props"?: RuleSetting;
176
232
  /**
177
233
  * 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.
178
234
  *
235
+ * **Default: on**, when `@legendapp/list` is a dependency.
236
+ *
179
237
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstacklegend-listno-mixed-children
180
238
  */
181
239
  "@ashstack/legend-list/no-mixed-children"?: RuleSetting;
182
240
  /**
183
241
  * 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.
184
242
  *
243
+ * **Default: on**, when `@legendapp/list` is a dependency.
244
+ *
185
245
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstacklegend-listno-flex-in-content-container
186
246
  */
187
247
  "@ashstack/legend-list/no-flex-in-content-container"?: RuleSetting;
188
248
  /**
189
249
  * Require `getItemType` when a row branches on `item.type`. Without it every layout shares one recycling pool and one size average.
190
250
  *
251
+ * **Default: on**, when `@legendapp/list` is a dependency.
252
+ *
191
253
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstacklegend-listtyped-items-need-item-type
192
254
  */
193
255
  "@ashstack/legend-list/typed-items-need-item-type"?: RuleSetting;
194
256
  /**
195
257
  * A `ScrollView` mounts every child up front, so a mapped collection does not belong in its children.
196
258
  *
259
+ * **Default: on**, when `@legendapp/list` is a dependency.
260
+ *
197
261
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstacklegend-listno-scrollview-map
198
262
  */
199
263
  "@ashstack/legend-list/no-scrollview-map"?: RuleSetting;
200
264
  /**
201
265
  * Disallow FlashList and FlatList props that Legend List v3 does not have. It ignores them rather than rejecting them, so the feature looks broken.
202
266
  *
267
+ * **Default: on**, when `@legendapp/list` is a dependency.
268
+ *
203
269
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstacklegend-listno-unsupported-props
204
270
  */
205
271
  "@ashstack/legend-list/no-unsupported-props"?: RuleSetting;
206
272
  /**
207
273
  * Write an observable with `.set(...)` or `.assign({...})`. Assigning or incrementing it with an operator is a silent no-op.
208
274
  *
275
+ * **Default: on**, when `@legendapp/state` is a dependency.
276
+ *
209
277
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstacklegend-stateno-assignment
210
278
  */
211
279
  "@ashstack/legend-state/no-assignment"?: RuleSetting;
212
280
  /**
213
281
  * A variable initialized from `observable()` or `useObservable()` needs a trailing `$`. The other rules in this module key off that suffix.
214
282
  *
283
+ * **Default: on**, when `@legendapp/state` is a dependency.
284
+ *
215
285
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstacklegend-statenaming
216
286
  */
217
287
  "@ashstack/legend-state/naming"?: RuleSetting;
218
288
  /**
219
289
  * Never pass an observable to `observable()` or `useObservable()`. The wrapper is a second node, and reads and writes on it never reach the original.
220
290
  *
291
+ * **Default: on**, when `@legendapp/state` is a dependency.
292
+ *
221
293
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstacklegend-stateno-nested-observable
222
294
  */
223
295
  "@ashstack/legend-state/no-nested-observable"?: RuleSetting;
224
296
  /**
225
297
  * Seeding `useState` from an observable's `get()` or `peek()` gives the value two owners. Read it with `useValue(...)` where it renders.
226
298
  *
299
+ * **Default: on**, when `@legendapp/state` is a dependency.
300
+ *
227
301
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstacklegend-stateno-react-mirror
228
302
  */
229
303
  "@ashstack/legend-state/no-react-mirror"?: RuleSetting;
230
304
  /**
231
305
  * A `get()` placed directly in a JSX expression container is a plain read. The value renders once and never updates.
232
306
  *
307
+ * **Default: on**, when `@legendapp/state` is a dependency.
308
+ *
233
309
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstacklegend-stateno-untracked-get-in-jsx
234
310
  */
235
311
  "@ashstack/legend-state/no-untracked-get-in-jsx"?: RuleSetting;
236
312
  /**
237
313
  * `peek()` never subscribes, so a selector or tracking callback that uses it never re-runs. Call `get()` there and keep `peek()` for handlers.
238
314
  *
315
+ * **Default: on**, when `@legendapp/state` is a dependency.
316
+ *
239
317
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstacklegend-stateno-peek-in-selector
240
318
  */
241
319
  "@ashstack/legend-state/no-peek-in-selector"?: RuleSetting;
242
320
  /**
243
321
  * A `useValue` selector that builds a new object or array returns a fresh identity every run. The component then re-renders on every store change.
244
322
  *
323
+ * **Default: on**, when `@legendapp/state` is a dependency.
324
+ *
245
325
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstacklegend-stateno-object-selector
246
326
  */
247
327
  "@ashstack/legend-state/no-object-selector"?: RuleSetting;
248
328
  /**
249
329
  * Calling `observable()` inside a component or hook makes a new observable on every render. Use `useObservable()` or a module-level store instead.
250
330
  *
331
+ * **Default: on**, when `@legendapp/state` is a dependency.
332
+ *
251
333
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstacklegend-stateno-observable-in-component
252
334
  */
253
335
  "@ashstack/legend-state/no-observable-in-component"?: RuleSetting;
254
336
  /**
255
337
  * A `useAnimatedReaction` result callback loops forever if it writes a shared value the prepare callback reads. Guard `scheduleOnRN` there on the current result differing from the previous one.
256
338
  *
339
+ * **Default: on**, when `react-native-reanimated` is a dependency.
340
+ *
257
341
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreanimatedanimated-reaction-safety
258
342
  */
259
343
  "@ashstack/reanimated/animated-reaction-safety"?: RuleSetting;
260
344
  /**
261
345
  * An animated style only takes effect on an `Animated.*` component. A plain element applies it once at mount and then never updates.
262
346
  *
347
+ * **Default: on**, when `react-native-reanimated` is a dependency.
348
+ *
263
349
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreanimatedanimated-style-needs-animated-component
264
350
  */
265
351
  "@ashstack/reanimated/animated-style-needs-animated-component"?: RuleSetting;
266
352
  /**
267
353
  * An updater passed to `useAnimatedStyle` or `useAnimatedProps` must stay pure, so it may not write a shared value or call `scheduleOnRN`.
268
354
  *
355
+ * **Default: on**, when `react-native-reanimated` is a dependency.
356
+ *
269
357
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreanimatedanimated-updater-purity
270
358
  */
271
359
  "@ashstack/reanimated/animated-updater-purity"?: RuleSetting;
272
360
  /**
273
361
  * Animate `transform` and `opacity` in `useAnimatedStyle` and `useAnimatedProps`. Layout properties such as `width` or `margin` recalculate layout every frame.
274
362
  *
363
+ * **Default: on**, when `react-native-reanimated` is a dependency.
364
+ *
275
365
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreanimatedgpu-properties-only
276
366
  */
277
367
  "@ashstack/reanimated/gpu-properties-only"?: RuleSetting;
278
368
  /**
279
369
  * A layout animation belongs at module scope, or inside a memo when it depends on component values. The `entering`/`exiting`/`layout` props otherwise rebuild it on every render.
280
370
  *
371
+ * **Default: on**, when `react-native-reanimated` is a dependency.
372
+ *
281
373
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreanimatedhoist-layout-animation-builder
282
374
  */
283
375
  "@ashstack/reanimated/hoist-layout-animation-builder"?: RuleSetting;
284
376
  /**
285
377
  * Give `interpolate()` a fourth argument that clamps. Without one, or with `EXTEND` or `IDENTITY`, the output keeps going past the ends of the input range.
286
378
  *
379
+ * **Default: on**, when `react-native-reanimated` is a dependency.
380
+ *
287
381
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreanimatedinterpolate-needs-clamp
288
382
  */
289
383
  "@ashstack/reanimated/interpolate-needs-clamp"?: RuleSetting;
290
384
  /**
291
385
  * Shared values are read with `.get()` and written with `.set(...)`, never through `.value`. React Compiler cannot track a `.value` access.
292
386
  *
387
+ * **Default: on**, when `react-native-reanimated` is a dependency.
388
+ *
293
389
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreanimatedno-shared-value-dot-value
294
390
  */
295
391
  "@ashstack/reanimated/no-shared-value-dot-value"?: RuleSetting;
296
392
  /**
297
393
  * A worklet that runs every frame must not send a React state setter through `scheduleOnRN`. That puts a Fabric commit on an animation frame.
298
394
  *
395
+ * **Default: on**, when `react-native-reanimated` is a dependency.
396
+ *
299
397
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreanimatedno-react-state-from-continuous-worklet
300
398
  */
301
399
  "@ashstack/reanimated/no-react-state-from-continuous-worklet"?: RuleSetting;
302
400
  /**
303
401
  * Pass a computed `useSharedValue` initial value as a function. An eager call runs on every render while only the first result is kept.
304
402
  *
403
+ * **Default: on**, when `react-native-reanimated` is a dependency.
404
+ *
305
405
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreanimatedprefer-lazy-shared-value-initializer
306
406
  */
307
407
  "@ashstack/reanimated/prefer-lazy-shared-value-initializer"?: RuleSetting;
308
408
  /**
309
409
  * `scheduleOnRN` takes a function declared in RN Runtime scope. An inline callback can end up created on the wrong runtime.
310
410
  *
411
+ * **Default: on**, when `react-native-reanimated` is a dependency.
412
+ *
311
413
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreanimatedschedule-on-rn-scope
312
414
  */
313
415
  "@ashstack/reanimated/schedule-on-rn-scope"?: RuleSetting;
314
416
  /**
315
417
  * Destructuring a shared value or mutating what its `get()` returned detaches it from Reanimated reactivity. Reading or writing one while JSX evaluates also makes render impure.
316
418
  *
419
+ * **Default: on**, when `react-native-reanimated` is a dependency.
420
+ *
317
421
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreanimatedshared-value-usage
318
422
  */
319
423
  "@ashstack/reanimated/shared-value-usage"?: RuleSetting;
320
424
  /**
321
425
  * Require `resize` on a TurboImage so the native decoder downsamples before the bitmap reaches memory. A full-resolution decode wastes tens of megabytes and stalls the first frame.
322
426
  *
427
+ * **Default: on**, when `react-native-turbo-image` is a dependency.
428
+ *
323
429
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackturbo-imagerequire-resize
324
430
  */
325
431
  "@ashstack/turbo-image/require-resize"?: RuleSetting;
326
432
  /**
327
433
  * Require `cachePolicy` on a TurboImage. Without one the image is re-fetched over the network on every cold start, so an already-scrolled feed costs its bandwidth again.
328
434
  *
435
+ * **Default: on**, when `react-native-turbo-image` is a dependency.
436
+ *
329
437
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackturbo-imagerequire-cache-policy
330
438
  */
331
439
  "@ashstack/turbo-image/require-cache-policy"?: RuleSetting;
332
440
  /**
333
441
  * Require an explicit `opaque` prop on a Skia `<Canvas>`. A fullscreen animated canvas wants it on; anything that needs transparency or a view transform wants it off.
334
442
  *
443
+ * **Default: on**, when `@shopify/react-native-skia` is a dependency.
444
+ *
335
445
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackskiacanvas-opaque
336
446
  */
337
447
  "@ashstack/skia/canvas-opaque"?: RuleSetting;
@@ -2,65 +2,123 @@ 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()` adds on top of the entry below it. */
5
- export type ReactRuleId = "@ashstack/react/no-unlabeled-icon-button" | "@ashstack/react/no-svg-without-title" | "@ashstack/query/no-inline-keys" | "@ashstack/query/no-deprecated-filters" | "@ashstack/query/require-destructured-hooks" | "@ashstack/query/no-fetch-in-query-fn" | "@ashstack/query/next-page-param-undefined" | "@ashstack/zustand/require-selector" | "@ashstack/i18n/no-bare-text" | "@ashstack/i18n/no-bare-attrs" | "@ashstack/i18n/no-bare-toast" | "@ashstack/tailwind/prefer-cn" | "@ashstack/tailwind/use-logical-classes" | "@ashstack/tanstack-router/require-selector" | "@ashstack/tanstack-router/no-search-casts" | "@ashstack/effects/no-derived-state" | "@ashstack/effects/no-chain-state-updates" | "@ashstack/effects/no-event-handler" | "@ashstack/effects/no-adjust-state-on-prop-change" | "@ashstack/effects/no-reset-all-state-on-prop-change" | "@ashstack/effects/no-pass-live-state-to-parent" | "@ashstack/effects/no-pass-data-to-parent" | "@ashstack/effects/no-external-store-subscription" | "@ashstack/effects/no-initialize-state";
5
+ export type ReactRuleId = "@ashstack/react/no-unlabeled-icon-button" | "@ashstack/react/no-svg-without-title" | "@ashstack/react/hoist-intl" | "@ashstack/react/prefer-design-system" | "@ashstack/react/components-tsx-only" | "@ashstack/query/no-inline-keys" | "@ashstack/query/no-deprecated-filters" | "@ashstack/query/require-destructured-hooks" | "@ashstack/query/no-fetch-in-query-fn" | "@ashstack/query/next-page-param-undefined" | "@ashstack/zustand/require-selector" | "@ashstack/i18n/no-bare-text" | "@ashstack/i18n/no-bare-attrs" | "@ashstack/i18n/no-bare-toast" | "@ashstack/tailwind/prefer-cn" | "@ashstack/tailwind/use-logical-classes" | "@ashstack/tanstack-router/require-selector" | "@ashstack/tanstack-router/no-search-casts" | "@ashstack/effects/no-derived-state" | "@ashstack/effects/no-chain-state-updates" | "@ashstack/effects/no-event-handler" | "@ashstack/effects/no-adjust-state-on-prop-change" | "@ashstack/effects/no-reset-all-state-on-prop-change" | "@ashstack/effects/no-pass-live-state-to-parent" | "@ashstack/effects/no-pass-data-to-parent" | "@ashstack/effects/no-external-store-subscription" | "@ashstack/effects/no-initialize-state";
6
6
  declare module "oxlint" {
7
7
  interface DummyRuleMap {
8
8
  /**
9
9
  * 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.
10
10
  *
11
+ * **Default: on.**
12
+ *
11
13
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreactno-unlabeled-icon-button
12
14
  */
13
15
  "@ashstack/react/no-unlabeled-icon-button"?: RuleSetting;
14
16
  /**
15
17
  * 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.
16
18
  *
19
+ * **Default: on.**
20
+ *
17
21
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreactno-svg-without-title
18
22
  */
19
23
  "@ashstack/react/no-svg-without-title"?: RuleSetting;
24
+ /**
25
+ * Disallow `new Intl.*` inside a function that renders JSX, unless the call already sits in `useMemo` or `useCallback`.
26
+ *
27
+ * **Default: on.**
28
+ *
29
+ * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreacthoist-intl
30
+ */
31
+ "@ashstack/react/hoist-intl"?: RuleSetting;
32
+ /**
33
+ * Disallow importing a raw primitive your design system already wraps. Wrappers come from scanning the design-system directory and from the `use` map. Files inside the design system are skipped.
34
+ *
35
+ * **Default: off.** Turn it on by id in your `rules` block.
36
+ *
37
+ * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreactprefer-design-system
38
+ */
39
+ "@ashstack/react/prefer-design-system"?: RuleSetting<[
40
+ {
41
+ dir?: string;
42
+ alias?: string;
43
+ use?: Record<string, string | Array<string> | {
44
+ replaces: string | Array<string>;
45
+ from?: string;
46
+ path?: string;
47
+ reason?: string;
48
+ }>;
49
+ exempt?: Array<string>;
50
+ }
51
+ ]>;
52
+ /**
53
+ * Require every file under the components directory to render JSX or be a re-export barrel.
54
+ *
55
+ * **Default: off.** Turn it on by id in your `rules` block.
56
+ *
57
+ * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackreactcomponents-tsx-only
58
+ */
59
+ "@ashstack/react/components-tsx-only"?: RuleSetting<[{
60
+ dir?: string;
61
+ }]>;
20
62
  /**
21
63
  * Disallow a query key written as an array literal at the call site instead of coming from a keys factory.
22
64
  *
65
+ * **Default: on**, when `@tanstack/react-query` is a dependency.
66
+ *
23
67
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackqueryno-inline-keys
24
68
  */
25
69
  "@ashstack/query/no-inline-keys"?: RuleSetting;
26
70
  /**
27
71
  * 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.
28
72
  *
73
+ * **Default: on**, when `@tanstack/react-query` is a dependency.
74
+ *
29
75
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackqueryno-deprecated-filters
30
76
  */
31
77
  "@ashstack/query/no-deprecated-filters"?: RuleSetting;
32
78
  /**
33
79
  * Require destructuring the result of a hook imported from an `@/api/*.queries` or `@/api/*.mutations` module.
34
80
  *
81
+ * **Default: on**, when `@tanstack/react-query` is a dependency.
82
+ *
35
83
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackqueryrequire-destructured-hooks
36
84
  */
37
85
  "@ashstack/query/require-destructured-hooks"?: RuleSetting;
38
86
  /**
39
87
  * Disallow a bare `fetch(` inside a `queryFn` or `mutationFn`.
40
88
  *
89
+ * **Default: on**, when `@tanstack/react-query` is a dependency.
90
+ *
41
91
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackqueryno-fetch-in-query-fn
42
92
  */
43
93
  "@ashstack/query/no-fetch-in-query-fn"?: RuleSetting;
44
94
  /**
45
95
  * Disallow `return null` in the body of a `getNextPageParam`.
46
96
  *
97
+ * **Default: on**, when `@tanstack/react-query` is a dependency.
98
+ *
47
99
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackquerynext-page-param-undefined
48
100
  */
49
101
  "@ashstack/query/next-page-param-undefined"?: RuleSetting;
50
102
  /**
51
103
  * Require a selector on a store hook, rather than no arguments or `undefined` in its place.
52
104
  *
105
+ * **Default: on**, when `zustand` is a dependency.
106
+ *
53
107
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackzustandrequire-selector
54
108
  */
55
109
  "@ashstack/zustand/require-selector"?: RuleSetting;
56
110
  /**
57
111
  * Disallow a JSX element that carries no attributes and whose single child is plain literal text.
58
112
  *
113
+ * **Default: on**, when `i18next`, `react-i18next`, `@lingui/core`, `react-intl`, `use-intl`, `next-intl` or `expo-localization` is a dependency.
114
+ *
59
115
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstacki18nno-bare-text
60
116
  */
61
117
  "@ashstack/i18n/no-bare-text"?: RuleSetting;
62
118
  /**
63
- * Disallow a plain string literal on a configurable list of user-visible JSX attributes, defaulting to placeholder, accessibilityLabel, accessibilityHint and title.
119
+ * Disallow a plain string literal on a user-visible JSX attribute.
120
+ *
121
+ * **Default: on**, when `i18next`, `react-i18next`, `@lingui/core`, `react-intl`, `use-intl`, `next-intl` or `expo-localization` is a dependency.
64
122
  *
65
123
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstacki18nno-bare-attrs
66
124
  */
@@ -70,84 +128,112 @@ declare module "oxlint" {
70
128
  /**
71
129
  * Disallow a `toast.*` call whose only argument is a string literal.
72
130
  *
131
+ * **Default: on**, when `i18next`, `react-i18next`, `@lingui/core`, `react-intl`, `use-intl`, `next-intl` or `expo-localization` is a dependency.
132
+ *
73
133
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstacki18nno-bare-toast
74
134
  */
75
135
  "@ashstack/i18n/no-bare-toast"?: RuleSetting;
76
136
  /**
77
137
  * 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.
78
138
  *
139
+ * **Default: on**, when `tailwindcss` is a dependency.
140
+ *
79
141
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstacktailwindprefer-cn
80
142
  */
81
143
  "@ashstack/tailwind/prefer-cn"?: RuleSetting;
82
144
  /**
83
145
  * 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.
84
146
  *
147
+ * **Default: on**, when `tailwindcss` is a dependency.
148
+ *
85
149
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstacktailwinduse-logical-classes
86
150
  */
87
151
  "@ashstack/tailwind/use-logical-classes"?: RuleSetting;
88
152
  /**
89
153
  * 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.
90
154
  *
155
+ * **Default: on**, when `@tanstack/react-router` is a dependency.
156
+ *
91
157
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstacktanstack-routerrequire-selector
92
158
  */
93
159
  "@ashstack/tanstack-router/require-selector"?: RuleSetting;
94
160
  /**
95
161
  * Disallow an `as` assertion on a `useSearch()` result or on `router.state.location.search`. The route's `validateSearch` schema is what supplies the type.
96
162
  *
163
+ * **Default: on**, when `@tanstack/react-router` is a dependency.
164
+ *
97
165
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstacktanstack-routerno-search-casts
98
166
  */
99
167
  "@ashstack/tanstack-router/no-search-casts"?: RuleSetting;
100
168
  /**
101
169
  * Disallow storing derived state in an effect. [Why](https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
102
170
  *
171
+ * **Default: on.**
172
+ *
103
173
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackeffectsno-derived-state
104
174
  */
105
175
  "@ashstack/effects/no-derived-state"?: RuleSetting;
106
176
  /**
107
177
  * Disallow chaining state changes in an effect. [Why](https://react.dev/learn/you-might-not-need-an-effect#chains-of-computations)
108
178
  *
179
+ * **Default: on.**
180
+ *
109
181
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackeffectsno-chain-state-updates
110
182
  */
111
183
  "@ashstack/effects/no-chain-state-updates"?: RuleSetting;
112
184
  /**
113
185
  * Disallow using state and an effect as an event handler. [Why](https://react.dev/learn/you-might-not-need-an-effect#sharing-logic-between-event-handlers)
114
186
  *
187
+ * **Default: on.**
188
+ *
115
189
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackeffectsno-event-handler
116
190
  */
117
191
  "@ashstack/effects/no-event-handler"?: RuleSetting;
118
192
  /**
119
193
  * Disallow adjusting state in an effect when a prop changes. [Why](https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes)
120
194
  *
195
+ * **Default: on.**
196
+ *
121
197
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackeffectsno-adjust-state-on-prop-change
122
198
  */
123
199
  "@ashstack/effects/no-adjust-state-on-prop-change"?: RuleSetting;
124
200
  /**
125
201
  * Disallow resetting all state in an effect when a prop changes. [Why](https://react.dev/learn/you-might-not-need-an-effect#resetting-all-state-when-a-prop-changes)
126
202
  *
203
+ * **Default: on.**
204
+ *
127
205
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackeffectsno-reset-all-state-on-prop-change
128
206
  */
129
207
  "@ashstack/effects/no-reset-all-state-on-prop-change"?: RuleSetting;
130
208
  /**
131
209
  * Disallow passing live state to parents in an effect. [Why](https://react.dev/learn/you-might-not-need-an-effect#notifying-parent-components-about-state-changes)
132
210
  *
211
+ * **Default: on.**
212
+ *
133
213
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackeffectsno-pass-live-state-to-parent
134
214
  */
135
215
  "@ashstack/effects/no-pass-live-state-to-parent"?: RuleSetting;
136
216
  /**
137
217
  * Disallow passing data to parents in an effect. [Why](https://react.dev/learn/you-might-not-need-an-effect#passing-data-to-the-parent)
138
218
  *
219
+ * **Default: on.**
220
+ *
139
221
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackeffectsno-pass-data-to-parent
140
222
  */
141
223
  "@ashstack/effects/no-pass-data-to-parent"?: RuleSetting;
142
224
  /**
143
225
  * Disallow subscribing to an external store in an effect. [Why](https://react.dev/learn/you-might-not-need-an-effect#subscribing-to-an-external-store)
144
226
  *
227
+ * **Default: on.**
228
+ *
145
229
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackeffectsno-external-store-subscription
146
230
  */
147
231
  "@ashstack/effects/no-external-store-subscription"?: RuleSetting;
148
232
  /**
149
233
  * Disallow initializing state in an effect. [Why](https://tkdodo.eu/blog/avoiding-hydration-mismatches-with-use-sync-external-store)
150
234
  *
235
+ * **Default: on.**
236
+ *
151
237
  * @see https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#ashstackeffectsno-initialize-state
152
238
  */
153
239
  "@ashstack/effects/no-initialize-state"?: RuleSetting;
@@ -1,4 +1,20 @@
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;
10
+ /**
11
+ * Tighter than core's 15, because a component's branching belongs in the tree
12
+ * it returns rather than in the function that returns it: a guard or two, a
13
+ * ternary or two, and the rest is JSX. Extracting a handler or a `map`
14
+ * callback drops the count on its own —
15
+ * `@ashstack/core/max-complexity` scores every function separately.
16
+ */
17
+ export declare const COMPONENT_MAX_COMPLEXITY = 10;
2
18
  /**
3
19
  * React (web) entry — everything in core plus react, jsx-a11y, React Compiler
4
20
  * 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,COMPONENT_MAX_COMPLEXITY=10;const x={...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:{...x,...f(e.reactCompiler??!0),...t,...r.rules,"@ashstack/core/max-lines":["error",COMPONENT_MAX_LINES],"@ashstack/core/max-complexity":["error",COMPONENT_MAX_COMPLEXITY]},overrides:[{files:u,rules:{"unicorn/filename-case":"off"}}]})};export{react};export default react;
@@ -1 +1 @@
1
- import{attributeName as r,optionsOf as s}from"../../../lib/ast.js";const l='Pass `t("<key>")` as this attribute value and add the key to every locale file.',o=["placeholder","accessibilityLabel","accessibilityHint","title"];export const noBareAttrs={meta:{type:"problem",docs:{description:"Disallow a plain string literal on a configurable list of user-visible JSX attributes, defaulting to placeholder, accessibilityLabel, accessibilityHint and title."},schema:[{type:"object",properties:{attributes:{type:"array",items:{type:"string"},minItems:1}},additionalProperties:!1}]},createOnce(a){const i=new Set;return{before(){i.clear();const t=s(a,{}).attributes;for(const e of t??o)i.add(e)},JSXAttribute(t){if(!i.has(r(t)))return;const{value:e}=t;if(e?.type!=="Literal"||typeof e.value!=="string"||e.value.length===0)return;a.report({node:t,message:l})}}}};
1
+ import{attributeName as s,optionsOf as o}from"../../../lib/ast.js";const l='Pass `t("<key>")` as this attribute value and add the key to every locale file.',a=["placeholder","accessibilityLabel","accessibilityHint","title"];export const noBareAttrs={meta:{type:"problem",docs:{description:"Disallow a plain string literal on a user-visible JSX attribute."},schema:[{type:"object",properties:{attributes:{type:"array",items:{type:"string"},minItems:1,default:a,description:"Attributes checked for bare strings. Replaces the list rather than adding to it."}},additionalProperties:!1}]},createOnce(i){const r=new Set;return{before(){r.clear();const t=o(i,{}).attributes;for(const e of t??a)r.add(e)},JSXAttribute(t){if(!r.has(s(t)))return;const{value:e}=t;if(e?.type!=="Literal"||typeof e.value!=="string"||e.value.length===0)return;i.report({node:t,message:l})}}}};
@@ -0,0 +1 @@
1
+ import{optionsOf as s}from"../../../lib/ast.js";const n="Move this file to `src/utils`, `src/hooks`, or `src/api/<feature>/` — `components/` holds only files that render JSX.",t="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."},schema:[{type:"object",properties:{dir:{type:"string",minLength:1,default:t,description:"Directory whose files must render JSX."}},additionalProperties:!1}],defaultOff:!0},createOnce(r){let e=!1;return{before(){e=!1;const{dir:o=t}=s(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:n})}}}};
@@ -1 +1 @@
1
- import{defineModule as t}from"../../../lib/module.js";import{noSvgWithoutTitle as e}from"./no-svg-without-title.js";import{noUnlabeledIconButton as o}from"./no-unlabeled-icon-button.js";export default t({meta:{name:"@ashstack/react"},url:import.meta.url,docsWhen:"always on via `react()` and every entry above it",rules:{"no-unlabeled-icon-button":o,"no-svg-without-title":e}});
1
+ import{defineModule as t}from"../../../lib/module.js";import{componentsTsxOnly as o}from"./components-tsx-only.js";import{hoistIntl as e}from"./hoist-intl.js";import{noSvgWithoutTitle as n}from"./no-svg-without-title.js";import{noUnlabeledIconButton as r}from"./no-unlabeled-icon-button.js";import{preferDesignSystem as i}from"./prefer-design-system.js";export default t({meta:{name:"@ashstack/react"},url:import.meta.url,docsWhen:"always on via `react()` and every entry above it",rules:{"no-unlabeled-icon-button":r,"no-svg-without-title":n,"hoist-intl":e,"prefer-design-system":i,"components-tsx-only":o}});
@@ -0,0 +1 @@
1
+ import{readdirSync as g}from"node:fs";import{isAbsolute as f,join as h}from"node:path";import{optionsOf as b}from"../../../lib/ast.js";const d="src/components/ui",l="@/components/ui",S=/\.(?:ios|android|native|web)$/,m=/^[@~#][^/]*\//,A={Pressable:["TouchableOpacity","TouchableHighlight","TouchableWithoutFeedback","TouchableNativeFeedback"]},y={anyOf:[{type:"string"},{type:"array",items:{type:"string"}}]},w={anyOf:[...y.anyOf,{type:"object",properties:{replaces:y,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,s)=>{const n=e.get(t)??new Map;n.set(r,s);e.set(t,n)},x=(e,t,r)=>{let s;try{s=g(f(t)?t:h(process.cwd(),t))}catch{return}for(const n of s){if(!n.endsWith(".tsx"))continue;const o=n.slice(0,-4).replace(S,"");if(o==="index")continue;const a=E(o),i=`${r}/${o}`;p(e,"react-native",a,{name:a,from:i});for(const c of A[a]??[])p(e,"react-native",c,{name:a,from:i})}},O=(e,t,r)=>{for(const[s,n]of Object.entries(r)){const o=typeof n==="string"||Array.isArray(n)?{replaces:n}:n,a={name:s,from:o.path??`${t}/${I(s)}`,reason:o.reason};for(const i of[o.replaces].flat())p(e,o.from??"react-native",i,a)}},T=(e,t,r)=>{const s=[e,t.replace(m,"")];for(const n of Object.values(r)){if(typeof n==="string"||Array.isArray(n)||n.path===void 0)continue;s.push(n.path.replace(m,""))}return s.filter((n)=>n.length>0)},u=new Map,_=(e,t)=>{if(!e)return!1;const r=e.replaceAll("\\","/");return t.some((s)=>r.includes(f(s)?s:`/${s}`))},v=(e)=>{const t=e.dir??d,r=e.alias??l,s=e.use??{},n=JSON.stringify([t,r,e.use]);let o=u.get(n);if(o===void 0){const a=new Map;x(a,t,r);O(a,r,s);o={banned:a,wrappers:T(t,r,s)};u.set(n,o)}return{banned:o.banned,exempt:[...o.wrappers,...e.exempt??[]]}};export const preferDesignSystem={meta:{type:"problem",docs:{description:"Disallow importing a raw primitive your design system already wraps. Wrappers come from scanning the design-system directory and from the `use` map. Files inside the design system are skipped."},schema:[{type:"object",properties:{dir:{type:"string",minLength:1,default:d,description:"Directory scanned for wrapper components. Every `.tsx` file in it becomes a wrapper."},alias:{type:"string",minLength:1,default:l,description:"Import prefix the diagnostic points at, and a second folder counted as the design system."},use:{type:"object",additionalProperties:w,default:{},description:"Wrappers the scan cannot find, keyed by component name.",examples:[{Button:"Pressable",Text:["Text","RNText"],Sheet:{replaces:"Modal",from:"react-native",path:"@/ui/sheet",reason:"It owns insets."}}]},exempt:{type:"array",items:{type:"string"},default:[],description:"Extra path fragments to skip, added to the design system's own folders."}},additionalProperties:!1}],defaultOff:!0},createOnce(e){let t=new Map;return{before(){const r=b(e,{}),s=v(r);t=s.banned;if(t.size===0)return!1;return!_(e.filename,s.exempt)},ImportDeclaration(r){const s=r.source.value,n=t.get(s);if(n===void 0)return;for(const o of r.specifiers){if(o.type!=="ImportSpecifier")continue;const{imported:a}=o;if(a.type!=="Identifier")continue;const i=n.get(a.name);if(i===void 0)continue;const c=i.reason?` ${i.reason}`:"";e.report({node:o,message:`Import \`${i.name}\` from "${i.from}" instead of \`${a.name}\` from ${s}.${c}`})}}}}};
@@ -1 +1 @@
1
- import{TEST_FILES as a}from"../core/index.js";import{mergeConfigs as r}from"../lib/merge.js";import{composeModules as s}from"../lib/module.js";import{coreRegistry as o,reactNativeRegistry as i,reactRegistry as n}from"../lib/registry.js";import c from"../react/index.js";export const banGroups=[{packages:["react-native-gesture-handler"],restrictedImports:{paths:[{name:"react-native",importNames:["Pressable","TouchableOpacity","TouchableHighlight","TouchableWithoutFeedback","TouchableNativeFeedback"],message:"Import `Pressable` from `react-native-gesture-handler`. Inside a SwiftUI/Compose host view, keep React Native's `Pressable` and suppress this rule naming the host."}]}},{packages:["expo-router"],restrictedImports:{patterns:[{group:["@react-navigation/stack","@react-navigation/bottom-tabs"],message:"Use expo-router's `Stack` / `NativeTabs` instead of a raw react-navigation navigator."}]}},{packages:["expo-font"],restrictedImports:{paths:[{name:"expo-font",importNames:["useFonts","loadAsync"],message:"Declare the fonts in the `expo-font` config plugin so they load at build time, instead of `useFonts`/`loadAsync` at runtime."}]}},{packages:["react-native-quick-crypto"],restrictedImports:{paths:[{name:"crypto-js",message:"Use `react-native-quick-crypto` instead — `crypto-js` is pure JavaScript and runs orders of magnitude slower."}]}}];const p={"eslint/no-empty":"error","eslint/no-empty-function":"error"},m={"@ashstack/effects/no-event-handler":"off"},l=(e)=>e?{}:{"@ashstack/react-native/no-manual-memo":"off"},reactNative=(e={})=>{const t=s([...o,...n,...i],e,banGroups);return r(c(e),{jsPlugins:t.jsPlugins,globals:{__DEV__:"readonly"},rules:{...p,...m,"no-restricted-imports":["error",t.restricted],...t.rules,...l(e.reactCompiler??!0)},ignorePatterns:["**/.expo/**","**/android/**","**/ios/**"],overrides:[{files:a,rules:{"@ashstack/react-native/no-dynamic-import":"off","@ashstack/react-native/hoist-stateless-function":"off"}}]})};export{reactNative};export default reactNative;
1
+ 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_COMPLEXITY as p,COMPONENT_MAX_LINES as m}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 l={"eslint/no-empty":"error","eslint/no-empty-function":"error"},u={"@ashstack/effects/no-event-handler":"off"},d=(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:{...l,...u,"no-restricted-imports":["error",t.restricted],...t.rules,"@ashstack/core/max-lines":["error",m],"@ashstack/core/max-complexity":["error",p],...d(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{gate as m,problem as u}from"../../../lib/ast.js";const i=new Set(["useMemo","useCallback","memo"]),c=/^why:/i,h=10,p=(e)=>e.type==="Identifier"&&i.has(e.name)?e.name:null,f=(e)=>{if(e.type!=="MemberExpression"||e.computed)return null;if(e.object.type!=="Identifier"||e.object.name!=="React")return null;const t=e.property.name;return i.has(t)?t:null},d=(e)=>{if(e.type!=="CallExpression")return null;const{callee:t}=e;if(t.type==="Identifier")return p(t);if(t.type==="MemberExpression")return f(t);return null},y=(e)=>{const t=[0];for(let n=0;n<e.length;n++)if(e.charCodeAt(n)===h)t.push(n+1);return t},b=(e,t)=>{let n=0,o=e.length-1;while(n<o){const s=Math.floor((n+o+1)/2);if((e[s]??0)<=t)n=s;else o=s-1}return n},g=(e,t)=>{const n=new Set;for(const o of e){if(!c.test(o.value.trim()))continue;for(let s=t(o.start);s<=t(o.end);s++)n.add(s)}return n};export const noManualMemo=u("Require a `// why:` line above every kept `useMemo`, `useCallback` and `memo`. The React Compiler memoises on a best-effort basis, not a guarantee, so a memo is allowed where the cost is real: something rendered per list row, or a computation measured as heavy. The `// why:` line names which of the two applies. `@ashstack/core/no-comments` keeps that line and never counts it against its `budget`, so both rules run together. Assumes the compiler is on — pass `reactCompiler: false` to the entry and this rule turns off, since without it a hand-written memo is the only memo there is.",{createOnce(e){let t=[];return{before(){t=[];return m(e,"useMemo","useCallback","memo")},CallExpression(n){const o=d(n);if(o!==null)t.push({node:n,name:o})},"Program:exit"(){if(t.length===0)return;const n=y(e.sourceCode.getText()),o=(r)=>b(n,r),s=g(e.sourceCode.getAllComments(),o);for(const{node:r,name:l}of t){const a=o(r.start);if(s.has(a)||s.has(a-1))continue;e.report({node:r,message:`Add a \`// why:\` line above this \`${l}\` naming the real cost — a list row, or a computation you measured as heavy. If neither applies, delete it.`})}}}}});
1
+ import{gate as m,problem as u}from"../../../lib/ast.js";const i=new Set(["useMemo","useCallback","memo"]),c=/^why:/i,f=10,p=(e)=>e.type==="Identifier"&&i.has(e.name)?e.name:null,h=(e)=>{if(e.type!=="MemberExpression"||e.computed)return null;if(e.object.type!=="Identifier"||e.object.name!=="React")return null;const t=e.property.name;return i.has(t)?t:null},d=(e)=>{if(e.type!=="CallExpression")return null;const{callee:t}=e;if(t.type==="Identifier")return p(t);if(t.type==="MemberExpression")return h(t);return null},y=(e)=>{const t=[0];for(let o=0;o<e.length;o++)if(e.charCodeAt(o)===f)t.push(o+1);return t},C=(e,t)=>{let o=0,n=e.length-1;while(o<n){const r=Math.floor((o+n+1)/2);if((e[r]??0)<=t)o=r;else n=r-1}return o},b=(e,t)=>{const o=new Set;for(const n of e){if(!c.test(n.value.trim()))continue;for(let r=t(n.start);r<=t(n.end);r++)o.add(r)}return o};export const noManualMemo=u("Require a `// why:` line above every kept `useMemo`, `useCallback` and `memo`. The React Compiler memoises on a best-effort basis, so a memo is allowed where the cost is real: a list row, or a computation measured as heavy. Turns off with `reactCompiler: false`.",{createOnce(e){let t=[];return{before(){t=[];return m(e,"useMemo","useCallback","memo")},CallExpression(o){const n=d(o);if(n!==null)t.push({node:o,name:n})},"Program:exit"(){if(t.length===0)return;const o=y(e.sourceCode.getText()),n=(s)=>C(o,s),r=b(e.sourceCode.getAllComments(),n);for(const{node:s,name:l}of t){const a=n(s.start);if(r.has(a)||r.has(a-1))continue;e.report({node:s,message:`Add a \`// why:\` line above this \`${l}\` naming the real cost — a list row, or a computation you measured as heavy. If neither applies, delete it.`})}}}}});
@@ -1 +1 @@
1
- import{calleeName as u,findInSubtree as l,gate as b,problem as d,subtreeHas as y,tagIdentifier as w}from"../../../lib/ast.js";import{isStyleSheetCreate as S,memberPath as h,propertyName as C,stylesObjectOf as g}from"./shared.js";const f={contentContainerRuntime:"Wrap this component with `withUnistyles`, or pass `contentContainerStyle` a hook-fed style — on a raw component it never subscribes to Unistyles updates.",contentContainerTheme:"Wrap this component with `withUnistyles`, or pass `contentContainerStyle` a hook-fed style — on a raw component it never subscribes to theme changes."};export const contentContainer=d("A raw component never subscribes its `contentContainerStyle` to theme or `rt` updates. Wrap the component with `withUnistyles` when the style depends on either.",{createOnce(p){const a=new Map,c=new Set,m=[];return{before(){a.clear();c.clear();m.length=0;return b(p,"contentContainerStyle")},VariableDeclarator(e){const{id:r,init:n}=e;if(r.type!=="Identifier")return;if(u(n)==="withUnistyles"){c.add(r.name);return}if(n?.type!=="CallExpression"||!S(n))return;const s=g(n.arguments[0]);if(!s)return;const i=new Map;for(const o of s.properties){if(o.type!=="Property")continue;i.set(C(o),o.value)}a.set(r.name,i)},JSXAttribute(e){if(e.name.type!=="JSXIdentifier"||e.name.name!=="contentContainerStyle")return;m.push(e)},"Program:exit"(){for(const e of m){const r=l(e.value,(t)=>t.type==="MemberExpression"&&t.object.type==="Identifier"&&a.has(t.object.name));if(r?.type!=="MemberExpression")continue;const n=e.parent;if(n.type==="JSXOpeningElement"&&c.has(w(n.name)))continue;const{object:s,property:i}=r;if(s.type!=="Identifier")continue;const o=a.get(s.name)?.get(i.type==="Identifier"?i.name:"");if(!o)continue;if(y(o,(t)=>t.type==="MemberExpression"&&h(t).startsWith("rt."))){p.report({node:e,message:f.contentContainerRuntime});continue}if(y(o,(t)=>t.type==="MemberExpression"&&h(t).startsWith("theme.")))p.report({node:e,message:f.contentContainerTheme})}}}}});
1
+ import{calleeName as h,findInSubtree as b,gate as d,problem as w,subtreeHas as S,tagIdentifier as C}from"../../../lib/ast.js";import{isStyleSheetCreate as g,memberPath as E,propertyName as I,stylesObjectOf as M}from"./shared.js";const y={contentContainerRuntime:"Wrap this component with `withUnistyles`, or pass `contentContainerStyle` a hook-fed style — on a raw component it never subscribes to Unistyles updates.",contentContainerTheme:"Wrap this component with `withUnistyles`, or pass `contentContainerStyle` a hook-fed style — on a raw component it never subscribes to theme changes."};export const contentContainer=w("A raw component never subscribes its `contentContainerStyle` to theme or `rt` updates. Wrap the component with `withUnistyles` when the style depends on either.",{createOnce(l){const a=new Map,p=new Set,c=[],f=(e)=>{const t=b(e.value,(s)=>s.type==="MemberExpression"&&s.object.type==="Identifier"&&a.has(s.object.name));if(t?.type!=="MemberExpression")return null;const n=e.parent;if(n.type==="JSXOpeningElement"&&p.has(C(n.name)))return null;const{object:o,property:i}=t;if(o.type!=="Identifier")return null;const r=a.get(o.name)?.get(i.type==="Identifier"?i.name:"");if(!r)return null;const m=(s)=>S(r,(u)=>u.type==="MemberExpression"&&E(u).startsWith(s));if(m("rt."))return y.contentContainerRuntime;return m("theme.")?y.contentContainerTheme:null};return{before(){a.clear();p.clear();c.length=0;return d(l,"contentContainerStyle")},VariableDeclarator(e){const{id:t,init:n}=e;if(t.type!=="Identifier")return;if(h(n)==="withUnistyles"){p.add(t.name);return}if(n?.type!=="CallExpression"||!g(n))return;const o=M(n.arguments[0]);if(!o)return;const i=new Map;for(const r of o.properties){if(r.type!=="Property")continue;i.set(I(r),r.value)}a.set(t.name,i)},JSXAttribute(e){if(e.name.type!=="JSXIdentifier"||e.name.name!=="contentContainerStyle")return;c.push(e)},"Program:exit"(){for(const e of c){const t=f(e);if(t!==null)l.report({node:e,message:t})}}}}});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ashstack/lint",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "ashStack shared oxlint configuration and custom rules. Entries: core, react, react-native, with per-library rule groups auto-detected from your dependencies. Consume from oxlint.config.mts.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1 +0,0 @@
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 +0,0 @@
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}`})}}}}};