@ashstack/lint 0.1.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 (202) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/RULES.md +2881 -0
  4. package/dist/core/index.d.ts +15 -0
  5. package/dist/core/index.js +222 -0
  6. package/dist/core/rules/core/components-tsx-only.d.ts +2 -0
  7. package/dist/core/rules/core/components-tsx-only.js +37 -0
  8. package/dist/core/rules/core/hoist-intl.d.ts +2 -0
  9. package/dist/core/rules/core/hoist-intl.js +32 -0
  10. package/dist/core/rules/core/index.d.ts +2 -0
  11. package/dist/core/rules/core/index.js +18 -0
  12. package/dist/core/rules/core/no-comments.d.ts +2 -0
  13. package/dist/core/rules/core/no-comments.js +194 -0
  14. package/dist/core/rules/core/no-naming-convention.d.ts +2 -0
  15. package/dist/core/rules/core/no-naming-convention.js +143 -0
  16. package/dist/core/rules/core/use-design-system.d.ts +2 -0
  17. package/dist/core/rules/core/use-design-system.js +147 -0
  18. package/dist/core/rules/zod/index.d.ts +2 -0
  19. package/dist/core/rules/zod/index.js +12 -0
  20. package/dist/core/rules/zod/prefer-enum.d.ts +2 -0
  21. package/dist/core/rules/zod/prefer-enum.js +44 -0
  22. package/dist/index.d.ts +4 -0
  23. package/dist/index.js +3 -0
  24. package/dist/lib/ast.d.ts +44 -0
  25. package/dist/lib/ast.js +162 -0
  26. package/dist/lib/detect.d.ts +9 -0
  27. package/dist/lib/detect.js +42 -0
  28. package/dist/lib/merge.d.ts +9 -0
  29. package/dist/lib/merge.js +18 -0
  30. package/dist/lib/module.d.ts +19 -0
  31. package/dist/lib/module.js +49 -0
  32. package/dist/lib/types.d.ts +114 -0
  33. package/dist/lib/types.js +1 -0
  34. package/dist/react/index.d.ts +9 -0
  35. package/dist/react/index.js +126 -0
  36. package/dist/react/rules/i18n/index.d.ts +2 -0
  37. package/dist/react/rules/i18n/index.js +16 -0
  38. package/dist/react/rules/i18n/no-bare-attrs.d.ts +2 -0
  39. package/dist/react/rules/i18n/no-bare-attrs.js +38 -0
  40. package/dist/react/rules/i18n/no-bare-text.d.ts +2 -0
  41. package/dist/react/rules/i18n/no-bare-text.js +26 -0
  42. package/dist/react/rules/i18n/no-bare-toast.d.ts +2 -0
  43. package/dist/react/rules/i18n/no-bare-toast.js +38 -0
  44. package/dist/react/rules/query/index.d.ts +2 -0
  45. package/dist/react/rules/query/index.js +20 -0
  46. package/dist/react/rules/query/next-page-param-undefined.d.ts +2 -0
  47. package/dist/react/rules/query/next-page-param-undefined.js +26 -0
  48. package/dist/react/rules/query/no-deprecated-filters.d.ts +2 -0
  49. package/dist/react/rules/query/no-deprecated-filters.js +63 -0
  50. package/dist/react/rules/query/no-fetch-in-query-fn.d.ts +2 -0
  51. package/dist/react/rules/query/no-fetch-in-query-fn.js +22 -0
  52. package/dist/react/rules/query/no-inline-keys.d.ts +2 -0
  53. package/dist/react/rules/query/no-inline-keys.js +107 -0
  54. package/dist/react/rules/query/require-destructured-hooks.d.ts +2 -0
  55. package/dist/react/rules/query/require-destructured-hooks.js +41 -0
  56. package/dist/react/rules/query/shared.d.ts +2 -0
  57. package/dist/react/rules/query/shared.js +10 -0
  58. package/dist/react/rules/zustand/index.d.ts +2 -0
  59. package/dist/react/rules/zustand/index.js +12 -0
  60. package/dist/react/rules/zustand/require-selector.d.ts +2 -0
  61. package/dist/react/rules/zustand/require-selector.js +62 -0
  62. package/dist/react-native/index.d.ts +11 -0
  63. package/dist/react-native/index.js +113 -0
  64. package/dist/react-native/rules/keyboard/index.d.ts +2 -0
  65. package/dist/react-native/rules/keyboard/index.js +18 -0
  66. package/dist/react-native/rules/legend-list/index.d.ts +2 -0
  67. package/dist/react-native/rules/legend-list/index.js +51 -0
  68. package/dist/react-native/rules/legend-list/no-flex-in-content-container.d.ts +2 -0
  69. package/dist/react-native/rules/legend-list/no-flex-in-content-container.js +77 -0
  70. package/dist/react-native/rules/legend-list/no-index-key-extractor.d.ts +2 -0
  71. package/dist/react-native/rules/legend-list/no-index-key-extractor.js +26 -0
  72. package/dist/react-native/rules/legend-list/no-inline-data.d.ts +2 -0
  73. package/dist/react-native/rules/legend-list/no-inline-data.js +25 -0
  74. package/dist/react-native/rules/legend-list/no-inline-extra-data.d.ts +2 -0
  75. package/dist/react-native/rules/legend-list/no-inline-extra-data.js +21 -0
  76. package/dist/react-native/rules/legend-list/no-inline-render-item-props.d.ts +2 -0
  77. package/dist/react-native/rules/legend-list/no-inline-render-item-props.js +35 -0
  78. package/dist/react-native/rules/legend-list/no-mixed-children.d.ts +2 -0
  79. package/dist/react-native/rules/legend-list/no-mixed-children.js +24 -0
  80. package/dist/react-native/rules/legend-list/no-remount-key.d.ts +2 -0
  81. package/dist/react-native/rules/legend-list/no-remount-key.js +18 -0
  82. package/dist/react-native/rules/legend-list/no-scrollview-map.d.ts +2 -0
  83. package/dist/react-native/rules/legend-list/no-scrollview-map.js +27 -0
  84. package/dist/react-native/rules/legend-list/no-unsupported-props.d.ts +2 -0
  85. package/dist/react-native/rules/legend-list/no-unsupported-props.js +29 -0
  86. package/dist/react-native/rules/legend-list/required-props.d.ts +2 -0
  87. package/dist/react-native/rules/legend-list/required-props.js +23 -0
  88. package/dist/react-native/rules/legend-list/shared.d.ts +19 -0
  89. package/dist/react-native/rules/legend-list/shared.js +25 -0
  90. package/dist/react-native/rules/legend-list/typed-items-need-item-type.d.ts +2 -0
  91. package/dist/react-native/rules/legend-list/typed-items-need-item-type.js +49 -0
  92. package/dist/react-native/rules/legend-state/index.d.ts +2 -0
  93. package/dist/react-native/rules/legend-state/index.js +35 -0
  94. package/dist/react-native/rules/legend-state/naming.d.ts +2 -0
  95. package/dist/react-native/rules/legend-state/naming.js +26 -0
  96. package/dist/react-native/rules/legend-state/no-assignment.d.ts +2 -0
  97. package/dist/react-native/rules/legend-state/no-assignment.js +31 -0
  98. package/dist/react-native/rules/legend-state/no-nested-observable.d.ts +2 -0
  99. package/dist/react-native/rules/legend-state/no-nested-observable.js +23 -0
  100. package/dist/react-native/rules/legend-state/no-object-selector.d.ts +2 -0
  101. package/dist/react-native/rules/legend-state/no-object-selector.js +47 -0
  102. package/dist/react-native/rules/legend-state/no-observable-in-component.d.ts +2 -0
  103. package/dist/react-native/rules/legend-state/no-observable-in-component.js +46 -0
  104. package/dist/react-native/rules/legend-state/no-peek-in-selector.d.ts +2 -0
  105. package/dist/react-native/rules/legend-state/no-peek-in-selector.js +67 -0
  106. package/dist/react-native/rules/legend-state/no-react-mirror.d.ts +2 -0
  107. package/dist/react-native/rules/legend-state/no-react-mirror.js +33 -0
  108. package/dist/react-native/rules/legend-state/no-untracked-get-in-jsx.d.ts +2 -0
  109. package/dist/react-native/rules/legend-state/no-untracked-get-in-jsx.js +52 -0
  110. package/dist/react-native/rules/legend-state/shared.d.ts +9 -0
  111. package/dist/react-native/rules/legend-state/shared.js +23 -0
  112. package/dist/react-native/rules/react-native/hoist-stateless-function.d.ts +2 -0
  113. package/dist/react-native/rules/react-native/hoist-stateless-function.js +59 -0
  114. package/dist/react-native/rules/react-native/index.d.ts +2 -0
  115. package/dist/react-native/rules/react-native/index.js +50 -0
  116. package/dist/react-native/rules/react-native/no-conditional-style-array.d.ts +2 -0
  117. package/dist/react-native/rules/react-native/no-conditional-style-array.js +23 -0
  118. package/dist/react-native/rules/react-native/no-dynamic-import.d.ts +2 -0
  119. package/dist/react-native/rules/react-native/no-dynamic-import.js +18 -0
  120. package/dist/react-native/rules/react-native/no-keyboard-will-events.d.ts +2 -0
  121. package/dist/react-native/rules/react-native/no-keyboard-will-events.js +18 -0
  122. package/dist/react-native/rules/react-native/no-leaked-render.d.ts +2 -0
  123. package/dist/react-native/rules/react-native/no-leaked-render.js +43 -0
  124. package/dist/react-native/rules/react-native/no-manual-memo.d.ts +2 -0
  125. package/dist/react-native/rules/react-native/no-manual-memo.js +87 -0
  126. package/dist/react-native/rules/react-native/no-redundant-view-nesting.d.ts +2 -0
  127. package/dist/react-native/rules/react-native/no-redundant-view-nesting.js +36 -0
  128. package/dist/react-native/rules/react-native/no-rn-image-network-source.d.ts +2 -0
  129. package/dist/react-native/rules/react-native/no-rn-image-network-source.js +58 -0
  130. package/dist/react-native/rules/react-native/no-rn-namespace-import.d.ts +2 -0
  131. package/dist/react-native/rules/react-native/no-rn-namespace-import.js +38 -0
  132. package/dist/react-native/rules/react-native/no-scroll-position-state.d.ts +2 -0
  133. package/dist/react-native/rules/react-native/no-scroll-position-state.js +26 -0
  134. package/dist/react-native/rules/react-native/no-unlabeled-icon-pressable.d.ts +2 -0
  135. package/dist/react-native/rules/react-native/no-unlabeled-icon-pressable.js +62 -0
  136. package/dist/react-native/rules/reanimated/animated-reaction-safety.d.ts +2 -0
  137. package/dist/react-native/rules/reanimated/animated-reaction-safety.js +110 -0
  138. package/dist/react-native/rules/reanimated/animated-style-needs-animated-component.d.ts +2 -0
  139. package/dist/react-native/rules/reanimated/animated-style-needs-animated-component.js +72 -0
  140. package/dist/react-native/rules/reanimated/animated-updater-purity.d.ts +2 -0
  141. package/dist/react-native/rules/reanimated/animated-updater-purity.js +38 -0
  142. package/dist/react-native/rules/reanimated/gpu-properties-only.d.ts +2 -0
  143. package/dist/react-native/rules/reanimated/gpu-properties-only.js +80 -0
  144. package/dist/react-native/rules/reanimated/hoist-layout-animation-builder.d.ts +2 -0
  145. package/dist/react-native/rules/reanimated/hoist-layout-animation-builder.js +43 -0
  146. package/dist/react-native/rules/reanimated/index.d.ts +2 -0
  147. package/dist/react-native/rules/reanimated/index.js +51 -0
  148. package/dist/react-native/rules/reanimated/interpolate-needs-clamp.d.ts +2 -0
  149. package/dist/react-native/rules/reanimated/interpolate-needs-clamp.js +41 -0
  150. package/dist/react-native/rules/reanimated/no-react-state-from-continuous-worklet.d.ts +2 -0
  151. package/dist/react-native/rules/reanimated/no-react-state-from-continuous-worklet.js +86 -0
  152. package/dist/react-native/rules/reanimated/no-shared-value-dot-value.d.ts +2 -0
  153. package/dist/react-native/rules/reanimated/no-shared-value-dot-value.js +112 -0
  154. package/dist/react-native/rules/reanimated/prefer-lazy-shared-value-initializer.d.ts +2 -0
  155. package/dist/react-native/rules/reanimated/prefer-lazy-shared-value-initializer.js +24 -0
  156. package/dist/react-native/rules/reanimated/schedule-on-rn-scope.d.ts +2 -0
  157. package/dist/react-native/rules/reanimated/schedule-on-rn-scope.js +21 -0
  158. package/dist/react-native/rules/reanimated/shared-value-usage.d.ts +2 -0
  159. package/dist/react-native/rules/reanimated/shared-value-usage.js +105 -0
  160. package/dist/react-native/rules/reanimated/shared.d.ts +5 -0
  161. package/dist/react-native/rules/reanimated/shared.js +13 -0
  162. package/dist/react-native/rules/skia/canvas-opaque.d.ts +2 -0
  163. package/dist/react-native/rules/skia/canvas-opaque.js +49 -0
  164. package/dist/react-native/rules/skia/index.d.ts +2 -0
  165. package/dist/react-native/rules/skia/index.js +21 -0
  166. package/dist/react-native/rules/turbo-image/index.d.ts +2 -0
  167. package/dist/react-native/rules/turbo-image/index.js +14 -0
  168. package/dist/react-native/rules/turbo-image/require-cache-policy.d.ts +2 -0
  169. package/dist/react-native/rules/turbo-image/require-cache-policy.js +21 -0
  170. package/dist/react-native/rules/turbo-image/require-resize.d.ts +2 -0
  171. package/dist/react-native/rules/turbo-image/require-resize.js +22 -0
  172. package/dist/react-native/rules/unistyles/animated-theme.d.ts +2 -0
  173. package/dist/react-native/rules/unistyles/animated-theme.js +37 -0
  174. package/dist/react-native/rules/unistyles/content-container.d.ts +2 -0
  175. package/dist/react-native/rules/unistyles/content-container.js +73 -0
  176. package/dist/react-native/rules/unistyles/in-sheet.d.ts +2 -0
  177. package/dist/react-native/rules/unistyles/in-sheet.js +116 -0
  178. package/dist/react-native/rules/unistyles/index.d.ts +2 -0
  179. package/dist/react-native/rules/unistyles/index.js +58 -0
  180. package/dist/react-native/rules/unistyles/insets.d.ts +2 -0
  181. package/dist/react-native/rules/unistyles/insets.js +92 -0
  182. package/dist/react-native/rules/unistyles/no-hardcoded-color.d.ts +2 -0
  183. package/dist/react-native/rules/unistyles/no-hardcoded-color.js +30 -0
  184. package/dist/react-native/rules/unistyles/no-hardcoded-spacing.d.ts +2 -0
  185. package/dist/react-native/rules/unistyles/no-hardcoded-spacing.js +78 -0
  186. package/dist/react-native/rules/unistyles/no-margin.d.ts +2 -0
  187. package/dist/react-native/rules/unistyles/no-margin.js +19 -0
  188. package/dist/react-native/rules/unistyles/no-style-spread.d.ts +2 -0
  189. package/dist/react-native/rules/unistyles/no-style-spread.js +41 -0
  190. package/dist/react-native/rules/unistyles/no-unused-styles.d.ts +2 -0
  191. package/dist/react-native/rules/unistyles/no-unused-styles.js +99 -0
  192. package/dist/react-native/rules/unistyles/rtl-style-call.d.ts +2 -0
  193. package/dist/react-native/rules/unistyles/rtl-style-call.js +42 -0
  194. package/dist/react-native/rules/unistyles/shared.d.ts +16 -0
  195. package/dist/react-native/rules/unistyles/shared.js +70 -0
  196. package/dist/react-native/rules/unistyles/theme-screen-component.d.ts +2 -0
  197. package/dist/react-native/rules/unistyles/theme-screen-component.js +22 -0
  198. package/dist/react-native/rules/unistyles/theme-style-attr.d.ts +2 -0
  199. package/dist/react-native/rules/unistyles/theme-style-attr.js +18 -0
  200. package/dist/react-native/stylesheet.d.ts +11 -0
  201. package/dist/react-native/stylesheet.js +44 -0
  202. package/package.json +70 -0
