@ashstack/lint 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/core/index.js +1 -222
- package/dist/core/rules/core/components-tsx-only.js +1 -37
- package/dist/core/rules/core/hoist-intl.js +1 -32
- package/dist/core/rules/core/index.js +1 -18
- package/dist/core/rules/core/no-comments.js +1 -194
- package/dist/core/rules/core/no-naming-convention.js +1 -143
- package/dist/core/rules/core/no-packed-condition.d.ts +2 -0
- package/dist/core/rules/core/no-packed-condition.js +1 -0
- package/dist/core/rules/core/use-design-system.js +1 -147
- package/dist/core/rules/zod/index.js +1 -12
- package/dist/core/rules/zod/prefer-enum.js +1 -44
- package/dist/index.js +1 -3
- package/dist/lib/ast.js +1 -162
- package/dist/lib/detect.js +1 -42
- package/dist/lib/merge.js +1 -18
- package/dist/lib/module.js +1 -49
- package/dist/lib/types.js +1 -1
- package/dist/react/index.js +1 -126
- package/dist/react/rules/i18n/index.js +1 -16
- package/dist/react/rules/i18n/no-bare-attrs.js +1 -38
- package/dist/react/rules/i18n/no-bare-text.js +1 -26
- package/dist/react/rules/i18n/no-bare-toast.js +1 -38
- package/dist/react/rules/query/index.js +1 -20
- package/dist/react/rules/query/next-page-param-undefined.js +1 -26
- package/dist/react/rules/query/no-deprecated-filters.js +1 -63
- package/dist/react/rules/query/no-fetch-in-query-fn.js +1 -22
- package/dist/react/rules/query/no-inline-keys.js +1 -107
- package/dist/react/rules/query/require-destructured-hooks.js +1 -41
- package/dist/react/rules/query/shared.js +1 -10
- package/dist/react/rules/zustand/index.js +1 -12
- package/dist/react/rules/zustand/require-selector.js +1 -62
- package/dist/react-native/index.js +1 -113
- package/dist/react-native/rules/keyboard/index.js +1 -18
- package/dist/react-native/rules/legend-list/index.js +1 -51
- package/dist/react-native/rules/legend-list/no-flex-in-content-container.js +1 -77
- package/dist/react-native/rules/legend-list/no-index-key-extractor.js +1 -26
- package/dist/react-native/rules/legend-list/no-inline-data.js +1 -25
- package/dist/react-native/rules/legend-list/no-inline-extra-data.js +1 -21
- package/dist/react-native/rules/legend-list/no-inline-render-item-props.js +1 -35
- package/dist/react-native/rules/legend-list/no-mixed-children.js +1 -24
- package/dist/react-native/rules/legend-list/no-remount-key.js +1 -18
- package/dist/react-native/rules/legend-list/no-scrollview-map.js +1 -27
- package/dist/react-native/rules/legend-list/no-unsupported-props.js +1 -29
- package/dist/react-native/rules/legend-list/required-props.js +1 -23
- package/dist/react-native/rules/legend-list/shared.js +1 -25
- package/dist/react-native/rules/legend-list/typed-items-need-item-type.js +1 -49
- package/dist/react-native/rules/legend-state/index.js +1 -35
- package/dist/react-native/rules/legend-state/naming.js +1 -26
- package/dist/react-native/rules/legend-state/no-assignment.js +1 -31
- package/dist/react-native/rules/legend-state/no-nested-observable.js +1 -23
- package/dist/react-native/rules/legend-state/no-object-selector.js +1 -47
- package/dist/react-native/rules/legend-state/no-observable-in-component.js +1 -46
- package/dist/react-native/rules/legend-state/no-peek-in-selector.js +1 -67
- package/dist/react-native/rules/legend-state/no-react-mirror.js +1 -33
- package/dist/react-native/rules/legend-state/no-untracked-get-in-jsx.js +1 -52
- package/dist/react-native/rules/legend-state/shared.js +1 -23
- package/dist/react-native/rules/react-native/hoist-stateless-function.js +1 -59
- package/dist/react-native/rules/react-native/index.js +1 -50
- package/dist/react-native/rules/react-native/no-conditional-style-array.js +1 -23
- package/dist/react-native/rules/react-native/no-dynamic-import.js +1 -18
- package/dist/react-native/rules/react-native/no-keyboard-will-events.js +1 -18
- package/dist/react-native/rules/react-native/no-leaked-render.js +1 -43
- package/dist/react-native/rules/react-native/no-manual-memo.js +1 -87
- package/dist/react-native/rules/react-native/no-redundant-view-nesting.js +1 -36
- package/dist/react-native/rules/react-native/no-rn-image-network-source.js +1 -58
- package/dist/react-native/rules/react-native/no-rn-namespace-import.js +1 -38
- package/dist/react-native/rules/react-native/no-scroll-position-state.js +1 -26
- package/dist/react-native/rules/react-native/no-unlabeled-icon-pressable.js +1 -62
- package/dist/react-native/rules/reanimated/animated-reaction-safety.js +1 -110
- package/dist/react-native/rules/reanimated/animated-style-needs-animated-component.js +1 -72
- package/dist/react-native/rules/reanimated/animated-updater-purity.js +1 -38
- package/dist/react-native/rules/reanimated/gpu-properties-only.js +1 -80
- package/dist/react-native/rules/reanimated/hoist-layout-animation-builder.js +1 -43
- package/dist/react-native/rules/reanimated/index.js +1 -51
- package/dist/react-native/rules/reanimated/interpolate-needs-clamp.js +1 -41
- package/dist/react-native/rules/reanimated/no-react-state-from-continuous-worklet.js +1 -86
- package/dist/react-native/rules/reanimated/no-shared-value-dot-value.js +1 -112
- package/dist/react-native/rules/reanimated/prefer-lazy-shared-value-initializer.js +1 -24
- package/dist/react-native/rules/reanimated/schedule-on-rn-scope.js +1 -21
- package/dist/react-native/rules/reanimated/shared-value-usage.js +1 -105
- package/dist/react-native/rules/reanimated/shared.js +1 -13
- package/dist/react-native/rules/skia/canvas-opaque.js +1 -49
- package/dist/react-native/rules/skia/index.js +1 -21
- package/dist/react-native/rules/turbo-image/index.js +1 -14
- package/dist/react-native/rules/turbo-image/require-cache-policy.js +1 -21
- package/dist/react-native/rules/turbo-image/require-resize.js +1 -22
- package/dist/react-native/rules/unistyles/animated-theme.js +1 -37
- package/dist/react-native/rules/unistyles/content-container.js +1 -73
- package/dist/react-native/rules/unistyles/in-sheet.js +1 -116
- package/dist/react-native/rules/unistyles/index.js +1 -58
- package/dist/react-native/rules/unistyles/insets.js +1 -92
- package/dist/react-native/rules/unistyles/no-hardcoded-color.js +1 -30
- package/dist/react-native/rules/unistyles/no-hardcoded-spacing.js +1 -78
- package/dist/react-native/rules/unistyles/no-margin.js +1 -19
- package/dist/react-native/rules/unistyles/no-style-spread.js +1 -41
- package/dist/react-native/rules/unistyles/no-unused-styles.js +1 -99
- package/dist/react-native/rules/unistyles/rtl-style-call.js +1 -42
- package/dist/react-native/rules/unistyles/shared.js +1 -70
- package/dist/react-native/rules/unistyles/theme-screen-component.js +1 -22
- package/dist/react-native/rules/unistyles/theme-style-attr.js +1 -18
- package/dist/react-native/stylesheet.js +1 -44
- package/package.json +3 -4
- package/RULES.md +0 -2881
|
@@ -1,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})}}));
|
|
@@ -1,78 +1 @@
|
|
|
1
|
-
import
|
|
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
|
-
}));
|
|
1
|
+
import{findInSubtree as m,hasAncestor as g,subtreeHas as l}from"../../../lib/ast.js";import{inCreate as d,memberPath as n,propertyName as h}from"./shared.js";const f=new Set(["padding","paddingTop","paddingBottom","paddingHorizontal","paddingVertical","paddingStart","paddingEnd","margin","marginTop","marginBottom","marginHorizontal","marginVertical","marginStart","marginEnd","gap","rowGap","columnGap","borderRadius","borderTopLeftRadius","borderTopRightRadius","borderBottomLeftRadius","borderBottomRightRadius","fontSize","lineHeight"]),r=/^-?[1-9][0-9]*(?:\.[0-9]+)?$/,c="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.",p=(i)=>g(i,(t)=>{if(t.type==="MemberExpression"){const e=n(t);if(e.startsWith("theme.spacing")||e.startsWith("theme.sizing"))return!0}if(t.type==="CallExpression"){const{callee:e}=t,s=e.type==="MemberExpression"?n(e):"";if(s==="theme.spacing.scale"||s==="theme.sizing.scale"||s==="theme.scale")return!0}return!1}),u=(i)=>l(i,(t)=>{if(t.type==="MemberExpression"){const e=n(t);return e.startsWith("theme.spacing")||e.startsWith("theme.sizing")}if(t.type==="CallExpression"&&t.callee.type==="MemberExpression"){const e=n(t.callee);return e==="theme.spacing.scale"||e==="theme.sizing.scale"||e==="theme.scale"}return!1});export const noHardcodedSpacing=d("Require `theme.spacing` or `theme.sizing.scale` for spacing, radius and type values inside `StyleSheet.create`, instead of raw numbers.",(i,t)=>({Property(e){if(!t())return;const s=h(e);if(!f.has(s))return;if(u(e.value))return;const o=m(e.value,(a)=>a.type==="Literal"&&typeof a.value==="number"&&r.test(String(a.value))&&!p(a)||a.type==="UnaryExpression"&&a.operator==="-"&&a.argument.type==="Literal"&&typeof a.argument.value==="number"&&r.test(`-${a.argument.value}`)&&!p(a));if(o)i.report({node:o,message:c})}}));
|
|
@@ -1,19 +1 @@
|
|
|
1
|
-
import
|
|
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
|
-
}));
|
|
1
|
+
import{subtreeHas as n}from"../../../lib/ast.js";import{inCreate as r,propertyName as i}from"./shared.js";const s=/^margin(?:$|Top$|Bottom$|Left$|Right$|Start$|End$|Horizontal$|Vertical$)/,p=(e)=>e.type==="UnaryExpression"&&e.operator==="-"||e.type==="Literal"&&typeof e.value==="number"&&e.value<=0,l="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.";export const noMargin=r("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.",(e,o)=>({Property(t){if(!o())return;const a=i(t);if(a===""||!s.test(a))return;if(n(t.value,p))return;e.report({node:t,message:l})}}));
|
|
@@ -1,41 +1 @@
|
|
|
1
|
-
import
|
|
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
|
-
});
|
|
1
|
+
import{gate as o,problem as i}from"../../../lib/ast.js";import{isStyleSheetCreate as p}from"./shared.js";const l="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.",c=(r)=>{let e=r;while(e?.type==="MemberExpression")e=e.object;return e?.type==="Identifier"?e.name:""};export const noStyleSpread=i("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.",{createOnce(r){const e=new Set,a=[];return{before(){e.clear();a.length=0;return o(r,"...")},VariableDeclarator(t){const{id:s,init:n}=t;if(s.type==="Identifier"&&p(n))e.add(s.name)},SpreadElement(t){if(t.parent.type!=="ObjectExpression")return;const s=c(t.argument);if(s!=="")a.push({node:t,base:s})},"Program:exit"(){for(const t of a){if(!e.has(t.base)&&!/[Ss]tyles$/.test(t.base))continue;r.report({node:t.node,message:l})}}}}});
|
|
@@ -1,99 +1 @@
|
|
|
1
|
-
import
|
|
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
|
-
});
|
|
1
|
+
import{gate as d,problem as u}from"../../../lib/ast.js";import{CREATE_MARKER as y,isStyleSheetCreate as m,propertyName as h,stylesObjectOf as k}from"./shared.js";const p="*",E=(i)=>{const a=new Map;for(const n of i.properties){if(n.type!=="Property"||n.computed)return null;const o=h(n);if(o==="")return null;a.set(o,n.key)}return a};export const noUnusedStyles=u("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.",{createOnce(i){let a=new Map,n=new Set,o=!1;const c=(e,t)=>{n.add(`${e}.${t}`)},s=(e)=>{c(e,p)};return{before(){a=new Map;n=new Set;o=!1;return d(i,y)},CallExpression(e){if(!m(e))return;const r=(e.parent.type==="VariableDeclarator"?e.parent:null)?.id;if(r?.type!=="Identifier"){o=!0;return}const l=k(e.arguments[0]),f=l===null?null:E(l);if(f===null){o=!0;return}a.set(r.name,f)},MemberExpression(e){const{object:t,property:r}=e;if(t.type!=="Identifier")return;if(e.computed){s(t.name);return}if(r.type==="Identifier")c(t.name,r.name)},ExportNamedDeclaration(e){const{declaration:t}=e;if(t?.type==="VariableDeclaration"){for(const r of t.declarations)if(r.id.type==="Identifier")s(r.id.name)}for(const r of e.specifiers)if(r.local.type==="Identifier")s(r.local.name)},ExportDefaultDeclaration(e){const{declaration:t}=e;if(t.type==="Identifier")s(t.name)},"Program:exit":()=>{if(o)return;for(const[e,t]of a){if(n.has(`${e}.${p}`))continue;for(const[r,l]of t){if(n.has(`${e}.${r}`))continue;i.report({node:l,message:`Delete \`${e}.${r}\` — nothing reads it, so it is dead weight that keeps a token alive nothing renders.`})}}}}}});
|
|
@@ -1,42 +1 @@
|
|
|
1
|
-
import
|
|
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
|
-
});
|
|
1
|
+
import{closestAncestor as o,gate as c,problem as l}from"../../../lib/ast.js";import{CREATE_MARKER as f,isStyleSheetCreate as d,memberPath as m}from"./shared.js";const p="Read `rt.rtl` inside the dynamic style instead of passing `I18nManager.isRTL` in from JSX.";export const rtlStyleCall=l("Disallow passing `I18nManager.isRTL` from JSX into a dynamic style function. Unistyles tracks the dependency itself once the style reads `rt.rtl`.",{createOnce(i){const n=new Set,s=[];return{before(){n.clear();s.length=0;return c(i,f)},VariableDeclarator(e){const{id:t,init:r}=e;if(t.type==="Identifier"&&d(r))n.add(t.name)},MemberExpression(e){if(m(e)!=="I18nManager.isRTL")return;const t=o(e,new Set(["CallExpression"]));if(t?.type!=="CallExpression")return;const{callee:r}=t;if(r.type!=="MemberExpression")return;const a=r.object;if(a.type!=="Identifier")return;s.push({call:t,receiver:a.name})},"Program:exit"(){for(const e of s){if(!n.has(e.receiver))continue;i.report({node:e.call,message:p})}}}}});
|
|
@@ -1,70 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { CREATE_MARKER, isStyleSheetCreate } from "../../stylesheet.js";
|
|
3
|
-
export { CREATE_MARKER, isStyleSheetCreate, propertyName, stylesObjectOf, } from "../../stylesheet.js";
|
|
4
|
-
export const memberPath = (node) => {
|
|
5
|
-
const parts = [];
|
|
6
|
-
let current = node;
|
|
7
|
-
while (current?.type === "MemberExpression") {
|
|
8
|
-
const { property } = current;
|
|
9
|
-
if (property.type === "Identifier")
|
|
10
|
-
parts.unshift(property.name);
|
|
11
|
-
else if (property.type === "Literal")
|
|
12
|
-
parts.unshift(String(property.value));
|
|
13
|
-
else
|
|
14
|
-
parts.unshift("*");
|
|
15
|
-
current = current.object;
|
|
16
|
-
}
|
|
17
|
-
if (current?.type === "Identifier")
|
|
18
|
-
parts.unshift(current.name);
|
|
19
|
-
return parts.join(".");
|
|
20
|
-
};
|
|
21
|
-
export const declaresUseUnistylesTheme = (scope) => subtreeHas(scope, current => {
|
|
22
|
-
if (current.type !== "VariableDeclarator")
|
|
23
|
-
return false;
|
|
24
|
-
const { id, init } = current;
|
|
25
|
-
return (id.type === "ObjectPattern" &&
|
|
26
|
-
calleeName(init) === "useUnistyles" &&
|
|
27
|
-
id.properties.some(property => property.type === "Property" && property.key.type === "Identifier" && property.key.name === "theme"));
|
|
28
|
-
});
|
|
29
|
-
/**
|
|
30
|
-
* The enclosing function that destructures a `useUnistyles()` theme, innermost first. Walking past the
|
|
31
|
-
* innermost function matters: the read is often inside a callback the component passes down.
|
|
32
|
-
*/
|
|
33
|
-
export const themeConsumingComponent = (node) => {
|
|
34
|
-
for (const current of ancestors(node)) {
|
|
35
|
-
if (isFunction(current) && declaresUseUnistylesTheme(current))
|
|
36
|
-
return current;
|
|
37
|
-
}
|
|
38
|
-
return null;
|
|
39
|
-
};
|
|
40
|
-
export const readsTheme = (node) => subtreeHas(node, current => current.type === "MemberExpression" && memberPath(current).startsWith("theme."));
|
|
41
|
-
/**
|
|
42
|
-
* Builds a rule whose visitors only fire inside `StyleSheet.create(...)`, by tracking how deeply
|
|
43
|
-
* nested the traversal currently is in such a call. The depth resets per file in `before()`, which
|
|
44
|
-
* also runs the text gate.
|
|
45
|
-
*/
|
|
46
|
-
export const inCreate = (description, visit) => problem(description, {
|
|
47
|
-
createOnce(context) {
|
|
48
|
-
let createDepth = 0;
|
|
49
|
-
const inside = () => createDepth > 0;
|
|
50
|
-
const visitors = visit(context, inside);
|
|
51
|
-
const { CallExpression: enter, "CallExpression:exit": exit } = visitors;
|
|
52
|
-
return {
|
|
53
|
-
...visitors,
|
|
54
|
-
before() {
|
|
55
|
-
createDepth = 0;
|
|
56
|
-
return gate(context, CREATE_MARKER);
|
|
57
|
-
},
|
|
58
|
-
CallExpression(node) {
|
|
59
|
-
if (isStyleSheetCreate(node))
|
|
60
|
-
createDepth += 1;
|
|
61
|
-
enter?.(node);
|
|
62
|
-
},
|
|
63
|
-
"CallExpression:exit"(node) {
|
|
64
|
-
exit?.(node);
|
|
65
|
-
if (isStyleSheetCreate(node))
|
|
66
|
-
createDepth -= 1;
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
},
|
|
70
|
-
});
|
|
1
|
+
import{ancestors as c,calleeName as f,gate as u,isFunction as y,problem as h,subtreeHas as a}from"../../../lib/ast.js";import{CREATE_MARKER as x,isStyleSheetCreate as p}from"../../stylesheet.js";export{CREATE_MARKER,isStyleSheetCreate,propertyName,stylesObjectOf}from"../../stylesheet.js";export const memberPath=(s)=>{const e=[];let t=s;while(t?.type==="MemberExpression"){const{property:r}=t;if(r.type==="Identifier")e.unshift(r.name);else if(r.type==="Literal")e.unshift(String(r.value));else e.unshift("*");t=t.object}if(t?.type==="Identifier")e.unshift(t.name);return e.join(".")},declaresUseUnistylesTheme=(s)=>a(s,(e)=>{if(e.type!=="VariableDeclarator")return!1;const{id:t,init:r}=e;return t.type==="ObjectPattern"&&f(r)==="useUnistyles"&&t.properties.some((n)=>n.type==="Property"&&n.key.type==="Identifier"&&n.key.name==="theme")}),themeConsumingComponent=(s)=>{for(const e of c(s))if(y(e)&&declaresUseUnistylesTheme(e))return e;return null},readsTheme=(s)=>a(s,(e)=>e.type==="MemberExpression"&&memberPath(e).startsWith("theme.")),inCreate=(s,e)=>h(s,{createOnce(t){let r=0;const o=e(t,()=>r>0),{CallExpression:l,"CallExpression:exit":m}=o;return{...o,before(){r=0;return u(t,x)},CallExpression(i){if(p(i))r+=1;l?.(i)},"CallExpression:exit"(i){m?.(i);if(p(i))r-=1}}}});
|
|
@@ -1,22 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { isStyleSheetCreate, memberPath, themeConsumingComponent } from "./shared.js";
|
|
3
|
-
const THEME_SCREEN_PREFIX = "theme.screen.";
|
|
4
|
-
const MESSAGE = "Read current screen values from `useUnistyles().rt.screen` or `useWindowDimensions()` instead of the theme's module-initialization snapshot.";
|
|
5
|
-
export const themeScreenComponent = problem("`theme.screen.*` is a snapshot taken at module initialization, so a component that reads it never sees the current size. `useUnistyles().rt.screen` and `useWindowDimensions` do.", {
|
|
6
|
-
createOnce(context) {
|
|
7
|
-
return {
|
|
8
|
-
MemberExpression(node) {
|
|
9
|
-
if (!memberPath(node).startsWith(THEME_SCREEN_PREFIX))
|
|
10
|
-
return;
|
|
11
|
-
const { parent } = node;
|
|
12
|
-
if (parent.type === "MemberExpression" && parent.object === node)
|
|
13
|
-
return;
|
|
14
|
-
if (hasAncestor(node, isStyleSheetCreate))
|
|
15
|
-
return;
|
|
16
|
-
if (!themeConsumingComponent(node))
|
|
17
|
-
return;
|
|
18
|
-
context.report({ node, message: MESSAGE });
|
|
19
|
-
},
|
|
20
|
-
};
|
|
21
|
-
},
|
|
22
|
-
});
|
|
1
|
+
import{hasAncestor as r,problem as n}from"../../../lib/ast.js";import{isStyleSheetCreate as i,memberPath as o,themeConsumingComponent as m}from"./shared.js";const a="theme.screen.",c="Read current screen values from `useUnistyles().rt.screen` or `useWindowDimensions()` instead of the theme's module-initialization snapshot.";export const themeScreenComponent=n("`theme.screen.*` is a snapshot taken at module initialization, so a component that reads it never sees the current size. `useUnistyles().rt.screen` and `useWindowDimensions` do.",{createOnce(s){return{MemberExpression(e){if(!o(e).startsWith(a))return;const{parent:t}=e;if(t.type==="MemberExpression"&&t.object===e)return;if(r(e,i))return;if(!m(e))return;s.report({node:e,message:c})}}}});
|
|
@@ -1,18 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { readsTheme, themeConsumingComponent } from "./shared.js";
|
|
3
|
-
const MESSAGE = "Resolve theme-dependent style values inside `StyleSheet.create` instead of reading `theme` in a JSX `style` prop or passing it into a dynamic style function.";
|
|
4
|
-
export const themeStyleAttr = problem("Theme-dependent values belong in `StyleSheet.create`, not in a JSX `style` prop that reads a `useUnistyles()` theme.", {
|
|
5
|
-
createOnce(context) {
|
|
6
|
-
return {
|
|
7
|
-
JSXAttribute(node) {
|
|
8
|
-
if (node.name.type !== "JSXIdentifier" || node.name.name !== "style")
|
|
9
|
-
return;
|
|
10
|
-
if (!readsTheme(node))
|
|
11
|
-
return;
|
|
12
|
-
if (!themeConsumingComponent(node))
|
|
13
|
-
return;
|
|
14
|
-
context.report({ node, message: MESSAGE });
|
|
15
|
-
},
|
|
16
|
-
};
|
|
17
|
-
},
|
|
18
|
-
});
|
|
1
|
+
import{problem as n}from"../../../lib/ast.js";import{readsTheme as r,themeConsumingComponent as i}from"./shared.js";const s="Resolve theme-dependent style values inside `StyleSheet.create` instead of reading `theme` in a JSX `style` prop or passing it into a dynamic style function.";export const themeStyleAttr=n("Theme-dependent values belong in `StyleSheet.create`, not in a JSX `style` prop that reads a `useUnistyles()` theme.",{createOnce(t){return{JSXAttribute(e){if(e.name.type!=="JSXIdentifier"||e.name.name!=="style")return;if(!r(e))return;if(!i(e))return;t.report({node:e,message:s})}}}});
|
|
@@ -1,44 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
/** Source-text gate for the stylesheet rules. Just the object, so `StyleSheet\n .create(...)` still counts. */
|
|
3
|
-
export const CREATE_MARKER = "StyleSheet";
|
|
4
|
-
export const isStyleSheetCreate = (node) => {
|
|
5
|
-
if (node?.type !== "CallExpression")
|
|
6
|
-
return false;
|
|
7
|
-
const { callee } = node;
|
|
8
|
-
if (callee.type !== "MemberExpression")
|
|
9
|
-
return false;
|
|
10
|
-
const { object, property } = callee;
|
|
11
|
-
return (object.type === "Identifier" &&
|
|
12
|
-
object.name === "StyleSheet" &&
|
|
13
|
-
property.type === "Identifier" &&
|
|
14
|
-
property.name === "create");
|
|
15
|
-
};
|
|
16
|
-
export const propertyName = (node) => {
|
|
17
|
-
if (node.type !== "Property" || node.computed)
|
|
18
|
-
return "";
|
|
19
|
-
const { key } = node;
|
|
20
|
-
if (key.type === "Identifier")
|
|
21
|
-
return key.name;
|
|
22
|
-
if (key.type === "Literal")
|
|
23
|
-
return String(key.value);
|
|
24
|
-
return "";
|
|
25
|
-
};
|
|
26
|
-
const themeFunctionBody = (factory) => factory.type === "ArrowFunctionExpression" || factory.type === "FunctionExpression" ? factory.body : null;
|
|
27
|
-
const returnsAnObject = (node) => node.type === "ReturnStatement" && node.argument?.type === "ObjectExpression";
|
|
28
|
-
const objectReturnedFrom = (body) => {
|
|
29
|
-
if (body.type === "ObjectExpression")
|
|
30
|
-
return body;
|
|
31
|
-
const found = findInSubtree(body, returnsAnObject);
|
|
32
|
-
if (found?.type !== "ReturnStatement" || found.argument?.type !== "ObjectExpression")
|
|
33
|
-
return null;
|
|
34
|
-
return found.argument;
|
|
35
|
-
};
|
|
36
|
-
/** The styles object a `StyleSheet.create` argument holds: a literal, or the one its theme function returns. */
|
|
37
|
-
export const stylesObjectOf = (factory) => {
|
|
38
|
-
if (!factory)
|
|
39
|
-
return null;
|
|
40
|
-
if (factory.type === "ObjectExpression")
|
|
41
|
-
return factory;
|
|
42
|
-
const body = themeFunctionBody(factory);
|
|
43
|
-
return body === null ? null : objectReturnedFrom(body);
|
|
44
|
-
};
|
|
1
|
+
import{findInSubtree as p}from"../lib/ast.js";export const CREATE_MARKER="StyleSheet",isStyleSheetCreate=(e)=>{if(e?.type!=="CallExpression")return!1;const{callee:t}=e;if(t.type!=="MemberExpression")return!1;const{object:r,property:n}=t;return r.type==="Identifier"&&r.name==="StyleSheet"&&n.type==="Identifier"&&n.name==="create"},propertyName=(e)=>{if(e.type!=="Property"||e.computed)return"";const{key:t}=e;if(t.type==="Identifier")return t.name;if(t.type==="Literal")return String(t.value);return""};const s=(e)=>e.type==="ArrowFunctionExpression"||e.type==="FunctionExpression"?e.body:null,u=(e)=>e.type==="ReturnStatement"&&e.argument?.type==="ObjectExpression",i=(e)=>{if(e.type==="ObjectExpression")return e;const t=p(e,u);if(t?.type!=="ReturnStatement"||t.argument?.type!=="ObjectExpression")return null;return t.argument};export const stylesObjectOf=(e)=>{if(!e)return null;if(e.type==="ObjectExpression")return e;const t=s(e);return t===null?null:i(t)};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ashstack/lint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "ashStack shared oxlint configuration and custom rules. Entries: core, react, react-native, with per-library rule groups auto-detected from your dependencies. Consume from oxlint.config.mts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,8 +42,7 @@
|
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
"files": [
|
|
45
|
-
"dist"
|
|
46
|
-
"RULES.md"
|
|
45
|
+
"dist"
|
|
47
46
|
],
|
|
48
47
|
"publishConfig": {
|
|
49
48
|
"access": "public"
|
|
@@ -61,7 +60,7 @@
|
|
|
61
60
|
}
|
|
62
61
|
},
|
|
63
62
|
"scripts": {
|
|
64
|
-
"prepack": "rm -rf dist && tsc"
|
|
63
|
+
"prepack": "rm -rf dist && tsc && bun build $(find dist -name '*.js') --minify --target=node --root dist --outdir dist --no-bundle"
|
|
65
64
|
},
|
|
66
65
|
"devDependencies": {
|
|
67
66
|
"@types/node": "^22.10.0",
|