@ashstack/lint 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +1 -1
  2. package/dist/core/index.js +1 -222
  3. package/dist/core/rules/core/components-tsx-only.js +1 -37
  4. package/dist/core/rules/core/hoist-intl.js +1 -32
  5. package/dist/core/rules/core/index.js +1 -18
  6. package/dist/core/rules/core/no-comments.js +1 -194
  7. package/dist/core/rules/core/no-naming-convention.js +1 -143
  8. package/dist/core/rules/core/no-packed-condition.d.ts +2 -0
  9. package/dist/core/rules/core/no-packed-condition.js +1 -0
  10. package/dist/core/rules/core/use-design-system.js +1 -147
  11. package/dist/core/rules/zod/index.js +1 -12
  12. package/dist/core/rules/zod/prefer-enum.js +1 -44
  13. package/dist/index.js +1 -3
  14. package/dist/lib/ast.js +1 -162
  15. package/dist/lib/detect.js +1 -42
  16. package/dist/lib/merge.js +1 -18
  17. package/dist/lib/module.js +1 -49
  18. package/dist/lib/types.js +1 -1
  19. package/dist/react/index.js +1 -126
  20. package/dist/react/rules/i18n/index.js +1 -16
  21. package/dist/react/rules/i18n/no-bare-attrs.js +1 -38
  22. package/dist/react/rules/i18n/no-bare-text.js +1 -26
  23. package/dist/react/rules/i18n/no-bare-toast.js +1 -38
  24. package/dist/react/rules/query/index.js +1 -20
  25. package/dist/react/rules/query/next-page-param-undefined.js +1 -26
  26. package/dist/react/rules/query/no-deprecated-filters.js +1 -63
  27. package/dist/react/rules/query/no-fetch-in-query-fn.js +1 -22
  28. package/dist/react/rules/query/no-inline-keys.js +1 -107
  29. package/dist/react/rules/query/require-destructured-hooks.js +1 -41
  30. package/dist/react/rules/query/shared.js +1 -10
  31. package/dist/react/rules/zustand/index.js +1 -12
  32. package/dist/react/rules/zustand/require-selector.js +1 -62
  33. package/dist/react-native/index.js +1 -113
  34. package/dist/react-native/rules/keyboard/index.js +1 -18
  35. package/dist/react-native/rules/legend-list/index.js +1 -51
  36. package/dist/react-native/rules/legend-list/no-flex-in-content-container.js +1 -77
  37. package/dist/react-native/rules/legend-list/no-index-key-extractor.js +1 -26
  38. package/dist/react-native/rules/legend-list/no-inline-data.js +1 -25
  39. package/dist/react-native/rules/legend-list/no-inline-extra-data.js +1 -21
  40. package/dist/react-native/rules/legend-list/no-inline-render-item-props.js +1 -35
  41. package/dist/react-native/rules/legend-list/no-mixed-children.js +1 -24
  42. package/dist/react-native/rules/legend-list/no-remount-key.js +1 -18
  43. package/dist/react-native/rules/legend-list/no-scrollview-map.js +1 -27
  44. package/dist/react-native/rules/legend-list/no-unsupported-props.js +1 -29
  45. package/dist/react-native/rules/legend-list/required-props.js +1 -23
  46. package/dist/react-native/rules/legend-list/shared.js +1 -25
  47. package/dist/react-native/rules/legend-list/typed-items-need-item-type.js +1 -49
  48. package/dist/react-native/rules/legend-state/index.js +1 -35
  49. package/dist/react-native/rules/legend-state/naming.js +1 -26
  50. package/dist/react-native/rules/legend-state/no-assignment.js +1 -31
  51. package/dist/react-native/rules/legend-state/no-nested-observable.js +1 -23
  52. package/dist/react-native/rules/legend-state/no-object-selector.js +1 -47
  53. package/dist/react-native/rules/legend-state/no-observable-in-component.js +1 -46
  54. package/dist/react-native/rules/legend-state/no-peek-in-selector.js +1 -67
  55. package/dist/react-native/rules/legend-state/no-react-mirror.js +1 -33
  56. package/dist/react-native/rules/legend-state/no-untracked-get-in-jsx.js +1 -52
  57. package/dist/react-native/rules/legend-state/shared.js +1 -23
  58. package/dist/react-native/rules/react-native/hoist-stateless-function.js +1 -59
  59. package/dist/react-native/rules/react-native/index.js +1 -50
  60. package/dist/react-native/rules/react-native/no-conditional-style-array.js +1 -23
  61. package/dist/react-native/rules/react-native/no-dynamic-import.js +1 -18
  62. package/dist/react-native/rules/react-native/no-keyboard-will-events.js +1 -18
  63. package/dist/react-native/rules/react-native/no-leaked-render.js +1 -43
  64. package/dist/react-native/rules/react-native/no-manual-memo.js +1 -87
  65. package/dist/react-native/rules/react-native/no-redundant-view-nesting.js +1 -36
  66. package/dist/react-native/rules/react-native/no-rn-image-network-source.js +1 -58
  67. package/dist/react-native/rules/react-native/no-rn-namespace-import.js +1 -38
  68. package/dist/react-native/rules/react-native/no-scroll-position-state.js +1 -26
  69. package/dist/react-native/rules/react-native/no-unlabeled-icon-pressable.js +1 -62
  70. package/dist/react-native/rules/reanimated/animated-reaction-safety.js +1 -110
  71. package/dist/react-native/rules/reanimated/animated-style-needs-animated-component.js +1 -72
  72. package/dist/react-native/rules/reanimated/animated-updater-purity.js +1 -38
  73. package/dist/react-native/rules/reanimated/gpu-properties-only.js +1 -80
  74. package/dist/react-native/rules/reanimated/hoist-layout-animation-builder.js +1 -43
  75. package/dist/react-native/rules/reanimated/index.js +1 -51
  76. package/dist/react-native/rules/reanimated/interpolate-needs-clamp.js +1 -41
  77. package/dist/react-native/rules/reanimated/no-react-state-from-continuous-worklet.js +1 -86
  78. package/dist/react-native/rules/reanimated/no-shared-value-dot-value.js +1 -112
  79. package/dist/react-native/rules/reanimated/prefer-lazy-shared-value-initializer.js +1 -24
  80. package/dist/react-native/rules/reanimated/schedule-on-rn-scope.js +1 -21
  81. package/dist/react-native/rules/reanimated/shared-value-usage.js +1 -105
  82. package/dist/react-native/rules/reanimated/shared.js +1 -13
  83. package/dist/react-native/rules/skia/canvas-opaque.js +1 -49
  84. package/dist/react-native/rules/skia/index.js +1 -21
  85. package/dist/react-native/rules/turbo-image/index.js +1 -14
  86. package/dist/react-native/rules/turbo-image/require-cache-policy.js +1 -21
  87. package/dist/react-native/rules/turbo-image/require-resize.js +1 -22
  88. package/dist/react-native/rules/unistyles/animated-theme.js +1 -37
  89. package/dist/react-native/rules/unistyles/content-container.js +1 -73
  90. package/dist/react-native/rules/unistyles/in-sheet.js +1 -116
  91. package/dist/react-native/rules/unistyles/index.js +1 -58
  92. package/dist/react-native/rules/unistyles/insets.js +1 -92
  93. package/dist/react-native/rules/unistyles/no-hardcoded-color.js +1 -30
  94. package/dist/react-native/rules/unistyles/no-hardcoded-spacing.js +1 -78
  95. package/dist/react-native/rules/unistyles/no-margin.js +1 -19
  96. package/dist/react-native/rules/unistyles/no-style-spread.js +1 -41
  97. package/dist/react-native/rules/unistyles/no-unused-styles.js +1 -99
  98. package/dist/react-native/rules/unistyles/rtl-style-call.js +1 -42
  99. package/dist/react-native/rules/unistyles/shared.js +1 -70
  100. package/dist/react-native/rules/unistyles/theme-screen-component.js +1 -22
  101. package/dist/react-native/rules/unistyles/theme-style-attr.js +1 -18
  102. package/dist/react-native/stylesheet.js +1 -44
  103. package/package.json +3 -4
  104. package/RULES.md +0 -2881
