@ashstack/lint 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/core/index.js +1 -222
- package/dist/core/rules/core/components-tsx-only.js +1 -37
- package/dist/core/rules/core/hoist-intl.js +1 -32
- package/dist/core/rules/core/index.js +1 -18
- package/dist/core/rules/core/no-comments.js +1 -194
- package/dist/core/rules/core/no-naming-convention.js +1 -143
- package/dist/core/rules/core/no-packed-condition.d.ts +2 -0
- package/dist/core/rules/core/no-packed-condition.js +1 -0
- package/dist/core/rules/core/use-design-system.js +1 -147
- package/dist/core/rules/zod/index.js +1 -12
- package/dist/core/rules/zod/prefer-enum.js +1 -44
- package/dist/index.js +1 -3
- package/dist/lib/ast.js +1 -162
- package/dist/lib/detect.js +1 -42
- package/dist/lib/merge.js +1 -18
- package/dist/lib/module.js +1 -49
- package/dist/lib/types.js +1 -1
- package/dist/react/index.js +1 -126
- package/dist/react/rules/i18n/index.js +1 -16
- package/dist/react/rules/i18n/no-bare-attrs.js +1 -38
- package/dist/react/rules/i18n/no-bare-text.js +1 -26
- package/dist/react/rules/i18n/no-bare-toast.js +1 -38
- package/dist/react/rules/query/index.js +1 -20
- package/dist/react/rules/query/next-page-param-undefined.js +1 -26
- package/dist/react/rules/query/no-deprecated-filters.js +1 -63
- package/dist/react/rules/query/no-fetch-in-query-fn.js +1 -22
- package/dist/react/rules/query/no-inline-keys.js +1 -107
- package/dist/react/rules/query/require-destructured-hooks.js +1 -41
- package/dist/react/rules/query/shared.js +1 -10
- package/dist/react/rules/zustand/index.js +1 -12
- package/dist/react/rules/zustand/require-selector.js +1 -62
- package/dist/react-native/index.js +1 -113
- package/dist/react-native/rules/keyboard/index.js +1 -18
- package/dist/react-native/rules/legend-list/index.js +1 -51
- package/dist/react-native/rules/legend-list/no-flex-in-content-container.js +1 -77
- package/dist/react-native/rules/legend-list/no-index-key-extractor.js +1 -26
- package/dist/react-native/rules/legend-list/no-inline-data.js +1 -25
- package/dist/react-native/rules/legend-list/no-inline-extra-data.js +1 -21
- package/dist/react-native/rules/legend-list/no-inline-render-item-props.js +1 -35
- package/dist/react-native/rules/legend-list/no-mixed-children.js +1 -24
- package/dist/react-native/rules/legend-list/no-remount-key.js +1 -18
- package/dist/react-native/rules/legend-list/no-scrollview-map.js +1 -27
- package/dist/react-native/rules/legend-list/no-unsupported-props.js +1 -29
- package/dist/react-native/rules/legend-list/required-props.js +1 -23
- package/dist/react-native/rules/legend-list/shared.js +1 -25
- package/dist/react-native/rules/legend-list/typed-items-need-item-type.js +1 -49
- package/dist/react-native/rules/legend-state/index.js +1 -35
- package/dist/react-native/rules/legend-state/naming.js +1 -26
- package/dist/react-native/rules/legend-state/no-assignment.js +1 -31
- package/dist/react-native/rules/legend-state/no-nested-observable.js +1 -23
- package/dist/react-native/rules/legend-state/no-object-selector.js +1 -47
- package/dist/react-native/rules/legend-state/no-observable-in-component.js +1 -46
- package/dist/react-native/rules/legend-state/no-peek-in-selector.js +1 -67
- package/dist/react-native/rules/legend-state/no-react-mirror.js +1 -33
- package/dist/react-native/rules/legend-state/no-untracked-get-in-jsx.js +1 -52
- package/dist/react-native/rules/legend-state/shared.js +1 -23
- package/dist/react-native/rules/react-native/hoist-stateless-function.js +1 -59
- package/dist/react-native/rules/react-native/index.js +1 -50
- package/dist/react-native/rules/react-native/no-conditional-style-array.js +1 -23
- package/dist/react-native/rules/react-native/no-dynamic-import.js +1 -18
- package/dist/react-native/rules/react-native/no-keyboard-will-events.js +1 -18
- package/dist/react-native/rules/react-native/no-leaked-render.js +1 -43
- package/dist/react-native/rules/react-native/no-manual-memo.js +1 -87
- package/dist/react-native/rules/react-native/no-redundant-view-nesting.js +1 -36
- package/dist/react-native/rules/react-native/no-rn-image-network-source.js +1 -58
- package/dist/react-native/rules/react-native/no-rn-namespace-import.js +1 -38
- package/dist/react-native/rules/react-native/no-scroll-position-state.js +1 -26
- package/dist/react-native/rules/react-native/no-unlabeled-icon-pressable.js +1 -62
- package/dist/react-native/rules/reanimated/animated-reaction-safety.js +1 -110
- package/dist/react-native/rules/reanimated/animated-style-needs-animated-component.js +1 -72
- package/dist/react-native/rules/reanimated/animated-updater-purity.js +1 -38
- package/dist/react-native/rules/reanimated/gpu-properties-only.js +1 -80
- package/dist/react-native/rules/reanimated/hoist-layout-animation-builder.js +1 -43
- package/dist/react-native/rules/reanimated/index.js +1 -51
- package/dist/react-native/rules/reanimated/interpolate-needs-clamp.js +1 -41
- package/dist/react-native/rules/reanimated/no-react-state-from-continuous-worklet.js +1 -86
- package/dist/react-native/rules/reanimated/no-shared-value-dot-value.js +1 -112
- package/dist/react-native/rules/reanimated/prefer-lazy-shared-value-initializer.js +1 -24
- package/dist/react-native/rules/reanimated/schedule-on-rn-scope.js +1 -21
- package/dist/react-native/rules/reanimated/shared-value-usage.js +1 -105
- package/dist/react-native/rules/reanimated/shared.js +1 -13
- package/dist/react-native/rules/skia/canvas-opaque.js +1 -49
- package/dist/react-native/rules/skia/index.js +1 -21
- package/dist/react-native/rules/turbo-image/index.js +1 -14
- package/dist/react-native/rules/turbo-image/require-cache-policy.js +1 -21
- package/dist/react-native/rules/turbo-image/require-resize.js +1 -22
- package/dist/react-native/rules/unistyles/animated-theme.js +1 -37
- package/dist/react-native/rules/unistyles/content-container.js +1 -73
- package/dist/react-native/rules/unistyles/in-sheet.js +1 -116
- package/dist/react-native/rules/unistyles/index.js +1 -58
- package/dist/react-native/rules/unistyles/insets.js +1 -92
- package/dist/react-native/rules/unistyles/no-hardcoded-color.js +1 -30
- package/dist/react-native/rules/unistyles/no-hardcoded-spacing.js +1 -78
- package/dist/react-native/rules/unistyles/no-margin.js +1 -19
- package/dist/react-native/rules/unistyles/no-style-spread.js +1 -41
- package/dist/react-native/rules/unistyles/no-unused-styles.js +1 -99
- package/dist/react-native/rules/unistyles/rtl-style-call.js +1 -42
- package/dist/react-native/rules/unistyles/shared.js +1 -70
- package/dist/react-native/rules/unistyles/theme-screen-component.js +1 -22
- package/dist/react-native/rules/unistyles/theme-style-attr.js +1 -18
- package/dist/react-native/stylesheet.js +1 -44
- package/package.json +3 -4
- package/RULES.md +0 -2881
|
@@ -1,72 +1 @@
|
|
|
1
|
-
import
|
|
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
|
|
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}}}});
|
|
@@ -1,80 +1 @@
|
|
|
1
|
-
import
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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})}}}});
|