@ashstack/lint 0.1.0 → 0.2.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 (102) 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/use-design-system.js +1 -147
  9. package/dist/core/rules/zod/index.js +1 -12
  10. package/dist/core/rules/zod/prefer-enum.js +1 -44
  11. package/dist/index.js +1 -3
  12. package/dist/lib/ast.js +1 -162
  13. package/dist/lib/detect.js +1 -42
  14. package/dist/lib/merge.js +1 -18
  15. package/dist/lib/module.js +1 -49
  16. package/dist/lib/types.js +1 -1
  17. package/dist/react/index.js +1 -126
  18. package/dist/react/rules/i18n/index.js +1 -16
  19. package/dist/react/rules/i18n/no-bare-attrs.js +1 -38
  20. package/dist/react/rules/i18n/no-bare-text.js +1 -26
  21. package/dist/react/rules/i18n/no-bare-toast.js +1 -38
  22. package/dist/react/rules/query/index.js +1 -20
  23. package/dist/react/rules/query/next-page-param-undefined.js +1 -26
  24. package/dist/react/rules/query/no-deprecated-filters.js +1 -63
  25. package/dist/react/rules/query/no-fetch-in-query-fn.js +1 -22
  26. package/dist/react/rules/query/no-inline-keys.js +1 -107
  27. package/dist/react/rules/query/require-destructured-hooks.js +1 -41
  28. package/dist/react/rules/query/shared.js +1 -10
  29. package/dist/react/rules/zustand/index.js +1 -12
  30. package/dist/react/rules/zustand/require-selector.js +1 -62
  31. package/dist/react-native/index.js +1 -113
  32. package/dist/react-native/rules/keyboard/index.js +1 -18
  33. package/dist/react-native/rules/legend-list/index.js +1 -51
  34. package/dist/react-native/rules/legend-list/no-flex-in-content-container.js +1 -77
  35. package/dist/react-native/rules/legend-list/no-index-key-extractor.js +1 -26
  36. package/dist/react-native/rules/legend-list/no-inline-data.js +1 -25
  37. package/dist/react-native/rules/legend-list/no-inline-extra-data.js +1 -21
  38. package/dist/react-native/rules/legend-list/no-inline-render-item-props.js +1 -35
  39. package/dist/react-native/rules/legend-list/no-mixed-children.js +1 -24
  40. package/dist/react-native/rules/legend-list/no-remount-key.js +1 -18
  41. package/dist/react-native/rules/legend-list/no-scrollview-map.js +1 -27
  42. package/dist/react-native/rules/legend-list/no-unsupported-props.js +1 -29
  43. package/dist/react-native/rules/legend-list/required-props.js +1 -23
  44. package/dist/react-native/rules/legend-list/shared.js +1 -25
  45. package/dist/react-native/rules/legend-list/typed-items-need-item-type.js +1 -49
  46. package/dist/react-native/rules/legend-state/index.js +1 -35
  47. package/dist/react-native/rules/legend-state/naming.js +1 -26
  48. package/dist/react-native/rules/legend-state/no-assignment.js +1 -31
  49. package/dist/react-native/rules/legend-state/no-nested-observable.js +1 -23
  50. package/dist/react-native/rules/legend-state/no-object-selector.js +1 -47
  51. package/dist/react-native/rules/legend-state/no-observable-in-component.js +1 -46
  52. package/dist/react-native/rules/legend-state/no-peek-in-selector.js +1 -67
  53. package/dist/react-native/rules/legend-state/no-react-mirror.js +1 -33
  54. package/dist/react-native/rules/legend-state/no-untracked-get-in-jsx.js +1 -52
  55. package/dist/react-native/rules/legend-state/shared.js +1 -23
  56. package/dist/react-native/rules/react-native/hoist-stateless-function.js +1 -59
  57. package/dist/react-native/rules/react-native/index.js +1 -50
  58. package/dist/react-native/rules/react-native/no-conditional-style-array.js +1 -23
  59. package/dist/react-native/rules/react-native/no-dynamic-import.js +1 -18
  60. package/dist/react-native/rules/react-native/no-keyboard-will-events.js +1 -18
  61. package/dist/react-native/rules/react-native/no-leaked-render.js +1 -43
  62. package/dist/react-native/rules/react-native/no-manual-memo.js +1 -87
  63. package/dist/react-native/rules/react-native/no-redundant-view-nesting.js +1 -36
  64. package/dist/react-native/rules/react-native/no-rn-image-network-source.js +1 -58
  65. package/dist/react-native/rules/react-native/no-rn-namespace-import.js +1 -38
  66. package/dist/react-native/rules/react-native/no-scroll-position-state.js +1 -26
  67. package/dist/react-native/rules/react-native/no-unlabeled-icon-pressable.js +1 -62
  68. package/dist/react-native/rules/reanimated/animated-reaction-safety.js +1 -110
  69. package/dist/react-native/rules/reanimated/animated-style-needs-animated-component.js +1 -72
  70. package/dist/react-native/rules/reanimated/animated-updater-purity.js +1 -38
  71. package/dist/react-native/rules/reanimated/gpu-properties-only.js +1 -80
  72. package/dist/react-native/rules/reanimated/hoist-layout-animation-builder.js +1 -43
  73. package/dist/react-native/rules/reanimated/index.js +1 -51
  74. package/dist/react-native/rules/reanimated/interpolate-needs-clamp.js +1 -41
  75. package/dist/react-native/rules/reanimated/no-react-state-from-continuous-worklet.js +1 -86
  76. package/dist/react-native/rules/reanimated/no-shared-value-dot-value.js +1 -112
  77. package/dist/react-native/rules/reanimated/prefer-lazy-shared-value-initializer.js +1 -24
  78. package/dist/react-native/rules/reanimated/schedule-on-rn-scope.js +1 -21
  79. package/dist/react-native/rules/reanimated/shared-value-usage.js +1 -105
  80. package/dist/react-native/rules/reanimated/shared.js +1 -13
  81. package/dist/react-native/rules/skia/canvas-opaque.js +1 -49
  82. package/dist/react-native/rules/skia/index.js +1 -21
  83. package/dist/react-native/rules/turbo-image/index.js +1 -14
  84. package/dist/react-native/rules/turbo-image/require-cache-policy.js +1 -21
  85. package/dist/react-native/rules/turbo-image/require-resize.js +1 -22
  86. package/dist/react-native/rules/unistyles/animated-theme.js +1 -37
  87. package/dist/react-native/rules/unistyles/content-container.js +1 -73
  88. package/dist/react-native/rules/unistyles/in-sheet.js +1 -116
  89. package/dist/react-native/rules/unistyles/index.js +1 -58
  90. package/dist/react-native/rules/unistyles/insets.js +1 -92
  91. package/dist/react-native/rules/unistyles/no-hardcoded-color.js +1 -30
  92. package/dist/react-native/rules/unistyles/no-hardcoded-spacing.js +1 -78
  93. package/dist/react-native/rules/unistyles/no-margin.js +1 -19
  94. package/dist/react-native/rules/unistyles/no-style-spread.js +1 -41
  95. package/dist/react-native/rules/unistyles/no-unused-styles.js +1 -99
  96. package/dist/react-native/rules/unistyles/rtl-style-call.js +1 -42
  97. package/dist/react-native/rules/unistyles/shared.js +1 -70
  98. package/dist/react-native/rules/unistyles/theme-screen-component.js +1 -22
  99. package/dist/react-native/rules/unistyles/theme-style-attr.js +1 -18
  100. package/dist/react-native/stylesheet.js +1 -44
  101. package/package.json +3 -4
  102. package/RULES.md +0 -2881
