@ashstack/lint 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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,49 +1 @@
|
|
|
1
|
-
import
|
|
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
|
-
});
|
|
1
|
+
import{attributeName as p,gate as f,importedSpecifiers as m,problem as l,tagPath as u}from"../../../lib/ast.js";const d="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.";export const canvasOpaque=l("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.",{createOnce(o){const n=new Set,r=new Set,s=[],i=(a)=>{const e=u(a),t=e.lastIndexOf(".");if(t===-1)return n.has(e);return r.has(e.slice(0,t))&&e.slice(t+1)==="Canvas"};return{before(){n.clear();r.clear();s.length=0;return f(o,"react-native-skia")},ImportDeclaration(a){for(const e of m(a,"@shopify/react-native-skia")){if(e.type==="ImportSpecifier"){const{imported:t,local:c}=e;if(t.type==="Identifier"&&t.name==="Canvas")n.add(c.name);continue}if(e.type==="ImportNamespaceSpecifier"||e.type==="ImportDefaultSpecifier")r.add(e.local.name)}},JSXOpeningElement(a){if(!i(a.name))return;if(!a.attributes.some((t)=>t.type==="JSXAttribute"&&p(t)==="opaque"))s.push(a)},"Program:exit"(){for(const a of s)o.report({node:a,message:d})}}}});
|
|
@@ -1,21 +1 @@
|
|
|
1
|
-
import
|
|
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
|
-
});
|
|
1
|
+
import{defineModule as e}from"../../../lib/module.js";import{canvasOpaque as a}from"./canvas-opaque.js";export default e({meta:{name:"@ashstack/skia"},url:import.meta.url,packages:["@shopify/react-native-skia"],option:"skia",docsWhen:"auto-enabled when `@shopify/react-native-skia` is a dependency",rules:{"canvas-opaque":a},restrictedImports:{paths:[{name:"@shopify/react-native-skia",importNames:["usePathValue","usePathInterpolation"],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."}]}});
|
|
@@ -1,14 +1 @@
|
|
|
1
|
-
import
|
|
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
|
-
});
|
|
1
|
+
import{defineModule as e}from"../../../lib/module.js";import{requireCachePolicy as r}from"./require-cache-policy.js";import{requireResize as a}from"./require-resize.js";export default e({meta:{name:"@ashstack/turbo-image"},url:import.meta.url,packages:["react-native-turbo-image"],option:"turboImage",docsWhen:"auto-enabled when `react-native-turbo-image` is a dependency",rules:{"require-resize":a,"require-cache-policy":r}});
|
|
@@ -1,21 +1 @@
|
|
|
1
|
-
import
|
|
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
|
-
});
|
|
1
|
+
import{attributeName as a,componentName as c,gate as n,problem as i}from"../../../lib/ast.js";const m='Add `cachePolicy` to this TurboImage (normally `cachePolicy="dataCache"`), or it re-fetches over the network on every cold start.';export const requireCachePolicy=i("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.",{createOnce(t){return{before(){return n(t,"TurboImage")},JSXOpeningElement(e){if(c(e.name)!=="TurboImage")return;const{attributes:o}=e;if(o.some((r)=>r.type==="JSXSpreadAttribute"))return;if(o.some((r)=>a(r)==="cachePolicy"))return;t.report({node:e.name,message:m})}}}});
|
|
@@ -1,22 +1 @@
|
|
|
1
|
-
import
|
|
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
|
-
});
|
|
1
|
+
import{attributeName as s,componentName as a,gate as i,problem as n}from"../../../lib/ast.js";const m="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).";export const requireResize=n("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.",{createOnce(r){return{before(){return i(r,"TurboImage")},JSXOpeningElement(t){if(a(t.name)!=="TurboImage")return;const{attributes:o}=t;if(o.some((e)=>e.type==="JSXSpreadAttribute"))return;if(o.some((e)=>e.type==="JSXAttribute"&&s(e)==="resize"))return;r.report({node:t.name,message:m})}}}});
|
|
@@ -1,37 +1 @@
|
|
|
1
|
-
import
|
|
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
|
-
});
|
|
1
|
+
import{calleeName as n,problem as m}from"../../../lib/ast.js";import{readsTheme as h}from"./shared.js";const d=new Set(["useAnimatedStyle","useDerivedValue","useAnimatedProps"]),l="Use `useAnimatedTheme()` and read its shared value inside the Reanimated worklet so theme changes reach the UI thread.";export const animatedTheme=m("A Reanimated worklet hook cannot see `useUnistyles()` theme changes on the UI thread. Read the shared value from `useAnimatedTheme()` there instead.",{createOnce(i){let t=!1;const a=[];return{before(){t=!1;a.length=0},VariableDeclarator(e){const{id:s,init:o}=e;if(s.type==="ObjectPattern"&&n(o)==="useUnistyles"&&s.properties.some((r)=>r.type==="Property"&&r.key.type==="Identifier"&&r.key.name==="theme"))t=!0},CallExpression(e){if(!d.has(n(e)))return;if(!h(e))return;a.push(e)},"Program:exit"(){if(!t)return;for(const e of a)i.report({node:e,message:l})}}}});
|
|
@@ -1,73 +1 @@
|
|
|
1
|
-
import
|
|
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
|
-
});
|
|
1
|
+
import{calleeName as f,findInSubtree as u,gate as b,problem as d,subtreeHas as y,tagIdentifier as S}from"../../../lib/ast.js";import{isStyleSheetCreate as w,memberPath as l,propertyName as C,stylesObjectOf as g}from"./shared.js";const h={contentContainerRuntime:"Wrap this component with `withUnistyles`, or pass `contentContainerStyle` a plain hook-fed style. On a raw component `contentContainerStyle` never subscribes to Unistyles updates.",contentContainerTheme:"Wrap this component with `withUnistyles`, or pass `contentContainerStyle` a plain hook-fed style. On a raw component `contentContainerStyle` never subscribes to theme changes."};export const contentContainer=d("A raw component never subscribes its `contentContainerStyle` to theme or `rt` updates. Wrap the component with `withUnistyles` when the style depends on either.",{createOnce(p){const a=new Map,c=new Set,m=[];return{before(){a.clear();c.clear();m.length=0;return b(p,"contentContainerStyle")},VariableDeclarator(e){const{id:r,init:n}=e;if(r.type!=="Identifier")return;if(f(n)==="withUnistyles"){c.add(r.name);return}if(n?.type!=="CallExpression"||!w(n))return;const s=g(n.arguments[0]);if(!s)return;const i=new Map;for(const o of s.properties){if(o.type!=="Property")continue;i.set(C(o),o.value)}a.set(r.name,i)},JSXAttribute(e){if(e.name.type!=="JSXIdentifier"||e.name.name!=="contentContainerStyle")return;m.push(e)},"Program:exit"(){for(const e of m){const r=u(e.value,(t)=>t.type==="MemberExpression"&&t.object.type==="Identifier"&&a.has(t.object.name));if(r?.type!=="MemberExpression")continue;const n=e.parent;if(n.type==="JSXOpeningElement"&&c.has(S(n.name)))continue;const{object:s,property:i}=r;if(s.type!=="Identifier")continue;const o=a.get(s.name)?.get(i.type==="Identifier"?i.name:"");if(!o)continue;if(y(o,(t)=>t.type==="MemberExpression"&&l(t).startsWith("rt."))){p.report({node:e,message:h.contentContainerRuntime});continue}if(y(o,(t)=>t.type==="MemberExpression"&&l(t).startsWith("theme.")))p.report({node:e,message:h.contentContainerTheme})}}}}});
|
|
@@ -1,116 +1 @@
|
|
|
1
|
-
import
|
|
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
|
-
}));
|
|
1
|
+
import{calleeName as h,subtreeHas as p}from"../../../lib/ast.js";import{inCreate as m,isStyleSheetCreate as u,memberPath as l,propertyName as c}from"./shared.js";const S=new Set(["paddingLeft","paddingRight","marginLeft","marginRight"]),f=new Set(["shadowColor","shadowOffset","shadowOpacity","shadowRadius","elevation"]),i={logicalSpacing:"Use `paddingStart`/`paddingEnd` or `marginStart`/`marginEnd` here so the directional spacing mirrors in RTL.",rtlInSheet:"Use `rt.rtl` for RTL-dependent styles so Unistyles tracks the dependency natively.",screenDimensions:"Use reactive `rt.screen` inside `StyleSheet.create` instead of a `Dimensions.get` snapshot.",pixelRatio:"Use reactive `rt.pixelRatio` inside `StyleSheet.create`.",fontScale:"Use reactive `rt.fontScale` inside `StyleSheet.create`.",statusBar:"Use reactive `rt.statusBar.height` inside `StyleSheet.create`.",colorScheme:"Use the Unistyles theme or reactive `rt.colorScheme` inside `StyleSheet.create`.",fullRuntime:"Use the injected mini runtime (`rt`) inside `StyleSheet.create` instead of `UnistylesRuntime`, so the style recalculates on its own.",fullRuntimeDestructure:"Destructure from the injected mini runtime (`rt`) inside `StyleSheet.create`, not from `UnistylesRuntime`.",themeScreen:"Use reactive `rt.screen`, or Unistyles breakpoints, instead of the theme's module-initialization screen snapshot.",borderCurve:'Add `borderCurve` (normally `"continuous"`) alongside `borderRadius`, or the corner renders as a circular arc rather than the squircle native iOS controls use.',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.',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."};export const inSheet=m("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`.",(s,a)=>({CallExpression(e){if(!a()||u(e))return;const{callee:r}=e,t=r.type==="MemberExpression"?l(r):h(e);if(t==="Dimensions.get")s.report({node:e,message:i.screenDimensions});else if(t==="PixelRatio.get")s.report({node:e,message:i.pixelRatio});else if(t==="PixelRatio.getFontScale")s.report({node:e,message:i.fontScale});else if(t==="Appearance.getColorScheme")s.report({node:e,message:i.colorScheme});else if(t==="useColorScheme")s.report({node:e,message:i.colorScheme})},MemberExpression(e){if(!a())return;const{parent:r}=e;if(r.type==="MemberExpression"&&r.object===e)return;const t=l(e);if(t==="I18nManager.isRTL"){s.report({node:e,message:i.rtlInSheet});return}if(t==="StatusBar.currentHeight"){s.report({node:e,message:i.statusBar});return}if(t.startsWith("UnistylesRuntime.")){s.report({node:e,message:i.fullRuntime});return}if(t.startsWith("theme.screen."))s.report({node:e,message:i.themeScreen})},VariableDeclarator(e){if(!a())return;const{id:r,init:t}=e;if(r.type!=="ObjectPattern")return;if(t?.type==="Identifier"&&t.name==="UnistylesRuntime")s.report({node:e,message:i.fullRuntimeDestructure})},TSAsExpression(e){if(!a())return;const{typeAnnotation:r}=e;if(r.type!=="TSTypeReference")return;const{typeName:t}=r;if(t.type!=="Identifier"||t.name!=="const")return;s.report({node:e,message:i.asConst})},ObjectExpression(e){if(!a())return;let r=null,t=!1;for(const n of e.properties){if(n.type!=="Property")continue;const o=c(n);if(o==="borderRadius")r=n;else if(o==="borderCurve")t=!0}if(r&&!t)s.report({node:r,message:i.borderCurve})},Property(e){if(!a())return;const r=c(e);if(f.has(r)){s.report({node:e,message:i.legacyShadow});return}if(!S.has(r))return;if(!p(e,(n)=>{if(n.type!=="MemberExpression")return!1;const o=l(n);return o==="rt.insets.left"||o==="rt.insets.right"}))s.report({node:e,message:i.logicalSpacing})}}));
|
|
@@ -1,58 +1 @@
|
|
|
1
|
-
import
|
|
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
|
-
});
|
|
1
|
+
import{defineModule as e}from"../../../lib/module.js";import{animatedTheme as t}from"./animated-theme.js";import{contentContainer as n}from"./content-container.js";import{inSheet as a}from"./in-sheet.js";import{insets as r}from"./insets.js";import{noHardcodedColor as s}from"./no-hardcoded-color.js";import{noHardcodedSpacing as o}from"./no-hardcoded-spacing.js";import{noMargin as i}from"./no-margin.js";import{noStyleSpread as m}from"./no-style-spread.js";import{noUnusedStyles as l}from"./no-unused-styles.js";import{rtlStyleCall as c}from"./rtl-style-call.js";import{themeScreenComponent as d}from"./theme-screen-component.js";import{themeStyleAttr as p}from"./theme-style-attr.js";export default e({meta:{name:"@ashstack/unistyles"},rules:{"animated-theme":t,"content-container":n,"in-sheet":a,insets:r,"no-hardcoded-color":s,"no-hardcoded-spacing":o,"no-margin":i,"no-style-spread":m,"no-unused-styles":l,"rtl-style-call":c,"theme-screen-component":d,"theme-style-attr":p},url:import.meta.url,packages:["react-native-unistyles"],option:"unistyles",docsWhen:"auto-enabled when `react-native-unistyles` is a dependency",restrictedImports:{paths:[{name:"react-native",importNames:["StyleSheet"],message:"Import `StyleSheet` from `react-native-unistyles` instead of `react-native`."},{name:"react-native",importNames:["useWindowDimensions","Dimensions"],message:"Read screen size from `rt.screen` inside `StyleSheet.create`, or `UnistylesRuntime.screen` outside it, instead of the `Dimensions` APIs."},{name:"react-native",importNames:["SafeAreaView"],message:'Set `contentInsetAdjustmentBehavior="automatic"` on the scroll view, or apply `rt.insets` from `react-native-unistyles`, instead of `SafeAreaView`.'},{name:"react-native-safe-area-context",importNames:["useSafeAreaInsets","useSafeAreaFrame","SafeAreaView"],message:"Read safe-area values from `rt.insets`, or `UnistylesRuntime.insets` outside a stylesheet, in `react-native-unistyles`."}]}});
|
|
@@ -1,92 +1 @@
|
|
|
1
|
-
import
|
|
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
|
-
});
|
|
1
|
+
import{calleeName as y,closestAncestor as S,gate as b,problem as d,subtreeHas as u}from"../../../lib/ast.js";import{isStyleSheetCreate as g}from"./shared.js";const m=/(?:inset|safearea|top|bottom|left|right)/i,p={insetsStyleCall:"Read safe-area values from `rt.insets` inside `StyleSheet.create` instead of passing `useSafeAreaInsets()` into a style function.",insetsAttribute:"Resolve these safe-area values through `rt.insets` in `StyleSheet.create` instead of a hook-fed inline JSX style object."},i=(t)=>{if(t.type==="Identifier")return[t.name];if(t.type==="AssignmentPattern")return i(t.left);if(t.type==="RestElement")return i(t.argument);if(t.type==="ArrayPattern")return t.elements.flatMap((s)=>s?i(s):[]);if(t.type!=="ObjectPattern")return[];return t.properties.flatMap((s)=>s.type==="Property"?i(s.value):i(s.argument))};export const insets=d("Disallow passing `useSafeAreaInsets()` values into a dynamic style function or an inline JSX style object. Read `rt.insets` inside `StyleSheet.create` instead.",{createOnce(t){const s=new Set,o=new Set,f=[],l=[];return{before(){s.clear();o.clear();f.length=0;l.length=0;return b(t,"useSafeAreaInsets")},VariableDeclarator(r){const{id:e,init:n}=r;if(e.type==="Identifier"&&g(n)){o.add(e.name);return}if(y(n)!=="useSafeAreaInsets")return;for(const a of i(e))s.add(a)},CallExpression(r){const{callee:e}=r;if(e.type!=="MemberExpression")return;const n=e.object;if(n.type!=="Identifier")return;const a=S(r,new Set(["JSXAttribute"]));if(a?.type==="JSXAttribute"&&a.name.type==="JSXIdentifier"&&a.name.name==="contentContainerStyle")return;f.push({node:r,receiver:n.name})},JSXAttribute(r){if(r.name.type!=="JSXIdentifier"||r.name.name!=="style")return;l.push(r)},"Program:exit"(){const r=(e)=>u(e,(n)=>n.type==="Identifier"&&s.has(n.name));for(const e of f){if(!o.has(e.receiver))continue;const n=t.sourceCode.getText(e.node);if(!m.test(n))continue;if(!r(e.node))continue;t.report({node:e.node,message:p.insetsStyleCall})}for(const e of l){const n=t.sourceCode.getText(e);if(!m.test(n))continue;if(!u(e,(c)=>c.type==="MemberExpression"&&c.object.type==="Identifier"&&s.has(c.object.name)))continue;t.report({node:e,message:p.insetsAttribute})}}}}});
|
|
@@ -1,30 +1 @@
|
|
|
1
|
-
import
|
|
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
|
-
}));
|
|
1
|
+
import{ancestors as o}from"../../../lib/ast.js";import{inCreate as s,propertyName as a}from"./shared.js";const n=/^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/,i=/^(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\(/i,l="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.",c=/^colors?$|Colors?$/,f=(t)=>{for(const e of o(t)){if(e.type==="ArrayExpression")continue;if(e.type!=="Property")return!1;return c.test(a(e))}return!1};export const noHardcodedColor=s("Disallow hex and CSS-function colors inside `StyleSheet.create`. A raw color skips dark mode and never changes with the theme.",(t,e)=>({Literal(r){if(!e())return;if(typeof r.value!=="string")return;if(!n.test(r.value)&&!i.test(r.value))return;if(!f(r))return;t.report({node:r,message:l})}}));
|