@tenphi/eslint-plugin-tasty 1.16.0 → 1.17.1
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 +2 -2
- package/dist/rules/no-runtime-styles-mutation.js +18 -0
- package/dist/rules/no-runtime-styles-mutation.js.map +1 -1
- package/dist/rules/no-style-prop.js +79 -12
- package/dist/rules/no-style-prop.js.map +1 -1
- package/dist/utils.js +10 -1
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -257,9 +257,9 @@ not matched.
|
|
|
257
257
|
| `tasty/prefer-shorthand-property` | warn | Use Tasty shorthand instead of native CSS properties (`backgroundColor` → `fill`, `paddingBlock` → `blockPadding`, etc.). Report-only warnings explain how to override one part through a token, with concrete min/max dimension examples. Extension warnings point at the base component and are skipped over bases you cannot edit (see `ownedSources`; [details](docs/rules/prefer-shorthand-property.md)) |
|
|
258
258
|
| `tasty/no-raw-color-values` | warn | Raw hex/rgb/`okhsl`/`okhst`/`oklch`/named colors instead of `#color` tokens |
|
|
259
259
|
| `tasty/no-raw-transition-duration` | warn | Hardcoded `transition` duration (`fill 0.2s`) in Tasty styles or a local `tasty()` component's `transition` prop. Suggests a configured duration token or the implicit per-name timing; zero and delays are left alone |
|
|
260
|
-
| `tasty/no-runtime-styles-mutation` | warn | Render-time JavaScript in Tasty style values (props, state, hook results, parameters, local bindings, calls, computed keys); use states and tokens instead.
|
|
260
|
+
| `tasty/no-runtime-styles-mutation` | warn | Render-time JavaScript in Tasty style values (props, state, hook results, parameters, local bindings, calls, computed keys); use states and tokens instead. A style object no function encloses is built once at load and not checked; inside a function, module-scope constants, imports, `undefined` and static `mergeStyles(...)` calls are fixed at load and allowed |
|
|
261
261
|
| `tasty/no-style-spread` | warn | Object or array spreads inside runtime Tasty styles; recommends `mergeStyles` for multiple root spreads; intentional spreads need a per-line suppression and reason |
|
|
262
|
-
| `tasty/no-style-prop` | warn | `style` on recognized Tasty components; use token references and the `tokens` prop. The `elements` option covers every component or element, so it can replace React's `style` restriction ([details](docs/rules/no-style-prop.md)) |
|
|
262
|
+
| `tasty/no-style-prop` | warn | `style` on recognized Tasty components; use token references and the `tokens` prop. The `elements` option covers every component or element, so it can replace React's `style` restriction; `values: 'static'` reports only static styles. Tasty syntax in `style` is always reported ([details](docs/rules/no-style-prop.md)) |
|
|
263
263
|
| `tasty/no-classname-prop` | warn | `className` on recognized Tasty components; keep styling in Tasty or use `data-element` for sub-elements ([details](docs/rules/no-classname-prop.md)) |
|
|
264
264
|
| `tasty/no-styles-prop` | warn | Instance `styles` on recognized Tasty components; use `tokens`, `mods`, exposed props/variants, or `tasty(Component, { styles })` ([details](docs/rules/no-styles-prop.md)) |
|
|
265
265
|
| `tasty/no-protected-restyle` | warn | Restyling a design system's `protectedComponents`: instance `styles` and `*Styles`, appearance style props, and `tasty()` wrappers that restyle them, followed through aliases, wrappers and relative re-exports. Inactive without the config ([details](docs/rules/no-protected-restyle.md)) |
|
|
@@ -85,10 +85,28 @@ var no_runtime_styles_mutation_default = createRule({
|
|
|
85
85
|
checkValue(prop.value, property, childIsStateMap);
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Whether an object is built once, while the module loads: no function
|
|
90
|
+
* encloses it, and neither does an instance field, which runs per
|
|
91
|
+
* instance. Every value in it is then fixed at load, calls included.
|
|
92
|
+
*/
|
|
93
|
+
function isBuiltAtLoad(node) {
|
|
94
|
+
for (let current = node.parent; current; current = current.parent) switch (current.type) {
|
|
95
|
+
case "FunctionDeclaration":
|
|
96
|
+
case "FunctionExpression":
|
|
97
|
+
case "ArrowFunctionExpression": return false;
|
|
98
|
+
case "PropertyDefinition":
|
|
99
|
+
case "AccessorProperty":
|
|
100
|
+
if (!current.static) return false;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
88
105
|
function handleStyleObject(node) {
|
|
89
106
|
const styleCtx = ctx.getStyleContext(node);
|
|
90
107
|
if (!styleCtx || styleCtx.isStaticCall) return;
|
|
91
108
|
if (ctx.getRootStyleObject(node) !== node) return;
|
|
109
|
+
if (isBuiltAtLoad(node)) return;
|
|
92
110
|
checkProperties(node);
|
|
93
111
|
}
|
|
94
112
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-runtime-styles-mutation.js","names":[],"sources":["../../src/rules/no-runtime-styles-mutation.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { SPECIAL_STYLE_KEYS } from '../constants.js';\nimport { getKeyName, isStaticValue, unwrapExpression } from '../utils.js';\n\ntype MessageIds = 'dynamicStyleValue' | 'dynamicStyleKey';\n\nexport default createRule<[], MessageIds>({\n name: 'no-runtime-styles-mutation',\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Warn when style objects contain JavaScript-computed values',\n },\n messages: {\n dynamicStyleValue:\n \"Prefer a static style value for '{{property}}'. Use states with mods, tokens, or styleProps for dynamic behavior. For necessary JavaScript computation, explicitly disable tasty/no-runtime-styles-mutation on this line with a reason.\",\n dynamicStyleKey:\n \"Prefer a static style key for '{{property}}'. Define states as literal keys instead of computing them in JavaScript. For necessary generated keys, explicitly disable tasty/no-runtime-styles-mutation on this line with a reason.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n /**\n * Whether a value is fixed once the module has loaded, so Tasty generates\n * its CSS once, exactly as for a literal: literals and `undefined`, module\n * `const` bindings and imports (and their members), and templates,\n * operators and `mergeStyles(...)` calls built only from those. Anything\n * that reaches render-time scope — props, state, hook results, parameters,\n * a local `let` — is not.\n */\n function isLoadTimeValue(node: TSESTree.Node): boolean {\n const value = unwrapExpression(node);\n if (isStaticValue(value)) return true;\n\n switch (value.type) {\n case 'Identifier':\n return isLoadTimeBinding(value);\n case 'ChainExpression':\n return isLoadTimeValue(value.expression);\n case 'MemberExpression':\n return (\n isLoadTimeValue(value.object) &&\n (!value.computed || isLoadTimeValue(value.property))\n );\n case 'TemplateLiteral':\n return value.expressions.every(isLoadTimeValue);\n case 'UnaryExpression':\n return value.operator !== 'delete' && isLoadTimeValue(value.argument);\n case 'BinaryExpression':\n case 'LogicalExpression':\n return (\n value.left.type !== 'PrivateIdentifier' &&\n isLoadTimeValue(value.left) &&\n isLoadTimeValue(value.right)\n );\n case 'ConditionalExpression':\n return (\n isLoadTimeValue(value.test) &&\n isLoadTimeValue(value.consequent) &&\n isLoadTimeValue(value.alternate)\n );\n case 'ArrayExpression':\n return value.elements.every(\n (element) =>\n element === null ||\n isLoadTimeValue(\n element.type === 'SpreadElement' ? element.argument : element,\n ),\n );\n case 'CallExpression':\n // Object-literal arguments are style objects of their own, and this\n // rule checks their values where they stand.\n return (\n ctx.isMergeStylesCall(value) &&\n value.arguments.every((arg) => {\n const argument = unwrapExpression(\n arg.type === 'SpreadElement' ? arg.argument : arg,\n );\n return (\n argument.type === 'ObjectExpression' ||\n isLoadTimeValue(argument)\n );\n })\n );\n default:\n return false;\n }\n }\n\n /**\n * A module-scope `const`, enum or import, or a render-scope `const` object;\n * `undefined` when not shadowed.\n */\n function isLoadTimeBinding(node: TSESTree.Identifier): boolean {\n let scope: ReturnType<typeof context.sourceCode.getScope> | null =\n context.sourceCode.getScope(node);\n while (scope && !scope.set.has(node.name)) scope = scope.upper;\n const variable = scope?.set.get(node.name);\n\n if (!scope || !variable || variable.defs.length === 0) {\n return node.name === 'undefined';\n }\n if (scope.type !== 'module' && scope.type !== 'global') {\n // A render-scope `const` object passed on as styles is a style object\n // of its own; its values are checked where it is written.\n return variable.defs.every(\n (def) =>\n def.type === 'Variable' &&\n def.parent.kind === 'const' &&\n def.node.init !== null &&\n unwrapExpression(def.node.init).type === 'ObjectExpression',\n );\n }\n\n return variable.defs.every(\n (def) =>\n def.type === 'ImportBinding' ||\n def.type === 'TSEnumName' ||\n (def.type === 'Variable' && def.parent.kind === 'const'),\n );\n }\n\n function checkValue(\n node: TSESTree.Node,\n property: string,\n isStateMap = false,\n ): void {\n const value = unwrapExpression(node);\n\n if (value.type === 'ObjectExpression') {\n checkProperties(value, property, isStateMap);\n } else if (value.type === 'ArrayExpression') {\n for (const element of value.elements) {\n // A separate rule reports spreads, including array spreads.\n if (element && element.type !== 'SpreadElement') {\n checkValue(element, property);\n }\n }\n } else if (!isLoadTimeValue(value)) {\n context.report({\n node: value,\n messageId: 'dynamicStyleValue',\n data: { property },\n });\n }\n }\n\n function checkProperties(\n node: TSESTree.ObjectExpression,\n parentProperty = '(style)',\n isStateMap = false,\n ): void {\n for (const prop of node.properties) {\n // Spreads have their own warning and explicit per-line suppression.\n if (prop.type === 'SpreadElement') continue;\n\n // At-rule definitions have their own shape and may legitimately be\n // assembled as JavaScript objects (for example, shared keyframes).\n const key = !prop.computed ? getKeyName(prop.key) : null;\n if (key && SPECIAL_STYLE_KEYS.has(key)) continue;\n\n const property =\n isStateMap || prop.computed\n ? parentProperty\n : (key ?? parentProperty);\n\n if (prop.computed && !isLoadTimeValue(prop.key)) {\n context.report({\n node: prop.key,\n messageId: 'dynamicStyleKey',\n data: { property },\n });\n }\n\n const value = unwrapExpression(prop.value);\n const childIsStateMap =\n value.type === 'ObjectExpression' && ctx.isStateMap(value, prop);\n checkValue(prop.value, property, childIsStateMap);\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression): void {\n const styleCtx = ctx.getStyleContext(node);\n // tastyStatic has a stricter build-time error for dynamic values.\n if (!styleCtx || styleCtx.isStaticCall) return;\n // Walk the full tree once; sub-element objects also match AST selectors.\n if (ctx.getRootStyleObject(node) !== node) return;\n\n checkProperties(node);\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"no-runtime-styles-mutation.js","names":[],"sources":["../../src/rules/no-runtime-styles-mutation.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { SPECIAL_STYLE_KEYS } from '../constants.js';\nimport { getKeyName, isStaticValue, unwrapExpression } from '../utils.js';\n\ntype MessageIds = 'dynamicStyleValue' | 'dynamicStyleKey';\n\nexport default createRule<[], MessageIds>({\n name: 'no-runtime-styles-mutation',\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Warn when style objects contain JavaScript-computed values',\n },\n messages: {\n dynamicStyleValue:\n \"Prefer a static style value for '{{property}}'. Use states with mods, tokens, or styleProps for dynamic behavior. For necessary JavaScript computation, explicitly disable tasty/no-runtime-styles-mutation on this line with a reason.\",\n dynamicStyleKey:\n \"Prefer a static style key for '{{property}}'. Define states as literal keys instead of computing them in JavaScript. For necessary generated keys, explicitly disable tasty/no-runtime-styles-mutation on this line with a reason.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n /**\n * Whether a value is fixed once the module has loaded, so Tasty generates\n * its CSS once, exactly as for a literal: literals and `undefined`, module\n * `const` bindings and imports (and their members), and templates,\n * operators and `mergeStyles(...)` calls built only from those. Anything\n * that reaches render-time scope — props, state, hook results, parameters,\n * a local `let` — is not.\n */\n function isLoadTimeValue(node: TSESTree.Node): boolean {\n const value = unwrapExpression(node);\n if (isStaticValue(value)) return true;\n\n switch (value.type) {\n case 'Identifier':\n return isLoadTimeBinding(value);\n case 'ChainExpression':\n return isLoadTimeValue(value.expression);\n case 'MemberExpression':\n return (\n isLoadTimeValue(value.object) &&\n (!value.computed || isLoadTimeValue(value.property))\n );\n case 'TemplateLiteral':\n return value.expressions.every(isLoadTimeValue);\n case 'UnaryExpression':\n return value.operator !== 'delete' && isLoadTimeValue(value.argument);\n case 'BinaryExpression':\n case 'LogicalExpression':\n return (\n value.left.type !== 'PrivateIdentifier' &&\n isLoadTimeValue(value.left) &&\n isLoadTimeValue(value.right)\n );\n case 'ConditionalExpression':\n return (\n isLoadTimeValue(value.test) &&\n isLoadTimeValue(value.consequent) &&\n isLoadTimeValue(value.alternate)\n );\n case 'ArrayExpression':\n return value.elements.every(\n (element) =>\n element === null ||\n isLoadTimeValue(\n element.type === 'SpreadElement' ? element.argument : element,\n ),\n );\n case 'CallExpression':\n // Object-literal arguments are style objects of their own, and this\n // rule checks their values where they stand.\n return (\n ctx.isMergeStylesCall(value) &&\n value.arguments.every((arg) => {\n const argument = unwrapExpression(\n arg.type === 'SpreadElement' ? arg.argument : arg,\n );\n return (\n argument.type === 'ObjectExpression' ||\n isLoadTimeValue(argument)\n );\n })\n );\n default:\n return false;\n }\n }\n\n /**\n * A module-scope `const`, enum or import, or a render-scope `const` object;\n * `undefined` when not shadowed.\n */\n function isLoadTimeBinding(node: TSESTree.Identifier): boolean {\n let scope: ReturnType<typeof context.sourceCode.getScope> | null =\n context.sourceCode.getScope(node);\n while (scope && !scope.set.has(node.name)) scope = scope.upper;\n const variable = scope?.set.get(node.name);\n\n if (!scope || !variable || variable.defs.length === 0) {\n return node.name === 'undefined';\n }\n if (scope.type !== 'module' && scope.type !== 'global') {\n // A render-scope `const` object passed on as styles is a style object\n // of its own; its values are checked where it is written.\n return variable.defs.every(\n (def) =>\n def.type === 'Variable' &&\n def.parent.kind === 'const' &&\n def.node.init !== null &&\n unwrapExpression(def.node.init).type === 'ObjectExpression',\n );\n }\n\n return variable.defs.every(\n (def) =>\n def.type === 'ImportBinding' ||\n def.type === 'TSEnumName' ||\n (def.type === 'Variable' && def.parent.kind === 'const'),\n );\n }\n\n function checkValue(\n node: TSESTree.Node,\n property: string,\n isStateMap = false,\n ): void {\n const value = unwrapExpression(node);\n\n if (value.type === 'ObjectExpression') {\n checkProperties(value, property, isStateMap);\n } else if (value.type === 'ArrayExpression') {\n for (const element of value.elements) {\n // A separate rule reports spreads, including array spreads.\n if (element && element.type !== 'SpreadElement') {\n checkValue(element, property);\n }\n }\n } else if (!isLoadTimeValue(value)) {\n context.report({\n node: value,\n messageId: 'dynamicStyleValue',\n data: { property },\n });\n }\n }\n\n function checkProperties(\n node: TSESTree.ObjectExpression,\n parentProperty = '(style)',\n isStateMap = false,\n ): void {\n for (const prop of node.properties) {\n // Spreads have their own warning and explicit per-line suppression.\n if (prop.type === 'SpreadElement') continue;\n\n // At-rule definitions have their own shape and may legitimately be\n // assembled as JavaScript objects (for example, shared keyframes).\n const key = !prop.computed ? getKeyName(prop.key) : null;\n if (key && SPECIAL_STYLE_KEYS.has(key)) continue;\n\n const property =\n isStateMap || prop.computed\n ? parentProperty\n : (key ?? parentProperty);\n\n if (prop.computed && !isLoadTimeValue(prop.key)) {\n context.report({\n node: prop.key,\n messageId: 'dynamicStyleKey',\n data: { property },\n });\n }\n\n const value = unwrapExpression(prop.value);\n const childIsStateMap =\n value.type === 'ObjectExpression' && ctx.isStateMap(value, prop);\n checkValue(prop.value, property, childIsStateMap);\n }\n }\n\n /**\n * Whether an object is built once, while the module loads: no function\n * encloses it, and neither does an instance field, which runs per\n * instance. Every value in it is then fixed at load, calls included.\n */\n function isBuiltAtLoad(node: TSESTree.Node): boolean {\n for (let current = node.parent; current; current = current.parent) {\n switch (current.type) {\n case 'FunctionDeclaration':\n case 'FunctionExpression':\n case 'ArrowFunctionExpression':\n return false;\n case 'PropertyDefinition':\n case 'AccessorProperty':\n if (!current.static) return false;\n break;\n }\n }\n return true;\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression): void {\n const styleCtx = ctx.getStyleContext(node);\n // tastyStatic has a stricter build-time error for dynamic values.\n if (!styleCtx || styleCtx.isStaticCall) return;\n // Walk the full tree once; sub-element objects also match AST selectors.\n if (ctx.getRootStyleObject(node) !== node) return;\n if (isBuiltAtLoad(node)) return;\n\n checkProperties(node);\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":"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"}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { createRule } from "../create-rule.js";
|
|
2
|
+
import { BUILT_IN_UNITS, CSS_UNITS } from "../constants.js";
|
|
3
|
+
import { extractCustomUnit, getStringValue, isRawHexColor, isStaticValue, unwrapExpression } from "../utils.js";
|
|
2
4
|
import { TastyContext } from "../context.js";
|
|
5
|
+
import { scanValueWords } from "../value-words.js";
|
|
3
6
|
import { jsxPropListeners } from "./tasty-component-props.js";
|
|
4
7
|
|
|
5
8
|
//#region src/rules/no-style-prop.ts
|
|
@@ -12,6 +15,18 @@ const KINDS = {
|
|
|
12
15
|
"element"
|
|
13
16
|
])
|
|
14
17
|
};
|
|
18
|
+
/** `lh` is a Tasty unit and a CSS one, so inline CSS reads it correctly. */
|
|
19
|
+
const INLINE_CSS_UNITS = new Set([...CSS_UNITS, "lh"]);
|
|
20
|
+
/** A word inline CSS cannot read: a Tasty unit, color token or `$` token. */
|
|
21
|
+
function tastySyntaxWord(value, config) {
|
|
22
|
+
for (const { value: word } of scanValueWords(value)) {
|
|
23
|
+
if (/^\$\$?[a-z]/i.test(word)) return word;
|
|
24
|
+
if (/^##?[a-z]/i.test(word) && !isRawHexColor(word)) return word;
|
|
25
|
+
const unit = extractCustomUnit(word);
|
|
26
|
+
if (unit !== null && !INLINE_CSS_UNITS.has(unit) && (BUILT_IN_UNITS.has(unit) || Array.isArray(config.units) && config.units.includes(unit))) return word;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
15
30
|
var no_style_prop_default = createRule({
|
|
16
31
|
name: "no-style-prop",
|
|
17
32
|
meta: {
|
|
@@ -19,25 +34,77 @@ var no_style_prop_default = createRule({
|
|
|
19
34
|
docs: { description: "Prefer tokens over React inline style on Tasty components" },
|
|
20
35
|
messages: {
|
|
21
36
|
noStyleProp: "Avoid the 'style' prop on Tasty components. Define styles with token references and pass dynamic values through 'tokens' (e.g. tokens={{ $size: value }}). If a third-party library requires inline styles, explicitly disable tasty/no-style-prop for this usage with an ESLint comment and a reason.",
|
|
22
|
-
noInlineStyle: "Avoid the 'style' prop. Move these styles into Tasty: render a tasty() element, or wrap the component with tasty(Component, { styles }), using token references and passing dynamic values through 'tokens'. If a third-party library requires inline styles, explicitly disable tasty/no-style-prop for this usage with an ESLint comment and a reason."
|
|
37
|
+
noInlineStyle: "Avoid the 'style' prop. Move these styles into Tasty: render a tasty() element, or wrap the component with tasty(Component, { styles }), using token references and passing dynamic values through 'tokens'. If a third-party library requires inline styles, explicitly disable tasty/no-style-prop for this usage with an ESLint comment and a reason.",
|
|
38
|
+
staticStyle: "This 'style' is static, so it belongs in Tasty styles: style props or the component's styles on a Tasty component, a tasty() element or tasty(Component, { styles }) elsewhere. If a third-party library requires inline styles, explicitly disable tasty/no-style-prop for this usage with an ESLint comment and a reason.",
|
|
39
|
+
tastySyntax: "'{{word}}' is Tasty syntax, but 'style' hands it to the browser unparsed, which drops the declaration. Move this style into Tasty styles."
|
|
23
40
|
},
|
|
24
41
|
schema: [{
|
|
25
42
|
type: "object",
|
|
26
|
-
properties: {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
properties: {
|
|
44
|
+
elements: {
|
|
45
|
+
type: "string",
|
|
46
|
+
enum: [
|
|
47
|
+
"tasty",
|
|
48
|
+
"components",
|
|
49
|
+
"all"
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
values: {
|
|
53
|
+
type: "string",
|
|
54
|
+
enum: ["all", "static"]
|
|
55
|
+
}
|
|
56
|
+
},
|
|
34
57
|
additionalProperties: false
|
|
35
58
|
}]
|
|
36
59
|
},
|
|
37
|
-
defaultOptions: [{
|
|
38
|
-
|
|
39
|
-
|
|
60
|
+
defaultOptions: [{
|
|
61
|
+
elements: "tasty",
|
|
62
|
+
values: "all"
|
|
63
|
+
}],
|
|
64
|
+
create(context, [{ elements = "tasty", values = "all" }]) {
|
|
65
|
+
const ctx = new TastyContext(context);
|
|
66
|
+
/** The style's own value: the attribute's expression or the spread key's. */
|
|
67
|
+
function styleValue(node) {
|
|
68
|
+
if (node.type === "Property") return unwrapExpression(node.value);
|
|
69
|
+
if (node.value?.type !== "JSXExpressionContainer") return null;
|
|
70
|
+
const expression = node.value.expression;
|
|
71
|
+
return expression.type === "JSXEmptyExpression" ? null : unwrapExpression(expression);
|
|
72
|
+
}
|
|
73
|
+
/** The object a style is written as, inline or through a local `const`. */
|
|
74
|
+
function styleObject(value) {
|
|
75
|
+
if (value?.type === "ObjectExpression") return value;
|
|
76
|
+
if (value?.type !== "Identifier") return null;
|
|
77
|
+
let scope = context.sourceCode.getScope(value);
|
|
78
|
+
while (scope && !scope.set.has(value.name)) scope = scope.upper;
|
|
79
|
+
const variable = scope?.set.get(value.name);
|
|
80
|
+
const def = variable?.defs.length === 1 ? variable.defs[0] : void 0;
|
|
81
|
+
if (def?.type !== "Variable" || def.parent.kind !== "const" || !def.node.init) return null;
|
|
82
|
+
const init = unwrapExpression(def.node.init);
|
|
83
|
+
return init.type === "ObjectExpression" ? init : null;
|
|
84
|
+
}
|
|
85
|
+
return jsxPropListeners(ctx, KINDS[elements], (name, node, kind) => {
|
|
40
86
|
if (name !== "style") return;
|
|
87
|
+
const object = styleObject(styleValue(node));
|
|
88
|
+
for (const prop of object?.properties ?? []) {
|
|
89
|
+
if (prop.type !== "Property") continue;
|
|
90
|
+
const text = getStringValue(unwrapExpression(prop.value));
|
|
91
|
+
const word = text === null ? null : tastySyntaxWord(text, ctx.config);
|
|
92
|
+
if (word !== null) {
|
|
93
|
+
context.report({
|
|
94
|
+
node,
|
|
95
|
+
messageId: "tastySyntax",
|
|
96
|
+
data: { word }
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (values === "static") {
|
|
102
|
+
if (object && isStaticValue(object)) context.report({
|
|
103
|
+
node,
|
|
104
|
+
messageId: "staticStyle"
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
41
108
|
context.report({
|
|
42
109
|
node,
|
|
43
110
|
messageId: kind === "tasty" ? "noStyleProp" : "noInlineStyle"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-style-prop.js","names":[],"sources":["../../src/rules/no-style-prop.ts"],"sourcesContent":["import { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport {\n type JSXElementKind,\n jsxPropListeners,\n} from './tasty-component-props.js';\n\ntype Elements = 'tasty' | 'components' | 'all';\n\nconst KINDS: Record<Elements, ReadonlySet<JSXElementKind>> = {\n tasty: new Set(['tasty']),\n components: new Set(['tasty', 'component']),\n all: new Set(['tasty', 'component', 'element']),\n};\n\nexport default createRule<\n [{ elements?: Elements }],\n 'noStyleProp' | 'noInlineStyle'\n>({\n name: 'no-style-prop',\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Prefer tokens over React inline style on Tasty components',\n },\n messages: {\n noStyleProp:\n \"Avoid the 'style' prop on Tasty components. Define styles with token references and pass dynamic values through 'tokens' (e.g. tokens={{ $size: value }}). If a third-party library requires inline styles, explicitly disable tasty/no-style-prop for this usage with an ESLint comment and a reason.\",\n noInlineStyle:\n \"Avoid the 'style' prop. Move these styles into Tasty: render a tasty() element, or wrap the component with tasty(Component, { styles }), using token references and passing dynamic values through 'tokens'. If a third-party library requires inline styles, explicitly disable tasty/no-style-prop for this usage with an ESLint comment and a reason.\",\n },\n schema: [\n {\n type: 'object',\n properties: {\n elements: { type: 'string', enum: ['tasty', 'components', 'all'] },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{ elements: 'tasty' }],\n create(context, [{ elements = 'tasty' }]) {\n const ctx = new TastyContext(context);\n return jsxPropListeners(ctx, KINDS[elements], (name, node, kind) => {\n if (name !== 'style') return;\n context.report({\n node,\n messageId: kind === 'tasty' ? 'noStyleProp' : 'noInlineStyle',\n });\n });\n },\n});\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"no-style-prop.js","names":[],"sources":["../../src/rules/no-style-prop.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { BUILT_IN_UNITS, CSS_UNITS } from '../constants.js';\nimport type { ResolvedConfig } from '../types.js';\nimport {\n extractCustomUnit,\n getStringValue,\n isRawHexColor,\n isStaticValue,\n unwrapExpression,\n} from '../utils.js';\nimport { scanValueWords } from '../value-words.js';\nimport {\n type JSXElementKind,\n type JSXPropNode,\n jsxPropListeners,\n} from './tasty-component-props.js';\n\ntype Elements = 'tasty' | 'components' | 'all';\ntype Values = 'all' | 'static';\n\nconst KINDS: Record<Elements, ReadonlySet<JSXElementKind>> = {\n tasty: new Set(['tasty']),\n components: new Set(['tasty', 'component']),\n all: new Set(['tasty', 'component', 'element']),\n};\n\n/** `lh` is a Tasty unit and a CSS one, so inline CSS reads it correctly. */\nconst INLINE_CSS_UNITS = new Set([...CSS_UNITS, 'lh']);\n\n/** A word inline CSS cannot read: a Tasty unit, color token or `$` token. */\nfunction tastySyntaxWord(value: string, config: ResolvedConfig): string | null {\n for (const { value: word } of scanValueWords(value)) {\n if (/^\\$\\$?[a-z]/i.test(word)) return word;\n if (/^##?[a-z]/i.test(word) && !isRawHexColor(word)) return word;\n const unit = extractCustomUnit(word);\n if (\n unit !== null &&\n !INLINE_CSS_UNITS.has(unit) &&\n (BUILT_IN_UNITS.has(unit) ||\n (Array.isArray(config.units) && config.units.includes(unit)))\n ) {\n return word;\n }\n }\n return null;\n}\n\nexport default createRule<\n [{ elements?: Elements; values?: Values }],\n 'noStyleProp' | 'noInlineStyle' | 'staticStyle' | 'tastySyntax'\n>({\n name: 'no-style-prop',\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Prefer tokens over React inline style on Tasty components',\n },\n messages: {\n noStyleProp:\n \"Avoid the 'style' prop on Tasty components. Define styles with token references and pass dynamic values through 'tokens' (e.g. tokens={{ $size: value }}). If a third-party library requires inline styles, explicitly disable tasty/no-style-prop for this usage with an ESLint comment and a reason.\",\n noInlineStyle:\n \"Avoid the 'style' prop. Move these styles into Tasty: render a tasty() element, or wrap the component with tasty(Component, { styles }), using token references and passing dynamic values through 'tokens'. If a third-party library requires inline styles, explicitly disable tasty/no-style-prop for this usage with an ESLint comment and a reason.\",\n staticStyle:\n \"This 'style' is static, so it belongs in Tasty styles: style props or the component's styles on a Tasty component, a tasty() element or tasty(Component, { styles }) elsewhere. If a third-party library requires inline styles, explicitly disable tasty/no-style-prop for this usage with an ESLint comment and a reason.\",\n tastySyntax:\n \"'{{word}}' is Tasty syntax, but 'style' hands it to the browser unparsed, which drops the declaration. Move this style into Tasty styles.\",\n },\n schema: [\n {\n type: 'object',\n properties: {\n elements: { type: 'string', enum: ['tasty', 'components', 'all'] },\n values: { type: 'string', enum: ['all', 'static'] },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{ elements: 'tasty', values: 'all' }],\n create(context, [{ elements = 'tasty', values = 'all' }]) {\n const ctx = new TastyContext(context);\n\n /** The style's own value: the attribute's expression or the spread key's. */\n function styleValue(node: JSXPropNode): TSESTree.Node | null {\n if (node.type === 'Property') return unwrapExpression(node.value);\n if (node.value?.type !== 'JSXExpressionContainer') return null;\n const expression = node.value.expression;\n return expression.type === 'JSXEmptyExpression'\n ? null\n : unwrapExpression(expression);\n }\n\n /** The object a style is written as, inline or through a local `const`. */\n function styleObject(\n value: TSESTree.Node | null,\n ): TSESTree.ObjectExpression | null {\n if (value?.type === 'ObjectExpression') return value;\n if (value?.type !== 'Identifier') return null;\n let scope: ReturnType<typeof context.sourceCode.getScope> | null =\n context.sourceCode.getScope(value);\n while (scope && !scope.set.has(value.name)) scope = scope.upper;\n const variable = scope?.set.get(value.name);\n const def = variable?.defs.length === 1 ? variable.defs[0] : undefined;\n if (\n def?.type !== 'Variable' ||\n def.parent.kind !== 'const' ||\n !def.node.init\n ) {\n return null;\n }\n const init = unwrapExpression(def.node.init);\n return init.type === 'ObjectExpression' ? init : null;\n }\n\n return jsxPropListeners(ctx, KINDS[elements], (name, node, kind) => {\n if (name !== 'style') return;\n const object = styleObject(styleValue(node));\n\n for (const prop of object?.properties ?? []) {\n if (prop.type !== 'Property') continue;\n const text = getStringValue(unwrapExpression(prop.value));\n const word = text === null ? null : tastySyntaxWord(text, ctx.config);\n if (word !== null) {\n context.report({ node, messageId: 'tastySyntax', data: { word } });\n return;\n }\n }\n\n if (values === 'static') {\n if (object && isStaticValue(object)) {\n context.report({ node, messageId: 'staticStyle' });\n }\n return;\n }\n\n context.report({\n node,\n messageId: kind === 'tasty' ? 'noStyleProp' : 'noInlineStyle',\n });\n });\n },\n});\n"],"mappings":"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"}
|
package/dist/utils.js
CHANGED
|
@@ -87,6 +87,15 @@ function isRawHexColor(value) {
|
|
|
87
87
|
return /^[0-9a-fA-F]+$/.test(hex);
|
|
88
88
|
}
|
|
89
89
|
/**
|
|
90
|
+
* Extracts custom unit from a value token like "2x", "1.5r", "3cols".
|
|
91
|
+
* Returns the unit name, or null if not a custom-unit value.
|
|
92
|
+
*/
|
|
93
|
+
function extractCustomUnit(token) {
|
|
94
|
+
const match = token.match(/^-?[\d.]+([a-zA-Z]+)$/);
|
|
95
|
+
if (!match) return null;
|
|
96
|
+
return match[1];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
90
99
|
* Checks if a unit is valid (built-in, CSS, or in config).
|
|
91
100
|
*/
|
|
92
101
|
function isValidUnit(unit, config) {
|
|
@@ -137,5 +146,5 @@ function collectLocalStateAliases(node) {
|
|
|
137
146
|
}
|
|
138
147
|
|
|
139
148
|
//#endregion
|
|
140
|
-
export { collectLocalStateAliases, getKeyName, getStringValue, isKnownStateAlias, isRawHexColor, isStaticValue, isValidSelector, isValidUnit, unwrapExpression, validateColorTokenSyntax, valueDefinition, wildcardPattern };
|
|
149
|
+
export { collectLocalStateAliases, extractCustomUnit, getKeyName, getStringValue, isKnownStateAlias, isRawHexColor, isStaticValue, isValidSelector, isValidUnit, unwrapExpression, validateColorTokenSyntax, valueDefinition, wildcardPattern };
|
|
141
150
|
//# sourceMappingURL=utils.js.map
|
package/dist/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","names":[],"sources":["../src/utils.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport type { Scope } from '@typescript-eslint/utils/ts-eslint';\nimport {\n BUILT_IN_UNITS,\n CSS_UNITS,\n BUILT_IN_STATE_PREFIXES,\n} from './constants.js';\nimport type { ResolvedConfig } from './types.js';\n\n/** Strip TypeScript wrappers that do not change an expression's runtime value. */\nexport function unwrapExpression(node: TSESTree.Node): TSESTree.Node {\n let current = node;\n\n while (\n current.type === 'TSAsExpression' ||\n current.type === 'TSSatisfiesExpression' ||\n current.type === 'TSNonNullExpression' ||\n current.type === 'TSTypeAssertion' ||\n current.type === 'TSInstantiationExpression'\n ) {\n current = current.expression;\n }\n\n return current;\n}\n\n/** A whole-string pattern in which `*` matches any run of characters. */\n/**\n * A variable's one value definition. TypeScript declares a same-named type or\n * interface (`const Box = tasty(…); type Box = …`) on the same variable, so\n * type definitions are set aside; null for a redeclared or type-only name.\n */\nexport function valueDefinition(\n variable: Scope.Variable,\n): Scope.Definition | null {\n const defs = variable.defs.filter((def) => def.type !== 'Type');\n return defs.length === 1 ? defs[0] : null;\n}\n\nexport function wildcardPattern(pattern: string): RegExp {\n const escaped = pattern.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n return new RegExp(`^${escaped.replace(/\\\\\\*/g, '.*')}$`);\n}\n\n/**\n * Gets the string value of a property key node.\n */\nexport function getKeyName(key: TSESTree.Node): string | null {\n if (key.type === 'Identifier') return key.name;\n if (key.type === 'Literal' && typeof key.value === 'string') return key.value;\n if (key.type === 'Literal' && typeof key.value === 'number')\n return String(key.value);\n return null;\n}\n\n/**\n * Gets the string value of a node if it is a string literal.\n */\nexport function getStringValue(node: TSESTree.Node): string | null {\n if (node.type === 'Literal' && typeof node.value === 'string') {\n return node.value;\n }\n if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {\n return node.quasis[0].value.cooked ?? null;\n }\n return null;\n}\n\n/**\n * Checks if a value node is a static literal.\n */\nexport function isStaticValue(node: TSESTree.Node): boolean {\n node = unwrapExpression(node);\n if (node.type === 'Literal') return true;\n if (\n node.type === 'UnaryExpression' &&\n node.operator === '-' &&\n node.argument.type === 'Literal'\n ) {\n return true;\n }\n if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {\n return true;\n }\n if (node.type === 'ArrayExpression') {\n return node.elements.every((el) => el !== null && isStaticValue(el));\n }\n if (node.type === 'ObjectExpression') {\n return node.properties.every(\n (prop) =>\n prop.type === 'Property' && !prop.computed && isStaticValue(prop.value),\n );\n }\n return false;\n}\n\n/**\n * Validates color token syntax.\n * Returns null if valid, or an error message if invalid.\n */\nexport function validateColorTokenSyntax(token: string): string | null {\n // Strip leading # or ##\n let name = token;\n if (name.startsWith('##')) {\n name = name.slice(2);\n } else if (name.startsWith('#')) {\n name = name.slice(1);\n } else {\n return 'Color token must start with #';\n }\n\n if (name.length === 0) return 'Empty color token name';\n\n // Check for opacity suffix\n const dotIndex = name.indexOf('.');\n if (dotIndex !== -1) {\n const tokenName = name.slice(0, dotIndex);\n const opacitySuffix = name.slice(dotIndex + 1);\n\n if (tokenName.length === 0) return 'Empty color token name before opacity';\n\n if (opacitySuffix.startsWith('$')) {\n // Dynamic opacity from CSS custom property — always valid\n return null;\n }\n\n if (opacitySuffix.length === 0) return 'Trailing dot with no opacity value';\n\n // Tasty reads the digits as a decimal fraction (`.5` and `.50` are 50%,\n // `.150` is 15%), so any digit string is in range.\n if (!/^\\d+$/.test(opacitySuffix)) {\n return /^-\\d+$/.test(opacitySuffix)\n ? 'Opacity cannot be negative'\n : `Invalid opacity value '${opacitySuffix}'`;\n }\n }\n\n return null;\n}\n\n/**\n * Checks if a string looks like a raw hex color (not a token).\n * Hex colors: #fff, #ffff, #ffffff, #ffffffff (3, 4, 6, or 8 hex chars).\n */\nexport function isRawHexColor(value: string): boolean {\n if (!value.startsWith('#')) return false;\n const hex = value.slice(1).split('.')[0];\n if (![3, 4, 6, 8].includes(hex.length)) return false;\n return /^[0-9a-fA-F]+$/.test(hex);\n}\n\n/**\n * Extracts custom unit from a value token like \"2x\", \"1.5r\", \"3cols\".\n * Returns the unit name, or null if not a custom-unit value.\n */\nexport function extractCustomUnit(token: string): string | null {\n const match = token.match(/^-?[\\d.]+([a-zA-Z]+)$/);\n if (!match) return null;\n return match[1];\n}\n\n/**\n * Checks if a unit is valid (built-in, CSS, or in config).\n */\nexport function isValidUnit(unit: string, config: ResolvedConfig): boolean {\n if (config.units === false) return true;\n if (BUILT_IN_UNITS.has(unit)) return true;\n if (CSS_UNITS.has(unit)) return true;\n if (Array.isArray(config.units) && config.units.includes(unit)) return true;\n return false;\n}\n\n/**\n * Checks if a state alias key (starting with @) is known.\n */\nexport function isKnownStateAlias(\n key: string,\n config: ResolvedConfig,\n): boolean {\n // Built-in prefixes\n for (const prefix of BUILT_IN_STATE_PREFIXES) {\n if (key === prefix || key.startsWith(prefix + '(')) return true;\n }\n // Container query shorthand\n if (key.startsWith('@(')) return true;\n // Config aliases\n return config.states.includes(key);\n}\n\n/**\n * Checks if a CSS selector string is basically valid.\n */\nexport function isValidSelector(selector: string): string | null {\n if (selector.length === 0) return 'Selector cannot be empty';\n\n // Check balanced brackets\n let depth = 0;\n for (const char of selector) {\n if (char === '(' || char === '[') depth++;\n if (char === ')' || char === ']') depth--;\n if (depth < 0) return 'Unbalanced brackets in selector';\n }\n if (depth !== 0) return 'Unbalanced brackets in selector';\n\n return null;\n}\n\n/**\n * Finds a property by key name in an object expression.\n */\nexport function findProperty(\n obj: TSESTree.ObjectExpression,\n name: string,\n): TSESTree.Property | undefined {\n for (const prop of obj.properties) {\n if (prop.type === 'Property' && !prop.computed) {\n const keyName = getKeyName(prop.key);\n if (keyName === name) return prop;\n }\n }\n return undefined;\n}\n\n/**\n * Collects local predefined state alias names from a styles ObjectExpression.\n * Local states are top-level `@name` keys with string literal values,\n * mirroring the runtime's `extractLocalPredefinedStates()`.\n */\nexport function collectLocalStateAliases(\n node: TSESTree.ObjectExpression,\n): string[] {\n const aliases: string[] = [];\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n if (!/^@[A-Za-z][A-Za-z0-9-]*$/.test(key)) continue;\n\n // Skip built-in prefixes — they are not local state definitions\n if (BUILT_IN_STATE_PREFIXES.has(key)) continue;\n\n // Must have a string value to be a state definition\n if (getStringValue(prop.value) !== null) {\n aliases.push(key);\n }\n }\n\n return aliases;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"utils.js","names":[],"sources":["../src/utils.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport type { Scope } from '@typescript-eslint/utils/ts-eslint';\nimport {\n BUILT_IN_UNITS,\n CSS_UNITS,\n BUILT_IN_STATE_PREFIXES,\n} from './constants.js';\nimport type { ResolvedConfig } from './types.js';\n\n/** Strip TypeScript wrappers that do not change an expression's runtime value. */\nexport function unwrapExpression(node: TSESTree.Node): TSESTree.Node {\n let current = node;\n\n while (\n current.type === 'TSAsExpression' ||\n current.type === 'TSSatisfiesExpression' ||\n current.type === 'TSNonNullExpression' ||\n current.type === 'TSTypeAssertion' ||\n current.type === 'TSInstantiationExpression'\n ) {\n current = current.expression;\n }\n\n return current;\n}\n\n/** A whole-string pattern in which `*` matches any run of characters. */\n/**\n * A variable's one value definition. TypeScript declares a same-named type or\n * interface (`const Box = tasty(…); type Box = …`) on the same variable, so\n * type definitions are set aside; null for a redeclared or type-only name.\n */\nexport function valueDefinition(\n variable: Scope.Variable,\n): Scope.Definition | null {\n const defs = variable.defs.filter((def) => def.type !== 'Type');\n return defs.length === 1 ? defs[0] : null;\n}\n\nexport function wildcardPattern(pattern: string): RegExp {\n const escaped = pattern.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n return new RegExp(`^${escaped.replace(/\\\\\\*/g, '.*')}$`);\n}\n\n/**\n * Gets the string value of a property key node.\n */\nexport function getKeyName(key: TSESTree.Node): string | null {\n if (key.type === 'Identifier') return key.name;\n if (key.type === 'Literal' && typeof key.value === 'string') return key.value;\n if (key.type === 'Literal' && typeof key.value === 'number')\n return String(key.value);\n return null;\n}\n\n/**\n * Gets the string value of a node if it is a string literal.\n */\nexport function getStringValue(node: TSESTree.Node): string | null {\n if (node.type === 'Literal' && typeof node.value === 'string') {\n return node.value;\n }\n if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {\n return node.quasis[0].value.cooked ?? null;\n }\n return null;\n}\n\n/**\n * Checks if a value node is a static literal.\n */\nexport function isStaticValue(node: TSESTree.Node): boolean {\n node = unwrapExpression(node);\n if (node.type === 'Literal') return true;\n if (\n node.type === 'UnaryExpression' &&\n node.operator === '-' &&\n node.argument.type === 'Literal'\n ) {\n return true;\n }\n if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {\n return true;\n }\n if (node.type === 'ArrayExpression') {\n return node.elements.every((el) => el !== null && isStaticValue(el));\n }\n if (node.type === 'ObjectExpression') {\n return node.properties.every(\n (prop) =>\n prop.type === 'Property' && !prop.computed && isStaticValue(prop.value),\n );\n }\n return false;\n}\n\n/**\n * Validates color token syntax.\n * Returns null if valid, or an error message if invalid.\n */\nexport function validateColorTokenSyntax(token: string): string | null {\n // Strip leading # or ##\n let name = token;\n if (name.startsWith('##')) {\n name = name.slice(2);\n } else if (name.startsWith('#')) {\n name = name.slice(1);\n } else {\n return 'Color token must start with #';\n }\n\n if (name.length === 0) return 'Empty color token name';\n\n // Check for opacity suffix\n const dotIndex = name.indexOf('.');\n if (dotIndex !== -1) {\n const tokenName = name.slice(0, dotIndex);\n const opacitySuffix = name.slice(dotIndex + 1);\n\n if (tokenName.length === 0) return 'Empty color token name before opacity';\n\n if (opacitySuffix.startsWith('$')) {\n // Dynamic opacity from CSS custom property — always valid\n return null;\n }\n\n if (opacitySuffix.length === 0) return 'Trailing dot with no opacity value';\n\n // Tasty reads the digits as a decimal fraction (`.5` and `.50` are 50%,\n // `.150` is 15%), so any digit string is in range.\n if (!/^\\d+$/.test(opacitySuffix)) {\n return /^-\\d+$/.test(opacitySuffix)\n ? 'Opacity cannot be negative'\n : `Invalid opacity value '${opacitySuffix}'`;\n }\n }\n\n return null;\n}\n\n/**\n * Checks if a string looks like a raw hex color (not a token).\n * Hex colors: #fff, #ffff, #ffffff, #ffffffff (3, 4, 6, or 8 hex chars).\n */\nexport function isRawHexColor(value: string): boolean {\n if (!value.startsWith('#')) return false;\n const hex = value.slice(1).split('.')[0];\n if (![3, 4, 6, 8].includes(hex.length)) return false;\n return /^[0-9a-fA-F]+$/.test(hex);\n}\n\n/**\n * Extracts custom unit from a value token like \"2x\", \"1.5r\", \"3cols\".\n * Returns the unit name, or null if not a custom-unit value.\n */\nexport function extractCustomUnit(token: string): string | null {\n const match = token.match(/^-?[\\d.]+([a-zA-Z]+)$/);\n if (!match) return null;\n return match[1];\n}\n\n/**\n * Checks if a unit is valid (built-in, CSS, or in config).\n */\nexport function isValidUnit(unit: string, config: ResolvedConfig): boolean {\n if (config.units === false) return true;\n if (BUILT_IN_UNITS.has(unit)) return true;\n if (CSS_UNITS.has(unit)) return true;\n if (Array.isArray(config.units) && config.units.includes(unit)) return true;\n return false;\n}\n\n/**\n * Checks if a state alias key (starting with @) is known.\n */\nexport function isKnownStateAlias(\n key: string,\n config: ResolvedConfig,\n): boolean {\n // Built-in prefixes\n for (const prefix of BUILT_IN_STATE_PREFIXES) {\n if (key === prefix || key.startsWith(prefix + '(')) return true;\n }\n // Container query shorthand\n if (key.startsWith('@(')) return true;\n // Config aliases\n return config.states.includes(key);\n}\n\n/**\n * Checks if a CSS selector string is basically valid.\n */\nexport function isValidSelector(selector: string): string | null {\n if (selector.length === 0) return 'Selector cannot be empty';\n\n // Check balanced brackets\n let depth = 0;\n for (const char of selector) {\n if (char === '(' || char === '[') depth++;\n if (char === ')' || char === ']') depth--;\n if (depth < 0) return 'Unbalanced brackets in selector';\n }\n if (depth !== 0) return 'Unbalanced brackets in selector';\n\n return null;\n}\n\n/**\n * Finds a property by key name in an object expression.\n */\nexport function findProperty(\n obj: TSESTree.ObjectExpression,\n name: string,\n): TSESTree.Property | undefined {\n for (const prop of obj.properties) {\n if (prop.type === 'Property' && !prop.computed) {\n const keyName = getKeyName(prop.key);\n if (keyName === name) return prop;\n }\n }\n return undefined;\n}\n\n/**\n * Collects local predefined state alias names from a styles ObjectExpression.\n * Local states are top-level `@name` keys with string literal values,\n * mirroring the runtime's `extractLocalPredefinedStates()`.\n */\nexport function collectLocalStateAliases(\n node: TSESTree.ObjectExpression,\n): string[] {\n const aliases: string[] = [];\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n if (!/^@[A-Za-z][A-Za-z0-9-]*$/.test(key)) continue;\n\n // Skip built-in prefixes — they are not local state definitions\n if (BUILT_IN_STATE_PREFIXES.has(key)) continue;\n\n // Must have a string value to be a state definition\n if (getStringValue(prop.value) !== null) {\n aliases.push(key);\n }\n }\n\n return aliases;\n}\n"],"mappings":"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"}
|