@@ -1,18 +1 @@
1
- import { gate, problem } from "../../../lib/ast.js";
2
- const IOS_ONLY_EVENT_NAMES = new Set(["keyboardWillShow", "keyboardWillHide", "keyboardWillChangeFrame"]);
3
- const MESSAGE = "Drive this from `rt.insets.ime`, which updates per frame on both platforms, or from a `react-native-keyboard-controller` component. `keyboardWill*` events are iOS-only, so on Android the listener registers and never fires.";
4
- const isIosOnlyEventName = (node) => node.type === "Literal" && typeof node.value === "string" && IOS_ONLY_EVENT_NAMES.has(node.value);
5
- export const noKeyboardWillEvents = problem("Disallow the `keyboardWill*` event names. They are iOS-only, so on Android the listener registers and never fires.", {
6
- createOnce(context) {
7
- return {
8
- before() {
9
- return gate(context, "keyboardWill");
10
- },
11
- Literal(node) {
12
- if (!isIosOnlyEventName(node))
13
- return;
14
- context.report({ node, message: MESSAGE });
15
- },
16
- };
17
- },
18
- });
1
+ import{gate as o,problem as t}from"../../../lib/ast.js";const n=new Set(["keyboardWillShow","keyboardWillHide","keyboardWillChangeFrame"]),a="Drive this from `rt.insets.ime`, which updates per frame on both platforms, or from a `react-native-keyboard-controller` component. `keyboardWill*` events are iOS-only, so on Android the listener registers and never fires.",i=(e)=>e.type==="Literal"&&typeof e.value==="string"&&n.has(e.value);export const noKeyboardWillEvents=t("Disallow the `keyboardWill*` event names. They are iOS-only, so on Android the listener registers and never fires.",{createOnce(e){return{before(){return o(e,"keyboardWill")},Literal(r){if(!i(r))return;e.report({node:r,message:a})}}}});
@@ -1,43 +1 @@
1
- import { problem } from "../../../lib/ast.js";
2
- const MESSAGE = 'Compare explicitly (`list.length > 0 &&`), coerce with `!!`, or use a ternary ending in `null`. A falsy number or string leaks into the tree: `0` renders a bare zero, which crashes React Native with "Text strings must be rendered within a <Text> component".';
3
- const COUNT_PROPERTIES = new Set(["length", "size"]);
4
- const ARITHMETIC = new Set(["+", "-", "*", "/", "%", "**"]);
5
- /**
6
- * A guard this rule can see is a number or a string, rather than one it merely
7
- * cannot prove is a boolean. `{isOpen && …}` stays quiet; `{count - 1 && …}`
8
- * does not. Only the right side of an `&&` chain decides, since that is the
9
- * value that reaches the tree.
10
- */
11
- const leaksAValue = (node) => {
12
- if (node === undefined)
13
- return false;
14
- if (node.type === "MemberExpression") {
15
- return node.property.type === "Identifier" && COUNT_PROPERTIES.has(node.property.name);
16
- }
17
- if (node.type === "Literal")
18
- return typeof node.value === "number" || typeof node.value === "string";
19
- if (node.type === "BinaryExpression")
20
- return ARITHMETIC.has(node.operator);
21
- if (node.type === "TemplateLiteral")
22
- return true;
23
- if (node.type === "LogicalExpression" && node.operator === "&&")
24
- return leaksAValue(node.right);
25
- return false;
26
- };
27
- export const noLeakedRender = problem('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
- createOnce(context) {
29
- return {
30
- JSXExpressionContainer(node) {
31
- const parentType = node.parent.type;
32
- if (parentType !== "JSXElement" && parentType !== "JSXFragment")
33
- return;
34
- const expression = node.expression;
35
- if (expression.type !== "LogicalExpression" || expression.operator !== "&&")
36
- return;
37
- if (!leaksAValue(expression.left))
38
- return;
39
- context.report({ node: expression, message: MESSAGE });
40
- },
41
- };
42
- },
43
- });
1
+ import{problem as i}from"../../../lib/ast.js";const s='Compare explicitly (`list.length > 0 &&`), coerce with `!!`, or use a ternary ending in `null`. A falsy number or string leaks into the tree: `0` renders a bare zero, which crashes React Native with "Text strings must be rendered within a <Text> component".',p=new Set(["length","size"]),o=new Set(["+","-","*","/","%","**"]),a=(e)=>{if(e===void 0)return!1;if(e.type==="MemberExpression")return e.property.type==="Identifier"&&p.has(e.property.name);if(e.type==="Literal")return typeof e.value==="number"||typeof e.value==="string";if(e.type==="BinaryExpression")return o.has(e.operator);if(e.type==="TemplateLiteral")return!0;if(e.type==="LogicalExpression"&&e.operator==="&&")return a(e.right);return!1};export const noLeakedRender=i('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".',{createOnce(e){return{JSXExpressionContainer(r){const n=r.parent.type;if(n!=="JSXElement"&&n!=="JSXFragment")return;const t=r.expression;if(t.type!=="LogicalExpression"||t.operator!=="&&")return;if(!a(t.left))return;e.report({node:t,message:s})}}}});
@@ -1,87 +1 @@
1
- import { gate, problem } from "../../../lib/ast.js";
2
- const MEMO_APIS = new Set(["useMemo", "useCallback", "memo"]);
3
- const REACT_COMPILER_PACKAGES = ["babel-plugin-react-compiler", "react-compiler-runtime", "react-compiler-marker"];
4
- const WHY_COMMENT = /^why:/i;
5
- const NEWLINE = 10;
6
- const bareMemoName = (callee) => callee.type === "Identifier" && MEMO_APIS.has(callee.name) ? callee.name : null;
7
- const reactNamespacedMemoName = (callee) => {
8
- if (callee.type !== "MemberExpression" || callee.computed)
9
- return null;
10
- if (callee.object.type !== "Identifier" || callee.object.name !== "React")
11
- return null;
12
- const property = callee.property.name;
13
- return MEMO_APIS.has(property) ? property : null;
14
- };
15
- const memoCalleeName = (node) => {
16
- if (node.type !== "CallExpression")
17
- return null;
18
- const { callee } = node;
19
- if (callee.type === "Identifier")
20
- return bareMemoName(callee);
21
- if (callee.type === "MemberExpression")
22
- return reactNamespacedMemoName(callee);
23
- return null;
24
- };
25
- const lineStartOffsets = (text) => {
26
- const starts = [0];
27
- for (let index = 0; index < text.length; index++) {
28
- if (text.charCodeAt(index) === NEWLINE)
29
- starts.push(index + 1);
30
- }
31
- return starts;
32
- };
33
- const lineContaining = (starts, offset) => {
34
- let low = 0;
35
- let high = starts.length - 1;
36
- while (low < high) {
37
- const middle = Math.floor((low + high + 1) / 2);
38
- if ((starts[middle] ?? 0) <= offset)
39
- low = middle;
40
- else
41
- high = middle - 1;
42
- }
43
- return low;
44
- };
45
- const linesCoveredByWhyComments = (comments, lineOf) => {
46
- const covered = new Set();
47
- for (const comment of comments) {
48
- if (!WHY_COMMENT.test(comment.value.trim()))
49
- continue;
50
- for (let line = lineOf(comment.start); line <= lineOf(comment.end); line++)
51
- covered.add(line);
52
- }
53
- return covered;
54
- };
55
- export const noManualMemo = problem("Disallow `useMemo`, `useCallback` and `memo` unless a `why:` comment on the line above states the case the React Compiler cannot see: something rendered per list row, or a cost that was measured.", {
56
- meta: { packages: REACT_COMPILER_PACKAGES },
57
- createOnce(context) {
58
- let calls = [];
59
- return {
60
- before() {
61
- calls = [];
62
- return gate(context, "useMemo", "useCallback", "memo");
63
- },
64
- CallExpression(node) {
65
- const name = memoCalleeName(node);
66
- if (name !== null)
67
- calls.push({ node, name });
68
- },
69
- "Program:exit"() {
70
- if (calls.length === 0)
71
- return;
72
- const lineStarts = lineStartOffsets(context.sourceCode.getText());
73
- const lineOf = (offset) => lineContaining(lineStarts, offset);
74
- const justified = linesCoveredByWhyComments(context.sourceCode.getAllComments(), lineOf);
75
- for (const { node, name } of calls) {
76
- const line = lineOf(node.start);
77
- if (justified.has(line) || justified.has(line - 1))
78
- continue;
79
- context.report({
80
- node,
81
- message: `Drop this \`${name}\` and let the React Compiler memoise it. Keep it only for what the compiler cannot see — something rendered per row in a list, or a computation you measured as heavy — and write a \`why:\` comment on the line above saying which of the two it is.`,
82
- });
83
- }
84
- },
85
- };
86
- },
87
- });
1
+ import{gate as l,problem as c}from"../../../lib/ast.js";const i=new Set(["useMemo","useCallback","memo"]),u=["babel-plugin-react-compiler","react-compiler-runtime","react-compiler-marker"],p=/^why:/i,h=10,f=(e)=>e.type==="Identifier"&&i.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 i.has(t)?t:null},C=(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},y=(e)=>{const t=[0];for(let n=0;n<e.length;n++)if(e.charCodeAt(n)===h)t.push(n+1);return t},g=(e,t)=>{let n=0,o=e.length-1;while(n<o){const r=Math.floor((n+o+1)/2);if((e[r]??0)<=t)n=r;else o=r-1}return n},w=(e,t)=>{const n=new Set;for(const o of e){if(!p.test(o.value.trim()))continue;for(let r=t(o.start);r<=t(o.end);r++)n.add(r)}return n};export const noManualMemo=c("Disallow `useMemo`, `useCallback` and `memo` unless a `why:` comment on the line above states the case the React Compiler cannot see: something rendered per list row, or a cost that was measured.",{meta:{packages:u},createOnce(e){let t=[];return{before(){t=[];return l(e,"useMemo","useCallback","memo")},CallExpression(n){const o=C(n);if(o!==null)t.push({node:n,name:o})},"Program:exit"(){if(t.length===0)return;const n=y(e.sourceCode.getText()),o=(s)=>g(n,s),r=w(e.sourceCode.getAllComments(),o);for(const{node:s,name:m}of t){const a=o(s.start);if(r.has(a)||r.has(a-1))continue;e.report({node:s,message:`Drop this \`${m}\` and let the React Compiler memoise it. Keep it only for what the compiler cannot see — something rendered per row in a list, or a computation you measured as heavy — and write a \`why:\` comment on the line above saying which of the two it is.`})}}}}});
@@ -1,36 +1 @@
1
- import { problem, tagPath } from "../../../lib/ast.js";
2
- const LAYOUT_ONLY_PROPS = new Set(["style"]);
3
- const MERGEABLE_WRAPPERS = new Set(["View", "Animated.View"]);
4
- const elementChildren = (children) => children.filter(child => child.type !== "JSXText" || child.value.trim() !== "");
5
- const onlyLayoutProps = (attributes) => attributes.every(attribute => attribute.type === "JSXAttribute" &&
6
- attribute.name.type === "JSXIdentifier" &&
7
- LAYOUT_ONLY_PROPS.has(attribute.name.name));
8
- export const noRedundantViewNesting = problem("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.", {
9
- createOnce(context) {
10
- return {
11
- JSXElement(node) {
12
- const opening = node.openingElement;
13
- const tag = tagPath(opening.name);
14
- if (!MERGEABLE_WRAPPERS.has(tag))
15
- return;
16
- if (!onlyLayoutProps(opening.attributes))
17
- return;
18
- const children = elementChildren(node.children);
19
- if (children.length !== 1)
20
- return;
21
- const child = children[0];
22
- if (child?.type !== "JSXElement")
23
- return;
24
- const childOpening = child.openingElement;
25
- if (tagPath(childOpening.name) !== tag)
26
- return;
27
- if (!onlyLayoutProps(childOpening.attributes))
28
- return;
29
- context.report({
30
- node: opening.name,
31
- message: `Merge these two <${tag}> style objects into the inner one and delete the outer wrapper — each carries nothing but a style, and every extra host view is a real node in the native tree.`,
32
- });
33
- },
34
- };
35
- },
36
- });
1
+ import{problem as h,tagPath as s}from"../../../lib/ast.js";const c=new Set(["style"]),m=new Set(["View","Animated.View"]),p=(t)=>t.filter((e)=>e.type!=="JSXText"||e.value.trim()!==""),l=(t)=>t.every((e)=>e.type==="JSXAttribute"&&e.name.type==="JSXIdentifier"&&c.has(e.name.name));export const noRedundantViewNesting=h("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.",{createOnce(t){return{JSXElement(e){const n=e.openingElement,r=s(n.name);if(!m.has(r))return;if(!l(n.attributes))return;const i=p(e.children);if(i.length!==1)return;const a=i[0];if(a?.type!=="JSXElement")return;const o=a.openingElement;if(s(o.name)!==r)return;if(!l(o.attributes))return;t.report({node:n.name,message:`Merge these two <${r}> style objects into the inner one and delete the outer wrapper — each carries nothing but a style, and every extra host view is a real node in the native tree.`})}}}});
@@ -1,58 +1 @@
1
- import { componentName, gate, importedSpecifiers, problem } from "../../../lib/ast.js";
2
- const MESSAGE = "Render this network image with `TurboImage` (react-native-turbo-image), passing `resize` and `cachePolicy`; keep react-native `<Image>` for local `require(...)` assets. RN `<Image>` has no disk cache and no decode sizing, so it re-downloads and decodes at full size on every cold start.";
3
- /** A property key reads by name (`{ uri }`) or, for a string key, by value (`{ "uri" }`). */
4
- const keyName = (key) => {
5
- if (key.type === "Identifier")
6
- return key.name;
7
- return key.type === "Literal" && typeof key.value === "string" ? key.value : undefined;
8
- };
9
- const hasUriProperty = (attribute) => {
10
- if (attribute.type !== "JSXAttribute")
11
- return false;
12
- const { value } = attribute;
13
- const expression = value?.type === "JSXExpressionContainer" ? value.expression : undefined;
14
- if (expression?.type !== "ObjectExpression")
15
- return false;
16
- return expression.properties.some(property => property.type === "Property" && keyName(property.key) === "uri");
17
- };
18
- export const noRnImageNetworkSource = problem("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.", {
19
- createOnce(context) {
20
- const imageBindings = new Set();
21
- const candidates = [];
22
- return {
23
- before() {
24
- imageBindings.clear();
25
- candidates.length = 0;
26
- return gate(context, "source");
27
- },
28
- ImportDeclaration(node) {
29
- for (const specifier of importedSpecifiers(node, "react-native")) {
30
- if (specifier.type !== "ImportSpecifier")
31
- continue;
32
- const { imported } = specifier;
33
- if (imported.type === "Identifier" && imported.name === "Image") {
34
- imageBindings.add(specifier.local.name);
35
- }
36
- }
37
- },
38
- JSXOpeningElement(node) {
39
- const tag = componentName(node.name);
40
- if (tag === "")
41
- return;
42
- const source = node.attributes.find(attribute => attribute.type === "JSXAttribute" &&
43
- attribute.name.type === "JSXIdentifier" &&
44
- attribute.name.name === "source");
45
- if (!source || !hasUriProperty(source))
46
- return;
47
- candidates.push({ node: node.name, tag });
48
- },
49
- "Program:exit"() {
50
- for (const candidate of candidates) {
51
- if (!imageBindings.has(candidate.tag))
52
- continue;
53
- context.report({ node: candidate.node, message: MESSAGE });
54
- }
55
- },
56
- };
57
- },
58
- });
1
+ import{componentName as s,gate as c,importedSpecifiers as d,problem as p}from"../../../lib/ast.js";const m="Render this network image with `TurboImage` (react-native-turbo-image), passing `resize` and `cachePolicy`; keep react-native `<Image>` for local `require(...)` assets. RN `<Image>` has no disk cache and no decode sizing, so it re-downloads and decodes at full size on every cold start.",f=(e)=>{if(e.type==="Identifier")return e.name;return e.type==="Literal"&&typeof e.value==="string"?e.value:void 0},u=(e)=>{if(e.type!=="JSXAttribute")return!1;const{value:t}=e,r=t?.type==="JSXExpressionContainer"?t.expression:void 0;if(r?.type!=="ObjectExpression")return!1;return r.properties.some((n)=>n.type==="Property"&&f(n.key)==="uri")};export const noRnImageNetworkSource=p("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.",{createOnce(e){const t=new Set,r=[];return{before(){t.clear();r.length=0;return c(e,"source")},ImportDeclaration(n){for(const o of d(n,"react-native")){if(o.type!=="ImportSpecifier")continue;const{imported:a}=o;if(a.type==="Identifier"&&a.name==="Image")t.add(o.local.name)}},JSXOpeningElement(n){const o=s(n.name);if(o==="")return;const a=n.attributes.find((i)=>i.type==="JSXAttribute"&&i.name.type==="JSXIdentifier"&&i.name.name==="source");if(!a||!u(a))return;r.push({node:n.name,tag:o})},"Program:exit"(){for(const n of r){if(!t.has(n.tag))continue;e.report({node:n.node,message:m})}}}}});
@@ -1,38 +1 @@
1
- import { gate, problem } from "../../../lib/ast.js";
2
- const MESSAGES = {
3
- namespaceImport: 'Import the react-native APIs by name instead: `import { View, Platform } from "react-native"`. A namespace import defeats Metro platform shaking, so dead `Platform.OS` branches ship in both bundles.',
4
- platformReExport: "Drop this re-export and have each consumer import `Platform` straight from react-native; re-exporting it defeats Metro platform shaking.",
5
- };
6
- export const noRnNamespaceImport = problem("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.", {
7
- createOnce(context) {
8
- return {
9
- before() {
10
- return gate(context, "react-native");
11
- },
12
- ImportDeclaration(node) {
13
- if (node.source.value !== "react-native")
14
- return;
15
- for (const specifier of node.specifiers) {
16
- if (specifier.type !== "ImportNamespaceSpecifier")
17
- continue;
18
- context.report({ node: specifier, message: MESSAGES.namespaceImport });
19
- }
20
- },
21
- ExportAllDeclaration(node) {
22
- if (node.source.value !== "react-native")
23
- return;
24
- context.report({ node, message: MESSAGES.namespaceImport });
25
- },
26
- ExportNamedDeclaration(node) {
27
- if (node.source?.value !== "react-native")
28
- return;
29
- for (const specifier of node.specifiers) {
30
- const { local } = specifier;
31
- if (local.type !== "Identifier" || local.name !== "Platform")
32
- continue;
33
- context.report({ node: specifier, message: MESSAGES.platformReExport });
34
- }
35
- },
36
- };
37
- },
38
- });
1
+ import{gate as i,problem as s}from"../../../lib/ast.js";const a={namespaceImport:'Import the react-native APIs by name instead: `import { View, Platform } from "react-native"`. A namespace import defeats Metro platform shaking, so dead `Platform.OS` branches ship in both bundles.',platformReExport:"Drop this re-export and have each consumer import `Platform` straight from react-native; re-exporting it defeats Metro platform shaking."};export const noRnNamespaceImport=s("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.",{createOnce(t){return{before(){return i(t,"react-native")},ImportDeclaration(e){if(e.source.value!=="react-native")return;for(const r of e.specifiers){if(r.type!=="ImportNamespaceSpecifier")continue;t.report({node:r,message:a.namespaceImport})}},ExportAllDeclaration(e){if(e.source.value!=="react-native")return;t.report({node:e,message:a.namespaceImport})},ExportNamedDeclaration(e){if(e.source?.value!=="react-native")return;for(const r of e.specifiers){const{local:o}=r;if(o.type!=="Identifier"||o.name!=="Platform")continue;t.report({node:r,message:a.platformReExport})}}}}});
@@ -1,26 +1 @@
1
- import { attributeName, gate, problem, subtreeHas } from "../../../lib/ast.js";
2
- const SCROLL_HANDLERS = new Set(["onScroll", "onScrollBeginDrag", "onScrollEndDrag", "onMomentumScrollEnd"]);
3
- const STATE_SETTER = /^set[A-Z]/;
4
- const MESSAGE = "Use `useAnimatedScrollHandler` with a shared value when this drives an animation, or a ref when nothing renders from it. Scroll fires every frame, so a state setter here re-renders the screen every frame.";
5
- export const noScrollPositionState = problem("Disallow a React state setter inside a scroll handler prop. Scroll fires every frame, and so would the re-render.", {
6
- createOnce(context) {
7
- return {
8
- before() {
9
- return gate(context, ...SCROLL_HANDLERS);
10
- },
11
- JSXAttribute(node) {
12
- if (!SCROLL_HANDLERS.has(attributeName(node)))
13
- return;
14
- const { value } = node;
15
- const expression = value?.type === "JSXExpressionContainer" ? value.expression : undefined;
16
- if (expression?.type !== "ArrowFunctionExpression" && expression?.type !== "FunctionExpression")
17
- return;
18
- const setsState = subtreeHas(expression.body, current => current.type === "CallExpression" &&
19
- current.callee.type === "Identifier" &&
20
- STATE_SETTER.test(current.callee.name));
21
- if (setsState)
22
- context.report({ node, message: MESSAGE });
23
- },
24
- };
25
- },
26
- });
1
+ import{attributeName as a,gate as i,problem as l,subtreeHas as c}from"../../../lib/ast.js";const o=new Set(["onScroll","onScrollBeginDrag","onScrollEndDrag","onMomentumScrollEnd"]),S=/^set[A-Z]/,p="Use `useAnimatedScrollHandler` with a shared value when this drives an animation, or a ref when nothing renders from it. Scroll fires every frame, so a state setter here re-renders the screen every frame.";export const noScrollPositionState=l("Disallow a React state setter inside a scroll handler prop. Scroll fires every frame, and so would the re-render.",{createOnce(n){return{before(){return i(n,...o)},JSXAttribute(e){if(!o.has(a(e)))return;const{value:s}=e,r=s?.type==="JSXExpressionContainer"?s.expression:void 0;if(r?.type!=="ArrowFunctionExpression"&&r?.type!=="FunctionExpression")return;if(c(r.body,(t)=>t.type==="CallExpression"&&t.callee.type==="Identifier"&&S.test(t.callee.name)))n.report({node:e,message:p})}}}});
@@ -1,62 +1 @@
1
- import { componentName, problem, tagIdentifier } from "../../../lib/ast.js";
2
- const TOUCHABLES = new Set(["Pressable", "PressableScale", "TouchableOpacity", "TouchableHighlight"]);
3
- const MESSAGES = {
4
- touchable: "Add an `accessibilityLabel` (plus an `accessibilityHint` when the outcome is not obvious), or render a visible `<Text>` child, so this icon-only touchable has an accessible name.",
5
- expoButton: "Add a `label`, or an `accessibilityLabel(...)` modifier, so this icon-only Expo UI `<Button>` has an accessible name.",
6
- };
7
- /** A plain (non-namespaced) attribute's name; anything else reads as `undefined`. */
8
- const plainAttributeName = (attribute) => attribute.type === "JSXAttribute" && attribute.name.type === "JSXIdentifier" ? attribute.name.name : undefined;
9
- const hasAccessibleName = (attributes) => attributes.some(attribute => {
10
- const name = plainAttributeName(attribute);
11
- return name === "accessibilityLabel" || name === "accessibilityHint";
12
- });
13
- const childKinds = (children) => {
14
- const kinds = { hasIcon: false, hasText: false, hasExpression: false };
15
- for (const child of children) {
16
- if (child.type === "JSXExpressionContainer")
17
- kinds.hasExpression = true;
18
- if (child.type === "JSXText" && child.value.trim() !== "")
19
- kinds.hasText = true;
20
- if (child.type !== "JSXElement")
21
- continue;
22
- const childTag = tagIdentifier(child.openingElement.name);
23
- if (childTag === "Text")
24
- kinds.hasText = true;
25
- if (childTag.endsWith("Icon"))
26
- kinds.hasIcon = true;
27
- }
28
- return kinds;
29
- };
30
- export const noUnlabeledIconPressable = problem("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.", {
31
- createOnce(context) {
32
- return {
33
- JSXElement(node) {
34
- const opening = node.openingElement;
35
- if (!TOUCHABLES.has(componentName(opening.name)))
36
- return;
37
- if (hasAccessibleName(opening.attributes))
38
- return;
39
- const { hasIcon, hasText, hasExpression } = childKinds(node.children);
40
- if (!hasIcon || hasText || hasExpression)
41
- return;
42
- context.report({ node: opening.name, message: MESSAGES.touchable });
43
- },
44
- JSXOpeningElement(node) {
45
- if (!node.selfClosing)
46
- return;
47
- if (componentName(node.name) !== "Button")
48
- return;
49
- const names = new Set(node.attributes.map(plainAttributeName).filter(Boolean));
50
- if (!names.has("systemImage"))
51
- return;
52
- if (names.has("label") ||
53
- names.has("accessibilityLabel") ||
54
- names.has("accessibilityHint") ||
55
- names.has("modifiers")) {
56
- return;
57
- }
58
- context.report({ node: node.name, message: MESSAGES.expoButton });
59
- },
60
- };
61
- },
62
- });
1
+ import{componentName as a,problem as l,tagIdentifier as h}from"../../../lib/ast.js";const m=new Set(["Pressable","PressableScale","TouchableOpacity","TouchableHighlight"]),i={touchable:"Add an `accessibilityLabel` (plus an `accessibilityHint` when the outcome is not obvious), or render a visible `<Text>` child, so this icon-only touchable has an accessible name.",expoButton:"Add a `label`, or an `accessibilityLabel(...)` modifier, so this icon-only Expo UI `<Button>` has an accessible name."},o=(t)=>t.type==="JSXAttribute"&&t.name.type==="JSXIdentifier"?t.name.name:void 0,u=(t)=>t.some((n)=>{const e=o(n);return e==="accessibilityLabel"||e==="accessibilityHint"}),b=(t)=>{const n={hasIcon:!1,hasText:!1,hasExpression:!1};for(const e of t){if(e.type==="JSXExpressionContainer")n.hasExpression=!0;if(e.type==="JSXText"&&e.value.trim()!=="")n.hasText=!0;if(e.type!=="JSXElement")continue;const s=h(e.openingElement.name);if(s==="Text")n.hasText=!0;if(s.endsWith("Icon"))n.hasIcon=!0}return n};export const noUnlabeledIconPressable=l("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.",{createOnce(t){return{JSXElement(n){const e=n.openingElement;if(!m.has(a(e.name)))return;if(u(e.attributes))return;const{hasIcon:s,hasText:c,hasExpression:r}=b(n.children);if(!s||c||r)return;t.report({node:e.name,message:i.touchable})},JSXOpeningElement(n){if(!n.selfClosing)return;if(a(n.name)!=="Button")return;const e=new Set(n.attributes.map(o).filter(Boolean));if(!e.has("systemImage"))return;if(e.has("label")||e.has("accessibilityLabel")||e.has("accessibilityHint")||e.has("modifiers"))return;t.report({node:n.name,message:i.expoButton})}}}});
@@ -1,110 +1 @@
1
- import { calleeName, gate, hasAncestor, isMemberCall, isWithin, problem, receiverName, subtreeHas, } from "../../../lib/ast.js";
2
- const MESSAGES = {
3
- hotBridgeUnguarded: "Guard this `scheduleOnRN` by comparing the current and previous prepared results, so high-frequency input does not bridge to RN every frame.",
4
- hotBridgeNoPrevious: "Take the previous prepared result as the callback's second parameter and guard `scheduleOnRN` on it differing from the current one, so high-frequency input does not bridge to RN every frame.",
5
- };
6
- const REACTION_HOOKS = new Set(["useAnimatedReaction"]);
7
- const NOT_EQUAL_OPERATORS = new Set(["!==", "!="]);
8
- const identifierName = (node) => (node?.type === "Identifier" ? node.name : null);
9
- const comparesWithNotEqual = (test, first, second) => {
10
- if (test.type !== "BinaryExpression" || !NOT_EQUAL_OPERATORS.has(test.operator))
11
- return false;
12
- const left = identifierName(test.left);
13
- const right = identifierName(test.right);
14
- return (left === first && right === second) || (left === second && right === first);
15
- };
16
- const comparesWithNegatedIs = (test, first, second) => {
17
- if (test.type !== "UnaryExpression" || test.operator !== "!")
18
- return false;
19
- const { argument } = test;
20
- if (argument.type !== "CallExpression" || calleeName(argument) !== "is")
21
- return false;
22
- const compared = argument.arguments;
23
- return (compared.some(entry => identifierName(entry) === first) && compared.some(entry => identifierName(entry) === second));
24
- };
25
- /** True when an enclosing `if` tests the two callback parameters against each other. */
26
- const guardsComparison = (node, first, second) => {
27
- if (!first || !second)
28
- return false;
29
- return hasAncestor(node, current => {
30
- if (current.type !== "IfStatement")
31
- return false;
32
- const { test } = current;
33
- return comparesWithNotEqual(test, first, second) || comparesWithNegatedIs(test, first, second);
34
- });
35
- };
36
- /** True when a `.set()`/`.modify()` in the result callback writes what the prepare callback reads. */
37
- const feedsOwnInput = (node, reaction) => {
38
- if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression")
39
- return false;
40
- const shared = receiverName(node);
41
- if (!shared)
42
- return false;
43
- if (reaction.type !== "CallExpression")
44
- return false;
45
- const [prepare, react] = reaction.arguments;
46
- if (!prepare || !react || !isWithin(node, react))
47
- return false;
48
- return subtreeHas(prepare, current => isMemberCall(current, "get") && receiverName(current) === shared);
49
- };
50
- /** The parameters of the reaction's result callback, or null when the second argument is not a function. */
51
- const resultCallbackParams = (reaction) => {
52
- if (reaction.type !== "CallExpression")
53
- return null;
54
- const callback = reaction.arguments[1];
55
- if (callback?.type !== "FunctionDeclaration" &&
56
- callback?.type !== "FunctionExpression" &&
57
- callback?.type !== "ArrowFunctionExpression") {
58
- return null;
59
- }
60
- return callback.params;
61
- };
62
- /** What is wrong with a `scheduleOnRN` inside a reaction, or null when it is guarded. */
63
- const hotBridgeProblem = (node, reaction) => {
64
- const params = resultCallbackParams(reaction);
65
- if (!params)
66
- return null;
67
- const previous = identifierName(params[1]);
68
- if (!previous)
69
- return MESSAGES.hotBridgeNoPrevious;
70
- return guardsComparison(node, identifierName(params[0]), previous) ? null : MESSAGES.hotBridgeUnguarded;
71
- };
72
- export const animatedReactionSafety = problem("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.", {
73
- createOnce(context) {
74
- let stack = [];
75
- return {
76
- before() {
77
- stack = [];
78
- return gate(context, "useAnimatedReaction");
79
- },
80
- CallExpression(node) {
81
- const name = calleeName(node);
82
- if (REACTION_HOOKS.has(name)) {
83
- stack.push(node);
84
- return;
85
- }
86
- const reaction = stack.at(-1);
87
- if (!reaction)
88
- return;
89
- if (name === "set" || name === "modify") {
90
- if (!feedsOwnInput(node, reaction))
91
- return;
92
- context.report({
93
- node,
94
- message: `Have this result callback ${name === "set" ? "write" : "modify"} a shared value its prepare callback does not read, or gate the call on a comparison; feeding its own input loops forever.`,
95
- });
96
- return;
97
- }
98
- if (name !== "scheduleOnRN")
99
- return;
100
- const problemMessage = hotBridgeProblem(node, reaction);
101
- if (problemMessage !== null)
102
- context.report({ node, message: problemMessage });
103
- },
104
- "CallExpression:exit"(node) {
105
- if (REACTION_HOOKS.has(calleeName(node)))
106
- stack.pop();
107
- },
108
- };
109
- },
110
- });
1
+ import{calleeName as i,gate as c,hasAncestor as f,isMemberCall as m,isWithin as h,problem as d,receiverName as l,subtreeHas as g}from"../../../lib/ast.js";const u={hotBridgeUnguarded:"Guard this `scheduleOnRN` by comparing the current and previous prepared results, so high-frequency input does not bridge to RN every frame.",hotBridgeNoPrevious:"Take the previous prepared result as the callback's second parameter and guard `scheduleOnRN` on it differing from the current one, so high-frequency input does not bridge to RN every frame."},p=new Set(["useAnimatedReaction"]),y=new Set(["!==","!="]),o=(e)=>e?.type==="Identifier"?e.name:null,b=(e,t,r)=>{if(e.type!=="BinaryExpression"||!y.has(e.operator))return!1;const s=o(e.left),a=o(e.right);return s===t&&a===r||s===r&&a===t},E=(e,t,r)=>{if(e.type!=="UnaryExpression"||e.operator!=="!")return!1;const{argument:s}=e;if(s.type!=="CallExpression"||i(s)!=="is")return!1;const a=s.arguments;return a.some((n)=>o(n)===t)&&a.some((n)=>o(n)===r)},N=(e,t,r)=>{if(!t||!r)return!1;return f(e,(s)=>{if(s.type!=="IfStatement")return!1;const{test:a}=s;return b(a,t,r)||E(a,t,r)})},v=(e,t)=>{if(e.type!=="CallExpression"||e.callee.type!=="MemberExpression")return!1;const r=l(e);if(!r)return!1;if(t.type!=="CallExpression")return!1;const[s,a]=t.arguments;if(!s||!a||!h(e,a))return!1;return g(s,(n)=>m(n,"get")&&l(n)===r)},R=(e)=>{if(e.type!=="CallExpression")return null;const t=e.arguments[1];if(t?.type!=="FunctionDeclaration"&&t?.type!=="FunctionExpression"&&t?.type!=="ArrowFunctionExpression")return null;return t.params},O=(e,t)=>{const r=R(t);if(!r)return null;const s=o(r[1]);if(!s)return u.hotBridgeNoPrevious;return N(e,o(r[0]),s)?null:u.hotBridgeUnguarded};export const animatedReactionSafety=d("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.",{createOnce(e){let t=[];return{before(){t=[];return c(e,"useAnimatedReaction")},CallExpression(r){const s=i(r);if(p.has(s)){t.push(r);return}const a=t.at(-1);if(!a)return;if(s==="set"||s==="modify"){if(!v(r,a))return;e.report({node:r,message:`Have this result callback ${s==="set"?"write":"modify"} a shared value its prepare callback does not read, or gate the call on a comparison; feeding its own input loops forever.`});return}if(s!=="scheduleOnRN")return;const n=O(r,a);if(n!==null)e.report({node:r,message:n})},"CallExpression:exit"(r){if(p.has(i(r)))t.pop()}}}});
@@ -1,72 +1 @@
1
- import { attributeName, calleeName, gate, problem, tagPath } from "../../../lib/ast.js";
2
- import { ANIMATED_STYLE_HOOKS } from "./shared.js";
3
- const NEEDS_ANIMATED_COMPONENT = "Render this with the matching `Animated.*` component so the animated style takes effect. On a plain element the style is applied once at mount and never updates, and nothing errors.";
4
- const ANIMATED_COMPONENT_FACTORIES = new Set(["createAnimatedComponent", "withUnistyles", "withAnimated"]);
5
- /** The tag name node of an element, whether handed the element or its opening tag. */
6
- const elementName = (node) => {
7
- if (node.type === "JSXElement")
8
- return node.openingElement.name;
9
- if (node.type === "JSXOpeningElement")
10
- return node.name;
11
- return null;
12
- };
13
- /** The identifiers a `style` prop value names, whether it holds one style or an array of them. */
14
- const referencedStyleNames = (value) => {
15
- if (value?.type !== "JSXExpressionContainer")
16
- return [];
17
- const { expression } = value;
18
- if (expression.type === "Identifier")
19
- return [expression.name];
20
- if (expression.type !== "ArrayExpression")
21
- return [];
22
- return expression.elements.flatMap(element => (element?.type === "Identifier" ? [element.name] : []));
23
- };
24
- export const animatedStyleNeedsAnimatedComponent = problem("An animated style only takes effect on an `Animated.*` component. A plain element applies it once at mount and then never updates.", {
25
- createOnce(context) {
26
- let animatedStyles = new Set();
27
- let animatedComponents = new Set();
28
- let candidates = [];
29
- return {
30
- before() {
31
- animatedStyles = new Set();
32
- animatedComponents = new Set();
33
- candidates = [];
34
- return gate(context, "useAnimatedStyle", "useAnimatedProps");
35
- },
36
- VariableDeclarator(node) {
37
- const { id, init } = node;
38
- if (id.type !== "Identifier")
39
- return;
40
- if (init?.type !== "CallExpression")
41
- return;
42
- const callee = calleeName(init);
43
- if (ANIMATED_STYLE_HOOKS.has(callee))
44
- animatedStyles.add(id.name);
45
- else if (ANIMATED_COMPONENT_FACTORIES.has(callee))
46
- animatedComponents.add(id.name);
47
- },
48
- "Program:exit"() {
49
- if (animatedStyles.size === 0)
50
- return;
51
- for (const { node, tag, referenced } of candidates) {
52
- if (animatedComponents.has(tag))
53
- continue;
54
- if (!referenced.some(name => animatedStyles.has(name)))
55
- continue;
56
- context.report({ node, message: NEEDS_ANIMATED_COMPONENT });
57
- }
58
- },
59
- JSXAttribute(node) {
60
- if (attributeName(node) !== "style")
61
- return;
62
- const tag = tagPath(elementName(node.parent));
63
- if (tag === "" || tag.startsWith("Animated"))
64
- return;
65
- const referenced = referencedStyleNames(node.value);
66
- if (referenced.length === 0)
67
- return;
68
- candidates.push({ node, tag, referenced });
69
- },
70
- };
71
- },
72
- });
1
+ import{attributeName as m,calleeName as l,gate as p,problem as f,tagPath as c}from"../../../lib/ast.js";import{ANIMATED_STYLE_HOOKS as d}from"./shared.js";const u="Render this with the matching `Animated.*` component so the animated style takes effect. On a plain element the style is applied once at mount and never updates, and nothing errors.",y=new Set(["createAnimatedComponent","withUnistyles","withAnimated"]),A=(e)=>{if(e.type==="JSXElement")return e.openingElement.name;if(e.type==="JSXOpeningElement")return e.name;return null},h=(e)=>{if(e?.type!=="JSXExpressionContainer")return[];const{expression:t}=e;if(t.type==="Identifier")return[t.name];if(t.type!=="ArrayExpression")return[];return t.elements.flatMap((i)=>i?.type==="Identifier"?[i.name]:[])};export const animatedStyleNeedsAnimatedComponent=f("An animated style only takes effect on an `Animated.*` component. A plain element applies it once at mount and then never updates.",{createOnce(e){let t=new Set,i=new Set,o=[];return{before(){t=new Set;i=new Set;o=[];return p(e,"useAnimatedStyle","useAnimatedProps")},VariableDeclarator(a){const{id:n,init:r}=a;if(n.type!=="Identifier")return;if(r?.type!=="CallExpression")return;const s=l(r);if(d.has(s))t.add(n.name);else if(y.has(s))i.add(n.name)},"Program:exit"(){if(t.size===0)return;for(const{node:a,tag:n,referenced:r}of o){if(i.has(n))continue;if(!r.some((s)=>t.has(s)))continue;e.report({node:a,message:u})}},JSXAttribute(a){if(m(a)!=="style")return;const n=c(A(a.parent));if(n===""||n.startsWith("Animated"))return;const r=h(a.value);if(r.length===0)return;o.push({node:a,tag:n,referenced:r})}}}});
@@ -1,38 +1 @@
1
- import { calleeName, problem } from "../../../lib/ast.js";
2
- import { ANIMATED_STYLE_HOOKS } from "./shared.js";
3
- const UPDATER_SIDE_EFFECT = "Schedule this RN side effect from an animation completion callback or `useAnimatedReaction` instead; `useAnimatedStyle`/`useAnimatedProps` updaters must stay pure.";
4
- export const animatedUpdaterPurity = problem("An updater passed to `useAnimatedStyle` or `useAnimatedProps` must stay pure, so it may not write a shared value or call `scheduleOnRN`.", {
5
- createOnce(context) {
6
- let depth = 0;
7
- return {
8
- before() {
9
- depth = 0;
10
- },
11
- CallExpression(node) {
12
- const name = calleeName(node);
13
- if (ANIMATED_STYLE_HOOKS.has(name)) {
14
- depth += 1;
15
- return;
16
- }
17
- if (depth === 0)
18
- return;
19
- if (name === "set" || name === "modify") {
20
- if (node.callee.type !== "MemberExpression")
21
- return;
22
- context.report({
23
- node,
24
- message: `Move this \`.${name}()\` write out to an event handler, an effect, a derived value, or \`useAnimatedReaction\`; \`useAnimatedStyle\`/\`useAnimatedProps\` updaters must stay pure.`,
25
- });
26
- return;
27
- }
28
- if (name !== "scheduleOnRN")
29
- return;
30
- context.report({ node, message: UPDATER_SIDE_EFFECT });
31
- },
32
- "CallExpression:exit"(node) {
33
- if (ANIMATED_STYLE_HOOKS.has(calleeName(node)))
34
- depth -= 1;
35
- },
36
- };
37
- },
38
- });
1
+ import{calleeName as s,problem as n}from"../../../lib/ast.js";import{ANIMATED_STYLE_HOOKS as i}from"./shared.js";const o="Schedule this RN side effect from an animation completion callback or `useAnimatedReaction` instead; `useAnimatedStyle`/`useAnimatedProps` updaters must stay pure.";export const animatedUpdaterPurity=n("An updater passed to `useAnimatedStyle` or `useAnimatedProps` must stay pure, so it may not write a shared value or call `scheduleOnRN`.",{createOnce(a){let r=0;return{before(){r=0},CallExpression(e){const t=s(e);if(i.has(t)){r+=1;return}if(r===0)return;if(t==="set"||t==="modify"){if(e.callee.type!=="MemberExpression")return;a.report({node:e,message:`Move this \`.${t}()\` write out to an event handler, an effect, a derived value, or \`useAnimatedReaction\`; \`useAnimatedStyle\`/\`useAnimatedProps\` updaters must stay pure.`});return}if(t!=="scheduleOnRN")return;a.report({node:e,message:o})},"CallExpression:exit"(e){if(i.has(s(e)))r-=1}}}});