@@ -0,0 +1,112 @@
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
+ };
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../../../lib/types.js";
2
+ export declare const preferLazySharedValueInitializer: Rule;
@@ -0,0 +1,24 @@
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
+ });
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../../../lib/types.js";
2
+ export declare const scheduleOnRnScope: Rule;
@@ -0,0 +1,21 @@
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
+ });
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../../../lib/types.js";
2
+ export declare const sharedValueUsage: Rule;
@@ -0,0 +1,105 @@
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
+ });
@@ -0,0 +1,5 @@
1
+ import type { AstNode } from "../../../lib/types.js";
2
+ /** `useSharedValue(...)` and friends, under a namespace or not — decides which locals hold a shared value. */
3
+ export declare const isProducerCall: (node: AstNode | null | undefined) => boolean;
4
+ /** The hooks whose callback is an animated updater. */
5
+ export declare const ANIMATED_STYLE_HOOKS: Set<string>;
@@ -0,0 +1,13 @@
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"]);
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../../../lib/types.js";
2
+ export declare const canvasOpaque: Rule;
@@ -0,0 +1,49 @@
1
+ import { attributeName, gate, importedSpecifiers, problem, tagPath } from "../../../lib/ast.js";
2
+ const MESSAGE = "Add an explicit `opaque` prop to this `<Canvas>`: `opaque={Platform.OS === 'android'}` for a fullscreen animated canvas, or `opaque={false}` when it needs transparency, view transforms, or ordinary stacking.";
3
+ export const canvasOpaque = problem("Require an explicit `opaque` prop on a Skia `<Canvas>`. A fullscreen animated canvas wants it on; anything that needs transparency or a view transform wants it off.", {
4
+ createOnce(context) {
5
+ const canvasLocals = new Set();
6
+ const namespaces = new Set();
7
+ const elements = [];
8
+ /** `<Canvas>` under its imported name, or `<Skia.Canvas>` under a namespace or default import. */
9
+ const namesACanvas = (name) => {
10
+ const path = tagPath(name);
11
+ const dot = path.lastIndexOf(".");
12
+ if (dot === -1)
13
+ return canvasLocals.has(path);
14
+ return namespaces.has(path.slice(0, dot)) && path.slice(dot + 1) === "Canvas";
15
+ };
16
+ return {
17
+ before() {
18
+ canvasLocals.clear();
19
+ namespaces.clear();
20
+ elements.length = 0;
21
+ return gate(context, "react-native-skia");
22
+ },
23
+ ImportDeclaration(node) {
24
+ for (const specifier of importedSpecifiers(node, "@shopify/react-native-skia")) {
25
+ if (specifier.type === "ImportSpecifier") {
26
+ const { imported, local } = specifier;
27
+ if (imported.type === "Identifier" && imported.name === "Canvas")
28
+ canvasLocals.add(local.name);
29
+ continue;
30
+ }
31
+ if (specifier.type === "ImportNamespaceSpecifier" || specifier.type === "ImportDefaultSpecifier") {
32
+ namespaces.add(specifier.local.name);
33
+ }
34
+ }
35
+ },
36
+ JSXOpeningElement(node) {
37
+ if (!namesACanvas(node.name))
38
+ return;
39
+ const hasOpaque = node.attributes.some(attribute => attribute.type === "JSXAttribute" && attributeName(attribute) === "opaque");
40
+ if (!hasOpaque)
41
+ elements.push(node);
42
+ },
43
+ "Program:exit"() {
44
+ for (const element of elements)
45
+ context.report({ node: element, message: MESSAGE });
46
+ },
47
+ };
48
+ },
49
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../../../index.js").ModuleManifest;
2
+ export default _default;
@@ -0,0 +1,21 @@
1
+ import { defineModule } from "../../../lib/module.js";
2
+ import { canvasOpaque } from "./canvas-opaque.js";
3
+ export default defineModule({
4
+ meta: { name: "@ashstack/skia" },
5
+ url: import.meta.url,
6
+ packages: ["@shopify/react-native-skia"],
7
+ option: "skia",
8
+ docsWhen: "auto-enabled when `@shopify/react-native-skia` is a dependency",
9
+ rules: {
10
+ "canvas-opaque": canvasOpaque,
11
+ },
12
+ restrictedImports: {
13
+ paths: [
14
+ {
15
+ name: "@shopify/react-native-skia",
16
+ importNames: ["usePathValue", "usePathInterpolation"],
17
+ message: "Keep one stable `SkPath` buffer and mutate it inside `useDerivedValue`, with consumers reading the driving shared value. These legacy path-value hooks self-dirty Reanimated mappers and re-record idle canvases.",
18
+ },
19
+ ],
20
+ },
21
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../../../index.js").ModuleManifest;
2
+ export default _default;
@@ -0,0 +1,14 @@
1
+ import { defineModule } from "../../../lib/module.js";
2
+ import { requireCachePolicy } from "./require-cache-policy.js";
3
+ import { requireResize } from "./require-resize.js";
4
+ export default defineModule({
5
+ meta: { name: "@ashstack/turbo-image" },
6
+ url: import.meta.url,
7
+ packages: ["react-native-turbo-image"],
8
+ option: "turboImage",
9
+ docsWhen: "auto-enabled when `react-native-turbo-image` is a dependency",
10
+ rules: {
11
+ "require-resize": requireResize,
12
+ "require-cache-policy": requireCachePolicy,
13
+ },
14
+ });
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../../../lib/types.js";
2
+ export declare const requireCachePolicy: Rule;
@@ -0,0 +1,21 @@
1
+ import { attributeName, componentName, gate, problem } from "../../../lib/ast.js";
2
+ const MESSAGE = 'Add `cachePolicy` to this TurboImage (normally `cachePolicy="dataCache"`), or it re-fetches over the network on every cold start.';
3
+ export const requireCachePolicy = problem("Require `cachePolicy` on a TurboImage. Without one the image is re-fetched over the network on every cold start, so an already-scrolled feed costs its bandwidth again.", {
4
+ createOnce(context) {
5
+ return {
6
+ before() {
7
+ return gate(context, "TurboImage");
8
+ },
9
+ JSXOpeningElement(node) {
10
+ if (componentName(node.name) !== "TurboImage")
11
+ return;
12
+ const { attributes } = node;
13
+ if (attributes.some(attribute => attribute.type === "JSXSpreadAttribute"))
14
+ return;
15
+ if (attributes.some(attribute => attributeName(attribute) === "cachePolicy"))
16
+ return;
17
+ context.report({ node: node.name, message: MESSAGE });
18
+ },
19
+ };
20
+ },
21
+ });
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../../../lib/types.js";
2
+ export declare const requireResize: Rule;
@@ -0,0 +1,22 @@
1
+ import { attributeName, componentName, gate, problem } from "../../../lib/ast.js";
2
+ const MESSAGE = "Add `resize` to this TurboImage, set slightly BELOW the rendered width in points, so the native decoder downsamples before the bitmap reaches memory. Rounding down costs nothing visible; a value above the rendered width keeps the full-resolution decode (a 4000px photo in a 100pt avatar holds roughly 48MB of pixels).";
3
+ export const requireResize = problem("Require `resize` on a TurboImage so the native decoder downsamples before the bitmap reaches memory. A full-resolution decode wastes tens of megabytes and stalls the first frame.", {
4
+ createOnce(context) {
5
+ return {
6
+ before() {
7
+ return gate(context, "TurboImage");
8
+ },
9
+ JSXOpeningElement(node) {
10
+ if (componentName(node.name) !== "TurboImage")
11
+ return;
12
+ const { attributes } = node;
13
+ if (attributes.some(attribute => attribute.type === "JSXSpreadAttribute"))
14
+ return;
15
+ const hasResize = attributes.some(attribute => attribute.type === "JSXAttribute" && attributeName(attribute) === "resize");
16
+ if (hasResize)
17
+ return;
18
+ context.report({ node: node.name, message: MESSAGE });
19
+ },
20
+ };
21
+ },
22
+ });
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../../../lib/types.js";
2
+ export declare const animatedTheme: Rule;
@@ -0,0 +1,37 @@
1
+ import { calleeName, problem } from "../../../lib/ast.js";
2
+ import { readsTheme } from "./shared.js";
3
+ const WORKLET_HOOKS = new Set(["useAnimatedStyle", "useDerivedValue", "useAnimatedProps"]);
4
+ const MESSAGE = "Use `useAnimatedTheme()` and read its shared value inside the Reanimated worklet so theme changes reach the UI thread.";
5
+ export const animatedTheme = problem("A Reanimated worklet hook cannot see `useUnistyles()` theme changes on the UI thread. Read the shared value from `useAnimatedTheme()` there instead.", {
6
+ createOnce(context) {
7
+ let declaresTheme = false;
8
+ const candidates = [];
9
+ return {
10
+ before() {
11
+ declaresTheme = false;
12
+ candidates.length = 0;
13
+ },
14
+ VariableDeclarator(node) {
15
+ const { id, init } = node;
16
+ if (id.type === "ObjectPattern" &&
17
+ calleeName(init) === "useUnistyles" &&
18
+ id.properties.some(property => property.type === "Property" && property.key.type === "Identifier" && property.key.name === "theme")) {
19
+ declaresTheme = true;
20
+ }
21
+ },
22
+ CallExpression(node) {
23
+ if (!WORKLET_HOOKS.has(calleeName(node)))
24
+ return;
25
+ if (!readsTheme(node))
26
+ return;
27
+ candidates.push(node);
28
+ },
29
+ "Program:exit"() {
30
+ if (!declaresTheme)
31
+ return;
32
+ for (const node of candidates)
33
+ context.report({ node, message: MESSAGE });
34
+ },
35
+ };
36
+ },
37
+ });
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../../../lib/types.js";
2
+ export declare const contentContainer: Rule;
@@ -0,0 +1,73 @@
1
+ import { calleeName, findInSubtree, gate, problem, subtreeHas, tagIdentifier } from "../../../lib/ast.js";
2
+ import { isStyleSheetCreate, memberPath, propertyName, stylesObjectOf } from "./shared.js";
3
+ const MESSAGES = {
4
+ contentContainerRuntime: "Wrap this component with `withUnistyles`, or pass `contentContainerStyle` a plain hook-fed style. On a raw component `contentContainerStyle` never subscribes to Unistyles updates.",
5
+ contentContainerTheme: "Wrap this component with `withUnistyles`, or pass `contentContainerStyle` a plain hook-fed style. On a raw component `contentContainerStyle` never subscribes to theme changes.",
6
+ };
7
+ export const contentContainer = problem("A raw component never subscribes its `contentContainerStyle` to theme or `rt` updates. Wrap the component with `withUnistyles` when the style depends on either.", {
8
+ createOnce(context) {
9
+ const sheetStyles = new Map();
10
+ const wrapped = new Set();
11
+ const attributes = [];
12
+ return {
13
+ before() {
14
+ sheetStyles.clear();
15
+ wrapped.clear();
16
+ attributes.length = 0;
17
+ return gate(context, "contentContainerStyle");
18
+ },
19
+ VariableDeclarator(node) {
20
+ const { id, init } = node;
21
+ if (id.type !== "Identifier")
22
+ return;
23
+ if (calleeName(init) === "withUnistyles") {
24
+ wrapped.add(id.name);
25
+ return;
26
+ }
27
+ if (init?.type !== "CallExpression" || !isStyleSheetCreate(init))
28
+ return;
29
+ const styles = stylesObjectOf(init.arguments[0]);
30
+ if (!styles)
31
+ return;
32
+ const map = new Map();
33
+ for (const property of styles.properties) {
34
+ if (property.type !== "Property")
35
+ continue;
36
+ map.set(propertyName(property), property.value);
37
+ }
38
+ sheetStyles.set(id.name, map);
39
+ },
40
+ JSXAttribute(node) {
41
+ if (node.name.type !== "JSXIdentifier" || node.name.name !== "contentContainerStyle")
42
+ return;
43
+ attributes.push(node);
44
+ },
45
+ "Program:exit"() {
46
+ for (const attribute of attributes) {
47
+ const reference = findInSubtree(attribute.value, current => current.type === "MemberExpression" &&
48
+ current.object.type === "Identifier" &&
49
+ sheetStyles.has(current.object.name));
50
+ if (reference?.type !== "MemberExpression")
51
+ continue;
52
+ const owner = attribute.parent;
53
+ if (owner.type === "JSXOpeningElement" && wrapped.has(tagIdentifier(owner.name)))
54
+ continue;
55
+ const { object, property } = reference;
56
+ if (object.type !== "Identifier")
57
+ continue;
58
+ const definition = sheetStyles.get(object.name)?.get(property.type === "Identifier" ? property.name : "");
59
+ if (!definition)
60
+ continue;
61
+ const usesRuntime = subtreeHas(definition, current => current.type === "MemberExpression" && memberPath(current).startsWith("rt."));
62
+ if (usesRuntime) {
63
+ context.report({ node: attribute, message: MESSAGES.contentContainerRuntime });
64
+ continue;
65
+ }
66
+ const usesTheme = subtreeHas(definition, current => current.type === "MemberExpression" && memberPath(current).startsWith("theme."));
67
+ if (usesTheme)
68
+ context.report({ node: attribute, message: MESSAGES.contentContainerTheme });
69
+ }
70
+ },
71
+ };
72
+ },
73
+ });
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../../../lib/types.js";
2
+ export declare const inSheet: Rule;