@ashstack/lint 0.3.1 → 0.5.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 +17 -3
- package/dist/core/index.d.ts +7 -3
- package/dist/core/index.js +1 -1
- package/dist/core/rules/core/no-comments.js +1 -1
- package/dist/lib/ast.js +1 -1
- package/dist/lib/effect-plugin.js +1 -0
- package/dist/lib/module.js +1 -1
- package/dist/lib/registry.js +1 -0
- package/dist/lib/rule-types/core.d.ts +72 -0
- package/dist/lib/rule-types/core.js +1 -0
- package/dist/lib/rule-types/react-native.d.ts +333 -0
- package/dist/lib/rule-types/react-native.js +1 -0
- package/dist/lib/rule-types/react.d.ts +153 -0
- package/dist/lib/rule-types/react.js +1 -0
- package/dist/lib/types.d.ts +39 -11
- package/dist/modules.js +1 -0
- package/dist/react/index.d.ts +6 -1
- package/dist/react/index.js +1 -1
- package/dist/react/rules/query/next-page-param-undefined.js +1 -1
- package/dist/react/rules/query/no-fetch-in-query-fn.js +1 -1
- package/dist/react/rules/query/no-inline-keys.js +1 -1
- package/dist/react/rules/react/index.js +1 -0
- package/dist/react/rules/react/no-svg-without-title.js +1 -0
- package/dist/react/rules/react/no-unlabeled-icon-button.js +1 -0
- package/dist/react/rules/react/shared.js +1 -0
- package/dist/react/rules/tailwind/index.js +1 -0
- package/dist/react/rules/tailwind/prefer-cn.js +1 -0
- package/dist/react/rules/tailwind/shared.js +1 -0
- package/dist/react/rules/tailwind/use-logical-classes.js +1 -0
- package/dist/react/rules/tanstack-router/index.js +1 -0
- package/dist/react/rules/tanstack-router/no-search-casts.js +1 -0
- package/dist/react/rules/tanstack-router/require-selector.js +1 -0
- package/dist/react/rules/tanstack-router/shared.js +1 -0
- package/dist/react-native/index.d.ts +7 -1
- package/dist/react-native/index.js +1 -1
- package/dist/react-native/rules/legend-list/index.js +1 -1
- package/dist/react-native/rules/react-native/no-manual-memo.js +1 -1
- package/package.json +6 -10
- package/vendor/react-effect/LICENSE +21 -0
- package/vendor/react-effect/README.md +25 -0
- package/vendor/react-effect/index.mjs +559 -0
- package/dist/core/rules/core/components-tsx-only.d.ts +0 -2
- package/dist/core/rules/core/hoist-intl.d.ts +0 -2
- package/dist/core/rules/core/index.d.ts +0 -2
- package/dist/core/rules/core/no-comments.d.ts +0 -2
- package/dist/core/rules/core/no-naming-convention.d.ts +0 -2
- package/dist/core/rules/core/no-packed-condition.d.ts +0 -2
- package/dist/core/rules/core/use-design-system.d.ts +0 -2
- package/dist/core/rules/zod/index.d.ts +0 -2
- package/dist/core/rules/zod/prefer-enum.d.ts +0 -2
- package/dist/index.d.ts +0 -4
- package/dist/index.js +0 -1
- package/dist/lib/ast.d.ts +0 -44
- package/dist/lib/detect.d.ts +0 -9
- package/dist/lib/merge.d.ts +0 -9
- package/dist/lib/module.d.ts +0 -19
- package/dist/react/rules/i18n/index.d.ts +0 -2
- package/dist/react/rules/i18n/no-bare-attrs.d.ts +0 -2
- package/dist/react/rules/i18n/no-bare-text.d.ts +0 -2
- package/dist/react/rules/i18n/no-bare-toast.d.ts +0 -2
- package/dist/react/rules/query/index.d.ts +0 -2
- package/dist/react/rules/query/next-page-param-undefined.d.ts +0 -2
- package/dist/react/rules/query/no-deprecated-filters.d.ts +0 -2
- package/dist/react/rules/query/no-fetch-in-query-fn.d.ts +0 -2
- package/dist/react/rules/query/no-inline-keys.d.ts +0 -2
- package/dist/react/rules/query/require-destructured-hooks.d.ts +0 -2
- package/dist/react/rules/query/shared.d.ts +0 -2
- package/dist/react/rules/query/shared.js +0 -1
- package/dist/react/rules/zustand/index.d.ts +0 -2
- package/dist/react/rules/zustand/require-selector.d.ts +0 -2
- package/dist/react-native/rules/keyboard/index.d.ts +0 -2
- package/dist/react-native/rules/legend-list/index.d.ts +0 -2
- package/dist/react-native/rules/legend-list/no-flex-in-content-container.d.ts +0 -2
- package/dist/react-native/rules/legend-list/no-index-key-extractor.d.ts +0 -2
- package/dist/react-native/rules/legend-list/no-inline-data.d.ts +0 -2
- package/dist/react-native/rules/legend-list/no-inline-data.js +0 -1
- package/dist/react-native/rules/legend-list/no-inline-extra-data.d.ts +0 -2
- package/dist/react-native/rules/legend-list/no-inline-extra-data.js +0 -1
- package/dist/react-native/rules/legend-list/no-inline-render-item-props.d.ts +0 -2
- package/dist/react-native/rules/legend-list/no-mixed-children.d.ts +0 -2
- package/dist/react-native/rules/legend-list/no-remount-key.d.ts +0 -2
- package/dist/react-native/rules/legend-list/no-scrollview-map.d.ts +0 -2
- package/dist/react-native/rules/legend-list/no-unsupported-props.d.ts +0 -2
- package/dist/react-native/rules/legend-list/required-props.d.ts +0 -2
- package/dist/react-native/rules/legend-list/shared.d.ts +0 -19
- package/dist/react-native/rules/legend-list/typed-items-need-item-type.d.ts +0 -2
- package/dist/react-native/rules/legend-state/index.d.ts +0 -2
- package/dist/react-native/rules/legend-state/naming.d.ts +0 -2
- package/dist/react-native/rules/legend-state/no-assignment.d.ts +0 -2
- package/dist/react-native/rules/legend-state/no-nested-observable.d.ts +0 -2
- package/dist/react-native/rules/legend-state/no-object-selector.d.ts +0 -2
- package/dist/react-native/rules/legend-state/no-observable-in-component.d.ts +0 -2
- package/dist/react-native/rules/legend-state/no-peek-in-selector.d.ts +0 -2
- package/dist/react-native/rules/legend-state/no-react-mirror.d.ts +0 -2
- package/dist/react-native/rules/legend-state/no-untracked-get-in-jsx.d.ts +0 -2
- package/dist/react-native/rules/legend-state/shared.d.ts +0 -9
- package/dist/react-native/rules/react-native/hoist-stateless-function.d.ts +0 -2
- package/dist/react-native/rules/react-native/index.d.ts +0 -2
- package/dist/react-native/rules/react-native/no-conditional-style-array.d.ts +0 -2
- package/dist/react-native/rules/react-native/no-dynamic-import.d.ts +0 -2
- package/dist/react-native/rules/react-native/no-keyboard-will-events.d.ts +0 -2
- package/dist/react-native/rules/react-native/no-leaked-render.d.ts +0 -2
- package/dist/react-native/rules/react-native/no-manual-memo.d.ts +0 -2
- package/dist/react-native/rules/react-native/no-redundant-view-nesting.d.ts +0 -2
- package/dist/react-native/rules/react-native/no-rn-image-network-source.d.ts +0 -2
- package/dist/react-native/rules/react-native/no-rn-namespace-import.d.ts +0 -2
- package/dist/react-native/rules/react-native/no-scroll-position-state.d.ts +0 -2
- package/dist/react-native/rules/react-native/no-unlabeled-icon-pressable.d.ts +0 -2
- package/dist/react-native/rules/reanimated/animated-reaction-safety.d.ts +0 -2
- package/dist/react-native/rules/reanimated/animated-style-needs-animated-component.d.ts +0 -2
- package/dist/react-native/rules/reanimated/animated-updater-purity.d.ts +0 -2
- package/dist/react-native/rules/reanimated/gpu-properties-only.d.ts +0 -2
- package/dist/react-native/rules/reanimated/hoist-layout-animation-builder.d.ts +0 -2
- package/dist/react-native/rules/reanimated/index.d.ts +0 -2
- package/dist/react-native/rules/reanimated/interpolate-needs-clamp.d.ts +0 -2
- package/dist/react-native/rules/reanimated/no-react-state-from-continuous-worklet.d.ts +0 -2
- package/dist/react-native/rules/reanimated/no-shared-value-dot-value.d.ts +0 -2
- package/dist/react-native/rules/reanimated/prefer-lazy-shared-value-initializer.d.ts +0 -2
- package/dist/react-native/rules/reanimated/schedule-on-rn-scope.d.ts +0 -2
- package/dist/react-native/rules/reanimated/shared-value-usage.d.ts +0 -2
- package/dist/react-native/rules/reanimated/shared.d.ts +0 -5
- package/dist/react-native/rules/skia/canvas-opaque.d.ts +0 -2
- package/dist/react-native/rules/skia/index.d.ts +0 -2
- package/dist/react-native/rules/turbo-image/index.d.ts +0 -2
- package/dist/react-native/rules/turbo-image/require-cache-policy.d.ts +0 -2
- package/dist/react-native/rules/turbo-image/require-resize.d.ts +0 -2
- package/dist/react-native/rules/unistyles/animated-theme.d.ts +0 -2
- package/dist/react-native/rules/unistyles/content-container.d.ts +0 -2
- package/dist/react-native/rules/unistyles/in-sheet.d.ts +0 -2
- package/dist/react-native/rules/unistyles/index.d.ts +0 -2
- package/dist/react-native/rules/unistyles/insets.d.ts +0 -2
- package/dist/react-native/rules/unistyles/no-hardcoded-color.d.ts +0 -2
- package/dist/react-native/rules/unistyles/no-hardcoded-spacing.d.ts +0 -2
- package/dist/react-native/rules/unistyles/no-margin.d.ts +0 -2
- package/dist/react-native/rules/unistyles/no-style-spread.d.ts +0 -2
- package/dist/react-native/rules/unistyles/no-unused-styles.d.ts +0 -2
- package/dist/react-native/rules/unistyles/rtl-style-call.d.ts +0 -2
- package/dist/react-native/rules/unistyles/shared.d.ts +0 -16
- package/dist/react-native/rules/unistyles/theme-screen-component.d.ts +0 -2
- package/dist/react-native/rules/unistyles/theme-style-attr.d.ts +0 -2
- package/dist/react-native/stylesheet.d.ts +0 -11
package/dist/react/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
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 l={"eslint/no-empty":"off","eslint/no-empty-function":"off"},u={"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"},p=["**/routes/**","**/src/app/**","**/app/**/_layout.tsx","**/app/**/+*.tsx"],f={...l,"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-constructed-context-values":"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",...u,"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=(r={})=>{const e=n([...c,...i],r);return s(o(r),{plugins:["react","jsx-a11y","react-perf"],jsPlugins:[a,...e.jsPlugins],rules:{...f,...t,...e.rules},overrides:[{files:p,rules:{"unicorn/filename-case":"off"}}]})};export{react};export default react;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{crossesFunctionBefore as n,findInSubtree as o,FUNCTION_TYPES as u,gate as l,problem as s,propertyKeyName as i}from"../../../lib/ast.js";const g="Return `undefined` to signal there are no more pages. `null` is a valid page param, so returning it reads as a real next cursor and the list keeps fetching forever.",p=(e)=>e.type==="ReturnStatement"&&e.argument?.type==="Literal"&&e.argument.value===null,m=(e)=>e.type==="ArrowFunctionExpression"&&e.body.type==="Literal"&&e.body.value===null?e.body:null;export const nextPageParamUndefined=s("Disallow `return null` in the body of a `getNextPageParam`.",{createOnce(e){return{before(){return l(e,"getNextPageParam")},Property(t){if(i(t)!=="getNextPageParam")return;const{value:r}=t,a=m(r)??o(r,p);if(!a||n(a,r,u))return;e.report({node:r,message:g})}}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{gate as r,problem as a,subtreeHas as
|
|
1
|
+
import{gate as r,problem as a,propertyKeyName as n,subtreeHas as s}from"../../../lib/ast.js";const o="Call a typed function from the feature's `*.requests.ts` module here instead of `fetch`. That client is what sends the auth header, applies the timeout, retries a 429 or 5xx, and throws the app's error type.",i=new Set(["queryFn","mutationFn"]),u=(e)=>e.type==="CallExpression"&&e.callee.type==="Identifier"&&e.callee.name==="fetch";export const noFetchInQueryFn=a("Disallow a bare `fetch(` inside a `queryFn` or `mutationFn`.",{createOnce(e){return{before(){return r(e,"queryFn","mutationFn")},Property(t){if(!i.has(n(t)))return;if(!s(t.value,u))return;e.report({node:t.value,message:o})}}}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{problem as a}from"../../../lib/ast.js";
|
|
1
|
+
import{problem as a,propertyKeyName as y}from"../../../lib/ast.js";const i={inlineQueryKey:"Define this key in the feature's `*.keys.ts` factory and reference it: `queryKey: someKeys.scope(...)`, so invalidation elsewhere matches the same key.",inlineGetQueryData:"Read through the feature's `*.keys.ts` factory: `getQueryData(someKeys.scope())`.",inlineSetQueryData:"Write through the feature's `*.keys.ts` factory: `setQueryData(someKeys.scope(), updater)`."},o=new Set(["useQuery","useSuspenseQuery","useInfiniteQuery","useSuspenseInfiniteQuery","useQueries"]),p=new Set(["invalidateQueries","removeQueries","refetchQueries","cancelQueries","resetQueries","fetchQuery","prefetchQuery","ensureQueryData","getQueriesData","setQueriesData"]),c=(t)=>{if(t.type!=="CallExpression")return"";const{callee:e}=t;if(e.type!=="MemberExpression"||e.computed)return"";const{property:r}=e;return r.type==="Identifier"?r.name:""},f=(t)=>{if(t.type!=="CallExpression")return;const[e]=t.arguments;return e?.type==="ArrayExpression"?e:void 0},l=(t,e)=>{const r=t.type==="CallExpression"?t.callee:void 0;return r?.type==="Identifier"&&o.has(r.name)||p.has(e)},Q=(t)=>{if(t.type!=="Property"||t.computed)return!1;const{key:e}=t;if(e.type==="Identifier")return e.name==="queryKey";return e.type==="Literal"&&e.value==="queryKey"},m=(t)=>{if(t.type!=="ObjectExpression")return[];const e=t.properties.find((r)=>r.type==="Property"&&y(r)==="queries");if(e?.type!=="Property"||e.value.type!=="ArrayExpression")return[t];return[t,...e.value.elements.filter((r)=>r!==null)]},h=(t)=>{if(t.type!=="CallExpression")return[];const[e]=t.arguments;if(e===void 0)return[];const r=[];for(const s of m(e)){if(s.type!=="ObjectExpression")continue;for(const n of s.properties){if(n.type!=="Property"||!Q(n))continue;const{value:u}=n;if(u.type==="ArrayExpression")r.push(u)}}return r};export const noInlineKeys=a("Disallow a query key written as an array literal at the call site instead of coming from a keys factory.",{createOnce(t){return{CallExpression(e){const r=c(e);if(r==="getQueryData"||r==="setQueryData"){const s=f(e);if(s===void 0)return;t.report({node:s,message:r==="getQueryData"?i.inlineGetQueryData:i.inlineSetQueryData});return}if(!l(e,r))return;for(const s of h(e))t.report({node:s,message:i.inlineQueryKey})}}}});
|
|
@@ -0,0 +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}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{attributeName as a,gate as s,problem as o}from"../../../lib/ast.js";import{childNames as l,namedBy as m,NAMING_ATTRIBUTES as c}from"./shared.js";const f=new Set(["presentation","none"]),p='Give this `<svg>` a `<title>` child with content, or an `aria-label`, so a screen reader can name it. If it carries no meaning of its own, mark it `aria-hidden="true"` or `role="presentation"` and let the text beside it do the naming.',u=(e)=>{if(e?.type==="Literal")return e.value!=="false";if(e?.type!=="JSXExpressionContainer")return!0;const{expression:t}=e;return!(t.type==="Literal"&&(t.value===!1||t.value==="false"))},d=(e)=>e?.type==="Literal"&&typeof e.value==="string"&&f.has(e.value),g=(e)=>{if(e.type!=="JSXAttribute")return!1;const t=a(e);if(t==="aria-hidden")return u(e.value);if(t==="role")return d(e.value);return m(e,c)};export const noSvgWithoutTitle=o("Require an accessible name on an inline `<svg>`: a `<title>` child with content, an `aria-label`, or a marker that it is decorative. Every child counts, and a self-closing `<svg />` reports.",{createOnce(e){const t=(r)=>{if(r.type!=="JSXElement")return!1;const{name:n}=r.openingElement;if(n.type!=="JSXIdentifier"||n.name!=="title")return!1;return r.children.some((i)=>l(i,e)||i.type==="JSXElement"||i.type==="JSXFragment")};return{before(){return s(e,"<svg")},JSXElement(r){const n=r.openingElement;if(n.name.type!=="JSXIdentifier"||n.name.name!=="svg")return;if(n.attributes.some(g))return;if(r.children.some(t))return;e.report({node:n.name,message:p})}}}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{attributeName as o,findInSubtree as i,gate as s,problem as l,tagIdentifier as m}from"../../../lib/ast.js";import{attributeHasContent as u,childNames as c,namedBy as b,NAMING_ATTRIBUTES as p}from"./shared.js";const g=new Set(["button","Button"]),S="Add an `aria-label` (or `aria-labelledby`), or render visible text, so this icon-only button has an accessible name. An `<img alt>` or an `<svg><title>` inside it counts too.",d=(e)=>e.type==="JSXAttribute"&&o(e)==="alt"&&u(e.value),f=(e)=>e.type==="JSXOpeningElement"&&m(e.name)==="img"&&e.attributes.some(d);export const noUnlabeledIconButton=l("Require an accessible name on an icon-only `<button>` or `<Button>`. Without a label, visible text or image alt text, a screen reader cannot reach the control.",{createOnce(e){const r=(t)=>c(t,e)||f(t);return{before(){return s(e,"button","Button")},JSXElement(t){const a=t.openingElement;if(a.name.type!=="JSXIdentifier"||!g.has(a.name.name))return;if(a.attributes.some((n)=>b(n,p)))return;if(!t.children.some((n)=>n.type==="JSXElement"||n.type==="JSXFragment"))return;if(t.children.some((n)=>i(n,r)!==null))return;e.report({node:a.name,message:S})}}}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{attributeName as r}from"../../../lib/ast.js";export const NAMING_ATTRIBUTES=new Set(["aria-label","aria-labelledby"]);const n=/^(?:null|undefined|false|true|""|''|``)$/;export const attributeHasContent=(e)=>{if(e?.type==="Literal")return typeof e.value==="string"&&e.value.trim()!=="";return e?.type==="JSXExpressionContainer"},namedBy=(e,t)=>e.type==="JSXAttribute"&&t.has(r(e))&&attributeHasContent(e.value),childNames=(e,t)=>{if(e.type==="JSXText")return e.value.trim()!=="";if(e.type!=="JSXExpressionContainer")return!1;return!n.test(t.sourceCode.getText(e.expression).trim())};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{defineModule as e}from"../../../lib/module.js";import{preferCn as a}from"./prefer-cn.js";import{useLogicalClasses as s}from"./use-logical-classes.js";export default e({meta:{name:"@ashstack/tailwind"},url:import.meta.url,packages:["tailwindcss"],option:"tailwind",docsWhen:"auto-enabled by `react()` when `tailwindcss` is a dependency",rules:{"prefer-cn":a,"use-logical-classes":s}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{attributeName as f,importedNames as h,problem as m}from"../../../lib/ast.js";import{CLASS_ATTRIBUTE as d,CLASS_BINDING as S,CLASS_MARKER as y,COMPOSER_MODULES as b,KNOWN_COMPOSERS as c}from"./shared.js";const g=new Set(["join","concat"]),w=new Set(["ConditionalExpression","LogicalExpression"]),a={attribute:"Wrap this class value in `cn(...)`. It folds the conditional inputs into one string and lets a later Tailwind utility win over an earlier one that sets the same property.",precomputed:"Wrap this value in `cn(...)` where it reaches the class prop, or build it with `cn(...)` where it is declared. Conflicting Tailwind utilities survive into the DOM otherwise.",declaration:"Build this class value with `cn(...)` instead of joining it by hand, so conflicting Tailwind utilities resolve to the last one and falsy branches drop out."},E=(e,r)=>{if(e.type==="MemberExpression")return e.property.type==="Identifier"&&g.has(e.property.name);if(e.type!=="Identifier")return!1;return c.has(e.name)?e.name!=="cn":r.has(e.name)},p=(e,r)=>{if(!e)return!1;if(w.has(e.type))return!0;if(e.type==="TemplateLiteral")return e.expressions.length>0;if(e.type==="BinaryExpression")return e.operator==="+";return e.type==="CallExpression"&&E(e.callee,r)},C=(e)=>{if(e.type!=="JSXAttribute")return null;const{value:r}=e;return r?.type==="JSXExpressionContainer"?r.expression:null};export const preferCn=m("Require a dynamic class value to go through `cn(...)` before it reaches a `class`, `className` or `*ClassName` prop. Reports both where the value reaches the prop and where a variable named after classes is declared.",{createOnce(e){const r=new Set,s=new Map,o=[],u=()=>{const t=new Set;for(const[n,i]of s){if(!S.test(n))continue;if(!p(i,r))continue;t.add(n);e.report({node:i.parent??i,message:a.declaration})}return t},l=(t)=>{for(const n of o){const i=C(n);if(!i)continue;if(p(i,r)){e.report({node:n,message:a.attribute});continue}if(i.type!=="Identifier"||t.has(i.name))continue;if(!p(s.get(i.name),r))continue;e.report({node:n,message:a.precomputed})}};return{before(){r.clear();s.clear();o.length=0;return y.test(e.sourceCode.text)},ImportDeclaration(t){for(const n of b)for(const{local:i}of h(t,n))if(!c.has(i))r.add(i)},VariableDeclarator(t){if(t.id.type==="Identifier"&&t.init)s.set(t.id.name,t.init)},JSXAttribute(t){if(d.test(f(t)))o.push(t)},"Program:exit"(){l(u())}}}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CLASS_ATTRIBUTE=/^(?:class|className|[A-Za-z_$][A-Za-z0-9_$]*ClassName)$/,CLASS_BINDING=/^[A-Za-z_$][A-Za-z0-9_$]*(?:class(?:es|name|names)?|styles?)$/i,CLASS_MARKER=/class|style/i,KNOWN_COMPOSERS=new Set(["cn","clsx","classNames","classnames","twMerge"]),COMPOSER_MODULES=["clsx","classnames","tailwind-merge"];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{attributeName as l,hasAncestor as o,problem as n}from"../../../lib/ast.js";import{CLASS_ATTRIBUTE as d,CLASS_BINDING as f}from"./shared.js";const u="ml|mr|pl|pr|left|right|scroll-ml|scroll-mr|scroll-pl|scroll-pr",c="border-l|border-r|rounded-l|rounded-r|rounded-tl|rounded-tr|rounded-bl|rounded-br",m="text-left|text-right|float-left|float-right|clear-left|clear-right",p=String.raw`(?:(?:${u})-\S+|(?:${c})(?:-\S+)?|${m})`,s=String.raw`[\s"'\`{}]`,r=new RegExp(String.raw`(?:^|${s})(?:[^\s:]+:)*!?-?${p}(?:$|${s})`),a="Swap this for its logical twin so the layout mirrors in Arabic and Hebrew: `ms`/`me` instead of `ml`/`mr`, `text-start` instead of `text-left`. Padding, insets, borders and corners follow the same pattern. Keep the physical utility only where the coordinate really is the screen rather than the reading order.",i=(t)=>o(t,(e)=>{if(e.type==="JSXAttribute")return d.test(l(e));if(e.type!=="VariableDeclarator")return!1;return e.id.type==="Identifier"&&f.test(e.id.name)});export const useLogicalClasses=n("Require a logical Tailwind utility over its physical left/right twin, so a right-to-left layout mirrors. Reads class props and bindings named after classes, through variant prefixes and the `!` and `-` modifiers.",{createOnce(t){return{before(){return r.test(t.sourceCode.text)},Literal(e){if(typeof e.value!=="string")return;if(!r.test(e.value))return;if(!i(e))return;t.report({node:e,message:a})},TemplateElement(e){if(!r.test(e.value.raw))return;if(!i(e))return;t.report({node:e,message:a})}}}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{defineModule as e}from"../../../lib/module.js";import{noSearchCasts as t}from"./no-search-casts.js";import{requireSelector as r}from"./require-selector.js";export default e({meta:{name:"@ashstack/tanstack-router"},url:import.meta.url,packages:["@tanstack/react-router"],option:"tanstackRouter",docsWhen:"auto-enabled by `react()` when `@tanstack/react-router` is a dependency",rules:{"require-selector":r,"no-search-casts":t}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{gate as u,importedNames as m,problem as f}from"../../../lib/ast.js";import{ROUTER_MODULE as h}from"./shared.js";const d=["search","location","state"],l={search:"Drop this assertion and let the route's `validateSearch` schema type the result. An `as` here claims a shape the URL never has to honour; widen the schema, or parse the value, if the type is wrong.",routerState:"Drop this assertion and read validated search through `useSearch({ from: ... })`. `router.state.location.search` is whatever the URL held, so an `as` here is a claim about a string nobody checked."},S=(t)=>{let e=t;while(e.type==="TSAsExpression")e=e.expression;return e},c=(t)=>t?.type==="CallExpression"&&t.callee.type==="Identifier"?t.callee.name:"",y=(t)=>{if(t.type!=="MemberExpression")return"";const{property:e}=t;if(e.type==="Identifier")return t.computed?"":e.name;return e.type==="Literal"?String(e.value):""},g=(t)=>{let e=t;for(const s of d){if(e.type!=="MemberExpression"||y(e)!==s)return null;e=e.object}return e};export const noSearchCasts=f("Disallow an `as` assertion on a `useSearch()` result or on `router.state.location.search`. The route's `validateSearch` schema is what supplies the type.",{createOnce(t){const e=new Set,s=new Set,n=new Set,i=[],p=(r)=>r.type==="Identifier"?n.has(r.name):s.has(c(r));return{before(){e.clear();s.clear();n.clear();i.length=0;return u(t,h)},ImportDeclaration(r){for(const{imported:a,local:o}of m(r,h))if(a==="useSearch")e.add(o);else if(a==="useRouter")s.add(o)},VariableDeclarator(r){if(r.id.type!=="Identifier")return;if(s.has(c(r.init)))n.add(r.id.name)},TSAsExpression(r){if(r.parent.type==="TSAsExpression")return;i.push({node:r,inner:S(r.expression)})},"Program:exit"(){for(const{node:r,inner:a}of i){if(e.has(c(a))){t.report({node:r,message:l.search});continue}const o=g(a);if(o&&p(o))t.report({node:r,message:l.routerState})}}}}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{gate as h,importedNames as m,problem as p,propertyValue as i}from"../../../lib/ast.js";import{ROUTER_MODULE as l}from"./shared.js";const d=new Set(["useLocation","useRouterState"]),c={whole:"This hook subscribes to the whole router value, so the component re-renders on every navigation. Pass a `select` that returns what it reads, e.g. `{ select: location => location.pathname }`.",noSelect:"This hook subscribes to the whole router value, so the component re-renders on every navigation. Add a `select` returning the smallest value it reads.",search:"`useSearch({ strict: false })` subscribes to every route-search change. Add a `select` returning the keys this component reads."},u=(t)=>i(t,"select")!==null,f=(t)=>{const s=i(t,"strict");return s?.type==="Literal"&&s.value===!1};export const requireSelector=p("Require a `select` on `useLocation`, `useRouterState` and non-strict `useSearch`, so a component reads the smallest router value it needs instead of re-rendering on every navigation.",{createOnce(t){const s=new Set,a=new Set,n=[];return{before(){s.clear();a.clear();n.length=0;return h(t,l)},ImportDeclaration(e){for(const{imported:o,local:r}of m(e,l))if(d.has(o))s.add(r);else if(o==="useSearch")a.add(r)},CallExpression(e){if(e.callee.type==="Identifier")n.push({node:e,name:e.callee.name,options:e.arguments[0]})},"Program:exit"(){for(const{node:e,name:o,options:r}of n){if(s.has(o)){if(!r)t.report({node:e,message:c.whole});else if(!u(r))t.report({node:e,message:c.noSelect});continue}if(!a.has(o)||!f(r))continue;if(!u(r))t.report({node:e,message:c.search})}}}}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ROUTER_MODULE="@tanstack/react-router";
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import type { BanGroup, OxlintConfig, ReactNativeOptions } from "../lib/types.js";
|
|
2
|
-
export declare const reactNativeModules: import("../lib/types.js").ModuleManifest[];
|
|
3
2
|
export declare const banGroups: BanGroup[];
|
|
4
3
|
/**
|
|
5
4
|
* React Native / Expo entry — everything in react plus generic RN rules, with
|
|
6
5
|
* per-library modules (unistyles, legendList, legendState, reanimated,
|
|
7
6
|
* turboImage, skia, keyboard) auto-detected from your dependencies. Your
|
|
8
7
|
* `rules` block always overrides.
|
|
8
|
+
*
|
|
9
|
+
* @see [every rule `react-native()` sets](https://github.com/AlshehriAli0/ashStack/blob/main/packages/lint/RULES.md#react-native)
|
|
9
10
|
*/
|
|
10
11
|
declare const reactNative: (options?: ReactNativeOptions) => OxlintConfig;
|
|
12
|
+
export { reactNative };
|
|
13
|
+
export type { CoreRuleId } from "../lib/rule-types/core.js";
|
|
14
|
+
export type { ReactRuleId } from "../lib/rule-types/react.js";
|
|
15
|
+
export type { ReactNativeRuleId } from "../lib/rule-types/react-native.js";
|
|
16
|
+
export type { ReactNativeOptions, ModuleManifest } from "../lib/types.js";
|
|
11
17
|
export default reactNative;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{TEST_FILES as
|
|
1
|
+
import{TEST_FILES as s}from"../core/index.js";import{mergeConfigs as r}from"../lib/merge.js";import{composeModules as a}from"../lib/module.js";import{coreRegistry as o,reactNativeRegistry as i,reactRegistry as n}from"../lib/registry.js";import c from"../react/index.js";export const banGroups=[{packages:["react-native-gesture-handler"],restrictedImports:{paths:[{name:"react-native",importNames:["Pressable","TouchableOpacity","TouchableHighlight","TouchableWithoutFeedback","TouchableNativeFeedback"],message:"Import `Pressable` from `react-native-gesture-handler` so it cooperates with the gesture system instead of competing with it. Exception: inside a SwiftUI/Compose host view touches never reach `GestureHandlerRootView` — keep React Native's `Pressable` there and suppress this rule with the host named in the reason."}]}},{packages:["expo-router"],restrictedImports:{patterns:[{group:["@react-navigation/stack","@react-navigation/bottom-tabs"],message:"Use expo-router's `Stack` / `NativeTabs` instead of a raw react-navigation navigator."}]}},{packages:["expo-font"],restrictedImports:{paths:[{name:"expo-font",importNames:["useFonts","loadAsync"],message:"Declare the fonts in the `expo-font` config plugin so they load at build time, instead of `useFonts`/`loadAsync` at runtime."}]}},{packages:["react-native-quick-crypto"],restrictedImports:{paths:[{name:"crypto-js",message:"Use `react-native-quick-crypto` instead — `crypto-js` is pure JavaScript and runs orders of magnitude slower."}]}}];const p={"eslint/no-empty":"error","eslint/no-empty-function":"error"},m={"@ashstack/effects/no-event-handler":"off"},reactNative=(t={})=>{const e=a([...o,...n,...i],t,banGroups);return r(c(t),{jsPlugins:e.jsPlugins,globals:{__DEV__:"readonly"},rules:{...p,...m,"no-restricted-imports":["error",e.restricted],...e.rules},ignorePatterns:["**/.expo/**","**/android/**","**/ios/**"],overrides:[{files:s,rules:{"@ashstack/react-native/no-dynamic-import":"off","@ashstack/react-native/hoist-stateless-function":"off"}}]})};export{reactNative};export default reactNative;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{defineModule as e}from"../../../lib/module.js";import{noFlexInContentContainer as t}from"./no-flex-in-content-container.js";import{noIndexKeyExtractor as
|
|
1
|
+
import{defineModule as e}from"../../../lib/module.js";import{noFlexInContentContainer as t}from"./no-flex-in-content-container.js";import{noIndexKeyExtractor as o}from"./no-index-key-extractor.js";import{noInlineRenderItemProps as r}from"./no-inline-render-item-props.js";import{noMixedChildren as n}from"./no-mixed-children.js";import{noRemountKey as i}from"./no-remount-key.js";import{noScrollviewMap as p}from"./no-scrollview-map.js";import{noUnsupportedProps as s}from"./no-unsupported-props.js";import{requiredProps as a}from"./required-props.js";import{typedItemsNeedItemType as m}from"./typed-items-need-item-type.js";export default e({meta:{name:"@ashstack/legend-list"},rules:{"required-props":a,"no-index-key-extractor":o,"no-remount-key":i,"no-inline-render-item-props":r,"no-mixed-children":n,"no-flex-in-content-container":t,"typed-items-need-item-type":m,"no-scrollview-map":p,"no-unsupported-props":s},url:import.meta.url,packages:["@legendapp/list"],option:"legendList",docsWhen:"auto-enabled when `@legendapp/list` is a dependency",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."}]}});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{gate as
|
|
1
|
+
import{gate as m,problem as c}from"../../../lib/ast.js";const l=new Set(["useMemo","useCallback","memo"]),u=["babel-plugin-react-compiler","react-compiler-runtime","react-compiler-marker"],h=/^why:/i,p=10,f=(e)=>e.type==="Identifier"&&l.has(e.name)?e.name:null,d=(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 l.has(t)?t:null},b=(e)=>{if(e.type!=="CallExpression")return null;const{callee:t}=e;if(t.type==="Identifier")return f(t);if(t.type==="MemberExpression")return d(t);return null},g=(e)=>{const t=[0];for(let o=0;o<e.length;o++)if(e.charCodeAt(o)===p)t.push(o+1);return t},y=(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},w=(e,t)=>{const o=new Set;for(const n of e){if(!h.test(n.value.trim()))continue;for(let r=t(n.start);r<=t(n.end);r++)o.add(r)}return o};export const noManualMemo=c("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.",{meta:{packages:u},createOnce(e){let t=[];return{before(){t=[];return m(e,"useMemo","useCallback","memo")},CallExpression(o){const n=b(o);if(n!==null)t.push({node:o,name:n})},"Program:exit"(){if(t.length===0)return;const o=g(e.sourceCode.getText()),n=(s)=>y(o,s),r=w(e.sourceCode.getAllComments(),n);for(const{node:s,name:a}of t){const i=n(s.start);if(r.has(i)||r.has(i-1))continue;e.report({node:s,message:`The React Compiler memoises on a best-effort basis, not a guarantee, so a \`${a}\` is allowed where you know the cost is real: something rendered per row in a list, or a computation you measured as heavy. Add a \`// why:\` line directly above it naming which of the two applies. If neither does, delete the \`${a}\`.`})}}}}});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ashstack/lint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -24,10 +24,6 @@
|
|
|
24
24
|
"expo"
|
|
25
25
|
],
|
|
26
26
|
"exports": {
|
|
27
|
-
".": {
|
|
28
|
-
"types": "./dist/index.d.ts",
|
|
29
|
-
"default": "./dist/index.js"
|
|
30
|
-
},
|
|
31
27
|
"./core": {
|
|
32
28
|
"types": "./dist/core/index.d.ts",
|
|
33
29
|
"default": "./dist/core/index.js"
|
|
@@ -42,14 +38,14 @@
|
|
|
42
38
|
}
|
|
43
39
|
},
|
|
44
40
|
"files": [
|
|
45
|
-
"dist"
|
|
41
|
+
"dist",
|
|
42
|
+
"vendor",
|
|
43
|
+
"!dist/index.js",
|
|
44
|
+
"!dist/index.d.ts"
|
|
46
45
|
],
|
|
47
46
|
"publishConfig": {
|
|
48
47
|
"access": "public"
|
|
49
48
|
},
|
|
50
|
-
"dependencies": {
|
|
51
|
-
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.1"
|
|
52
|
-
},
|
|
53
49
|
"peerDependencies": {
|
|
54
50
|
"oxlint": ">=1.79.0",
|
|
55
51
|
"oxlint-tsgolint": ">=7.0.0"
|
|
@@ -60,7 +56,7 @@
|
|
|
60
56
|
}
|
|
61
57
|
},
|
|
62
58
|
"scripts": {
|
|
63
|
-
"prepack": "rm -rf dist && tsc && bun build $(find dist -name '*.js') --minify --target=node --root dist --outdir dist --no-bundle"
|
|
59
|
+
"prepack": "rm -rf dist && tsc && bun build $(find dist -name '*.js') --minify --target=node --root dist --outdir dist --no-bundle && bun ../../scripts/prune-types.ts"
|
|
64
60
|
},
|
|
65
61
|
"devDependencies": {
|
|
66
62
|
"@types/node": "^22.10.0",
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Nick van Dyke
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the “Software”), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# eslint-plugin-react-you-might-not-need-an-effect
|
|
2
|
+
|
|
3
|
+
Vendored verbatim at **1.0.2**, MIT, from
|
|
4
|
+
https://github.com/NickvanDyke/eslint-plugin-react-you-might-not-need-an-effect
|
|
5
|
+
|
|
6
|
+
`index.mjs` is its published `dist/index.mjs`, unmodified. `LICENSE` is its own.
|
|
7
|
+
|
|
8
|
+
## Why it is here
|
|
9
|
+
|
|
10
|
+
Depending on the package installs 12.3 MB across 60 packages, because it
|
|
11
|
+
declares `eslint` as a non-optional peer and npm installs peers. Its code never
|
|
12
|
+
imports eslint — oxlint loads it through its own plugin bridge — so the whole
|
|
13
|
+
ESLint tree sat in every consumer's `node_modules` without ever running. The
|
|
14
|
+
file itself is 32 KB and imports only `globals`, which stays a dependency.
|
|
15
|
+
|
|
16
|
+
## Updating
|
|
17
|
+
|
|
18
|
+
npm pack eslint-plugin-react-you-might-not-need-an-effect@<version>
|
|
19
|
+
tar xzf eslint-plugin-*.tgz
|
|
20
|
+
bun run vendor:effects --from=package/dist/index.mjs
|
|
21
|
+
|
|
22
|
+
Then update the version above. `scripts/vendor-effects.ts` drops the config
|
|
23
|
+
presets and the `globals` import they need, and refuses to write a file that
|
|
24
|
+
still imports anything. `tests/effects.test.ts` fails if the rule ids this
|
|
25
|
+
package enables stop matching the ones the file exports.
|