@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,80 +1 @@
1
- import { calleeName, gate, problem } from "../../../lib/ast.js";
2
- import { ANIMATED_STYLE_HOOKS } from "./shared.js";
3
- const GPU_PROPERTIES_ONLY = "Animate `transform` and `opacity` instead: a panel that grows is `scaleY` with `transformOrigin`, a thing that slides is `translateY`. This property recalculates layout on every frame.";
4
- /** Style properties whose animation forces a layout pass. */
5
- const LAYOUT_PROPS = new Set([
6
- "width",
7
- "height",
8
- "minWidth",
9
- "minHeight",
10
- "maxWidth",
11
- "maxHeight",
12
- "top",
13
- "left",
14
- "right",
15
- "bottom",
16
- "start",
17
- "end",
18
- "flex",
19
- "flexBasis",
20
- "padding",
21
- "paddingTop",
22
- "paddingBottom",
23
- "paddingLeft",
24
- "paddingRight",
25
- "paddingStart",
26
- "paddingEnd",
27
- "paddingHorizontal",
28
- "paddingVertical",
29
- "margin",
30
- "marginTop",
31
- "marginBottom",
32
- "marginLeft",
33
- "marginRight",
34
- "marginStart",
35
- "marginEnd",
36
- "marginHorizontal",
37
- "marginVertical",
38
- "gap",
39
- "rowGap",
40
- "columnGap",
41
- ]);
42
- const styleKeyName = (property) => {
43
- if (property.type !== "Property")
44
- return null;
45
- const { key } = property;
46
- if (key.type === "Identifier")
47
- return key.name;
48
- if (key.type === "Literal")
49
- return String(key.value);
50
- return null;
51
- };
52
- export const gpuPropertiesOnly = problem("Animate `transform` and `opacity` in `useAnimatedStyle` and `useAnimatedProps`. Layout properties such as `width` or `margin` recalculate layout every frame.", {
53
- createOnce(context) {
54
- let depth = 0;
55
- return {
56
- before() {
57
- depth = 0;
58
- return gate(context, "useAnimatedStyle", "useAnimatedProps");
59
- },
60
- CallExpression(node) {
61
- if (ANIMATED_STYLE_HOOKS.has(calleeName(node)))
62
- depth += 1;
63
- },
64
- "CallExpression:exit"(node) {
65
- if (ANIMATED_STYLE_HOOKS.has(calleeName(node)))
66
- depth -= 1;
67
- },
68
- Property(node) {
69
- if (depth === 0)
70
- return;
71
- if (node.parent.type !== "ObjectExpression")
72
- return;
73
- const name = styleKeyName(node);
74
- if (name !== null && LAYOUT_PROPS.has(name)) {
75
- context.report({ node, message: GPU_PROPERTIES_ONLY });
76
- }
77
- },
78
- };
79
- },
80
- });
1
+ import{calleeName as i,gate as o,problem as s}from"../../../lib/ast.js";import{ANIMATED_STYLE_HOOKS as n}from"./shared.js";const p="Animate `transform` and `opacity` instead: a panel that grows is `scaleY` with `transformOrigin`, a thing that slides is `translateY`. This property recalculates layout on every frame.",m=new Set(["width","height","minWidth","minHeight","maxWidth","maxHeight","top","left","right","bottom","start","end","flex","flexBasis","padding","paddingTop","paddingBottom","paddingLeft","paddingRight","paddingStart","paddingEnd","paddingHorizontal","paddingVertical","margin","marginTop","marginBottom","marginLeft","marginRight","marginStart","marginEnd","marginHorizontal","marginVertical","gap","rowGap","columnGap"]),l=(a)=>{if(a.type!=="Property")return null;const{key:t}=a;if(t.type==="Identifier")return t.name;if(t.type==="Literal")return String(t.value);return null};export const gpuPropertiesOnly=s("Animate `transform` and `opacity` in `useAnimatedStyle` and `useAnimatedProps`. Layout properties such as `width` or `margin` recalculate layout every frame.",{createOnce(a){let t=0;return{before(){t=0;return o(a,"useAnimatedStyle","useAnimatedProps")},CallExpression(e){if(n.has(i(e)))t+=1},"CallExpression:exit"(e){if(n.has(i(e)))t-=1},Property(e){if(t===0)return;if(e.parent.type!=="ObjectExpression")return;const r=l(e);if(r!==null&&m.has(r))a.report({node:e,message:p})}}}});
@@ -1,43 +1 @@
1
- import { calleeName, findInSubtree, FUNCTION_TYPES, hasAncestor, problem } from "../../../lib/ast.js";
2
- const LAYOUT_BUILDER = "Build this layout animation at module scope when it is static, or memoize it when it depends on component values.";
3
- const LAYOUT_ATTRIBUTES = new Set(["entering", "exiting", "layout"]);
4
- const LAYOUT_BUILDER_METHODS = new Set([
5
- "duration",
6
- "delay",
7
- "springify",
8
- "damping",
9
- "stiffness",
10
- "mass",
11
- "easing",
12
- "withCallback",
13
- "withInitialValues",
14
- "withTargetValues",
15
- ]);
16
- /** The builder call inside a layout-animation prop value, if there is one. */
17
- const findBuilderCall = (value) => findInSubtree(value, current => {
18
- if (current.type !== "CallExpression" || current.callee.type !== "MemberExpression")
19
- return false;
20
- const { property } = current.callee;
21
- return property.type === "Identifier" && LAYOUT_BUILDER_METHODS.has(property.name);
22
- });
23
- const MEMO_HOOKS = new Set(["useMemo", "useCallback"]);
24
- export const hoistLayoutAnimationBuilder = problem("A layout animation belongs at module scope, or inside a memo when it depends on component values. The `entering`/`exiting`/`layout` props otherwise rebuild it on every render.", {
25
- createOnce(context) {
26
- return {
27
- JSXAttribute(node) {
28
- if (node.name.type !== "JSXIdentifier")
29
- return;
30
- if (!LAYOUT_ATTRIBUTES.has(node.name.name))
31
- return;
32
- if (!hasAncestor(node, current => FUNCTION_TYPES.has(current.type)))
33
- return;
34
- const builderCall = findBuilderCall(node.value);
35
- if (!builderCall)
36
- return;
37
- if (hasAncestor(builderCall, current => MEMO_HOOKS.has(calleeName(current))))
38
- return;
39
- context.report({ node: builderCall, message: LAYOUT_BUILDER });
40
- },
41
- };
42
- },
43
- });
1
+ import{calleeName as s,findInSubtree as r,FUNCTION_TYPES as o,hasAncestor as a,problem as l}from"../../../lib/ast.js";const u="Build this layout animation at module scope when it is static, or memoize it when it depends on component values.",m=new Set(["entering","exiting","layout"]),p=new Set(["duration","delay","springify","damping","stiffness","mass","easing","withCallback","withInitialValues","withTargetValues"]),c=(n)=>r(n,(e)=>{if(e.type!=="CallExpression"||e.callee.type!=="MemberExpression")return!1;const{property:t}=e.callee;return t.type==="Identifier"&&p.has(t.name)}),d=new Set(["useMemo","useCallback"]);export const hoistLayoutAnimationBuilder=l("A layout animation belongs at module scope, or inside a memo when it depends on component values. The `entering`/`exiting`/`layout` props otherwise rebuild it on every render.",{createOnce(n){return{JSXAttribute(e){if(e.name.type!=="JSXIdentifier")return;if(!m.has(e.name.name))return;if(!a(e,(i)=>o.has(i.type)))return;const t=c(e.value);if(!t)return;if(a(t,(i)=>d.has(s(i))))return;n.report({node:t,message:u})}}}});
@@ -1,51 +1 @@
1
- import { defineModule } from "../../../lib/module.js";
2
- import { animatedReactionSafety } from "./animated-reaction-safety.js";
3
- import { animatedStyleNeedsAnimatedComponent } from "./animated-style-needs-animated-component.js";
4
- import { animatedUpdaterPurity } from "./animated-updater-purity.js";
5
- import { gpuPropertiesOnly } from "./gpu-properties-only.js";
6
- import { hoistLayoutAnimationBuilder } from "./hoist-layout-animation-builder.js";
7
- import { interpolateNeedsClamp } from "./interpolate-needs-clamp.js";
8
- import { noReactStateFromContinuousWorklet } from "./no-react-state-from-continuous-worklet.js";
9
- import { noSharedValueDotValue } from "./no-shared-value-dot-value.js";
10
- import { preferLazySharedValueInitializer } from "./prefer-lazy-shared-value-initializer.js";
11
- import { scheduleOnRnScope } from "./schedule-on-rn-scope.js";
12
- import { sharedValueUsage } from "./shared-value-usage.js";
13
- export default defineModule({
14
- meta: { name: "@ashstack/reanimated" },
15
- url: import.meta.url,
16
- packages: ["react-native-reanimated"],
17
- option: "reanimated",
18
- docsWhen: "auto-enabled when `react-native-reanimated` is a dependency",
19
- rules: {
20
- "animated-reaction-safety": animatedReactionSafety,
21
- "animated-style-needs-animated-component": animatedStyleNeedsAnimatedComponent,
22
- "animated-updater-purity": animatedUpdaterPurity,
23
- "gpu-properties-only": gpuPropertiesOnly,
24
- "hoist-layout-animation-builder": hoistLayoutAnimationBuilder,
25
- "interpolate-needs-clamp": interpolateNeedsClamp,
26
- "no-shared-value-dot-value": noSharedValueDotValue,
27
- "no-react-state-from-continuous-worklet": noReactStateFromContinuousWorklet,
28
- "prefer-lazy-shared-value-initializer": preferLazySharedValueInitializer,
29
- "schedule-on-rn-scope": scheduleOnRnScope,
30
- "shared-value-usage": sharedValueUsage,
31
- },
32
- restrictedImports: {
33
- paths: [
34
- {
35
- name: "react-native",
36
- importNames: ["Animated"],
37
- message: "Use `react-native-reanimated` instead of React Native's `Animated` API.",
38
- },
39
- {
40
- name: "react-native-reanimated",
41
- importNames: ["runOnJS", "runOnUI"],
42
- message: "Use `scheduleOnRN` and `scheduleOnUI` from `react-native-worklets`; `runOnJS` and `runOnUI` are deprecated.",
43
- },
44
- {
45
- name: "react-native-worklets",
46
- importNames: ["runOnJS", "runOnUI"],
47
- message: "Use `scheduleOnRN` and `scheduleOnUI` from `react-native-worklets`; `runOnJS` and `runOnUI` are deprecated.",
48
- },
49
- ],
50
- },
51
- });
1
+ import{defineModule as e}from"../../../lib/module.js";import{animatedReactionSafety as a}from"./animated-reaction-safety.js";import{animatedStyleNeedsAnimatedComponent as t}from"./animated-style-needs-animated-component.js";import{animatedUpdaterPurity as r}from"./animated-updater-purity.js";import{gpuPropertiesOnly as n}from"./gpu-properties-only.js";import{hoistLayoutAnimationBuilder as o}from"./hoist-layout-animation-builder.js";import{interpolateNeedsClamp as i}from"./interpolate-needs-clamp.js";import{noReactStateFromContinuousWorklet as m}from"./no-react-state-from-continuous-worklet.js";import{noSharedValueDotValue as d}from"./no-shared-value-dot-value.js";import{preferLazySharedValueInitializer as s}from"./prefer-lazy-shared-value-initializer.js";import{scheduleOnRnScope as p}from"./schedule-on-rn-scope.js";import{sharedValueUsage as u}from"./shared-value-usage.js";export default e({meta:{name:"@ashstack/reanimated"},url:import.meta.url,packages:["react-native-reanimated"],option:"reanimated",docsWhen:"auto-enabled when `react-native-reanimated` is a dependency",rules:{"animated-reaction-safety":a,"animated-style-needs-animated-component":t,"animated-updater-purity":r,"gpu-properties-only":n,"hoist-layout-animation-builder":o,"interpolate-needs-clamp":i,"no-shared-value-dot-value":d,"no-react-state-from-continuous-worklet":m,"prefer-lazy-shared-value-initializer":s,"schedule-on-rn-scope":p,"shared-value-usage":u},restrictedImports:{paths:[{name:"react-native",importNames:["Animated"],message:"Use `react-native-reanimated` instead of React Native's `Animated` API."},{name:"react-native-reanimated",importNames:["runOnJS","runOnUI"],message:"Use `scheduleOnRN` and `scheduleOnUI` from `react-native-worklets`; `runOnJS` and `runOnUI` are deprecated."},{name:"react-native-worklets",importNames:["runOnJS","runOnUI"],message:"Use `scheduleOnRN` and `scheduleOnUI` from `react-native-worklets`; `runOnJS` and `runOnUI` are deprecated."}]}});
@@ -1,41 +1 @@
1
- import { gate, problem } from "../../../lib/ast.js";
2
- const MISSING = "Pass `Extrapolation.CLAMP` as the fourth argument to `interpolate`. Without it the output keeps extrapolating past the input range, so a scroll offset of 400 against `[0, 100]` runs well past where it should stop.";
3
- const NOT_CLAMPING = "Change this fourth argument to `Extrapolation.CLAMP`. `EXTEND` and `IDENTITY` both keep extrapolating past the input range, so a scroll offset of 400 against `[0, 100]` runs well past where it should stop.";
4
- const NON_CLAMPING = new Set(["EXTEND", "IDENTITY", "extend", "identity"]);
5
- /** An extrapolation this rule can see is not clamping. An opaque one — a variable, a call — reports nothing. */
6
- const isNonClamping = (node) => {
7
- if (node.type === "Literal")
8
- return typeof node.value === "string" && NON_CLAMPING.has(node.value);
9
- if (node.type === "Identifier")
10
- return NON_CLAMPING.has(node.name);
11
- if (node.type === "MemberExpression") {
12
- return node.property.type === "Identifier" && NON_CLAMPING.has(node.property.name);
13
- }
14
- if (node.type === "ObjectExpression") {
15
- return node.properties.some(property => property.type === "Property" && isNonClamping(property.value));
16
- }
17
- return false;
18
- };
19
- export const interpolateNeedsClamp = problem("Give `interpolate()` a fourth argument that clamps. Without one, or with `EXTEND` or `IDENTITY`, the output keeps going past the ends of the input range.", {
20
- createOnce(context) {
21
- return {
22
- before() {
23
- return gate(context, "interpolate");
24
- },
25
- CallExpression(node) {
26
- const { callee, arguments: args } = node;
27
- if (callee.type !== "Identifier" || callee.name !== "interpolate")
28
- return;
29
- if (args.length < 3 || args.length > 4)
30
- return;
31
- if (args.some(argument => argument.type === "SpreadElement"))
32
- return;
33
- const extrapolation = args[3];
34
- if (extrapolation === undefined)
35
- context.report({ node, message: MISSING });
36
- else if (isNonClamping(extrapolation))
37
- context.report({ node: extrapolation, message: NOT_CLAMPING });
38
- },
39
- };
40
- },
41
- });
1
+ import{gate as p,problem as l}from"../../../lib/ast.js";const u="Pass `Extrapolation.CLAMP` as the fourth argument to `interpolate`. Without it the output keeps extrapolating past the input range, so a scroll offset of 400 against `[0, 100]` runs well past where it should stop.",f="Change this fourth argument to `Extrapolation.CLAMP`. `EXTEND` and `IDENTITY` both keep extrapolating past the input range, so a scroll offset of 400 against `[0, 100]` runs well past where it should stop.",s=new Set(["EXTEND","IDENTITY","extend","identity"]),o=(e)=>{if(e.type==="Literal")return typeof e.value==="string"&&s.has(e.value);if(e.type==="Identifier")return s.has(e.name);if(e.type==="MemberExpression")return e.property.type==="Identifier"&&s.has(e.property.name);if(e.type==="ObjectExpression")return e.properties.some((t)=>t.type==="Property"&&o(t.value));return!1};export const interpolateNeedsClamp=l("Give `interpolate()` a fourth argument that clamps. Without one, or with `EXTEND` or `IDENTITY`, the output keeps going past the ends of the input range.",{createOnce(e){return{before(){return p(e,"interpolate")},CallExpression(t){const{callee:n,arguments:r}=t;if(n.type!=="Identifier"||n.name!=="interpolate")return;if(r.length<3||r.length>4)return;if(r.some((i)=>i.type==="SpreadElement"))return;const a=r[3];if(a===void 0)e.report({node:t,message:u});else if(o(a))e.report({node:a,message:f})}}}});
@@ -1,86 +1 @@
1
- import { calleeName, enclosingCall, gate, importedSpecifiers, isFunction, problem, subtreeHas, } from "../../../lib/ast.js";
2
- const CONTINUOUS_WORKLET_STATE = "Keep this gate in shared or native state, or apply it through an imperative ref. A continuously evaluated worklet that schedules React state can land a Fabric commit or a Skia re-recording on an animation frame.";
3
- /** Hooks whose callback is re-evaluated as the animation runs. */
4
- const CONTINUOUS_HOOKS = new Set([
5
- "useAnimatedReaction",
6
- "useFrameCallback",
7
- "useDerivedValue",
8
- "useAnimatedStyle",
9
- "useAnimatedProps",
10
- "useAnimatedScrollHandler",
11
- ]);
12
- /** True for `import { name } from ...` — a named import, not a default or namespace one. */
13
- const importsName = (specifier, name) => specifier.type === "ImportSpecifier" && specifier.imported.type === "Identifier" && specifier.imported.name === name;
14
- /** The setters this argument names, whether handed over directly or called inside an inline callback. */
15
- const settersReached = (target) => {
16
- if (target.type === "Identifier")
17
- return [target.name];
18
- if (!isFunction(target))
19
- return [];
20
- const called = [];
21
- subtreeHas(target, current => {
22
- if (current.type === "CallExpression" && current.callee.type === "Identifier")
23
- called.push(current.callee.name);
24
- return false;
25
- });
26
- return called;
27
- };
28
- export const noReactStateFromContinuousWorklet = problem("A worklet that runs every frame must not send a React state setter through `scheduleOnRN`. That puts a Fabric commit on an animation frame.", {
29
- createOnce(context) {
30
- const setters = new Set();
31
- const pending = [];
32
- let useStateFromReact = false;
33
- let scheduleFromWorklets = false;
34
- return {
35
- before() {
36
- setters.clear();
37
- pending.length = 0;
38
- useStateFromReact = false;
39
- scheduleFromWorklets = false;
40
- return gate(context, "scheduleOnRN");
41
- },
42
- ImportDeclaration(node) {
43
- for (const specifier of importedSpecifiers(node, "react")) {
44
- if (importsName(specifier, "useState"))
45
- useStateFromReact = true;
46
- }
47
- for (const specifier of importedSpecifiers(node, "react-native-worklets")) {
48
- if (importsName(specifier, "scheduleOnRN"))
49
- scheduleFromWorklets = true;
50
- }
51
- },
52
- VariableDeclarator(node) {
53
- const { id, init } = node;
54
- if (id.type !== "ArrayPattern")
55
- return;
56
- if (init?.type !== "CallExpression")
57
- return;
58
- if (init.callee.type !== "Identifier" || init.callee.name !== "useState")
59
- return;
60
- const setter = id.elements[1];
61
- if (setter?.type === "Identifier")
62
- setters.add(setter.name);
63
- },
64
- CallExpression(node) {
65
- if (calleeName(node) !== "scheduleOnRN")
66
- return;
67
- const target = node.arguments[0];
68
- if (target === undefined)
69
- return;
70
- if (!enclosingCall(node, CONTINUOUS_HOOKS))
71
- return;
72
- for (const name of settersReached(target))
73
- pending.push({ node, name });
74
- },
75
- "Program:exit"() {
76
- if (!useStateFromReact || !scheduleFromWorklets)
77
- return;
78
- for (const entry of pending) {
79
- if (!setters.has(entry.name))
80
- continue;
81
- context.report({ node: entry.node, message: CONTINUOUS_WORKLET_STATE });
82
- }
83
- },
84
- };
85
- },
86
- });
1
+ import{calleeName as f,enclosingCall as m,gate as p,importedSpecifiers as c,isFunction as d,problem as h,subtreeHas as y}from"../../../lib/ast.js";const S="Keep this gate in shared or native state, or apply it through an imperative ref. A continuously evaluated worklet that schedules React state can land a Fabric commit or a Skia re-recording on an animation frame.",O=new Set(["useAnimatedReaction","useFrameCallback","useDerivedValue","useAnimatedStyle","useAnimatedProps","useAnimatedScrollHandler"]),u=(t,a)=>t.type==="ImportSpecifier"&&t.imported.type==="Identifier"&&t.imported.name===a,R=(t)=>{if(t.type==="Identifier")return[t.name];if(!d(t))return[];const a=[];y(t,(n)=>{if(n.type==="CallExpression"&&n.callee.type==="Identifier")a.push(n.callee.name);return!1});return a};export const noReactStateFromContinuousWorklet=h("A worklet that runs every frame must not send a React state setter through `scheduleOnRN`. That puts a Fabric commit on an animation frame.",{createOnce(t){const a=new Set,n=[];let i=!1,o=!1;return{before(){a.clear();n.length=0;i=!1;o=!1;return p(t,"scheduleOnRN")},ImportDeclaration(e){for(const r of c(e,"react"))if(u(r,"useState"))i=!0;for(const r of c(e,"react-native-worklets"))if(u(r,"scheduleOnRN"))o=!0},VariableDeclarator(e){const{id:r,init:s}=e;if(r.type!=="ArrayPattern")return;if(s?.type!=="CallExpression")return;if(s.callee.type!=="Identifier"||s.callee.name!=="useState")return;const l=r.elements[1];if(l?.type==="Identifier")a.add(l.name)},CallExpression(e){if(f(e)!=="scheduleOnRN")return;const r=e.arguments[0];if(r===void 0)return;if(!m(e,O))return;for(const s of R(r))n.push({node:e,name:s})},"Program:exit"(){if(!i||!o)return;for(const e of n){if(!a.has(e.name))continue;t.report({node:e.node,message:S})}}}}});
@@ -1,112 +1 @@
1
- import { isProducerCall } from "./shared.js";
2
- const MESSAGES = {
3
- read: "Read this shared value with `.get()` so the React Compiler can track the read.",
4
- write: "Write this shared value with `.set(...)` so the React Compiler can track the mutation.",
5
- compound: "Write this shared value with `.set(v => ...)`, or `.set(x.get() ...)`, so the React Compiler can track the mutation.",
6
- };
7
- const COMPOUND_OPERATORS = new Map([
8
- ["+=", "+"],
9
- ["-=", "-"],
10
- ["*=", "*"],
11
- ["/=", "/"],
12
- ]);
13
- /** The identifier a plain `x.value` reads from, or null when the node is not one. */
14
- const dotValueOwner = (node) => {
15
- if (node?.type !== "MemberExpression" || node.optional || node.computed)
16
- return null;
17
- const { object, property } = node;
18
- if (object.type !== "Identifier")
19
- return null;
20
- if (property.type !== "Identifier" || property.name !== "value")
21
- return null;
22
- return object.name;
23
- };
24
- /** A parent that makes `x.value` a write target no `.get()` rewrite can stand in for. */
25
- const WRITE_TARGET_PARENTS = new Set([
26
- "UpdateExpression",
27
- "ArrayPattern",
28
- "ObjectPattern",
29
- "RestElement",
30
- "AssignmentPattern",
31
- ]);
32
- export const noSharedValueDotValue = {
33
- meta: {
34
- type: "problem",
35
- docs: {
36
- description: "Shared values are read with `.get()` and written with `.set(...)`, never through `.value`. React Compiler cannot track a `.value` access.",
37
- },
38
- hasSuggestions: true,
39
- },
40
- createOnce(context) {
41
- const names = new Set();
42
- const candidates = [];
43
- return {
44
- before() {
45
- names.clear();
46
- candidates.length = 0;
47
- },
48
- VariableDeclarator(node) {
49
- if (node.id.type === "Identifier" && isProducerCall(node.init)) {
50
- names.add(node.id.name);
51
- }
52
- },
53
- AssignmentExpression(node) {
54
- const name = dotValueOwner(node.left);
55
- if (name === null)
56
- return;
57
- const operator = COMPOUND_OPERATORS.get(node.operator);
58
- if (operator) {
59
- candidates.push({ kind: "compound", node, name, operator, right: node.right });
60
- return;
61
- }
62
- if (node.operator !== "=")
63
- return;
64
- candidates.push({ kind: "write", node, name, right: node.right });
65
- },
66
- UpdateExpression(node) {
67
- const name = dotValueOwner(node.argument);
68
- if (name === null)
69
- return;
70
- candidates.push({ kind: "update", node, name });
71
- },
72
- MemberExpression(node) {
73
- const name = dotValueOwner(node);
74
- if (name === null)
75
- return;
76
- if (node.parent.type === "AssignmentExpression" && node.parent.left === node)
77
- return;
78
- if (WRITE_TARGET_PARENTS.has(node.parent.type))
79
- return;
80
- candidates.push({ kind: "read", node, name });
81
- },
82
- "Program:exit"() {
83
- for (const candidate of candidates) {
84
- const { node, name } = candidate;
85
- if (!names.has(name))
86
- continue;
87
- if (candidate.kind === "update") {
88
- context.report({ node, message: MESSAGES.compound });
89
- continue;
90
- }
91
- if (candidate.kind === "read") {
92
- context.report({
93
- node,
94
- message: MESSAGES.read,
95
- suggest: [{ desc: `Rewrite as ${name}.get()`, fix: fixer => fixer.replaceText(node, `${name}.get()`) }],
96
- });
97
- continue;
98
- }
99
- const right = context.sourceCode.getText(candidate.right);
100
- const replacement = candidate.kind === "compound"
101
- ? `${name}.set(${name}.get() ${candidate.operator} (${right}))`
102
- : `${name}.set(${right})`;
103
- context.report({
104
- node,
105
- message: candidate.kind === "compound" ? MESSAGES.compound : MESSAGES.write,
106
- suggest: [{ desc: `Rewrite as ${name}.set(...)`, fix: fixer => fixer.replaceText(node, replacement) }],
107
- });
108
- }
109
- },
110
- };
111
- },
112
- };
1
+ import{isProducerCall as l}from"./shared.js";const s={read:"Read this shared value with `.get()` so the React Compiler can track the read.",write:"Write this shared value with `.set(...)` so the React Compiler can track the mutation.",compound:"Write this shared value with `.set(v => ...)`, or `.set(x.get() ...)`, so the React Compiler can track the mutation."},m=new Map([["+=","+"],["-=","-"],["*=","*"],["/=","/"]]),c=(a)=>{if(a?.type!=="MemberExpression"||a.optional||a.computed)return null;const{object:i,property:n}=a;if(i.type!=="Identifier")return null;if(n.type!=="Identifier"||n.name!=="value")return null;return i.name},h=new Set(["UpdateExpression","ArrayPattern","ObjectPattern","RestElement","AssignmentPattern"]);export const noSharedValueDotValue={meta:{type:"problem",docs:{description:"Shared values are read with `.get()` and written with `.set(...)`, never through `.value`. React Compiler cannot track a `.value` access."},hasSuggestions:!0},createOnce(a){const i=new Set,n=[];return{before(){i.clear();n.length=0},VariableDeclarator(e){if(e.id.type==="Identifier"&&l(e.init))i.add(e.id.name)},AssignmentExpression(e){const t=c(e.left);if(t===null)return;const r=m.get(e.operator);if(r){n.push({kind:"compound",node:e,name:t,operator:r,right:e.right});return}if(e.operator!=="=")return;n.push({kind:"write",node:e,name:t,right:e.right})},UpdateExpression(e){const t=c(e.argument);if(t===null)return;n.push({kind:"update",node:e,name:t})},MemberExpression(e){const t=c(e);if(t===null)return;if(e.parent.type==="AssignmentExpression"&&e.parent.left===e)return;if(h.has(e.parent.type))return;n.push({kind:"read",node:e,name:t})},"Program:exit"(){for(const e of n){const{node:t,name:r}=e;if(!i.has(r))continue;if(e.kind==="update"){a.report({node:t,message:s.compound});continue}if(e.kind==="read"){a.report({node:t,message:s.read,suggest:[{desc:`Rewrite as ${r}.get()`,fix:(o)=>o.replaceText(t,`${r}.get()`)}]});continue}const u=a.sourceCode.getText(e.right),p=e.kind==="compound"?`${r}.set(${r}.get() ${e.operator} (${u}))`:`${r}.set(${u})`;a.report({node:t,message:e.kind==="compound"?s.compound:s.write,suggest:[{desc:`Rewrite as ${r}.set(...)`,fix:(o)=>o.replaceText(t,p)}]})}}}}};
@@ -1,24 +1 @@
1
- import { gate, problem } from "../../../lib/ast.js";
2
- const EAGER_INITIALIZER = "Wrap this in a lazy initializer: `useSharedValue(() => compute())`, since the eager call runs on every React render.";
3
- export const preferLazySharedValueInitializer = problem("Pass a computed `useSharedValue` initial value as a function. An eager call runs on every render while only the first result is kept.", {
4
- createOnce(context) {
5
- return {
6
- before() {
7
- return gate(context, "useSharedValue");
8
- },
9
- CallExpression(node) {
10
- const { callee } = node;
11
- if (callee.type !== "Identifier" || callee.name !== "useSharedValue")
12
- return;
13
- const argument = node.arguments[0];
14
- if (!argument)
15
- return;
16
- const eager = (argument.type === "CallExpression" && argument.callee.type === "Identifier") ||
17
- argument.type === "NewExpression";
18
- if (!eager)
19
- return;
20
- context.report({ node, message: EAGER_INITIALIZER });
21
- },
22
- };
23
- },
24
- });
1
+ import{gate as n,problem as i}from"../../../lib/ast.js";const s="Wrap this in a lazy initializer: `useSharedValue(() => compute())`, since the eager call runs on every React render.";export const preferLazySharedValueInitializer=i("Pass a computed `useSharedValue` initial value as a function. An eager call runs on every render while only the first result is kept.",{createOnce(a){return{before(){return n(a,"useSharedValue")},CallExpression(r){const{callee:t}=r;if(t.type!=="Identifier"||t.name!=="useSharedValue")return;const e=r.arguments[0];if(!e)return;if(!(e.type==="CallExpression"&&e.callee.type==="Identifier"||e.type==="NewExpression"))return;a.report({node:r,message:s})}}}});
@@ -1,21 +1 @@
1
- import { calleeName, gate, isFunction, problem } from "../../../lib/ast.js";
2
- const INLINE_CALLBACK = "Pass a function declared in RN Runtime scope to `scheduleOnRN`; an inline callback has ambiguous runtime ownership and can be created on the wrong runtime.";
3
- export const scheduleOnRnScope = problem("`scheduleOnRN` takes a function declared in RN Runtime scope. An inline callback can end up created on the wrong runtime.", {
4
- createOnce(context) {
5
- return {
6
- before() {
7
- return gate(context, "scheduleOnRN");
8
- },
9
- CallExpression(node) {
10
- if (calleeName(node) !== "scheduleOnRN")
11
- return;
12
- const [callback] = node.arguments;
13
- if (callback === undefined)
14
- return;
15
- if (isFunction(callback) && callback.type !== "FunctionDeclaration") {
16
- context.report({ node: callback, message: INLINE_CALLBACK });
17
- }
18
- },
19
- };
20
- },
21
- });
1
+ import{calleeName as a,gate as r,isFunction as t,problem as i}from"../../../lib/ast.js";const o="Pass a function declared in RN Runtime scope to `scheduleOnRN`; an inline callback has ambiguous runtime ownership and can be created on the wrong runtime.";export const scheduleOnRnScope=i("`scheduleOnRN` takes a function declared in RN Runtime scope. An inline callback can end up created on the wrong runtime.",{createOnce(n){return{before(){return r(n,"scheduleOnRN")},CallExpression(c){if(a(c)!=="scheduleOnRN")return;const[e]=c.arguments;if(e===void 0)return;if(t(e)&&e.type!=="FunctionDeclaration")n.report({node:e,message:o})}}}});
@@ -1,105 +1 @@
1
- import { closestAncestor, crossesFunctionBefore, FUNCTION_TYPES, isMemberCall, problem, receiverName, subtreeHas, } from "../../../lib/ast.js";
2
- import { isProducerCall } from "./shared.js";
3
- const MESSAGES = {
4
- renderRead: "Read this inside `useAnimatedStyle`/`useAnimatedProps`, or mirror non-visual state through an explicit callback. A `.get()` while JSX is evaluated is untracked, so the rendered value never updates.",
5
- renderWrite: "Move this write into an event handler, an effect, or an animation callback — render must stay pure.",
6
- destructure: "Keep the SharedValue object itself and read or write it with `.get()` / `.set(...)`; destructuring detaches the value from Reanimated reactivity.",
7
- nestedProperty: "Assign a new value with `.set(...)`, or use `.modify()` for a large object; mutating a property returned by `.get()` bypasses shared-value reactivity.",
8
- nestedCollection: "Assign a new collection with `.set(...)`, or mutate inside `.modify()`; mutating the collection returned by `.get()` bypasses shared-value reactivity.",
9
- };
10
- const MUTATING_METHODS = new Set([
11
- "push",
12
- "pop",
13
- "shift",
14
- "unshift",
15
- "splice",
16
- "sort",
17
- "reverse",
18
- "copyWithin",
19
- "fill",
20
- "set",
21
- "add",
22
- "delete",
23
- "clear",
24
- ]);
25
- const JSX_HOSTS = new Set(["JSXExpressionContainer", "JSXAttribute"]);
26
- /** True for `sv.get().push(...)` and friends — a mutation of the collection `get()` handed back. */
27
- const mutatesWhatGetReturned = (node) => {
28
- if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression")
29
- return false;
30
- const { object, property } = node.callee;
31
- return property.type === "Identifier" && MUTATING_METHODS.has(property.name) && isMemberCall(object, "get");
32
- };
33
- /** True when the call is evaluated as JSX is built, rather than inside a callback JSX only passes along. */
34
- const runsWhileJsxEvaluates = (node) => {
35
- const host = closestAncestor(node, JSX_HOSTS);
36
- if (!host)
37
- return false;
38
- if (crossesFunctionBefore(node, host, FUNCTION_TYPES))
39
- return false;
40
- return !subtreeHas(host, current => FUNCTION_TYPES.has(current.type));
41
- };
42
- export const sharedValueUsage = problem("Destructuring a shared value or mutating what its `get()` returned detaches it from Reanimated reactivity. Reading or writing one while JSX evaluates also makes render impure.", {
43
- createOnce(context) {
44
- const names = new Set();
45
- const candidates = [];
46
- return {
47
- before() {
48
- names.clear();
49
- candidates.length = 0;
50
- },
51
- VariableDeclarator(node) {
52
- const { id, init } = node;
53
- if (id.type === "Identifier" && isProducerCall(init)) {
54
- names.add(id.name);
55
- return;
56
- }
57
- if (id.type !== "ObjectPattern")
58
- return;
59
- if (init?.type !== "CallExpression")
60
- return;
61
- if (init.callee.type !== "Identifier" || init.callee.name !== "useSharedValue")
62
- return;
63
- const destructuresValue = id.properties.some(property => {
64
- if (property.type !== "Property")
65
- return false;
66
- const { key } = property;
67
- return key.type === "Identifier" && key.name === "value";
68
- });
69
- if (destructuresValue)
70
- context.report({ node, message: MESSAGES.destructure });
71
- },
72
- AssignmentExpression(node) {
73
- if (node.left.type !== "MemberExpression")
74
- return;
75
- if (!isMemberCall(node.left.object, "get"))
76
- return;
77
- const shared = receiverName(node.left.object);
78
- if (!shared)
79
- return;
80
- candidates.push({ node, shared, message: MESSAGES.nestedProperty });
81
- },
82
- CallExpression(node) {
83
- if (mutatesWhatGetReturned(node)) {
84
- context.report({ node, message: MESSAGES.nestedCollection });
85
- return;
86
- }
87
- const isGet = isMemberCall(node, "get") && node.arguments.length === 0;
88
- if (!isGet && !isMemberCall(node, "set"))
89
- return;
90
- const shared = receiverName(node);
91
- if (!shared)
92
- return;
93
- if (!runsWhileJsxEvaluates(node))
94
- return;
95
- candidates.push({ node, shared, message: isGet ? MESSAGES.renderRead : MESSAGES.renderWrite });
96
- },
97
- "Program:exit"() {
98
- for (const { node, shared, message } of candidates) {
99
- if (names.has(shared))
100
- context.report({ node, message });
101
- }
102
- },
103
- };
104
- },
105
- });
1
+ import{closestAncestor as p,crossesFunctionBefore as f,FUNCTION_TYPES as c,isMemberCall as u,problem as m,receiverName as d,subtreeHas as h}from"../../../lib/ast.js";import{isProducerCall as g}from"./shared.js";const n={renderRead:"Read this inside `useAnimatedStyle`/`useAnimatedProps`, or mirror non-visual state through an explicit callback. A `.get()` while JSX is evaluated is untracked, so the rendered value never updates.",renderWrite:"Move this write into an event handler, an effect, or an animation callback — render must stay pure.",destructure:"Keep the SharedValue object itself and read or write it with `.get()` / `.set(...)`; destructuring detaches the value from Reanimated reactivity.",nestedProperty:"Assign a new value with `.set(...)`, or use `.modify()` for a large object; mutating a property returned by `.get()` bypasses shared-value reactivity.",nestedCollection:"Assign a new collection with `.set(...)`, or mutate inside `.modify()`; mutating the collection returned by `.get()` bypasses shared-value reactivity."},y=new Set(["push","pop","shift","unshift","splice","sort","reverse","copyWithin","fill","set","add","delete","clear"]),b=new Set(["JSXExpressionContainer","JSXAttribute"]),v=(r)=>{if(r.type!=="CallExpression"||r.callee.type!=="MemberExpression")return!1;const{object:s,property:a}=r.callee;return a.type==="Identifier"&&y.has(a.name)&&u(s,"get")},S=(r)=>{const s=p(r,b);if(!s)return!1;if(f(r,s,c))return!1;return!h(s,(a)=>c.has(a.type))};export const sharedValueUsage=m("Destructuring a shared value or mutating what its `get()` returned detaches it from Reanimated reactivity. Reading or writing one while JSX evaluates also makes render impure.",{createOnce(r){const s=new Set,a=[];return{before(){s.clear();a.length=0},VariableDeclarator(e){const{id:t,init:i}=e;if(t.type==="Identifier"&&g(i)){s.add(t.name);return}if(t.type!=="ObjectPattern")return;if(i?.type!=="CallExpression")return;if(i.callee.type!=="Identifier"||i.callee.name!=="useSharedValue")return;if(t.properties.some((o)=>{if(o.type!=="Property")return!1;const{key:l}=o;return l.type==="Identifier"&&l.name==="value"}))r.report({node:e,message:n.destructure})},AssignmentExpression(e){if(e.left.type!=="MemberExpression")return;if(!u(e.left.object,"get"))return;const t=d(e.left.object);if(!t)return;a.push({node:e,shared:t,message:n.nestedProperty})},CallExpression(e){if(v(e)){r.report({node:e,message:n.nestedCollection});return}const t=u(e,"get")&&e.arguments.length===0;if(!t&&!u(e,"set"))return;const i=d(e);if(!i)return;if(!S(e))return;a.push({node:e,shared:i,message:t?n.renderRead:n.renderWrite})},"Program:exit"(){for(const{node:e,shared:t,message:i}of a)if(s.has(t))r.report({node:e,message:i})}}}});
@@ -1,13 +1 @@
1
- import { calleeName } from "../../../lib/ast.js";
2
- /** Calls that hand back a SharedValue. */
3
- const PRODUCERS = new Set([
4
- "useSharedValue",
5
- "useDerivedValue",
6
- "useScrollOffset",
7
- "useScrollViewOffset",
8
- "makeMutable",
9
- ]);
10
- /** `useSharedValue(...)` and friends, under a namespace or not — decides which locals hold a shared value. */
11
- export const isProducerCall = (node) => node?.type === "CallExpression" && PRODUCERS.has(calleeName(node));
12
- /** The hooks whose callback is an animated updater. */
13
- export const ANIMATED_STYLE_HOOKS = new Set(["useAnimatedStyle", "useAnimatedProps"]);
1
+ import{calleeName as s}from"../../../lib/ast.js";const t=new Set(["useSharedValue","useDerivedValue","useScrollOffset","useScrollViewOffset","makeMutable"]);export const isProducerCall=(e)=>e?.type==="CallExpression"&&t.has(s(e)),ANIMATED_STYLE_HOOKS=new Set(["useAnimatedStyle","useAnimatedProps"]);