@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,116 @@
1
+ import { calleeName, subtreeHas } from "../../../lib/ast.js";
2
+ import { inCreate, isStyleSheetCreate, memberPath, propertyName } from "./shared.js";
3
+ const LOGICAL_SPACING = new Set(["paddingLeft", "paddingRight", "marginLeft", "marginRight"]);
4
+ const LEGACY_SHADOW = new Set(["shadowColor", "shadowOffset", "shadowOpacity", "shadowRadius", "elevation"]);
5
+ const MESSAGES = {
6
+ logicalSpacing: "Use `paddingStart`/`paddingEnd` or `marginStart`/`marginEnd` here so the directional spacing mirrors in RTL.",
7
+ rtlInSheet: "Use `rt.rtl` for RTL-dependent styles so Unistyles tracks the dependency natively.",
8
+ screenDimensions: "Use reactive `rt.screen` inside `StyleSheet.create` instead of a `Dimensions.get` snapshot.",
9
+ pixelRatio: "Use reactive `rt.pixelRatio` inside `StyleSheet.create`.",
10
+ fontScale: "Use reactive `rt.fontScale` inside `StyleSheet.create`.",
11
+ statusBar: "Use reactive `rt.statusBar.height` inside `StyleSheet.create`.",
12
+ colorScheme: "Use the Unistyles theme or reactive `rt.colorScheme` inside `StyleSheet.create`.",
13
+ fullRuntime: "Use the injected mini runtime (`rt`) inside `StyleSheet.create` instead of `UnistylesRuntime`, so the style recalculates on its own.",
14
+ fullRuntimeDestructure: "Destructure from the injected mini runtime (`rt`) inside `StyleSheet.create`, not from `UnistylesRuntime`.",
15
+ themeScreen: "Use reactive `rt.screen`, or Unistyles breakpoints, instead of the theme's module-initialization screen snapshot.",
16
+ borderCurve: 'Add `borderCurve` (normally `"continuous"`) alongside `borderRadius`, or the corner renders as a circular arc rather than the squircle native iOS controls use.',
17
+ legacyShadow: 'Use `boxShadow` with CSS syntax, e.g. `boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)"`, which renders on both platforms on the New Architecture, instead of the legacy `shadow*`/`elevation` props.',
18
+ asConst: "Drop `as const` here — the typed `StyleSheet.create` already narrows the literal. If TS still widens the value to `string`, fix the `StyleSheet` import instead.",
19
+ };
20
+ export const inSheet = inCreate("Inside `StyleSheet.create`, require `rt` and theme values rather than Dimensions, PixelRatio, Appearance, I18nManager, StatusBar or `UnistylesRuntime`. It also covers logical spacing, `boxShadow`, `borderCurve` and a redundant `as const`.", (context, inside) => ({
21
+ CallExpression(node) {
22
+ if (!inside() || isStyleSheetCreate(node))
23
+ return;
24
+ const { callee } = node;
25
+ const path = callee.type === "MemberExpression" ? memberPath(callee) : calleeName(node);
26
+ if (path === "Dimensions.get")
27
+ context.report({ node, message: MESSAGES.screenDimensions });
28
+ else if (path === "PixelRatio.get")
29
+ context.report({ node, message: MESSAGES.pixelRatio });
30
+ else if (path === "PixelRatio.getFontScale")
31
+ context.report({ node, message: MESSAGES.fontScale });
32
+ else if (path === "Appearance.getColorScheme")
33
+ context.report({ node, message: MESSAGES.colorScheme });
34
+ else if (path === "useColorScheme")
35
+ context.report({ node, message: MESSAGES.colorScheme });
36
+ },
37
+ MemberExpression(node) {
38
+ if (!inside())
39
+ return;
40
+ const { parent } = node;
41
+ if (parent.type === "MemberExpression" && parent.object === node)
42
+ return;
43
+ const path = memberPath(node);
44
+ if (path === "I18nManager.isRTL") {
45
+ context.report({ node, message: MESSAGES.rtlInSheet });
46
+ return;
47
+ }
48
+ if (path === "StatusBar.currentHeight") {
49
+ context.report({ node, message: MESSAGES.statusBar });
50
+ return;
51
+ }
52
+ if (path.startsWith("UnistylesRuntime.")) {
53
+ context.report({ node, message: MESSAGES.fullRuntime });
54
+ return;
55
+ }
56
+ if (path.startsWith("theme.screen."))
57
+ context.report({ node, message: MESSAGES.themeScreen });
58
+ },
59
+ VariableDeclarator(node) {
60
+ if (!inside())
61
+ return;
62
+ const { id, init } = node;
63
+ if (id.type !== "ObjectPattern")
64
+ return;
65
+ if (init?.type === "Identifier" && init.name === "UnistylesRuntime") {
66
+ context.report({ node, message: MESSAGES.fullRuntimeDestructure });
67
+ }
68
+ },
69
+ TSAsExpression(node) {
70
+ if (!inside())
71
+ return;
72
+ const { typeAnnotation } = node;
73
+ if (typeAnnotation.type !== "TSTypeReference")
74
+ return;
75
+ const { typeName } = typeAnnotation;
76
+ if (typeName.type !== "Identifier" || typeName.name !== "const")
77
+ return;
78
+ context.report({ node, message: MESSAGES.asConst });
79
+ },
80
+ ObjectExpression(node) {
81
+ if (!inside())
82
+ return;
83
+ let borderRadius = null;
84
+ let hasBorderCurve = false;
85
+ for (const property of node.properties) {
86
+ if (property.type !== "Property")
87
+ continue;
88
+ const name = propertyName(property);
89
+ if (name === "borderRadius")
90
+ borderRadius = property;
91
+ else if (name === "borderCurve")
92
+ hasBorderCurve = true;
93
+ }
94
+ if (borderRadius && !hasBorderCurve)
95
+ context.report({ node: borderRadius, message: MESSAGES.borderCurve });
96
+ },
97
+ Property(node) {
98
+ if (!inside())
99
+ return;
100
+ const name = propertyName(node);
101
+ if (LEGACY_SHADOW.has(name)) {
102
+ context.report({ node, message: MESSAGES.legacyShadow });
103
+ return;
104
+ }
105
+ if (!LOGICAL_SPACING.has(name))
106
+ return;
107
+ const usesInsets = subtreeHas(node, current => {
108
+ if (current.type !== "MemberExpression")
109
+ return false;
110
+ const path = memberPath(current);
111
+ return path === "rt.insets.left" || path === "rt.insets.right";
112
+ });
113
+ if (!usesInsets)
114
+ context.report({ node, message: MESSAGES.logicalSpacing });
115
+ },
116
+ }));
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../../../index.js").ModuleManifest;
2
+ export default _default;
@@ -0,0 +1,58 @@
1
+ import { defineModule } from "../../../lib/module.js";
2
+ import { animatedTheme } from "./animated-theme.js";
3
+ import { contentContainer } from "./content-container.js";
4
+ import { inSheet } from "./in-sheet.js";
5
+ import { insets } from "./insets.js";
6
+ import { noHardcodedColor } from "./no-hardcoded-color.js";
7
+ import { noHardcodedSpacing } from "./no-hardcoded-spacing.js";
8
+ import { noMargin } from "./no-margin.js";
9
+ import { noStyleSpread } from "./no-style-spread.js";
10
+ import { noUnusedStyles } from "./no-unused-styles.js";
11
+ import { rtlStyleCall } from "./rtl-style-call.js";
12
+ import { themeScreenComponent } from "./theme-screen-component.js";
13
+ import { themeStyleAttr } from "./theme-style-attr.js";
14
+ export default defineModule({
15
+ meta: { name: "@ashstack/unistyles" },
16
+ rules: {
17
+ "animated-theme": animatedTheme,
18
+ "content-container": contentContainer,
19
+ "in-sheet": inSheet,
20
+ insets: insets,
21
+ "no-hardcoded-color": noHardcodedColor,
22
+ "no-hardcoded-spacing": noHardcodedSpacing,
23
+ "no-margin": noMargin,
24
+ "no-style-spread": noStyleSpread,
25
+ "no-unused-styles": noUnusedStyles,
26
+ "rtl-style-call": rtlStyleCall,
27
+ "theme-screen-component": themeScreenComponent,
28
+ "theme-style-attr": themeStyleAttr,
29
+ },
30
+ url: import.meta.url,
31
+ packages: ["react-native-unistyles"],
32
+ option: "unistyles",
33
+ docsWhen: "auto-enabled when `react-native-unistyles` is a dependency",
34
+ restrictedImports: {
35
+ paths: [
36
+ {
37
+ name: "react-native",
38
+ importNames: ["StyleSheet"],
39
+ message: "Import `StyleSheet` from `react-native-unistyles` instead of `react-native`.",
40
+ },
41
+ {
42
+ name: "react-native",
43
+ importNames: ["useWindowDimensions", "Dimensions"],
44
+ message: "Read screen size from `rt.screen` inside `StyleSheet.create`, or `UnistylesRuntime.screen` outside it, instead of the `Dimensions` APIs.",
45
+ },
46
+ {
47
+ name: "react-native",
48
+ importNames: ["SafeAreaView"],
49
+ message: 'Set `contentInsetAdjustmentBehavior="automatic"` on the scroll view, or apply `rt.insets` from `react-native-unistyles`, instead of `SafeAreaView`.',
50
+ },
51
+ {
52
+ name: "react-native-safe-area-context",
53
+ importNames: ["useSafeAreaInsets", "useSafeAreaFrame", "SafeAreaView"],
54
+ message: "Read safe-area values from `rt.insets`, or `UnistylesRuntime.insets` outside a stylesheet, in `react-native-unistyles`.",
55
+ },
56
+ ],
57
+ },
58
+ });
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../../../lib/types.js";
2
+ export declare const insets: Rule;
@@ -0,0 +1,92 @@
1
+ import { calleeName, closestAncestor, gate, problem, subtreeHas } from "../../../lib/ast.js";
2
+ import { isStyleSheetCreate } from "./shared.js";
3
+ const INSET_TEXT = /(?:inset|safearea|top|bottom|left|right)/i;
4
+ const MESSAGES = {
5
+ insetsStyleCall: "Read safe-area values from `rt.insets` inside `StyleSheet.create` instead of passing `useSafeAreaInsets()` into a style function.",
6
+ insetsAttribute: "Resolve these safe-area values through `rt.insets` in `StyleSheet.create` instead of a hook-fed inline JSX style object.",
7
+ };
8
+ const boundNames = (id) => {
9
+ if (id.type === "Identifier")
10
+ return [id.name];
11
+ if (id.type === "AssignmentPattern")
12
+ return boundNames(id.left);
13
+ if (id.type === "RestElement")
14
+ return boundNames(id.argument);
15
+ if (id.type === "ArrayPattern")
16
+ return id.elements.flatMap(element => (element ? boundNames(element) : []));
17
+ if (id.type !== "ObjectPattern")
18
+ return [];
19
+ return id.properties.flatMap(property => property.type === "Property" ? boundNames(property.value) : boundNames(property.argument));
20
+ };
21
+ export const insets = problem("Disallow passing `useSafeAreaInsets()` values into a dynamic style function or an inline JSX style object. Read `rt.insets` inside `StyleSheet.create` instead.", {
22
+ createOnce(context) {
23
+ const insetBindings = new Set();
24
+ const sheets = new Set();
25
+ const styleCalls = [];
26
+ const styleAttributes = [];
27
+ return {
28
+ before() {
29
+ insetBindings.clear();
30
+ sheets.clear();
31
+ styleCalls.length = 0;
32
+ styleAttributes.length = 0;
33
+ return gate(context, "useSafeAreaInsets");
34
+ },
35
+ VariableDeclarator(node) {
36
+ const { id, init } = node;
37
+ if (id.type === "Identifier" && isStyleSheetCreate(init)) {
38
+ sheets.add(id.name);
39
+ return;
40
+ }
41
+ if (calleeName(init) !== "useSafeAreaInsets")
42
+ return;
43
+ for (const name of boundNames(id))
44
+ insetBindings.add(name);
45
+ },
46
+ CallExpression(node) {
47
+ const { callee } = node;
48
+ if (callee.type !== "MemberExpression")
49
+ return;
50
+ const receiver = callee.object;
51
+ if (receiver.type !== "Identifier")
52
+ return;
53
+ const exempt = closestAncestor(node, new Set(["JSXAttribute"]));
54
+ if (exempt?.type === "JSXAttribute" &&
55
+ exempt.name.type === "JSXIdentifier" &&
56
+ exempt.name.name === "contentContainerStyle") {
57
+ return;
58
+ }
59
+ styleCalls.push({ node, receiver: receiver.name });
60
+ },
61
+ JSXAttribute(node) {
62
+ if (node.name.type !== "JSXIdentifier" || node.name.name !== "style")
63
+ return;
64
+ styleAttributes.push(node);
65
+ },
66
+ "Program:exit"() {
67
+ const referencesBinding = (node) => subtreeHas(node, current => current.type === "Identifier" && insetBindings.has(current.name));
68
+ for (const entry of styleCalls) {
69
+ if (!sheets.has(entry.receiver))
70
+ continue;
71
+ const text = context.sourceCode.getText(entry.node);
72
+ if (!INSET_TEXT.test(text))
73
+ continue;
74
+ if (!referencesBinding(entry.node))
75
+ continue;
76
+ context.report({ node: entry.node, message: MESSAGES.insetsStyleCall });
77
+ }
78
+ for (const attribute of styleAttributes) {
79
+ const text = context.sourceCode.getText(attribute);
80
+ if (!INSET_TEXT.test(text))
81
+ continue;
82
+ const usesInsetsMember = subtreeHas(attribute, current => current.type === "MemberExpression" &&
83
+ current.object.type === "Identifier" &&
84
+ insetBindings.has(current.object.name));
85
+ if (!usesInsetsMember)
86
+ continue;
87
+ context.report({ node: attribute, message: MESSAGES.insetsAttribute });
88
+ }
89
+ },
90
+ };
91
+ },
92
+ });
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../../../lib/types.js";
2
+ export declare const noHardcodedColor: Rule;
@@ -0,0 +1,30 @@
1
+ import { ancestors } from "../../../lib/ast.js";
2
+ import { inCreate, propertyName } from "./shared.js";
3
+ const HEX_COLOR = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
4
+ const CSS_COLOR_FUNCTION = /^(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\(/i;
5
+ const MESSAGE = "Use a `theme.colors` token for this value — a raw color bypasses dark mode and never shifts with the theme. If no token fits, add one to the theme.";
6
+ const COLOR_KEY = /^colors?$|Colors?$/;
7
+ /** The nearest style property this literal belongs to, seen through the arrays a gradient uses. */
8
+ const underAColorKey = (node) => {
9
+ for (const current of ancestors(node)) {
10
+ if (current.type === "ArrayExpression")
11
+ continue;
12
+ if (current.type !== "Property")
13
+ return false;
14
+ return COLOR_KEY.test(propertyName(current));
15
+ }
16
+ return false;
17
+ };
18
+ export const noHardcodedColor = inCreate("Disallow hex and CSS-function colors inside `StyleSheet.create`. A raw color skips dark mode and never changes with the theme.", (context, inside) => ({
19
+ Literal(node) {
20
+ if (!inside())
21
+ return;
22
+ if (typeof node.value !== "string")
23
+ return;
24
+ if (!HEX_COLOR.test(node.value) && !CSS_COLOR_FUNCTION.test(node.value))
25
+ return;
26
+ if (!underAColorKey(node))
27
+ return;
28
+ context.report({ node, message: MESSAGE });
29
+ },
30
+ }));
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../../../lib/types.js";
2
+ export declare const noHardcodedSpacing: Rule;
@@ -0,0 +1,78 @@
1
+ import { findInSubtree, hasAncestor, subtreeHas } from "../../../lib/ast.js";
2
+ import { inCreate, memberPath, propertyName } from "./shared.js";
3
+ const SPACING_AND_TYPE_SCALE = new Set([
4
+ "padding",
5
+ "paddingTop",
6
+ "paddingBottom",
7
+ "paddingHorizontal",
8
+ "paddingVertical",
9
+ "paddingStart",
10
+ "paddingEnd",
11
+ "margin",
12
+ "marginTop",
13
+ "marginBottom",
14
+ "marginHorizontal",
15
+ "marginVertical",
16
+ "marginStart",
17
+ "marginEnd",
18
+ "gap",
19
+ "rowGap",
20
+ "columnGap",
21
+ "borderRadius",
22
+ "borderTopLeftRadius",
23
+ "borderTopRightRadius",
24
+ "borderBottomLeftRadius",
25
+ "borderBottomRightRadius",
26
+ "fontSize",
27
+ "lineHeight",
28
+ ]);
29
+ const SIGNIFICANT_NUMBER = /^-?[1-9][0-9]*(?:\.[0-9]+)?$/;
30
+ const MESSAGE = "Use `theme.spacing[...]` or `theme.sizing.scale(...)` for this value, so a rhythm change stays one edit. If no token fits, add one to the theme.";
31
+ const isThemeScaleHost = (node) => hasAncestor(node, current => {
32
+ if (current.type === "MemberExpression") {
33
+ const path = memberPath(current);
34
+ if (path.startsWith("theme.spacing") || path.startsWith("theme.sizing"))
35
+ return true;
36
+ }
37
+ if (current.type === "CallExpression") {
38
+ const { callee } = current;
39
+ const path = callee.type === "MemberExpression" ? memberPath(callee) : "";
40
+ if (path === "theme.spacing.scale" || path === "theme.sizing.scale" || path === "theme.scale")
41
+ return true;
42
+ }
43
+ return false;
44
+ });
45
+ const isTokenDerived = (node) => subtreeHas(node, current => {
46
+ if (current.type === "MemberExpression") {
47
+ const path = memberPath(current);
48
+ return path.startsWith("theme.spacing") || path.startsWith("theme.sizing");
49
+ }
50
+ if (current.type === "CallExpression" && current.callee.type === "MemberExpression") {
51
+ const path = memberPath(current.callee);
52
+ return path === "theme.spacing.scale" || path === "theme.sizing.scale" || path === "theme.scale";
53
+ }
54
+ return false;
55
+ });
56
+ export const noHardcodedSpacing = inCreate("Require `theme.spacing` or `theme.sizing.scale` for spacing, radius and type values inside `StyleSheet.create`, instead of raw numbers.", (context, inside) => ({
57
+ Property(node) {
58
+ if (!inside())
59
+ return;
60
+ const name = propertyName(node);
61
+ if (!SPACING_AND_TYPE_SCALE.has(name))
62
+ return;
63
+ if (isTokenDerived(node.value))
64
+ return;
65
+ const rawNumber = findInSubtree(node.value, current => (current.type === "Literal" &&
66
+ typeof current.value === "number" &&
67
+ SIGNIFICANT_NUMBER.test(String(current.value)) &&
68
+ !isThemeScaleHost(current)) ||
69
+ (current.type === "UnaryExpression" &&
70
+ current.operator === "-" &&
71
+ current.argument.type === "Literal" &&
72
+ typeof current.argument.value === "number" &&
73
+ SIGNIFICANT_NUMBER.test(`-${current.argument.value}`) &&
74
+ !isThemeScaleHost(current)));
75
+ if (rawNumber)
76
+ context.report({ node: rawNumber, message: MESSAGE });
77
+ },
78
+ }));
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../../../lib/types.js";
2
+ export declare const noMargin: Rule;
@@ -0,0 +1,19 @@
1
+ import { subtreeHas } from "../../../lib/ast.js";
2
+ import { inCreate, propertyName } from "./shared.js";
3
+ const ANY_MARGIN = /^margin(?:$|Top$|Bottom$|Left$|Right$|Start$|End$|Horizontal$|Vertical$)/;
4
+ /** A negative margin overlaps on purpose, and a zero one resets; neither is the spacing this rule replaces. */
5
+ const isNegationOrZero = (current) => (current.type === "UnaryExpression" && current.operator === "-") ||
6
+ (current.type === "Literal" && typeof current.value === "number" && current.value <= 0);
7
+ const MESSAGE = "Use `gap` on the parent or `padding` on this element instead of `margin`. Margin escapes the child's own box, so it leaves stray space behind when the first or last child is removed; a negative margin stays allowed for overlap and half-size centering, and `0` for a reset.";
8
+ export const noMargin = inCreate("Disallow non-negative `margin` inside `StyleSheet.create`; `gap` on the parent or `padding` on the element spaces children without leaving a hole when one is removed.", (context, inside) => ({
9
+ Property(node) {
10
+ if (!inside())
11
+ return;
12
+ const name = propertyName(node);
13
+ if (name === "" || !ANY_MARGIN.test(name))
14
+ return;
15
+ if (subtreeHas(node.value, isNegationOrZero))
16
+ return;
17
+ context.report({ node, message: MESSAGE });
18
+ },
19
+ }));
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../../../lib/types.js";
2
+ export declare const noStyleSpread: Rule;
@@ -0,0 +1,41 @@
1
+ import { gate, problem } from "../../../lib/ast.js";
2
+ import { isStyleSheetCreate } from "./shared.js";
3
+ const MESSAGE = "Compose styles with an array — `[styles.a, styles.b]` — instead of spreading. A spread reads the object once and breaks the Unistyles C++ proxy, so the style silently stops reacting to the theme.";
4
+ const spreadBase = (node) => {
5
+ let current = node;
6
+ while (current?.type === "MemberExpression")
7
+ current = current.object;
8
+ return current?.type === "Identifier" ? current.name : "";
9
+ };
10
+ export const noStyleSpread = problem("Disallow spreading a stylesheet style into another object. The spread reads through the Unistyles C++ proxy once, so the result stops reacting to the theme.", {
11
+ createOnce(context) {
12
+ const sheets = new Set();
13
+ const candidates = [];
14
+ return {
15
+ before() {
16
+ sheets.clear();
17
+ candidates.length = 0;
18
+ return gate(context, "...");
19
+ },
20
+ VariableDeclarator(node) {
21
+ const { id, init } = node;
22
+ if (id.type === "Identifier" && isStyleSheetCreate(init))
23
+ sheets.add(id.name);
24
+ },
25
+ SpreadElement(node) {
26
+ if (node.parent.type !== "ObjectExpression")
27
+ return;
28
+ const base = spreadBase(node.argument);
29
+ if (base !== "")
30
+ candidates.push({ node, base });
31
+ },
32
+ "Program:exit"() {
33
+ for (const candidate of candidates) {
34
+ if (!sheets.has(candidate.base) && !/[Ss]tyles$/.test(candidate.base))
35
+ continue;
36
+ context.report({ node: candidate.node, message: MESSAGE });
37
+ }
38
+ },
39
+ };
40
+ },
41
+ });
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../../../lib/types.js";
2
+ export declare const noUnusedStyles: Rule;
@@ -0,0 +1,99 @@
1
+ import { gate, problem } from "../../../lib/ast.js";
2
+ import { CREATE_MARKER, isStyleSheetCreate, propertyName, stylesObjectOf } from "./shared.js";
3
+ const EVERY_KEY = "*";
4
+ const namedStyleKeys = (styles) => {
5
+ const keys = new Map();
6
+ for (const property of styles.properties) {
7
+ if (property.type !== "Property" || property.computed)
8
+ return null;
9
+ const name = propertyName(property);
10
+ if (name === "")
11
+ return null;
12
+ keys.set(name, property.key);
13
+ }
14
+ return keys;
15
+ };
16
+ export const noUnusedStyles = problem("Disallow stylesheet keys that nothing in the file reads. A computed key, a computed read, or a sheet that leaves the module skips the whole file.", {
17
+ createOnce(context) {
18
+ let declaredStyleKeys = new Map();
19
+ let readStyleKeys = new Set();
20
+ let aUseCouldBeHidden = false;
21
+ const markRead = (sheet, key) => {
22
+ readStyleKeys.add(`${sheet}.${key}`);
23
+ };
24
+ const markWholeSheetRead = (sheet) => {
25
+ markRead(sheet, EVERY_KEY);
26
+ };
27
+ const reportUnusedAtEndOfFile = () => {
28
+ if (aUseCouldBeHidden)
29
+ return;
30
+ for (const [sheet, keys] of declaredStyleKeys) {
31
+ if (readStyleKeys.has(`${sheet}.${EVERY_KEY}`))
32
+ continue;
33
+ for (const [name, keyNode] of keys) {
34
+ if (readStyleKeys.has(`${sheet}.${name}`))
35
+ continue;
36
+ context.report({
37
+ node: keyNode,
38
+ message: `Delete \`${sheet}.${name}\` — nothing reads it, so it is dead weight that keeps a token alive nothing renders.`,
39
+ });
40
+ }
41
+ }
42
+ };
43
+ return {
44
+ before() {
45
+ declaredStyleKeys = new Map();
46
+ readStyleKeys = new Set();
47
+ aUseCouldBeHidden = false;
48
+ return gate(context, CREATE_MARKER);
49
+ },
50
+ CallExpression(node) {
51
+ if (!isStyleSheetCreate(node))
52
+ return;
53
+ const declarator = node.parent.type === "VariableDeclarator" ? node.parent : null;
54
+ const id = declarator?.id;
55
+ if (id?.type !== "Identifier") {
56
+ aUseCouldBeHidden = true;
57
+ return;
58
+ }
59
+ const styles = stylesObjectOf(node.arguments[0]);
60
+ const keys = styles === null ? null : namedStyleKeys(styles);
61
+ if (keys === null) {
62
+ aUseCouldBeHidden = true;
63
+ return;
64
+ }
65
+ declaredStyleKeys.set(id.name, keys);
66
+ },
67
+ MemberExpression(node) {
68
+ const { object, property } = node;
69
+ if (object.type !== "Identifier")
70
+ return;
71
+ if (node.computed) {
72
+ markWholeSheetRead(object.name);
73
+ return;
74
+ }
75
+ if (property.type === "Identifier")
76
+ markRead(object.name, property.name);
77
+ },
78
+ ExportNamedDeclaration(node) {
79
+ const { declaration } = node;
80
+ if (declaration?.type === "VariableDeclaration") {
81
+ for (const child of declaration.declarations) {
82
+ if (child.id.type === "Identifier")
83
+ markWholeSheetRead(child.id.name);
84
+ }
85
+ }
86
+ for (const specifier of node.specifiers) {
87
+ if (specifier.local.type === "Identifier")
88
+ markWholeSheetRead(specifier.local.name);
89
+ }
90
+ },
91
+ ExportDefaultDeclaration(node) {
92
+ const { declaration } = node;
93
+ if (declaration.type === "Identifier")
94
+ markWholeSheetRead(declaration.name);
95
+ },
96
+ "Program:exit": reportUnusedAtEndOfFile,
97
+ };
98
+ },
99
+ });
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../../../lib/types.js";
2
+ export declare const rtlStyleCall: Rule;
@@ -0,0 +1,42 @@
1
+ import { closestAncestor, gate, problem } from "../../../lib/ast.js";
2
+ import { CREATE_MARKER, isStyleSheetCreate, memberPath } from "./shared.js";
3
+ const MESSAGE = "Read `rt.rtl` inside the dynamic style instead of passing `I18nManager.isRTL` in from JSX.";
4
+ export const rtlStyleCall = problem("Disallow passing `I18nManager.isRTL` from JSX into a dynamic style function. Unistyles tracks the dependency itself once the style reads `rt.rtl`.", {
5
+ createOnce(context) {
6
+ const sheets = new Set();
7
+ const candidates = [];
8
+ return {
9
+ before() {
10
+ sheets.clear();
11
+ candidates.length = 0;
12
+ return gate(context, CREATE_MARKER);
13
+ },
14
+ VariableDeclarator(node) {
15
+ const { id, init } = node;
16
+ if (id.type === "Identifier" && isStyleSheetCreate(init))
17
+ sheets.add(id.name);
18
+ },
19
+ MemberExpression(node) {
20
+ if (memberPath(node) !== "I18nManager.isRTL")
21
+ return;
22
+ const call = closestAncestor(node, new Set(["CallExpression"]));
23
+ if (call?.type !== "CallExpression")
24
+ return;
25
+ const { callee } = call;
26
+ if (callee.type !== "MemberExpression")
27
+ return;
28
+ const receiver = callee.object;
29
+ if (receiver.type !== "Identifier")
30
+ return;
31
+ candidates.push({ call, receiver: receiver.name });
32
+ },
33
+ "Program:exit"() {
34
+ for (const candidate of candidates) {
35
+ if (!sheets.has(candidate.receiver))
36
+ continue;
37
+ context.report({ node: candidate.call, message: MESSAGE });
38
+ }
39
+ },
40
+ };
41
+ },
42
+ });
@@ -0,0 +1,16 @@
1
+ import type { AstNode, Rule, RuleContext, Visitor } from "../../../lib/types.js";
2
+ export { CREATE_MARKER, isStyleSheetCreate, propertyName, stylesObjectOf, type StylesObject, } from "../../stylesheet.js";
3
+ export declare const memberPath: (node: AstNode | null | undefined) => string;
4
+ export declare const declaresUseUnistylesTheme: (scope: AstNode) => boolean;
5
+ /**
6
+ * The enclosing function that destructures a `useUnistyles()` theme, innermost first. Walking past the
7
+ * innermost function matters: the read is often inside a callback the component passes down.
8
+ */
9
+ export declare const themeConsumingComponent: (node: AstNode) => AstNode | null;
10
+ export declare const readsTheme: (node: AstNode) => boolean;
11
+ /**
12
+ * Builds a rule whose visitors only fire inside `StyleSheet.create(...)`, by tracking how deeply
13
+ * nested the traversal currently is in such a call. The depth resets per file in `before()`, which
14
+ * also runs the text gate.
15
+ */
16
+ export declare const inCreate: (description: string, visit: (context: RuleContext, inside: () => boolean) => Visitor) => Rule;