@@ -1,25 +1 @@
1
- import { attributeName, componentName } from "../../../lib/ast.js";
2
- export const LIST = "LegendList";
3
- export const isListElement = (node) => node?.type === "JSXElement" && componentName(node.openingElement.name) === LIST;
4
- export const attributesOf = (node) => node.type === "JSXElement" ? node.openingElement.attributes : [];
5
- export const attributeNamed = (node, wanted) => {
6
- for (const attribute of attributesOf(node)) {
7
- if (attribute.type === "JSXAttribute" && attributeName(attribute) === wanted)
8
- return attribute;
9
- }
10
- return undefined;
11
- };
12
- export const hasSpread = (node) => attributesOf(node).some(attribute => attribute.type === "JSXSpreadAttribute");
13
- export const expressionOf = (attribute) => {
14
- if (attribute?.type !== "JSXAttribute")
15
- return null;
16
- const { value } = attribute;
17
- return value?.type === "JSXExpressionContainer" ? value.expression : null;
18
- };
19
- export const literalKind = (node) => {
20
- if (node?.type === "ObjectExpression")
21
- return "object";
22
- if (node?.type === "ArrayExpression")
23
- return "array";
24
- return null;
25
- };
1
+ import{attributeName as n,componentName as o}from"../../../lib/ast.js";export const LIST="LegendList",isListElement=(t)=>t?.type==="JSXElement"&&o(t.openingElement.name)===LIST,attributesOf=(t)=>t.type==="JSXElement"?t.openingElement.attributes:[],attributeNamed=(t,e)=>{for(const r of attributesOf(t))if(r.type==="JSXAttribute"&&n(r)===e)return r;return},hasSpread=(t)=>attributesOf(t).some((e)=>e.type==="JSXSpreadAttribute"),expressionOf=(t)=>{if(t?.type!=="JSXAttribute")return null;const{value:e}=t;return e?.type==="JSXExpressionContainer"?e.expression:null},literalKind=(t)=>{if(t?.type==="ObjectExpression")return"object";if(t?.type==="ArrayExpression")return"array";return null};
@@ -1,49 +1 @@
1
- import { gate, problem, subtreeHas } from "../../../lib/ast.js";
2
- import { attributeNamed, expressionOf, hasSpread, isListElement, LIST } from "./shared.js";
3
- const branchesOnItemType = (node) => subtreeHas(node, current => current.type === "MemberExpression" && current.property.type === "Identifier" && current.property.name === "type");
4
- export const typedItemsNeedItemType = problem("Require `getItemType` when a row branches on `item.type`. Without it every layout shares one recycling pool and one size average.", {
5
- createOnce(context) {
6
- let rowRenderersByName;
7
- let listsWithoutItemType;
8
- const reportMissingItemTypeAtEndOfFile = () => {
9
- for (const { node, renderItem } of listsWithoutItemType) {
10
- const expression = expressionOf(renderItem);
11
- const body = expression?.type === "Identifier" ? rowRenderersByName.get(expression.name) : expression;
12
- if (body === undefined || body === null || !branchesOnItemType(body))
13
- continue;
14
- context.report({
15
- node: node.openingElement,
16
- message: "Add `getItemType={item => item.type}` to match the branch this row makes on `item.type`. It gives each layout its own recycling pool and its own measured-size average, instead of handing a header's view to a photo row.",
17
- });
18
- }
19
- };
20
- return {
21
- before() {
22
- rowRenderersByName = new Map();
23
- listsWithoutItemType = [];
24
- return gate(context, LIST);
25
- },
26
- FunctionDeclaration(node) {
27
- if (node.id?.type === "Identifier")
28
- rowRenderersByName.set(node.id.name, node);
29
- },
30
- VariableDeclarator(node) {
31
- const { id, init } = node;
32
- if (id.type !== "Identifier" || init === null)
33
- return;
34
- rowRenderersByName.set(id.name, init);
35
- },
36
- JSXElement(node) {
37
- if (!isListElement(node) || hasSpread(node))
38
- return;
39
- if (attributeNamed(node, "getItemType"))
40
- return;
41
- const renderItem = attributeNamed(node, "renderItem");
42
- if (!renderItem)
43
- return;
44
- listsWithoutItemType.push({ node, renderItem });
45
- },
46
- "Program:exit": reportMissingItemTypeAtEndOfFile,
47
- };
48
- },
49
- });
1
+ import{gate as p,problem as m,subtreeHas as y}from"../../../lib/ast.js";import{attributeNamed as a,expressionOf as d,hasSpread as l,isListElement as c,LIST as f}from"./shared.js";const u=(i)=>y(i,(t)=>t.type==="MemberExpression"&&t.property.type==="Identifier"&&t.property.name==="type");export const typedItemsNeedItemType=m("Require `getItemType` when a row branches on `item.type`. Without it every layout shares one recycling pool and one size average.",{createOnce(i){let t,o;return{before(){t=new Map;o=[];return p(i,f)},FunctionDeclaration(e){if(e.id?.type==="Identifier")t.set(e.id.name,e)},VariableDeclarator(e){const{id:r,init:n}=e;if(r.type!=="Identifier"||n===null)return;t.set(r.name,n)},JSXElement(e){if(!c(e)||l(e))return;if(a(e,"getItemType"))return;const r=a(e,"renderItem");if(!r)return;o.push({node:e,renderItem:r})},"Program:exit":()=>{for(const{node:e,renderItem:r}of o){const n=d(r),s=n?.type==="Identifier"?t.get(n.name):n;if(s===void 0||s===null||!u(s))continue;i.report({node:e.openingElement,message:"Add `getItemType={item => item.type}` to match the branch this row makes on `item.type`. It gives each layout its own recycling pool and its own measured-size average, instead of handing a header's view to a photo row."})}}}}});
@@ -1,35 +1 @@
1
- import { defineModule } from "../../../lib/module.js";
2
- import { naming } from "./naming.js";
3
- import { noAssignment } from "./no-assignment.js";
4
- import { noNestedObservable } from "./no-nested-observable.js";
5
- import { noObjectSelector } from "./no-object-selector.js";
6
- import { noObservableInComponent } from "./no-observable-in-component.js";
7
- import { noPeekInSelector } from "./no-peek-in-selector.js";
8
- import { noReactMirror } from "./no-react-mirror.js";
9
- import { noUntrackedGetInJsx } from "./no-untracked-get-in-jsx.js";
10
- export default defineModule({
11
- meta: { name: "@ashstack/legend-state" },
12
- url: import.meta.url,
13
- packages: ["@legendapp/state"],
14
- option: "legendState",
15
- docsWhen: "auto-enabled when `@legendapp/state` is a dependency",
16
- rules: {
17
- "no-assignment": noAssignment,
18
- naming: naming,
19
- "no-nested-observable": noNestedObservable,
20
- "no-react-mirror": noReactMirror,
21
- "no-untracked-get-in-jsx": noUntrackedGetInJsx,
22
- "no-peek-in-selector": noPeekInSelector,
23
- "no-object-selector": noObjectSelector,
24
- "no-observable-in-component": noObservableInComponent,
25
- },
26
- restrictedImports: {
27
- paths: [
28
- {
29
- name: "@legendapp/state/react",
30
- importNames: ["use$", "useSelector"],
31
- message: "Use `useValue` from `@legendapp/state/react`; `use$` and `useSelector` are deprecated.",
32
- },
33
- ],
34
- },
35
- });
1
+ import{defineModule as e}from"../../../lib/module.js";import{naming as o}from"./naming.js";import{noAssignment as t}from"./no-assignment.js";import{noNestedObservable as n}from"./no-nested-observable.js";import{noObjectSelector as r}from"./no-object-selector.js";import{noObservableInComponent as a}from"./no-observable-in-component.js";import{noPeekInSelector as s}from"./no-peek-in-selector.js";import{noReactMirror as m}from"./no-react-mirror.js";import{noUntrackedGetInJsx as p}from"./no-untracked-get-in-jsx.js";export default e({meta:{name:"@ashstack/legend-state"},url:import.meta.url,packages:["@legendapp/state"],option:"legendState",docsWhen:"auto-enabled when `@legendapp/state` is a dependency",rules:{"no-assignment":t,naming:o,"no-nested-observable":n,"no-react-mirror":m,"no-untracked-get-in-jsx":p,"no-peek-in-selector":s,"no-object-selector":r,"no-observable-in-component":a},restrictedImports:{paths:[{name:"@legendapp/state/react",importNames:["use$","useSelector"],message:"Use `useValue` from `@legendapp/state/react`; `use$` and `useSelector` are deprecated."}]}});
@@ -1,26 +1 @@
1
- import { gate, problem } from "../../../lib/ast.js";
2
- import { factoryCalled, OBS } from "./shared.js";
3
- export const naming = problem("A variable initialized from `observable()` or `useObservable()` needs a trailing `$`. The other rules in this module key off that suffix.", {
4
- createOnce(context) {
5
- return {
6
- before() {
7
- return gate(context, "observable", "useObservable");
8
- },
9
- VariableDeclarator(node) {
10
- const { id, init } = node;
11
- const factory = factoryCalled(init);
12
- if (!factory || id.type !== "Identifier")
13
- return;
14
- const { name } = id;
15
- if (OBS.test(name))
16
- return;
17
- context.report({
18
- node: id,
19
- message: name.includes("$")
20
- ? `Give this observable a trailing \`$\` and no other. The suffix is how a reader tells an observable from a plain value, and every other observable rule keys off it.`
21
- : `Rename this to \`${name}$\`. The trailing \`$\` is how a reader tells an observable from a plain value, and every other observable rule keys off it.`,
22
- });
23
- },
24
- };
25
- },
26
- });
1
+ import{gate as i,problem as l}from"../../../lib/ast.js";import{factoryCalled as s,OBS as n}from"./shared.js";export const naming=l("A variable initialized from `observable()` or `useObservable()` needs a trailing `$`. The other rules in this module key off that suffix.",{createOnce(a){return{before(){return i(a,"observable","useObservable")},VariableDeclarator(o){const{id:e,init:t}=o;if(!s(t)||e.type!=="Identifier")return;const{name:r}=e;if(n.test(r))return;a.report({node:e,message:r.includes("$")?"Give this observable a trailing `$` and no other. The suffix is how a reader tells an observable from a plain value, and every other observable rule keys off it.":`Rename this to \`${r}$\`. The trailing \`$\` is how a reader tells an observable from a plain value, and every other observable rule keys off it.`})}}}});
@@ -1,31 +1 @@
1
- import { gate, problem } from "../../../lib/ast.js";
2
- import { isObservableRef, textOf } from "./shared.js";
3
- export const noAssignment = problem("Write an observable with `.set(...)` or `.assign({...})`. Assigning or incrementing it with an operator is a silent no-op.", {
4
- createOnce(context) {
5
- return {
6
- before() {
7
- return gate(context, "$");
8
- },
9
- AssignmentExpression(node) {
10
- const { left } = node;
11
- if (!isObservableRef(left))
12
- return;
13
- const target = textOf(context, left);
14
- context.report({
15
- node,
16
- message: `Write through \`${target}.set(...)\`, or \`.assign({...})\` to merge several fields. Assigning to an observable with \`${node.operator}\` replaces the observable in place of its value, so nothing downstream ever updates.`,
17
- });
18
- },
19
- UpdateExpression(node) {
20
- const { argument, operator } = node;
21
- if (!isObservableRef(argument))
22
- return;
23
- const target = textOf(context, argument);
24
- context.report({
25
- node,
26
- message: `Use \`${target}.set(v => v ${operator[0]} 1)\` — \`${operator}\` on an observable is a silent no-op.`,
27
- });
28
- },
29
- };
30
- },
31
- });
1
+ import{gate as a,problem as g}from"../../../lib/ast.js";import{isObservableRef as o,textOf as n}from"./shared.js";export const noAssignment=g("Write an observable with `.set(...)` or `.assign({...})`. Assigning or incrementing it with an operator is a silent no-op.",{createOnce(r){return{before(){return a(r,"$")},AssignmentExpression(e){const{left:s}=e;if(!o(s))return;const t=n(r,s);r.report({node:e,message:`Write through \`${t}.set(...)\`, or \`.assign({...})\` to merge several fields. Assigning to an observable with \`${e.operator}\` replaces the observable in place of its value, so nothing downstream ever updates.`})},UpdateExpression(e){const{argument:s,operator:t}=e;if(!o(s))return;const i=n(r,s);r.report({node:e,message:`Use \`${i}.set(v => v ${t[0]} 1)\` — \`${t}\` on an observable is a silent no-op.`})}}}});
@@ -1,23 +1 @@
1
- import { gate, problem } from "../../../lib/ast.js";
2
- import { factoryCalled, isObservableRef } from "./shared.js";
3
- export const noNestedObservable = problem("Never pass an observable to `observable()` or `useObservable()`. The wrapper is a second node, and reads and writes on it never reach the original.", {
4
- createOnce(context) {
5
- return {
6
- before() {
7
- return gate(context, "observable", "useObservable");
8
- },
9
- CallExpression(node) {
10
- const factory = factoryCalled(node);
11
- if (!factory)
12
- return;
13
- const argument = node.arguments[0];
14
- if (!argument || !isObservableRef(argument))
15
- return;
16
- context.report({
17
- node: argument,
18
- message: `Use the existing observable reference directly instead of passing it to \`${factory}()\`; wrapping it creates a second node whose reads and writes never reach the original.`,
19
- });
20
- },
21
- };
22
- },
23
- });
1
+ import{gate as o,problem as t}from"../../../lib/ast.js";import{factoryCalled as n,isObservableRef as i}from"./shared.js";export const noNestedObservable=t("Never pass an observable to `observable()` or `useObservable()`. The wrapper is a second node, and reads and writes on it never reach the original.",{createOnce(r){return{before(){return o(r,"observable","useObservable")},CallExpression(s){const a=n(s);if(!a)return;const e=s.arguments[0];if(!e||!i(e))return;r.report({node:e,message:`Use the existing observable reference directly instead of passing it to \`${a}()\`; wrapping it creates a second node whose reads and writes never reach the original.`})}}}});
@@ -1,47 +1 @@
1
- import { FUNCTION_TYPES, crossesFunctionBefore, findInSubtree, gate, problem } from "../../../lib/ast.js";
2
- const literalKind = (node) => {
3
- if (node.type === "ObjectExpression")
4
- return "object";
5
- if (node.type === "ArrayExpression")
6
- return "array";
7
- return null;
8
- };
9
- /** What a selector hands back, whether written as an expression body or as a `return`. */
10
- const returnedValue = (fn) => {
11
- const { body } = fn.type === "ArrowFunctionExpression" || fn.type === "FunctionExpression" ? fn : { body: null };
12
- if (body === null)
13
- return null;
14
- if (body.type !== "BlockStatement")
15
- return body;
16
- const found = findInSubtree(body, (current) => current.type === "ReturnStatement");
17
- if (found?.type !== "ReturnStatement" || crossesFunctionBefore(found, body, FUNCTION_TYPES))
18
- return null;
19
- return found.argument;
20
- };
21
- export const noObjectSelector = problem("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.", {
22
- createOnce(context) {
23
- return {
24
- before() {
25
- return gate(context, "useValue");
26
- },
27
- CallExpression(node) {
28
- const { callee } = node;
29
- if (callee.type !== "Identifier" || callee.name !== "useValue")
30
- return;
31
- const argument = node.arguments[0];
32
- if (argument?.type !== "ArrowFunctionExpression" && argument?.type !== "FunctionExpression")
33
- return;
34
- const returned = returnedValue(argument);
35
- if (returned === null)
36
- return;
37
- const kind = literalKind(returned);
38
- if (kind === null)
39
- return;
40
- context.report({
41
- node: returned,
42
- message: `Return the primitive that decides the render from this selector, or call \`useValue\` once per field. A new ${kind} each run has a new identity, so the component re-renders on every store change.`,
43
- });
44
- },
45
- };
46
- },
47
- });
1
+ import{FUNCTION_TYPES as s,crossesFunctionBefore as i,findInSubtree as l,gate as a,problem as c}from"../../../lib/ast.js";const p=(e)=>{if(e.type==="ObjectExpression")return"object";if(e.type==="ArrayExpression")return"array";return null},y=(e)=>{const{body:r}=e.type==="ArrowFunctionExpression"||e.type==="FunctionExpression"?e:{body:null};if(r===null)return null;if(r.type!=="BlockStatement")return r;const t=l(r,(n)=>n.type==="ReturnStatement");if(t?.type!=="ReturnStatement"||i(t,r,s))return null;return t.argument};export const noObjectSelector=c("A `useValue` selector that builds a new object or array returns a fresh identity every run. The component then re-renders on every store change.",{createOnce(e){return{before(){return a(e,"useValue")},CallExpression(r){const{callee:t}=r;if(t.type!=="Identifier"||t.name!=="useValue")return;const n=r.arguments[0];if(n?.type!=="ArrowFunctionExpression"&&n?.type!=="FunctionExpression")return;const o=y(n);if(o===null)return;const u=p(o);if(u===null)return;e.report({node:o,message:`Return the primitive that decides the render from this selector, or call \`useValue\` once per field. A new ${u} each run has a new identity, so the component re-renders on every store change.`})}}}});
@@ -1,46 +1 @@
1
- import { COMPONENT_OR_HOOK, gate, isFunction, problem } from "../../../lib/ast.js";
2
- export const noObservableInComponent = problem("Calling `observable()` inside a component or hook makes a new observable on every render. Use `useObservable()` or a module-level store instead.", {
3
- createOnce(context) {
4
- let componentDepth = 0;
5
- const enter = (name) => {
6
- if (name !== undefined && COMPONENT_OR_HOOK.test(name))
7
- componentDepth++;
8
- };
9
- const exit = (name) => {
10
- if (name !== undefined && COMPONENT_OR_HOOK.test(name))
11
- componentDepth--;
12
- };
13
- const declaredName = (id) => id?.type === "Identifier" ? id.name : undefined;
14
- return {
15
- before() {
16
- componentDepth = 0;
17
- return gate(context, "observable");
18
- },
19
- FunctionDeclaration(node) {
20
- enter(declaredName(node.id));
21
- },
22
- "FunctionDeclaration:exit"(node) {
23
- exit(declaredName(node.id));
24
- },
25
- VariableDeclarator(node) {
26
- if (isFunction(node.init))
27
- enter(declaredName(node.id));
28
- },
29
- "VariableDeclarator:exit"(node) {
30
- if (isFunction(node.init))
31
- exit(declaredName(node.id));
32
- },
33
- CallExpression(node) {
34
- if (componentDepth === 0)
35
- return;
36
- const { callee } = node;
37
- if (callee.type !== "Identifier" || callee.name !== "observable")
38
- return;
39
- context.report({
40
- node,
41
- message: "Use `useObservable()` for component-lifetime state, or move this observable into a store in `src/stores` and import it. `observable()` inside a component makes a new observable every render, so nothing that read the previous one is listening.",
42
- });
43
- },
44
- };
45
- },
46
- });
1
+ import{COMPONENT_OR_HOOK as s,gate as b,isFunction as l,problem as c}from"../../../lib/ast.js";export const noObservableInComponent=c("Calling `observable()` inside a component or hook makes a new observable on every render. Use `useObservable()` or a module-level store instead.",{createOnce(n){let r=0;const i=(e)=>{if(e!==void 0&&s.test(e))r++},o=(e)=>{if(e!==void 0&&s.test(e))r--},t=(e)=>e?.type==="Identifier"?e.name:void 0;return{before(){r=0;return b(n,"observable")},FunctionDeclaration(e){i(t(e.id))},"FunctionDeclaration:exit"(e){o(t(e.id))},VariableDeclarator(e){if(l(e.init))i(t(e.id))},"VariableDeclarator:exit"(e){if(l(e.init))o(t(e.id))},CallExpression(e){if(r===0)return;const{callee:a}=e;if(a.type!=="Identifier"||a.name!=="observable")return;n.report({node:e,message:"Use `useObservable()` for component-lifetime state, or move this observable into a store in `src/stores` and import it. `observable()` inside a component makes a new observable every render, so nothing that read the previous one is listening."})}}}});
@@ -1,67 +1 @@
1
- import { gate, isFunction, problem } from "../../../lib/ast.js";
2
- import { isObservableRef, textOf } from "./shared.js";
3
- const TRACKING_CONTEXT_CALLEES = new Set([
4
- "useValue",
5
- "observe",
6
- "useObserve",
7
- "useObserveEffect",
8
- "when",
9
- "whenReady",
10
- ]);
11
- const trackingCallbackOf = (node) => {
12
- const { callee } = node;
13
- if (callee.type !== "Identifier")
14
- return null;
15
- if (!TRACKING_CONTEXT_CALLEES.has(callee.name))
16
- return null;
17
- const argument = node.arguments[0];
18
- return argument && isFunction(argument) ? argument : null;
19
- };
20
- const peekedObservable = (callee) => {
21
- if (callee.type !== "MemberExpression" || callee.computed)
22
- return null;
23
- if (callee.property.type !== "Identifier" || callee.property.name !== "peek")
24
- return null;
25
- const { object } = callee;
26
- return isObservableRef(object) ? object : null;
27
- };
28
- export const noPeekInSelector = problem("`peek()` never subscribes, so a selector or tracking callback that uses it never re-runs. Call `get()` there and keep `peek()` for handlers.", {
29
- createOnce(context) {
30
- let trackingCallbacks = new WeakSet();
31
- let depth = 0;
32
- const enterIfTracking = (node) => {
33
- if (trackingCallbacks.has(node))
34
- depth++;
35
- };
36
- const exitIfTracking = (node) => {
37
- if (trackingCallbacks.has(node))
38
- depth--;
39
- };
40
- return {
41
- before() {
42
- trackingCallbacks = new WeakSet();
43
- depth = 0;
44
- return gate(context, ".peek()");
45
- },
46
- CallExpression(node) {
47
- const callback = trackingCallbackOf(node);
48
- if (callback)
49
- trackingCallbacks.add(callback);
50
- if (depth === 0)
51
- return;
52
- const observable = peekedObservable(node.callee);
53
- if (!observable)
54
- return;
55
- const target = textOf(context, observable);
56
- context.report({
57
- node,
58
- message: `Use \`${target}.get()\` inside this selector and keep \`peek()\` for handlers and async work. \`peek()\` never subscribes, so nothing re-runs when the value changes and the first read is the only one.`,
59
- });
60
- },
61
- ArrowFunctionExpression: enterIfTracking,
62
- "ArrowFunctionExpression:exit": exitIfTracking,
63
- FunctionExpression: enterIfTracking,
64
- "FunctionExpression:exit": exitIfTracking,
65
- };
66
- },
67
- });
1
+ import{gate as l,isFunction as u,problem as p}from"../../../lib/ast.js";import{isObservableRef as f,textOf as k}from"./shared.js";const b=new Set(["useValue","observe","useObserve","useObserveEffect","when","whenReady"]),h=(e)=>{const{callee:r}=e;if(r.type!=="Identifier")return null;if(!b.has(r.name))return null;const n=e.arguments[0];return n&&u(n)?n:null},g=(e)=>{if(e.type!=="MemberExpression"||e.computed)return null;if(e.property.type!=="Identifier"||e.property.name!=="peek")return null;const{object:r}=e;return f(r)?r:null};export const noPeekInSelector=p("`peek()` never subscribes, so a selector or tracking callback that uses it never re-runs. Call `get()` there and keep `peek()` for handlers.",{createOnce(e){let r=new WeakSet,n=0;const s=(t)=>{if(r.has(t))n++},o=(t)=>{if(r.has(t))n--};return{before(){r=new WeakSet;n=0;return l(e,".peek()")},CallExpression(t){const i=h(t);if(i)r.add(i);if(n===0)return;const a=g(t.callee);if(!a)return;const c=k(e,a);e.report({node:t,message:`Use \`${c}.get()\` inside this selector and keep \`peek()\` for handlers and async work. \`peek()\` never subscribes, so nothing re-runs when the value changes and the first read is the only one.`})},ArrowFunctionExpression:s,"ArrowFunctionExpression:exit":o,FunctionExpression:s,"FunctionExpression:exit":o}}});
@@ -1,33 +1 @@
1
- import { gate, problem } from "../../../lib/ast.js";
2
- import { isObservableRef } from "./shared.js";
3
- const OBSERVABLE_READS = new Set(["get", "peek"]);
4
- const readsObservable = (node) => {
5
- if (node?.type !== "CallExpression")
6
- return false;
7
- const { callee } = node;
8
- if (callee.type !== "MemberExpression" || callee.computed)
9
- return false;
10
- if (callee.property.type !== "Identifier" || !OBSERVABLE_READS.has(callee.property.name))
11
- return false;
12
- return isObservableRef(callee.object);
13
- };
14
- export const noReactMirror = problem("Seeding `useState` from an observable's `get()` or `peek()` gives the value two owners. Read it with `useValue(...)` where it renders.", {
15
- createOnce(context) {
16
- return {
17
- before() {
18
- return gate(context, "useState");
19
- },
20
- CallExpression(node) {
21
- const { callee } = node;
22
- if (callee.type !== "Identifier" || callee.name !== "useState")
23
- return;
24
- if (!readsObservable(node.arguments[0]))
25
- return;
26
- context.report({
27
- node,
28
- message: "Drop this `useState` mirror and read the observable with `useValue(...)` where it renders, so the observable stays the single owner of the value.",
29
- });
30
- },
31
- };
32
- },
33
- });
1
+ import{gate as s,problem as a}from"../../../lib/ast.js";import{isObservableRef as o}from"./shared.js";const n=new Set(["get","peek"]),i=(r)=>{if(r?.type!=="CallExpression")return!1;const{callee:e}=r;if(e.type!=="MemberExpression"||e.computed)return!1;if(e.property.type!=="Identifier"||!n.has(e.property.name))return!1;return o(e.object)};export const noReactMirror=a("Seeding `useState` from an observable's `get()` or `peek()` gives the value two owners. Read it with `useValue(...)` where it renders.",{createOnce(r){return{before(){return s(r,"useState")},CallExpression(e){const{callee:t}=e;if(t.type!=="Identifier"||t.name!=="useState")return;if(!i(e.arguments[0]))return;r.report({node:e,message:"Drop this `useState` mirror and read the observable with `useValue(...)` where it renders, so the observable stays the single owner of the value."})}}}});
@@ -1,52 +1 @@
1
- import { gate, problem } from "../../../lib/ast.js";
2
- import { isObservableRef, textOf } from "./shared.js";
3
- export const noUntrackedGetInJsx = problem("A `get()` placed directly in a JSX expression container is a plain read. The value renders once and never updates.", {
4
- createOnce(context) {
5
- let functionDepth = 0;
6
- let functionDepthAtContainerStart = [];
7
- const enterFunction = () => {
8
- functionDepth++;
9
- };
10
- const exitFunction = () => {
11
- functionDepth--;
12
- };
13
- const directlyInJsxContainer = () => functionDepthAtContainerStart.length > 0 &&
14
- functionDepthAtContainerStart[functionDepthAtContainerStart.length - 1] === functionDepth;
15
- return {
16
- before() {
17
- functionDepth = 0;
18
- functionDepthAtContainerStart = [];
19
- return gate(context, ".get()");
20
- },
21
- FunctionDeclaration: enterFunction,
22
- "FunctionDeclaration:exit": exitFunction,
23
- FunctionExpression: enterFunction,
24
- "FunctionExpression:exit": exitFunction,
25
- ArrowFunctionExpression: enterFunction,
26
- "ArrowFunctionExpression:exit": exitFunction,
27
- JSXExpressionContainer() {
28
- functionDepthAtContainerStart.push(functionDepth);
29
- },
30
- "JSXExpressionContainer:exit"() {
31
- functionDepthAtContainerStart.pop();
32
- },
33
- CallExpression(node) {
34
- if (!directlyInJsxContainer())
35
- return;
36
- const { callee } = node;
37
- if (callee.type !== "MemberExpression" || callee.computed)
38
- return;
39
- if (callee.property.type !== "Identifier" || callee.property.name !== "get")
40
- return;
41
- const { object } = callee;
42
- if (!isObservableRef(object))
43
- return;
44
- const target = textOf(context, object);
45
- context.report({
46
- node,
47
- message: `Read it with \`useValue(${target})\` at the top of the component, or wrap this fragment in \`<Memo>\` so the read happens inside a tracking context. A \`get()\` here is a plain read, so it renders the first value and never updates.`,
48
- });
49
- },
50
- };
51
- },
52
- });
1
+ import{gate as u,problem as l}from"../../../lib/ast.js";import{isObservableRef as x,textOf as d}from"./shared.js";export const noUntrackedGetInJsx=l("A `get()` placed directly in a JSX expression container is a plain read. The value renders once and never updates.",{createOnce(r){let t=0,e=[];const i=()=>{t++},o=()=>{t--},p=()=>e.length>0&&e[e.length-1]===t;return{before(){t=0;e=[];return u(r,".get()")},FunctionDeclaration:i,"FunctionDeclaration:exit":o,FunctionExpression:i,"FunctionExpression:exit":o,ArrowFunctionExpression:i,"ArrowFunctionExpression:exit":o,JSXExpressionContainer(){e.push(t)},"JSXExpressionContainer:exit"(){e.pop()},CallExpression(s){if(!p())return;const{callee:n}=s;if(n.type!=="MemberExpression"||n.computed)return;if(n.property.type!=="Identifier"||n.property.name!=="get")return;const{object:a}=n;if(!x(a))return;const c=d(r,a);r.report({node:s,message:`Read it with \`useValue(${c})\` at the top of the component, or wrap this fragment in \`<Memo>\` so the read happens inside a tracking context. A \`get()\` here is a plain read, so it renders the first value and never updates.`})}}}});
@@ -1,23 +1 @@
1
- export const OBSERVABLE_FACTORIES = new Set(["observable", "useObservable"]);
2
- /** The trailing `$` that marks an observable binding. */
3
- export const OBS = /\$$/;
4
- /** The identifier a member chain is rooted at: `count$`, or the `settings$` of `settings$.theme.color`. */
5
- const rootName = (node) => {
6
- let current = node;
7
- while (current?.type === "MemberExpression")
8
- current = current.object;
9
- return current?.type === "Identifier" ? current.name : null;
10
- };
11
- export const isObservableRef = (node) => {
12
- const root = rootName(node);
13
- return root != null && OBS.test(root);
14
- };
15
- /** The factory name when this call is `observable(...)` or `useObservable(...)`. */
16
- export const factoryCalled = (node) => {
17
- if (node?.type !== "CallExpression")
18
- return null;
19
- const { callee } = node;
20
- return callee.type === "Identifier" && OBSERVABLE_FACTORIES.has(callee.name) ? callee.name : null;
21
- };
22
- /** Source text for a node, for quoting it back in a message. */
23
- export const textOf = (context, node) => context.sourceCode.getText(node);
1
+ export const OBSERVABLE_FACTORIES=new Set(["observable","useObservable"]),OBS=/\$$/;const r=(t)=>{let e=t;while(e?.type==="MemberExpression")e=e.object;return e?.type==="Identifier"?e.name:null};export const isObservableRef=(t)=>{const e=r(t);return e!=null&&OBS.test(e)},factoryCalled=(t)=>{if(t?.type!=="CallExpression")return null;const{callee:e}=t;return e.type==="Identifier"&&OBSERVABLE_FACTORIES.has(e.name)?e.name:null},textOf=(t,e)=>t.sourceCode.getText(e);
@@ -1,59 +1 @@
1
- import { ancestors, COMPONENT_OR_HOOK, isFunction, problem } from "../../../lib/ast.js";
2
- const COMPONENT_NAME = /^[A-Z]/;
3
- const boundFunctionName = (node) => {
4
- if (node.type === "FunctionDeclaration")
5
- return node.id?.name ?? null;
6
- const { parent } = node;
7
- if (parent === null || parent.type !== "VariableDeclarator" || parent.id.type !== "Identifier")
8
- return null;
9
- return parent.id.name;
10
- };
11
- const enclosingReactFunction = (node) => {
12
- for (const current of ancestors(node)) {
13
- if (!isFunction(current))
14
- continue;
15
- const name = boundFunctionName(current);
16
- if (name !== null && COMPONENT_OR_HOOK.test(name))
17
- return current;
18
- }
19
- return null;
20
- };
21
- const scopeContains = (scope, ancestor) => {
22
- for (let current = scope; current !== null && current !== undefined; current = current.upper) {
23
- if (current === ancestor)
24
- return true;
25
- }
26
- return false;
27
- };
28
- const readsComponentScope = (scope, componentScope) => {
29
- for (const reference of scope.through) {
30
- if (scopeContains(reference.resolved?.scope, componentScope))
31
- return true;
32
- }
33
- return false;
34
- };
35
- export const hoistStatelessFunction = problem("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.", {
36
- createOnce(context) {
37
- const check = (node) => {
38
- const name = boundFunctionName(node);
39
- if (name === null || COMPONENT_NAME.test(name))
40
- return;
41
- const component = enclosingReactFunction(node);
42
- if (component === null)
43
- return;
44
- const scope = context.sourceCode.getScope(node);
45
- const componentScope = context.sourceCode.getScope(component);
46
- if (readsComponentScope(scope, componentScope))
47
- return;
48
- context.report({
49
- node,
50
- message: `Move \`${name}\` to module scope: it reads nothing from the component, so out there it is created once and keeps a stable identity without memoising. If it was meant to read a prop or a piece of state, wire that read up instead.`,
51
- });
52
- };
53
- return {
54
- FunctionDeclaration: check,
55
- FunctionExpression: check,
56
- ArrowFunctionExpression: check,
57
- };
58
- },
59
- });
1
+ import{ancestors as u,COMPONENT_OR_HOOK as a,isFunction as p,problem as l}from"../../../lib/ast.js";const f=/^[A-Z]/,c=(n)=>{if(n.type==="FunctionDeclaration")return n.id?.name??null;const{parent:e}=n;if(e===null||e.type!=="VariableDeclarator"||e.id.type!=="Identifier")return null;return e.id.name},d=(n)=>{for(const e of u(n)){if(!p(e))continue;const t=c(e);if(t!==null&&a.test(t))return e}return null},m=(n,e)=>{for(let t=n;t!==null&&t!==void 0;t=t.upper)if(t===e)return!0;return!1},h=(n,e)=>{for(const t of n.through)if(m(t.resolved?.scope,e))return!0;return!1};export const hoistStatelessFunction=l("Require module scope for a non-component function that reads nothing from the component around it. Out there it is created once, keeps a stable identity, and can be tested without rendering.",{createOnce(n){const e=(t)=>{const o=c(t);if(o===null||f.test(o))return;const r=d(t);if(r===null)return;const i=n.sourceCode.getScope(t),s=n.sourceCode.getScope(r);if(h(i,s))return;n.report({node:t,message:`Move \`${o}\` to module scope: it reads nothing from the component, so out there it is created once and keeps a stable identity without memoising. If it was meant to read a prop or a piece of state, wire that read up instead.`})};return{FunctionDeclaration:e,FunctionExpression:e,ArrowFunctionExpression:e}}});
@@ -1,50 +1 @@
1
- import { defineModule } from "../../../lib/module.js";
2
- import { hoistStatelessFunction } from "./hoist-stateless-function.js";
3
- import { noConditionalStyleArray } from "./no-conditional-style-array.js";
4
- import { noDynamicImport } from "./no-dynamic-import.js";
5
- import { noKeyboardWillEvents } from "./no-keyboard-will-events.js";
6
- import { noLeakedRender } from "./no-leaked-render.js";
7
- import { noManualMemo } from "./no-manual-memo.js";
8
- import { noRedundantViewNesting } from "./no-redundant-view-nesting.js";
9
- import { noRnImageNetworkSource } from "./no-rn-image-network-source.js";
10
- import { noRnNamespaceImport } from "./no-rn-namespace-import.js";
11
- import { noScrollPositionState } from "./no-scroll-position-state.js";
12
- import { noUnlabeledIconPressable } from "./no-unlabeled-icon-pressable.js";
13
- export default defineModule({
14
- meta: { name: "@ashstack/react-native" },
15
- url: import.meta.url,
16
- docsWhen: "always on via `reactNative()`",
17
- rules: {
18
- "no-keyboard-will-events": noKeyboardWillEvents,
19
- "no-scroll-position-state": noScrollPositionState,
20
- "no-conditional-style-array": noConditionalStyleArray,
21
- "no-leaked-render": noLeakedRender,
22
- "no-rn-image-network-source": noRnImageNetworkSource,
23
- "no-redundant-view-nesting": noRedundantViewNesting,
24
- "no-rn-namespace-import": noRnNamespaceImport,
25
- "no-unlabeled-icon-pressable": noUnlabeledIconPressable,
26
- "hoist-stateless-function": hoistStatelessFunction,
27
- "no-manual-memo": noManualMemo,
28
- "no-dynamic-import": noDynamicImport,
29
- },
30
- restrictedImports: {
31
- paths: [
32
- {
33
- name: "react",
34
- importNames: ["forwardRef"],
35
- message: "Accept `ref` as a regular prop and delete the `forwardRef` wrapper — React 19 passes `ref` like any other prop.",
36
- },
37
- {
38
- name: "react",
39
- importNames: ["useContext"],
40
- message: "Call `use(Context)` instead of `useContext(Context)`; React 19 replaces the hook with it.",
41
- },
42
- ],
43
- patterns: [
44
- {
45
- group: ["expo-linear-gradient", "react-native-linear-gradient"],
46
- message: "Apply the gradient through the `experimental_backgroundImage` style property instead of a gradient component.",
47
- },
48
- ],
49
- },
50
- });
1
+ import{defineModule as e}from"../../../lib/module.js";import{hoistStatelessFunction as o}from"./hoist-stateless-function.js";import{noConditionalStyleArray as t}from"./no-conditional-style-array.js";import{noDynamicImport as r}from"./no-dynamic-import.js";import{noKeyboardWillEvents as a}from"./no-keyboard-will-events.js";import{noLeakedRender as n}from"./no-leaked-render.js";import{noManualMemo as i}from"./no-manual-memo.js";import{noRedundantViewNesting as m}from"./no-redundant-view-nesting.js";import{noRnImageNetworkSource as s}from"./no-rn-image-network-source.js";import{noRnNamespaceImport as p}from"./no-rn-namespace-import.js";import{noScrollPositionState as l}from"./no-scroll-position-state.js";import{noUnlabeledIconPressable as d}from"./no-unlabeled-icon-pressable.js";export default e({meta:{name:"@ashstack/react-native"},url:import.meta.url,docsWhen:"always on via `reactNative()`",rules:{"no-keyboard-will-events":a,"no-scroll-position-state":l,"no-conditional-style-array":t,"no-leaked-render":n,"no-rn-image-network-source":s,"no-redundant-view-nesting":m,"no-rn-namespace-import":p,"no-unlabeled-icon-pressable":d,"hoist-stateless-function":o,"no-manual-memo":i,"no-dynamic-import":r},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."}]}});