@tenphi/eslint-plugin-tasty 1.5.1 → 1.6.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 +90 -4
- package/dist/configs.js +3 -1
- package/dist/configs.js.map +1 -1
- package/dist/constants.js +1 -1
- package/dist/context.js +44 -28
- package/dist/context.js.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/rules/consistent-token-usage.js +88 -87
- package/dist/rules/consistent-token-usage.js.map +1 -1
- package/dist/rules/no-classname-prop.js +27 -0
- package/dist/rules/no-classname-prop.js.map +1 -0
- package/dist/rules/no-important.js +23 -28
- package/dist/rules/no-important.js.map +1 -1
- package/dist/rules/no-raw-color-values.js +40 -59
- package/dist/rules/no-raw-color-values.js.map +1 -1
- package/dist/rules/no-raw-motion-duration.js +6 -24
- package/dist/rules/no-raw-motion-duration.js.map +1 -1
- package/dist/rules/no-raw-transition-duration.js +7 -49
- package/dist/rules/no-raw-transition-duration.js.map +1 -1
- package/dist/rules/no-runtime-styles-mutation.js +2 -2
- package/dist/rules/no-runtime-styles-mutation.js.map +1 -1
- package/dist/rules/no-state-in-selector.js +1 -1
- package/dist/rules/no-state-in-selector.js.map +1 -1
- package/dist/rules/no-style-prop.js +27 -0
- package/dist/rules/no-style-prop.js.map +1 -0
- package/dist/rules/no-style-spread.js +2 -2
- package/dist/rules/no-style-spread.js.map +1 -1
- package/dist/rules/no-styles-prop.js +7 -5
- package/dist/rules/no-styles-prop.js.map +1 -1
- package/dist/rules/prefer-auto-calc.js +5 -27
- package/dist/rules/prefer-auto-calc.js.map +1 -1
- package/dist/rules/prefer-custom-property-syntax.js +12 -31
- package/dist/rules/prefer-custom-property-syntax.js.map +1 -1
- package/dist/rules/prefer-shorthand-property.js +32 -10
- package/dist/rules/prefer-shorthand-property.js.map +1 -1
- package/dist/rules/tasty-component-props.js +33 -0
- package/dist/rules/tasty-component-props.js.map +1 -0
- package/dist/rules/valid-boolean-property.js +6 -26
- package/dist/rules/valid-boolean-property.js.map +1 -1
- package/dist/rules/valid-color-token.js +11 -26
- package/dist/rules/valid-color-token.js.map +1 -1
- package/dist/rules/valid-custom-property.js +11 -25
- package/dist/rules/valid-custom-property.js.map +1 -1
- package/dist/rules/valid-custom-unit.js +10 -27
- package/dist/rules/valid-custom-unit.js.map +1 -1
- package/dist/rules/valid-directional-modifier.js +5 -27
- package/dist/rules/valid-directional-modifier.js.map +1 -1
- package/dist/rules/valid-preset.js +5 -29
- package/dist/rules/valid-preset.js.map +1 -1
- package/dist/rules/valid-radius-shape.js +5 -25
- package/dist/rules/valid-radius-shape.js.map +1 -1
- package/dist/rules/valid-recipe.js +5 -20
- package/dist/rules/valid-recipe.js.map +1 -1
- package/dist/rules/valid-transition.js +5 -25
- package/dist/rules/valid-transition.js.map +1 -1
- package/dist/rules/valid-value.js +5 -31
- package/dist/rules/valid-value.js.map +1 -1
- package/dist/style-values.js +71 -0
- package/dist/style-values.js.map +1 -0
- package/dist/utils.js +1 -10
- package/dist/utils.js.map +1 -1
- package/dist/value-words.js +76 -0
- package/dist/value-words.js.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-raw-motion-duration.js","names":[],"sources":["../../src/rules/no-raw-motion-duration.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext
|
|
1
|
+
{"version":3,"file":"no-raw-motion-duration.js","names":[],"sources":["../../src/rules/no-raw-motion-duration.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { stringStyleValueListeners } from '../style-values.js';\nimport { replaceInStringValue } from '../fix-utils.js';\nimport { scanTokens, type ScannedToken } from '../parsers/utils.js';\nimport {\n getDurationTokens,\n hasRawTimeInExpression,\n isNonzeroTime,\n isTimeExpression,\n isTimeLiteral,\n isZeroTime,\n} from './motion-duration-utils.js';\n\ntype MessageIds = 'rawMotionDuration' | 'useDurationToken';\n\nconst PROPERTIES = new Set([\n 'animation',\n 'animationDuration',\n 'transitionDuration',\n]);\n\nexport default createRule<[], MessageIds>({\n name: 'no-raw-motion-duration',\n meta: {\n type: 'suggestion',\n hasSuggestions: true,\n docs: {\n description:\n 'Suggest motion tokens for animation and explicit CSS duration properties',\n },\n messages: {\n rawMotionDuration:\n \"{{property}} duration '{{duration}}' is hardcoded. Use a duration token. For intentional custom timing, explicitly disable tasty/no-raw-motion-duration on this line with a reason.\",\n useDurationToken: \"Replace '{{duration}}' with '{{token}}'\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n const durationTokens = getDurationTokens(ctx.config.tokens);\n\n function checkDuration(\n token: ScannedToken,\n property: string,\n node: TSESTree.Node,\n ): void {\n const value = token.value;\n if (!isNonzeroTime(value) && !hasRawTimeInExpression(value)) return;\n\n // Animation has no implicit timing token. Only names that read as an\n // animation or generic duration are useful suggestions for that property.\n const candidates =\n property === 'transitionDuration'\n ? durationTokens\n : durationTokens.filter((name) => /animation|duration/i.test(name));\n const isLiteral = isTimeLiteral(value);\n context.report({\n node,\n messageId: 'rawMotionDuration',\n data: { property, duration: value },\n suggest: isLiteral\n ? candidates.map((candidate) => ({\n messageId: 'useDurationToken' as const,\n data: { duration: value, token: candidate },\n fix: (fixer: Parameters<typeof replaceInStringValue>[0]) =>\n replaceInStringValue(\n fixer,\n node,\n [\n {\n start: token.offset,\n end: token.offset + value.length,\n replacement: candidate,\n },\n ],\n context.sourceCode,\n ),\n }))\n : [],\n });\n }\n\n function checkGroup(\n group: ScannedToken[],\n property: string,\n node: TSESTree.Node,\n ): void {\n for (const token of group) {\n // The first time in an animation shorthand is its duration; a second\n // time is a delay. A variable can also be a duration, so stop there\n // rather than misreporting a later literal delay.\n if (\n property === 'animation' &&\n (/^\\$[\\w-]+$/.test(token.value) ||\n /^var\\(/i.test(token.value) ||\n isZeroTime(token.value) ||\n (isTimeExpression(token.value) &&\n !hasRawTimeInExpression(token.value)))\n ) {\n return;\n }\n if (isTimeLiteral(token.value)) {\n checkDuration(token, property, node);\n if (property === 'animation') return;\n } else if (hasRawTimeInExpression(token.value)) {\n checkDuration(token, property, node);\n if (property === 'animation') return;\n }\n }\n }\n\n function checkValue(\n value: string,\n property: string,\n node: TSESTree.Node,\n ): void {\n let group: ScannedToken[] = [];\n for (const token of scanTokens(value)) {\n if (token.value) group.push(token);\n if (token.isComma) {\n checkGroup(group, property, node);\n group = [];\n }\n }\n checkGroup(group, property, node);\n }\n\n return stringStyleValueListeners(ctx, (value, node, property) => {\n if (PROPERTIES.has(property)) checkValue(value, property, node);\n });\n },\n});\n"],"mappings":";;;;;;;;AAiBA,MAAM,aAAa,IAAI,IAAI;CACzB;CACA;CACA;CACD,CAAC;AAEF,qCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,gBAAgB;EAChB,MAAM,EACJ,aACE,4EACH;EACD,UAAU;GACR,mBACE;GACF,kBAAkB;GACnB;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EACrC,MAAM,iBAAiB,kBAAkB,IAAI,OAAO,OAAO;EAE3D,SAAS,cACP,OACA,UACA,MACM;GACN,MAAM,QAAQ,MAAM;AACpB,OAAI,CAAC,cAAc,MAAM,IAAI,CAAC,uBAAuB,MAAM,CAAE;GAI7D,MAAM,aACJ,aAAa,uBACT,iBACA,eAAe,QAAQ,SAAS,sBAAsB,KAAK,KAAK,CAAC;GACvE,MAAM,YAAY,cAAc,MAAM;AACtC,WAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM;KAAE;KAAU,UAAU;KAAO;IACnC,SAAS,YACL,WAAW,KAAK,eAAe;KAC7B,WAAW;KACX,MAAM;MAAE,UAAU;MAAO,OAAO;MAAW;KAC3C,MAAM,UACJ,qBACE,OACA,MACA,CACE;MACE,OAAO,MAAM;MACb,KAAK,MAAM,SAAS,MAAM;MAC1B,aAAa;MACd,CACF,EACD,QAAQ,WACT;KACJ,EAAE,GACH,EAAE;IACP,CAAC;;EAGJ,SAAS,WACP,OACA,UACA,MACM;AACN,QAAK,MAAM,SAAS,OAAO;AAIzB,QACE,aAAa,gBACZ,aAAa,KAAK,MAAM,MAAM,IAC7B,UAAU,KAAK,MAAM,MAAM,IAC3B,WAAW,MAAM,MAAM,IACtB,iBAAiB,MAAM,MAAM,IAC5B,CAAC,uBAAuB,MAAM,MAAM,EAExC;AAEF,QAAI,cAAc,MAAM,MAAM,EAAE;AAC9B,mBAAc,OAAO,UAAU,KAAK;AACpC,SAAI,aAAa,YAAa;eACrB,uBAAuB,MAAM,MAAM,EAAE;AAC9C,mBAAc,OAAO,UAAU,KAAK;AACpC,SAAI,aAAa,YAAa;;;;EAKpC,SAAS,WACP,OACA,UACA,MACM;GACN,IAAI,QAAwB,EAAE;AAC9B,QAAK,MAAM,SAAS,WAAW,MAAM,EAAE;AACrC,QAAI,MAAM,MAAO,OAAM,KAAK,MAAM;AAClC,QAAI,MAAM,SAAS;AACjB,gBAAW,OAAO,UAAU,KAAK;AACjC,aAAQ,EAAE;;;AAGd,cAAW,OAAO,UAAU,KAAK;;AAGnC,SAAO,0BAA0B,MAAM,OAAO,MAAM,aAAa;AAC/D,OAAI,WAAW,IAAI,SAAS,CAAE,YAAW,OAAO,UAAU,KAAK;IAC/D;;CAEL,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRule } from "../create-rule.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { TastyContext } from "../context.js";
|
|
3
|
+
import { stringStyleValueListeners } from "../style-values.js";
|
|
4
4
|
import { scanTokens } from "../parsers/utils.js";
|
|
5
5
|
import { replaceInStringValue } from "../fix-utils.js";
|
|
6
6
|
import { getDurationTokens, hasRawTimeInExpression, isNonzeroTime, isTimeLiteral } from "./motion-duration-utils.js";
|
|
@@ -32,8 +32,8 @@ var no_raw_transition_duration_default = createRule({
|
|
|
32
32
|
hasSuggestions: true,
|
|
33
33
|
docs: { description: "Suggest a duration token, or tasty's default timing, instead of a hardcoded transition duration" },
|
|
34
34
|
messages: {
|
|
35
|
-
rawTransitionDuration: "Transition duration '{{duration}}' is hardcoded. Use a duration token{{available}}, or omit it to inherit {{fallback}}.",
|
|
36
|
-
rawTransitionDurationExpression: "Transition duration '{{duration}}' contains a hardcoded time. Derive it from a duration token instead.",
|
|
35
|
+
rawTransitionDuration: "Transition duration '{{duration}}' is hardcoded. Use a duration token{{available}}, or omit it to inherit {{fallback}}. For intentional custom timing, explicitly disable tasty/no-raw-transition-duration on this line with a reason.",
|
|
36
|
+
rawTransitionDurationExpression: "Transition duration '{{duration}}' contains a hardcoded time. Derive it from a duration token instead. For intentional custom timing, explicitly disable tasty/no-raw-transition-duration on this line with a reason.",
|
|
37
37
|
useDurationToken: "Replace '{{duration}}' with '{{token}}'",
|
|
38
38
|
useDefaultDuration: "Remove '{{duration}}' to inherit the default transition timing"
|
|
39
39
|
},
|
|
@@ -103,51 +103,9 @@ var no_raw_transition_duration_default = createRule({
|
|
|
103
103
|
}
|
|
104
104
|
checkGroup(group, node);
|
|
105
105
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const variable = scope.set.get(name);
|
|
110
|
-
if (variable) return variable.defs.some((definition) => {
|
|
111
|
-
if (definition.type !== "Variable") return false;
|
|
112
|
-
const declaration = definition.node;
|
|
113
|
-
return declaration.id.type === "Identifier" && declaration.parent.type === "VariableDeclaration" && declaration.parent.kind === "const" && declaration.init?.type === "CallExpression" && ctx.isTastyCall(declaration.init)?.importedName === "tasty";
|
|
114
|
-
});
|
|
115
|
-
scope = scope.upper;
|
|
116
|
-
}
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
119
|
-
function handleStyleObject(node) {
|
|
120
|
-
if (!ctx.isStyleObject(node)) return;
|
|
121
|
-
for (const prop of node.properties) {
|
|
122
|
-
if (prop.type !== "Property" || prop.computed) continue;
|
|
123
|
-
if (getKeyName(prop.key) !== "transition") continue;
|
|
124
|
-
const str = getStringValue(prop.value);
|
|
125
|
-
if (str) {
|
|
126
|
-
checkTransitionValue(str, prop.value);
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
if (prop.value.type === "ObjectExpression" && ctx.isStateMap(prop.value, prop)) for (const stateProp of prop.value.properties) {
|
|
130
|
-
if (stateProp.type !== "Property") continue;
|
|
131
|
-
const stateStr = getStringValue(stateProp.value);
|
|
132
|
-
if (stateStr) checkTransitionValue(stateStr, stateProp.value);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return {
|
|
137
|
-
ImportDeclaration(node) {
|
|
138
|
-
ctx.trackImport(node);
|
|
139
|
-
},
|
|
140
|
-
JSXAttribute(node) {
|
|
141
|
-
if (node.name.type !== "JSXIdentifier" || node.name.name !== "transition") return;
|
|
142
|
-
const opening = node.parent;
|
|
143
|
-
if (opening?.type !== "JSXOpeningElement" || opening.name.type !== "JSXIdentifier" || !isLocalTastyComponent(node, opening.name.name)) return;
|
|
144
|
-
const valueNode = node.value?.type === "JSXExpressionContainer" ? node.value.expression : node.value;
|
|
145
|
-
if (!valueNode) return;
|
|
146
|
-
const value = getStringValue(valueNode);
|
|
147
|
-
if (value) checkTransitionValue(value, valueNode);
|
|
148
|
-
},
|
|
149
|
-
...styleObjectListeners(handleStyleObject)
|
|
150
|
-
};
|
|
106
|
+
return stringStyleValueListeners(ctx, (value, node, property) => {
|
|
107
|
+
if (property === "transition") checkTransitionValue(value, node);
|
|
108
|
+
}, { requireTastyJSX: true });
|
|
151
109
|
}
|
|
152
110
|
});
|
|
153
111
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-raw-transition-duration.js","names":[],"sources":["../../src/rules/no-raw-transition-duration.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport { replaceInStringValue } from '../fix-utils.js';\nimport { scanTokens, type ScannedToken } from '../parsers/utils.js';\nimport {\n getDurationTokens,\n hasRawTimeInExpression,\n isNonzeroTime,\n isTimeLiteral,\n} from './motion-duration-utils.js';\n\ntype MessageIds =\n | 'rawTransitionDuration'\n | 'rawTransitionDurationExpression'\n | 'useDurationToken'\n | 'useDefaultDuration';\n\n/**\n * A plausible transition name: a semantic name, a CSS property, or a `$$name` /\n * `##name` / `--name` custom-property reference.\n *\n * This excludes malformed groups from duration advice.\n */\nconst TRANSITION_NAME = /^(?:\\$\\$|##|--)?[a-zA-Z][\\w-]*$/;\nconst NON_TRANSITION_NAMES = new Set([\n 'none',\n 'inherit',\n 'initial',\n 'unset',\n 'revert',\n 'revert-layer',\n]);\n\n/** The default timing tasty substitutes for a group whose duration is omitted. */\nfunction defaultTimingHint(name: string): string {\n // The parser turns $$name into --name and ##name into --name-color before\n // the transition handler selects its per-property timing variable.\n const property = name.startsWith('$$')\n ? name.slice(2)\n : name.startsWith('##')\n ? `${name.slice(2)}-color`\n : name.startsWith('--')\n ? name.slice(2)\n : name;\n return `$${property}-transition (falling back to $transition)`;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'no-raw-transition-duration',\n meta: {\n type: 'suggestion',\n hasSuggestions: true,\n docs: {\n description:\n \"Suggest a duration token, or tasty's default timing, instead of a hardcoded transition duration\",\n },\n messages: {\n rawTransitionDuration:\n \"Transition duration '{{duration}}' is hardcoded. Use a duration token{{available}}, or omit it to inherit {{fallback}}.\",\n rawTransitionDurationExpression:\n \"Transition duration '{{duration}}' contains a hardcoded time. Derive it from a duration token instead.\",\n useDurationToken: \"Replace '{{duration}}' with '{{token}}'\",\n useDefaultDuration:\n \"Remove '{{duration}}' to inherit the default transition timing\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n /** `$`-prefixed config tokens whose name reads as a duration. */\n const durationTokens = getDurationTokens(ctx.config.tokens);\n\n function checkGroup(tokens: ScannedToken[], node: TSESTree.Node): void {\n const [name, timing] = tokens;\n\n // Only the second token is the duration slot. A later time value is a\n // delay, for which removing the value has different semantics.\n if (!name || !timing || !TRANSITION_NAME.test(name.value)) return;\n if (NON_TRANSITION_NAMES.has(name.value)) return;\n\n const isLiteral = isTimeLiteral(timing.value);\n if (isLiteral && !isNonzeroTime(timing.value)) return;\n if (!isLiteral && !hasRawTimeInExpression(timing.value)) return;\n\n if (!isLiteral) {\n // Replacing or dropping a whole expression can change its behavior in\n // ways the rule cannot infer, so offer a report without a quick fix.\n context.report({\n node,\n messageId: 'rawTransitionDurationExpression',\n data: { duration: timing.value },\n });\n return;\n }\n\n const start = timing.offset;\n const end = start + timing.value.length;\n // Removal swallows the separating whitespace too, so dropping the\n // duration does not leave `fill ease-in` behind.\n const removeStart = name.offset + name.value.length;\n\n context.report({\n node,\n messageId: 'rawTransitionDuration',\n data: {\n duration: timing.value,\n available: durationTokens.length\n ? ` (${durationTokens.join(', ')})`\n : '',\n fallback: defaultTimingHint(name.value),\n },\n suggest: [\n // Config tokens first: naming the project's own duration is better\n // advice than deleting the author's intent, when one exists.\n ...durationTokens.map((token) => ({\n messageId: 'useDurationToken' as const,\n data: { duration: timing.value, token },\n fix: (fixer: Parameters<typeof replaceInStringValue>[0]) =>\n replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement: token }],\n context.sourceCode,\n ),\n })),\n {\n messageId: 'useDefaultDuration' as const,\n data: { duration: timing.value },\n fix: (fixer: Parameters<typeof replaceInStringValue>[0]) =>\n replaceInStringValue(\n fixer,\n node,\n [{ start: removeStart, end, replacement: '' }],\n context.sourceCode,\n ),\n },\n ],\n });\n }\n\n function checkTransitionValue(value: string, node: TSESTree.Node): void {\n let group: ScannedToken[] = [];\n for (const token of scanTokens(value)) {\n if (token.value) group.push(token);\n if (token.isComma) {\n checkGroup(group, node);\n group = [];\n }\n }\n checkGroup(group, node);\n }\n\n function isLocalTastyComponent(node: TSESTree.Node, name: string): boolean {\n let scope: ReturnType<typeof context.sourceCode.getScope> | null =\n context.sourceCode.getScope(node);\n while (scope) {\n const variable = scope.set.get(name);\n if (variable) {\n return variable.defs.some((definition) => {\n if (definition.type !== 'Variable') return false;\n const declaration = definition.node;\n return (\n declaration.id.type === 'Identifier' &&\n declaration.parent.type === 'VariableDeclaration' &&\n declaration.parent.kind === 'const' &&\n declaration.init?.type === 'CallExpression' &&\n ctx.isTastyCall(declaration.init)?.importedName === 'tasty'\n );\n });\n }\n scope = scope.upper;\n }\n return false;\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\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 !== 'transition') continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkTransitionValue(str, prop.value);\n continue;\n }\n\n if (\n prop.value.type === 'ObjectExpression' &&\n ctx.isStateMap(prop.value, prop)\n ) {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateStr = getStringValue(stateProp.value);\n if (stateStr) {\n checkTransitionValue(stateStr, stateProp.value);\n }\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n JSXAttribute(node) {\n if (\n node.name.type !== 'JSXIdentifier' ||\n node.name.name !== 'transition'\n ) {\n return;\n }\n const opening = node.parent;\n if (\n opening?.type !== 'JSXOpeningElement' ||\n opening.name.type !== 'JSXIdentifier' ||\n !isLocalTastyComponent(node, opening.name.name)\n ) {\n return;\n }\n\n const valueNode =\n node.value?.type === 'JSXExpressionContainer'\n ? node.value.expression\n : node.value;\n if (!valueNode) return;\n const value = getStringValue(valueNode);\n if (value) checkTransitionValue(value, valueNode);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"no-raw-transition-duration.js","names":[],"sources":["../../src/rules/no-raw-transition-duration.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { stringStyleValueListeners } from '../style-values.js';\nimport { replaceInStringValue } from '../fix-utils.js';\nimport { scanTokens, type ScannedToken } from '../parsers/utils.js';\nimport {\n getDurationTokens,\n hasRawTimeInExpression,\n isNonzeroTime,\n isTimeLiteral,\n} from './motion-duration-utils.js';\n\ntype MessageIds =\n | 'rawTransitionDuration'\n | 'rawTransitionDurationExpression'\n | 'useDurationToken'\n | 'useDefaultDuration';\n\n/**\n * A plausible transition name: a semantic name, a CSS property, or a `$$name` /\n * `##name` / `--name` custom-property reference.\n *\n * This excludes malformed groups from duration advice.\n */\nconst TRANSITION_NAME = /^(?:\\$\\$|##|--)?[a-zA-Z][\\w-]*$/;\nconst NON_TRANSITION_NAMES = new Set([\n 'none',\n 'inherit',\n 'initial',\n 'unset',\n 'revert',\n 'revert-layer',\n]);\n\n/** The default timing tasty substitutes for a group whose duration is omitted. */\nfunction defaultTimingHint(name: string): string {\n // The parser turns $$name into --name and ##name into --name-color before\n // the transition handler selects its per-property timing variable.\n const property = name.startsWith('$$')\n ? name.slice(2)\n : name.startsWith('##')\n ? `${name.slice(2)}-color`\n : name.startsWith('--')\n ? name.slice(2)\n : name;\n return `$${property}-transition (falling back to $transition)`;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'no-raw-transition-duration',\n meta: {\n type: 'suggestion',\n hasSuggestions: true,\n docs: {\n description:\n \"Suggest a duration token, or tasty's default timing, instead of a hardcoded transition duration\",\n },\n messages: {\n rawTransitionDuration:\n \"Transition duration '{{duration}}' is hardcoded. Use a duration token{{available}}, or omit it to inherit {{fallback}}. For intentional custom timing, explicitly disable tasty/no-raw-transition-duration on this line with a reason.\",\n rawTransitionDurationExpression:\n \"Transition duration '{{duration}}' contains a hardcoded time. Derive it from a duration token instead. For intentional custom timing, explicitly disable tasty/no-raw-transition-duration on this line with a reason.\",\n useDurationToken: \"Replace '{{duration}}' with '{{token}}'\",\n useDefaultDuration:\n \"Remove '{{duration}}' to inherit the default transition timing\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n /** `$`-prefixed config tokens whose name reads as a duration. */\n const durationTokens = getDurationTokens(ctx.config.tokens);\n\n function checkGroup(tokens: ScannedToken[], node: TSESTree.Node): void {\n const [name, timing] = tokens;\n\n // Only the second token is the duration slot. A later time value is a\n // delay, for which removing the value has different semantics.\n if (!name || !timing || !TRANSITION_NAME.test(name.value)) return;\n if (NON_TRANSITION_NAMES.has(name.value)) return;\n\n const isLiteral = isTimeLiteral(timing.value);\n if (isLiteral && !isNonzeroTime(timing.value)) return;\n if (!isLiteral && !hasRawTimeInExpression(timing.value)) return;\n\n if (!isLiteral) {\n // Replacing or dropping a whole expression can change its behavior in\n // ways the rule cannot infer, so offer a report without a quick fix.\n context.report({\n node,\n messageId: 'rawTransitionDurationExpression',\n data: { duration: timing.value },\n });\n return;\n }\n\n const start = timing.offset;\n const end = start + timing.value.length;\n // Removal swallows the separating whitespace too, so dropping the\n // duration does not leave `fill ease-in` behind.\n const removeStart = name.offset + name.value.length;\n\n context.report({\n node,\n messageId: 'rawTransitionDuration',\n data: {\n duration: timing.value,\n available: durationTokens.length\n ? ` (${durationTokens.join(', ')})`\n : '',\n fallback: defaultTimingHint(name.value),\n },\n suggest: [\n // Config tokens first: naming the project's own duration is better\n // advice than deleting the author's intent, when one exists.\n ...durationTokens.map((token) => ({\n messageId: 'useDurationToken' as const,\n data: { duration: timing.value, token },\n fix: (fixer: Parameters<typeof replaceInStringValue>[0]) =>\n replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement: token }],\n context.sourceCode,\n ),\n })),\n {\n messageId: 'useDefaultDuration' as const,\n data: { duration: timing.value },\n fix: (fixer: Parameters<typeof replaceInStringValue>[0]) =>\n replaceInStringValue(\n fixer,\n node,\n [{ start: removeStart, end, replacement: '' }],\n context.sourceCode,\n ),\n },\n ],\n });\n }\n\n function checkTransitionValue(value: string, node: TSESTree.Node): void {\n let group: ScannedToken[] = [];\n for (const token of scanTokens(value)) {\n if (token.value) group.push(token);\n if (token.isComma) {\n checkGroup(group, node);\n group = [];\n }\n }\n checkGroup(group, node);\n }\n\n return stringStyleValueListeners(\n ctx,\n (value, node, property) => {\n if (property === 'transition') checkTransitionValue(value, node);\n },\n { requireTastyJSX: true },\n );\n },\n});\n"],"mappings":";;;;;;;;;;;;;;AAyBA,MAAM,kBAAkB;AACxB,MAAM,uBAAuB,IAAI,IAAI;CACnC;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;AAGF,SAAS,kBAAkB,MAAsB;AAU/C,QAAO,IAPU,KAAK,WAAW,KAAK,GAClC,KAAK,MAAM,EAAE,GACb,KAAK,WAAW,KAAK,GACnB,GAAG,KAAK,MAAM,EAAE,CAAC,UACjB,KAAK,WAAW,KAAK,GACnB,KAAK,MAAM,EAAE,GACb,KACY;;AAGtB,yCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,gBAAgB;EAChB,MAAM,EACJ,aACE,mGACH;EACD,UAAU;GACR,uBACE;GACF,iCACE;GACF,kBAAkB;GAClB,oBACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;;EAGrC,MAAM,iBAAiB,kBAAkB,IAAI,OAAO,OAAO;EAE3D,SAAS,WAAW,QAAwB,MAA2B;GACrE,MAAM,CAAC,MAAM,UAAU;AAIvB,OAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,gBAAgB,KAAK,KAAK,MAAM,CAAE;AAC3D,OAAI,qBAAqB,IAAI,KAAK,MAAM,CAAE;GAE1C,MAAM,YAAY,cAAc,OAAO,MAAM;AAC7C,OAAI,aAAa,CAAC,cAAc,OAAO,MAAM,CAAE;AAC/C,OAAI,CAAC,aAAa,CAAC,uBAAuB,OAAO,MAAM,CAAE;AAEzD,OAAI,CAAC,WAAW;AAGd,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM,EAAE,UAAU,OAAO,OAAO;KACjC,CAAC;AACF;;GAGF,MAAM,QAAQ,OAAO;GACrB,MAAM,MAAM,QAAQ,OAAO,MAAM;GAGjC,MAAM,cAAc,KAAK,SAAS,KAAK,MAAM;AAE7C,WAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM;KACJ,UAAU,OAAO;KACjB,WAAW,eAAe,SACtB,KAAK,eAAe,KAAK,KAAK,CAAC,KAC/B;KACJ,UAAU,kBAAkB,KAAK,MAAM;KACxC;IACD,SAAS,CAGP,GAAG,eAAe,KAAK,WAAW;KAChC,WAAW;KACX,MAAM;MAAE,UAAU,OAAO;MAAO;MAAO;KACvC,MAAM,UACJ,qBACE,OACA,MACA,CAAC;MAAE;MAAO;MAAK,aAAa;MAAO,CAAC,EACpC,QAAQ,WACT;KACJ,EAAE,EACH;KACE,WAAW;KACX,MAAM,EAAE,UAAU,OAAO,OAAO;KAChC,MAAM,UACJ,qBACE,OACA,MACA,CAAC;MAAE,OAAO;MAAa;MAAK,aAAa;MAAI,CAAC,EAC9C,QAAQ,WACT;KACJ,CACF;IACF,CAAC;;EAGJ,SAAS,qBAAqB,OAAe,MAA2B;GACtE,IAAI,QAAwB,EAAE;AAC9B,QAAK,MAAM,SAAS,WAAW,MAAM,EAAE;AACrC,QAAI,MAAM,MAAO,OAAM,KAAK,MAAM;AAClC,QAAI,MAAM,SAAS;AACjB,gBAAW,OAAO,KAAK;AACvB,aAAQ,EAAE;;;AAGd,cAAW,OAAO,KAAK;;AAGzB,SAAO,0BACL,MACC,OAAO,MAAM,aAAa;AACzB,OAAI,aAAa,aAAc,sBAAqB,OAAO,KAAK;KAElE,EAAE,iBAAiB,MAAM,CAC1B;;CAEJ,CAAC"}
|
|
@@ -10,8 +10,8 @@ var no_runtime_styles_mutation_default = createRule({
|
|
|
10
10
|
type: "suggestion",
|
|
11
11
|
docs: { description: "Warn when style objects contain JavaScript-computed values" },
|
|
12
12
|
messages: {
|
|
13
|
-
dynamicStyleValue: "
|
|
14
|
-
dynamicStyleKey: "
|
|
13
|
+
dynamicStyleValue: "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.",
|
|
14
|
+
dynamicStyleKey: "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."
|
|
15
15
|
},
|
|
16
16
|
schema: []
|
|
17
17
|
},
|
|
@@ -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 \"
|
|
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 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 (!isStaticValue(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) {\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":";;;;;;AAQA,yCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,8DACd;EACD,UAAU;GACR,mBACE;GACF,iBACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,WACP,MACA,UACA,aAAa,OACP;GACN,MAAM,QAAQ,iBAAiB,KAAK;AAEpC,OAAI,MAAM,SAAS,mBACjB,iBAAgB,OAAO,UAAU,WAAW;YACnC,MAAM,SAAS,mBACxB;SAAK,MAAM,WAAW,MAAM,SAE1B,KAAI,WAAW,QAAQ,SAAS,gBAC9B,YAAW,SAAS,SAAS;cAGxB,CAAC,cAAc,MAAM,CAC9B,SAAQ,OAAO;IACb,MAAM;IACN,WAAW;IACX,MAAM,EAAE,UAAU;IACnB,CAAC;;EAIN,SAAS,gBACP,MACA,iBAAiB,WACjB,aAAa,OACP;AACN,QAAK,MAAM,QAAQ,KAAK,YAAY;AAElC,QAAI,KAAK,SAAS,gBAAiB;IAInC,MAAM,MAAM,CAAC,KAAK,WAAW,WAAW,KAAK,IAAI,GAAG;AACpD,QAAI,OAAO,mBAAmB,IAAI,IAAI,CAAE;IAExC,MAAM,WACJ,cAAc,KAAK,WACf,iBACC,OAAO;AAEd,QAAI,KAAK,SACP,SAAQ,OAAO;KACb,MAAM,KAAK;KACX,WAAW;KACX,MAAM,EAAE,UAAU;KACnB,CAAC;IAGJ,MAAM,QAAQ,iBAAiB,KAAK,MAAM;IAC1C,MAAM,kBACJ,MAAM,SAAS,sBAAsB,IAAI,WAAW,OAAO,KAAK;AAClE,eAAW,KAAK,OAAO,UAAU,gBAAgB;;;EAIrD,SAAS,kBAAkB,MAAuC;GAChE,MAAM,WAAW,IAAI,gBAAgB,KAAK;AAE1C,OAAI,CAAC,YAAY,SAAS,aAAc;AAExC,OAAI,IAAI,mBAAmB,KAAK,KAAK,KAAM;AAE3C,mBAAgB,KAAK;;AAGvB,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|
|
@@ -41,7 +41,7 @@ var no_state_in_selector_default = createRule({
|
|
|
41
41
|
meta: {
|
|
42
42
|
type: "suggestion",
|
|
43
43
|
docs: { description: "Keep sub-element selectors structural and express conditions in property state maps" },
|
|
44
|
-
messages: { stateInSelector: "Move state condition '{{condition}}' out of the sub-element '$' selector and into property state maps. Use @own(...) for the sub-element's own state." },
|
|
44
|
+
messages: { stateInSelector: "Move state condition '{{condition}}' out of the sub-element '$' selector and into property state maps. Use @own(...) for the sub-element's own state. If the condition must stay in this selector, explicitly disable tasty/no-state-in-selector on this line with a reason." },
|
|
45
45
|
schema: []
|
|
46
46
|
},
|
|
47
47
|
defaultOptions: [],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-state-in-selector.js","names":[],"sources":["../../src/rules/no-state-in-selector.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport selectorParser from 'postcss-selector-parser';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue, unwrapExpression } from '../utils.js';\n\nconst STRUCTURAL_PSEUDOS = new Set([':is', ':where']);\nconst LEGACY_PSEUDO_ELEMENTS = new Set([\n ':before',\n ':after',\n ':first-line',\n ':first-letter',\n]);\n\nfunction firstStateCondition(selector: string): string | null {\n let root: selectorParser.Root;\n try {\n root = selectorParser().astSync(selector);\n } catch {\n // This recommendation is not a CSS syntax validator. Incomplete selectors\n // must not crash lint or produce a misleading state-map recommendation.\n return null;\n }\n\n let condition: string | null = null;\n root.walk((node) => {\n if (node.type === 'attribute') {\n // An exact data-element identity is structural, like Tasty's uppercase\n // element names. Other attributes express conditions on that element.\n if (\n node.attribute === 'data-element' &&\n !node.namespace &&\n node.operator === '=' &&\n node.value\n ) {\n return;\n }\n } else if (node.type === 'pseudo') {\n let name = node.value.toLowerCase();\n if (name.includes('\\\\')) {\n // The parser decodes tag identifiers but preserves pseudo spelling.\n const colons = name.startsWith('::') ? '::' : ':';\n const identifier = selectorParser().astSync(name.slice(colons.length))\n .first?.first;\n if (identifier?.type === 'tag') {\n name = colons + identifier.value.toLowerCase();\n }\n }\n if (\n name.startsWith('::') ||\n LEGACY_PSEUDO_ELEMENTS.has(name) ||\n STRUCTURAL_PSEUDOS.has(name)\n ) {\n // Continue walking: :where(img:hover) and ::slotted(img[width])\n // still contain conditions, although the outer wrapper is structural.\n return;\n }\n } else {\n return;\n }\n\n condition = node.toString().trim();\n return false;\n });\n return condition;\n}\n\nexport default createRule<[], 'stateInSelector'>({\n name: 'no-state-in-selector',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Keep sub-element selectors structural and express conditions in property state maps',\n },\n messages: {\n stateInSelector:\n \"Move state condition '{{condition}}' out of the sub-element '$' selector and into property state maps. Use @own(...) for the sub-element's own state.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n const visited = new WeakSet<TSESTree.ObjectExpression>();\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (visited.has(node) || !ctx.isStyleObject(node)) return;\n visited.add(node);\n const isSubElement = ctx.isInsideSubElement(node);\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property') continue;\n const key = prop.computed\n ? getStringValue(unwrapExpression(prop.key))\n : getKeyName(prop.key);\n // Some listeners match only the shared/JSX root. Follow its owned\n // sub-elements explicitly, without entering state maps or metadata.\n if (!prop.computed && key && ctx.isSubElementKey(key)) {\n const child = unwrapExpression(prop.value);\n if (child.type === 'ObjectExpression') handleStyleObject(child);\n }\n if (!isSubElement || key !== '$') continue;\n\n const selector = getStringValue(unwrapExpression(prop.value));\n if (selector === null) continue;\n const condition = firstStateCondition(selector);\n if (condition === null) continue;\n\n context.report({\n node: prop.value,\n messageId: 'stateInSelector',\n data: { condition },\n });\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;AAMA,MAAM,qBAAqB,IAAI,IAAI,CAAC,OAAO,SAAS,CAAC;AACrD,MAAM,yBAAyB,IAAI,IAAI;CACrC;CACA;CACA;CACA;CACD,CAAC;AAEF,SAAS,oBAAoB,UAAiC;CAC5D,IAAI;AACJ,KAAI;AACF,SAAO,gBAAgB,CAAC,QAAQ,SAAS;SACnC;AAGN,SAAO;;CAGT,IAAI,YAA2B;AAC/B,MAAK,MAAM,SAAS;AAClB,MAAI,KAAK,SAAS,aAGhB;OACE,KAAK,cAAc,kBACnB,CAAC,KAAK,aACN,KAAK,aAAa,OAClB,KAAK,MAEL;aAEO,KAAK,SAAS,UAAU;GACjC,IAAI,OAAO,KAAK,MAAM,aAAa;AACnC,OAAI,KAAK,SAAS,KAAK,EAAE;IAEvB,MAAM,SAAS,KAAK,WAAW,KAAK,GAAG,OAAO;IAC9C,MAAM,aAAa,gBAAgB,CAAC,QAAQ,KAAK,MAAM,OAAO,OAAO,CAAC,CACnE,OAAO;AACV,QAAI,YAAY,SAAS,MACvB,QAAO,SAAS,WAAW,MAAM,aAAa;;AAGlD,OACE,KAAK,WAAW,KAAK,IACrB,uBAAuB,IAAI,KAAK,IAChC,mBAAmB,IAAI,KAAK,CAI5B;QAGF;AAGF,cAAY,KAAK,UAAU,CAAC,MAAM;AAClC,SAAO;GACP;AACF,QAAO;;AAGT,mCAAe,WAAkC;CAC/C,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,uFACH;EACD,UAAU,EACR,iBACE,
|
|
1
|
+
{"version":3,"file":"no-state-in-selector.js","names":[],"sources":["../../src/rules/no-state-in-selector.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport selectorParser from 'postcss-selector-parser';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue, unwrapExpression } from '../utils.js';\n\nconst STRUCTURAL_PSEUDOS = new Set([':is', ':where']);\nconst LEGACY_PSEUDO_ELEMENTS = new Set([\n ':before',\n ':after',\n ':first-line',\n ':first-letter',\n]);\n\nfunction firstStateCondition(selector: string): string | null {\n let root: selectorParser.Root;\n try {\n root = selectorParser().astSync(selector);\n } catch {\n // This recommendation is not a CSS syntax validator. Incomplete selectors\n // must not crash lint or produce a misleading state-map recommendation.\n return null;\n }\n\n let condition: string | null = null;\n root.walk((node) => {\n if (node.type === 'attribute') {\n // An exact data-element identity is structural, like Tasty's uppercase\n // element names. Other attributes express conditions on that element.\n if (\n node.attribute === 'data-element' &&\n !node.namespace &&\n node.operator === '=' &&\n node.value\n ) {\n return;\n }\n } else if (node.type === 'pseudo') {\n let name = node.value.toLowerCase();\n if (name.includes('\\\\')) {\n // The parser decodes tag identifiers but preserves pseudo spelling.\n const colons = name.startsWith('::') ? '::' : ':';\n const identifier = selectorParser().astSync(name.slice(colons.length))\n .first?.first;\n if (identifier?.type === 'tag') {\n name = colons + identifier.value.toLowerCase();\n }\n }\n if (\n name.startsWith('::') ||\n LEGACY_PSEUDO_ELEMENTS.has(name) ||\n STRUCTURAL_PSEUDOS.has(name)\n ) {\n // Continue walking: :where(img:hover) and ::slotted(img[width])\n // still contain conditions, although the outer wrapper is structural.\n return;\n }\n } else {\n return;\n }\n\n condition = node.toString().trim();\n return false;\n });\n return condition;\n}\n\nexport default createRule<[], 'stateInSelector'>({\n name: 'no-state-in-selector',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Keep sub-element selectors structural and express conditions in property state maps',\n },\n messages: {\n stateInSelector:\n \"Move state condition '{{condition}}' out of the sub-element '$' selector and into property state maps. Use @own(...) for the sub-element's own state. If the condition must stay in this selector, explicitly disable tasty/no-state-in-selector on this line with a reason.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n const visited = new WeakSet<TSESTree.ObjectExpression>();\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (visited.has(node) || !ctx.isStyleObject(node)) return;\n visited.add(node);\n const isSubElement = ctx.isInsideSubElement(node);\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property') continue;\n const key = prop.computed\n ? getStringValue(unwrapExpression(prop.key))\n : getKeyName(prop.key);\n // Some listeners match only the shared/JSX root. Follow its owned\n // sub-elements explicitly, without entering state maps or metadata.\n if (!prop.computed && key && ctx.isSubElementKey(key)) {\n const child = unwrapExpression(prop.value);\n if (child.type === 'ObjectExpression') handleStyleObject(child);\n }\n if (!isSubElement || key !== '$') continue;\n\n const selector = getStringValue(unwrapExpression(prop.value));\n if (selector === null) continue;\n const condition = firstStateCondition(selector);\n if (condition === null) continue;\n\n context.report({\n node: prop.value,\n messageId: 'stateInSelector',\n data: { condition },\n });\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;AAMA,MAAM,qBAAqB,IAAI,IAAI,CAAC,OAAO,SAAS,CAAC;AACrD,MAAM,yBAAyB,IAAI,IAAI;CACrC;CACA;CACA;CACA;CACD,CAAC;AAEF,SAAS,oBAAoB,UAAiC;CAC5D,IAAI;AACJ,KAAI;AACF,SAAO,gBAAgB,CAAC,QAAQ,SAAS;SACnC;AAGN,SAAO;;CAGT,IAAI,YAA2B;AAC/B,MAAK,MAAM,SAAS;AAClB,MAAI,KAAK,SAAS,aAGhB;OACE,KAAK,cAAc,kBACnB,CAAC,KAAK,aACN,KAAK,aAAa,OAClB,KAAK,MAEL;aAEO,KAAK,SAAS,UAAU;GACjC,IAAI,OAAO,KAAK,MAAM,aAAa;AACnC,OAAI,KAAK,SAAS,KAAK,EAAE;IAEvB,MAAM,SAAS,KAAK,WAAW,KAAK,GAAG,OAAO;IAC9C,MAAM,aAAa,gBAAgB,CAAC,QAAQ,KAAK,MAAM,OAAO,OAAO,CAAC,CACnE,OAAO;AACV,QAAI,YAAY,SAAS,MACvB,QAAO,SAAS,WAAW,MAAM,aAAa;;AAGlD,OACE,KAAK,WAAW,KAAK,IACrB,uBAAuB,IAAI,KAAK,IAChC,mBAAmB,IAAI,KAAK,CAI5B;QAGF;AAGF,cAAY,KAAK,UAAU,CAAC,MAAM;AAClC,SAAO;GACP;AACF,QAAO;;AAGT,mCAAe,WAAkC;CAC/C,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,uFACH;EACD,UAAU,EACR,iBACE,gRACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EACrC,MAAM,0BAAU,IAAI,SAAoC;EAExD,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,QAAQ,IAAI,KAAK,IAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AACnD,WAAQ,IAAI,KAAK;GACjB,MAAM,eAAe,IAAI,mBAAmB,KAAK;AAEjD,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,WAAY;IAC9B,MAAM,MAAM,KAAK,WACb,eAAe,iBAAiB,KAAK,IAAI,CAAC,GAC1C,WAAW,KAAK,IAAI;AAGxB,QAAI,CAAC,KAAK,YAAY,OAAO,IAAI,gBAAgB,IAAI,EAAE;KACrD,MAAM,QAAQ,iBAAiB,KAAK,MAAM;AAC1C,SAAI,MAAM,SAAS,mBAAoB,mBAAkB,MAAM;;AAEjE,QAAI,CAAC,gBAAgB,QAAQ,IAAK;IAElC,MAAM,WAAW,eAAe,iBAAiB,KAAK,MAAM,CAAC;AAC7D,QAAI,aAAa,KAAM;IACvB,MAAM,YAAY,oBAAoB,SAAS;AAC/C,QAAI,cAAc,KAAM;AAExB,YAAQ,OAAO;KACb,MAAM,KAAK;KACX,WAAW;KACX,MAAM,EAAE,WAAW;KACpB,CAAC;;;AAIN,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createRule } from "../create-rule.js";
|
|
2
|
+
import { TastyContext } from "../context.js";
|
|
3
|
+
import { tastyComponentPropListeners } from "./tasty-component-props.js";
|
|
4
|
+
|
|
5
|
+
//#region src/rules/no-style-prop.ts
|
|
6
|
+
var no_style_prop_default = createRule({
|
|
7
|
+
name: "no-style-prop",
|
|
8
|
+
meta: {
|
|
9
|
+
type: "suggestion",
|
|
10
|
+
docs: { description: "Prefer tokens over React inline style on Tasty components" },
|
|
11
|
+
messages: { 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." },
|
|
12
|
+
schema: []
|
|
13
|
+
},
|
|
14
|
+
defaultOptions: [],
|
|
15
|
+
create(context) {
|
|
16
|
+
return tastyComponentPropListeners(new TastyContext(context), (name, node) => {
|
|
17
|
+
if (name === "style") context.report({
|
|
18
|
+
node,
|
|
19
|
+
messageId: "noStyleProp"
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
export { no_style_prop_default as default };
|
|
27
|
+
//# sourceMappingURL=no-style-prop.js.map
|
|
@@ -0,0 +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 { tastyComponentPropListeners } from './tasty-component-props.js';\n\nexport default createRule<[], 'noStyleProp'>({\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 },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n return tastyComponentPropListeners(ctx, (name, node) => {\n if (name === 'style') context.report({ node, messageId: 'noStyleProp' });\n });\n },\n});\n"],"mappings":";;;;;AAIA,4BAAe,WAA8B;CAC3C,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,6DACd;EACD,UAAU,EACR,aACE,0SACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;AAEd,SAAO,4BADK,IAAI,aAAa,QAAQ,GACI,MAAM,SAAS;AACtD,OAAI,SAAS,QAAS,SAAQ,OAAO;IAAE;IAAM,WAAW;IAAe,CAAC;IACxE;;CAEL,CAAC"}
|
|
@@ -9,8 +9,8 @@ var no_style_spread_default = createRule({
|
|
|
9
9
|
type: "suggestion",
|
|
10
10
|
docs: { description: "Warn about style spreads and recommend mergeStyles for composition" },
|
|
11
11
|
messages: {
|
|
12
|
-
styleSpread: "List Tasty style properties explicitly instead of spreading. If this spread is intentional,
|
|
13
|
-
preferMergeStyles: "Use mergeStyles(...) to compose Tasty style objects. Object spread is shallow and can discard sub-element properties and state-map entries. If shallow replacement is intentional,
|
|
12
|
+
styleSpread: "List Tasty style properties explicitly instead of spreading. If this spread is intentional, explicitly disable tasty/no-style-spread on this line with an ESLint comment and a reason.",
|
|
13
|
+
preferMergeStyles: "Use mergeStyles(...) to compose Tasty style objects. Object spread is shallow and can discard sub-element properties and state-map entries. If shallow replacement is intentional, explicitly disable tasty/no-style-spread on this line with an ESLint comment and a reason."
|
|
14
14
|
},
|
|
15
15
|
schema: []
|
|
16
16
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-style-spread.js","names":[],"sources":["../../src/rules/no-style-spread.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { unwrapExpression } from '../utils.js';\n\ntype MessageIds = 'styleSpread' | 'preferMergeStyles';\n\nexport default createRule<[], MessageIds>({\n name: 'no-style-spread',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Warn about style spreads and recommend mergeStyles for composition',\n },\n messages: {\n styleSpread:\n 'List Tasty style properties explicitly instead of spreading. If this spread is intentional,
|
|
1
|
+
{"version":3,"file":"no-style-spread.js","names":[],"sources":["../../src/rules/no-style-spread.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { unwrapExpression } from '../utils.js';\n\ntype MessageIds = 'styleSpread' | 'preferMergeStyles';\n\nexport default createRule<[], MessageIds>({\n name: 'no-style-spread',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Warn about style spreads and recommend mergeStyles for composition',\n },\n messages: {\n styleSpread:\n 'List Tasty style properties explicitly instead of spreading. If this spread is intentional, explicitly disable tasty/no-style-spread on this line with an ESLint comment and a reason.',\n preferMergeStyles:\n 'Use mergeStyles(...) to compose Tasty style objects. Object spread is shallow and can discard sub-element properties and state-map entries. If shallow replacement is intentional, explicitly disable tasty/no-style-spread on this line with an ESLint comment and a reason.',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n const visited = new WeakSet<TSESTree.ObjectExpression>();\n\n function checkValue(node: TSESTree.Node): void {\n const value = unwrapExpression(node);\n\n if (value.type === 'ObjectExpression') {\n for (const prop of value.properties) {\n if (prop.type === 'SpreadElement') {\n context.report({ node: prop, messageId: 'styleSpread' });\n } else {\n checkValue(prop.value);\n }\n }\n } else if (value.type === 'ArrayExpression') {\n for (const element of value.elements) {\n if (!element) continue;\n if (element.type === 'SpreadElement') {\n context.report({ node: element, messageId: 'styleSpread' });\n } else {\n checkValue(element);\n }\n }\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression): void {\n if (visited.has(node)) return;\n const styleCtx = ctx.getStyleContext(node);\n // tastyStatic rejects spreads during extraction, so its existing error\n // gives the useful diagnostic without a second warning.\n if (!styleCtx || styleCtx.isStaticCall) return;\n if (ctx.getRootStyleObject(node) !== node) return;\n visited.add(node);\n\n const spreads = node.properties.filter(\n (prop): prop is TSESTree.SpreadElement => prop.type === 'SpreadElement',\n );\n if (spreads.length < 2) {\n checkValue(node);\n return;\n }\n\n // Report composition once, at its first spread so the existing per-line\n // suppression convention still works. Nested state maps/arrays are not\n // Styles objects and must keep their ordinary spread diagnostics.\n context.report({ node: spreads[0], messageId: 'preferMergeStyles' });\n for (const prop of node.properties) {\n if (prop.type === 'Property') checkValue(prop.value);\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n VariableDeclarator(node) {\n if (!node.init) return;\n // The shared selectors cover only one as/satisfies wrapper. Typed\n // composition can also have nested wrappers or a non-null assertion.\n const value = unwrapExpression(node.init);\n if (value.type === 'ObjectExpression') handleStyleObject(value);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;AAOA,8BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,sEACH;EACD,UAAU;GACR,aACE;GACF,mBACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EACrC,MAAM,0BAAU,IAAI,SAAoC;EAExD,SAAS,WAAW,MAA2B;GAC7C,MAAM,QAAQ,iBAAiB,KAAK;AAEpC,OAAI,MAAM,SAAS,mBACjB,MAAK,MAAM,QAAQ,MAAM,WACvB,KAAI,KAAK,SAAS,gBAChB,SAAQ,OAAO;IAAE,MAAM;IAAM,WAAW;IAAe,CAAC;OAExD,YAAW,KAAK,MAAM;YAGjB,MAAM,SAAS,kBACxB,MAAK,MAAM,WAAW,MAAM,UAAU;AACpC,QAAI,CAAC,QAAS;AACd,QAAI,QAAQ,SAAS,gBACnB,SAAQ,OAAO;KAAE,MAAM;KAAS,WAAW;KAAe,CAAC;QAE3D,YAAW,QAAQ;;;EAM3B,SAAS,kBAAkB,MAAuC;AAChE,OAAI,QAAQ,IAAI,KAAK,CAAE;GACvB,MAAM,WAAW,IAAI,gBAAgB,KAAK;AAG1C,OAAI,CAAC,YAAY,SAAS,aAAc;AACxC,OAAI,IAAI,mBAAmB,KAAK,KAAK,KAAM;AAC3C,WAAQ,IAAI,KAAK;GAEjB,MAAM,UAAU,KAAK,WAAW,QAC7B,SAAyC,KAAK,SAAS,gBACzD;AACD,OAAI,QAAQ,SAAS,GAAG;AACtB,eAAW,KAAK;AAChB;;AAMF,WAAQ,OAAO;IAAE,MAAM,QAAQ;IAAI,WAAW;IAAqB,CAAC;AACpE,QAAK,MAAM,QAAQ,KAAK,WACtB,KAAI,KAAK,SAAS,WAAY,YAAW,KAAK,MAAM;;AAIxD,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,mBAAmB,MAAM;AACvB,QAAI,CAAC,KAAK,KAAM;IAGhB,MAAM,QAAQ,iBAAiB,KAAK,KAAK;AACzC,QAAI,MAAM,SAAS,mBAAoB,mBAAkB,MAAM;;GAEjE,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
import { createRule } from "../create-rule.js";
|
|
2
|
+
import { TastyContext } from "../context.js";
|
|
3
|
+
import { tastyComponentPropListeners } from "./tasty-component-props.js";
|
|
2
4
|
|
|
3
5
|
//#region src/rules/no-styles-prop.ts
|
|
4
6
|
var no_styles_prop_default = createRule({
|
|
5
7
|
name: "no-styles-prop",
|
|
6
8
|
meta: {
|
|
7
9
|
type: "suggestion",
|
|
8
|
-
docs: { description: "
|
|
9
|
-
messages: { noStylesProp: "Avoid
|
|
10
|
+
docs: { description: "Prefer tokens, mods, and styled wrappers over instance styles on Tasty components" },
|
|
11
|
+
messages: { noStylesProp: "Avoid the 'styles' prop on Tasty components. Use 'tokens' for dynamic values, 'mods' for state changes, exposed style props or variants, or a wrapper with tasty(Component, { styles: ... }). For an edge case that requires instance overrides, explicitly disable tasty/no-styles-prop for this usage with an ESLint comment and a reason." },
|
|
10
12
|
schema: []
|
|
11
13
|
},
|
|
12
14
|
defaultOptions: [],
|
|
13
15
|
create(context) {
|
|
14
|
-
return
|
|
15
|
-
if (
|
|
16
|
+
return tastyComponentPropListeners(new TastyContext(context), (name, node) => {
|
|
17
|
+
if (name === "styles") context.report({
|
|
16
18
|
node,
|
|
17
19
|
messageId: "noStylesProp"
|
|
18
20
|
});
|
|
19
|
-
}
|
|
21
|
+
});
|
|
20
22
|
}
|
|
21
23
|
});
|
|
22
24
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-styles-prop.js","names":[],"sources":["../../src/rules/no-styles-prop.ts"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"file":"no-styles-prop.js","names":[],"sources":["../../src/rules/no-styles-prop.ts"],"sourcesContent":["import { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { tastyComponentPropListeners } from './tasty-component-props.js';\n\ntype MessageIds = 'noStylesProp';\n\nexport default createRule<[], MessageIds>({\n name: 'no-styles-prop',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Prefer tokens, mods, and styled wrappers over instance styles on Tasty components',\n },\n messages: {\n noStylesProp:\n \"Avoid the 'styles' prop on Tasty components. Use 'tokens' for dynamic values, 'mods' for state changes, exposed style props or variants, or a wrapper with tasty(Component, { styles: ... }). For an edge case that requires instance overrides, explicitly disable tasty/no-styles-prop for this usage with an ESLint comment and a reason.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n return tastyComponentPropListeners(ctx, (name, node) => {\n if (name === 'styles')\n context.report({ node, messageId: 'noStylesProp' });\n });\n },\n});\n"],"mappings":";;;;;AAMA,6BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,qFACH;EACD,UAAU,EACR,cACE,gVACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;AAEd,SAAO,4BADK,IAAI,aAAa,QAAQ,GACI,MAAM,SAAS;AACtD,OAAI,SAAS,SACX,SAAQ,OAAO;IAAE;IAAM,WAAW;IAAgB,CAAC;IACrD;;CAEL,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRule } from "../create-rule.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { TastyContext } from "../context.js";
|
|
3
|
+
import { stringStyleValueListeners } from "../style-values.js";
|
|
4
4
|
import { parseValue } from "../parsers/value-parser.js";
|
|
5
5
|
import { replaceInStringValue } from "../fix-utils.js";
|
|
6
6
|
|
|
@@ -48,31 +48,9 @@ var prefer_auto_calc_default = createRule({
|
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
if (prop.type !== "Property" || prop.computed) continue;
|
|
55
|
-
const key = getKeyName(prop.key);
|
|
56
|
-
if (key === null) continue;
|
|
57
|
-
if (/^[A-Z@&$#]/.test(key)) continue;
|
|
58
|
-
const str = getStringValue(prop.value);
|
|
59
|
-
if (str) {
|
|
60
|
-
checkValue(str, prop.value);
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
if (prop.value.type === "ObjectExpression") for (const stateProp of prop.value.properties) {
|
|
64
|
-
if (stateProp.type !== "Property") continue;
|
|
65
|
-
const stateStr = getStringValue(stateProp.value);
|
|
66
|
-
if (stateStr) checkValue(stateStr, stateProp.value);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return {
|
|
71
|
-
ImportDeclaration(node) {
|
|
72
|
-
ctx.trackImport(node);
|
|
73
|
-
},
|
|
74
|
-
...styleObjectListeners(handleStyleObject)
|
|
75
|
-
};
|
|
51
|
+
return stringStyleValueListeners(ctx, (value, node, property) => {
|
|
52
|
+
if (!/^[#$]/.test(property)) checkValue(value, node);
|
|
53
|
+
}, { requireTastyJSX: true });
|
|
76
54
|
}
|
|
77
55
|
});
|
|
78
56
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-auto-calc.js","names":[],"sources":["../../src/rules/prefer-auto-calc.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext
|
|
1
|
+
{"version":3,"file":"prefer-auto-calc.js","names":[],"sources":["../../src/rules/prefer-auto-calc.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { stringStyleValueListeners } from '../style-values.js';\nimport { parseValue } from '../parsers/value-parser.js';\nimport { replaceInStringValue } from '../fix-utils.js';\n\ntype MessageIds = 'preferAutoCalc';\n\nexport default createRule<[], MessageIds>({\n name: 'prefer-auto-calc',\n meta: {\n type: 'suggestion',\n fixable: 'code',\n docs: {\n description:\n 'Suggest Tasty auto-calc parentheses instead of explicit calc()',\n },\n messages: {\n preferAutoCalc:\n \"Replace 'calc({{inner}})' with Tasty auto-calc '({{inner}})' (calc() is added automatically).\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkValue(value: string, node: TSESTree.Node): void {\n const result = parseValue(value, { skipUnitValidation: true });\n\n let cursor = 0;\n for (const group of result.groups) {\n for (const part of group.parts) {\n for (const token of part.tokens) {\n if (token.type !== 'css-function' || token.name !== 'calc') {\n continue;\n }\n\n const start = value.indexOf(token.raw, cursor);\n if (start === -1) {\n context.report({\n node,\n messageId: 'preferAutoCalc',\n data: { inner: token.args },\n });\n continue;\n }\n cursor = start + token.raw.length;\n const end = cursor;\n const replacement = `(${token.args})`;\n\n context.report({\n node,\n messageId: 'preferAutoCalc',\n data: { inner: token.args },\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement }],\n context.sourceCode,\n );\n },\n });\n }\n }\n }\n }\n\n return stringStyleValueListeners(\n ctx,\n (value, node, property) => {\n if (!/^[#$]/.test(property)) checkValue(value, node);\n },\n { requireTastyJSX: true },\n );\n },\n});\n"],"mappings":";;;;;;;AASA,+BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,SAAS;EACT,MAAM,EACJ,aACE,kEACH;EACD,UAAU,EACR,gBACE,iGACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,WAAW,OAAe,MAA2B;GAC5D,MAAM,SAAS,WAAW,OAAO,EAAE,oBAAoB,MAAM,CAAC;GAE9D,IAAI,SAAS;AACb,QAAK,MAAM,SAAS,OAAO,OACzB,MAAK,MAAM,QAAQ,MAAM,MACvB,MAAK,MAAM,SAAS,KAAK,QAAQ;AAC/B,QAAI,MAAM,SAAS,kBAAkB,MAAM,SAAS,OAClD;IAGF,MAAM,QAAQ,MAAM,QAAQ,MAAM,KAAK,OAAO;AAC9C,QAAI,UAAU,IAAI;AAChB,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM,EAAE,OAAO,MAAM,MAAM;MAC5B,CAAC;AACF;;AAEF,aAAS,QAAQ,MAAM,IAAI;IAC3B,MAAM,MAAM;IACZ,MAAM,cAAc,IAAI,MAAM,KAAK;AAEnC,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM,EAAE,OAAO,MAAM,MAAM;KAC3B,IAAI,OAAO;AACT,aAAO,qBACL,OACA,MACA,CAAC;OAAE;OAAO;OAAK;OAAa,CAAC,EAC7B,QAAQ,WACT;;KAEJ,CAAC;;;AAMV,SAAO,0BACL,MACC,OAAO,MAAM,aAAa;AACzB,OAAI,CAAC,QAAQ,KAAK,SAAS,CAAE,YAAW,OAAO,KAAK;KAEtD,EAAE,iBAAiB,MAAM,CAC1B;;CAEJ,CAAC"}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { createRule } from "../create-rule.js";
|
|
2
2
|
import { PROPERTIES_WITHOUT_COLOR_TOKEN_EXPANSION, PROPERTIES_WITHOUT_CUSTOM_PROPERTY_EXPANSION } from "../constants.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { TastyContext } from "../context.js";
|
|
4
|
+
import { stringStyleValueListeners } from "../style-values.js";
|
|
5
|
+
import { maskValueLiterals } from "../value-words.js";
|
|
5
6
|
import { replaceInStringValue } from "../fix-utils.js";
|
|
6
7
|
|
|
7
8
|
//#region src/rules/prefer-custom-property-syntax.ts
|
|
8
9
|
const VAR_REGEX = /var\(\s*--([a-zA-Z0-9_-]+)\s*(?:,\s*([^)]+))?\s*\)/g;
|
|
9
10
|
const COLOR_PROP_REGEX = /(?<![$#])\$([a-zA-Z][a-zA-Z0-9_-]*)-color(?![a-zA-Z0-9_-])(\.[0-9]+|\.\$[a-zA-Z][a-zA-Z0-9_-]*)?/g;
|
|
10
|
-
const KEYWORD_REGEX =
|
|
11
|
+
const KEYWORD_REGEX = /(?<![\w$#-])(transparent|currentColor)(?![\w-])/gi;
|
|
11
12
|
function normalizeFallback(fallback) {
|
|
12
13
|
const trimmed = fallback.trim();
|
|
13
14
|
const lower = trimmed.toLowerCase();
|
|
@@ -62,11 +63,13 @@ var prefer_custom_property_syntax_default = createRule({
|
|
|
62
63
|
return true;
|
|
63
64
|
}
|
|
64
65
|
function checkValue(value, node, property) {
|
|
66
|
+
const masked = maskValueLiterals(value);
|
|
65
67
|
const varSpans = [];
|
|
66
68
|
VAR_REGEX.lastIndex = 0;
|
|
67
69
|
let match;
|
|
68
|
-
while ((match = VAR_REGEX.exec(
|
|
70
|
+
while ((match = VAR_REGEX.exec(masked)) !== null) {
|
|
69
71
|
const raw = match[0];
|
|
72
|
+
if (raw !== value.slice(match.index, match.index + raw.length)) continue;
|
|
70
73
|
const name = match[1];
|
|
71
74
|
const fallback = match[2];
|
|
72
75
|
const suggestion = suggestVarSyntax(name, fallback);
|
|
@@ -94,7 +97,7 @@ var prefer_custom_property_syntax_default = createRule({
|
|
|
94
97
|
});
|
|
95
98
|
}
|
|
96
99
|
COLOR_PROP_REGEX.lastIndex = 0;
|
|
97
|
-
while ((match = COLOR_PROP_REGEX.exec(
|
|
100
|
+
while ((match = COLOR_PROP_REGEX.exec(masked)) !== null) {
|
|
98
101
|
const raw = match[0];
|
|
99
102
|
const suggestion = `#${match[1]}${match[2] ?? ""}`;
|
|
100
103
|
const start = match.index;
|
|
@@ -117,7 +120,7 @@ var prefer_custom_property_syntax_default = createRule({
|
|
|
117
120
|
});
|
|
118
121
|
}
|
|
119
122
|
KEYWORD_REGEX.lastIndex = 0;
|
|
120
|
-
while ((match = KEYWORD_REGEX.exec(
|
|
123
|
+
while ((match = KEYWORD_REGEX.exec(masked)) !== null) {
|
|
121
124
|
const start = match.index;
|
|
122
125
|
const end = start + match[0].length;
|
|
123
126
|
if (isInsideSpans(start, end, varSpans)) continue;
|
|
@@ -137,31 +140,9 @@ var prefer_custom_property_syntax_default = createRule({
|
|
|
137
140
|
});
|
|
138
141
|
}
|
|
139
142
|
}
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
if (prop.type !== "Property" || prop.computed) continue;
|
|
144
|
-
const key = getKeyName(prop.key);
|
|
145
|
-
if (key === null) continue;
|
|
146
|
-
if (/^[A-Z@&$#]/.test(key)) continue;
|
|
147
|
-
const str = getStringValue(prop.value);
|
|
148
|
-
if (str) {
|
|
149
|
-
checkValue(str, prop.value, key);
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
if (prop.value.type === "ObjectExpression") for (const stateProp of prop.value.properties) {
|
|
153
|
-
if (stateProp.type !== "Property") continue;
|
|
154
|
-
const stateStr = getStringValue(stateProp.value);
|
|
155
|
-
if (stateStr) checkValue(stateStr, stateProp.value, key);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return {
|
|
160
|
-
ImportDeclaration(node) {
|
|
161
|
-
ctx.trackImport(node);
|
|
162
|
-
},
|
|
163
|
-
...styleObjectListeners(handleStyleObject)
|
|
164
|
-
};
|
|
143
|
+
return stringStyleValueListeners(ctx, (value, node, property) => {
|
|
144
|
+
if (!/^[#$]/.test(property)) checkValue(value, node, property);
|
|
145
|
+
}, { requireTastyJSX: true });
|
|
165
146
|
}
|
|
166
147
|
});
|
|
167
148
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-custom-property-syntax.js","names":[],"sources":["../../src/rules/prefer-custom-property-syntax.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport {\n PROPERTIES_WITHOUT_COLOR_TOKEN_EXPANSION,\n PROPERTIES_WITHOUT_CUSTOM_PROPERTY_EXPANSION,\n} from '../constants.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport { replaceInStringValue } from '../fix-utils.js';\n\ntype MessageIds =\n | 'preferCustomPropertySyntax'\n | 'preferColorToken'\n | 'preferClearToken'\n | 'preferCurrentToken';\n\nconst VAR_REGEX = /var\\(\\s*--([a-zA-Z0-9_-]+)\\s*(?:,\\s*([^)]+))?\\s*\\)/g;\n// `$x-color` → `#x`. Negative lookbehind avoids `$$func` / `##transition`.\n// Optional opacity suffix.\n//\n// `-color` must END the identifier, hence the lookahead rather than `\\b`. `\\b`\n// matches between `color` and a following `-`, so `$purple-color-rgb` matched as\n// `$purple-color` and was rewritten to `#purple-rgb` — a property that does not\n// exist. That bit in practice: `var(--purple-color-rgb)` became `$purple-color-rgb`\n// on one --fix pass and `#purple-rgb` on the next, and the declaration was dropped\n// at computed-value time with no error anywhere.\nconst COLOR_PROP_REGEX =\n /(?<![$#])\\$([a-zA-Z][a-zA-Z0-9_-]*)-color(?![a-zA-Z0-9_-])(\\.[0-9]+|\\.\\$[a-zA-Z][a-zA-Z0-9_-]*)?/g;\nconst KEYWORD_REGEX = /\\b(transparent|currentColor)\\b/gi;\n\nfunction normalizeFallback(fallback: string): string {\n const trimmed = fallback.trim();\n const lower = trimmed.toLowerCase();\n if (lower === 'transparent') return '#clear';\n if (lower === 'currentcolor') return '#current';\n return trimmed;\n}\n\nfunction suggestVarSyntax(name: string, fallback?: string): string {\n if (name.endsWith('-color')) {\n const tokenName = name.slice(0, -'-color'.length);\n const tastyToken = `#${tokenName}`;\n if (fallback) {\n return `(${tastyToken}, ${normalizeFallback(fallback)})`;\n }\n return tastyToken;\n }\n\n const propToken = `$${name}`;\n if (fallback) {\n return `(${propToken}, ${normalizeFallback(fallback)})`;\n }\n return propToken;\n}\n\ninterface Span {\n start: number;\n end: number;\n}\n\nfunction isInsideSpans(start: number, end: number, spans: Span[]): boolean {\n for (const span of spans) {\n if (start >= span.start && end <= span.end) return true;\n }\n return false;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'prefer-custom-property-syntax',\n meta: {\n type: 'suggestion',\n fixable: 'code',\n docs: {\n description:\n 'Prefer Tasty token syntax ($prop, #color, #clear, #current) over raw CSS (var(), $x-color, transparent, currentColor)',\n },\n messages: {\n preferCustomPropertySyntax:\n \"Use Tasty token syntax '{{suggestion}}' instead of '{{raw}}'.\",\n preferColorToken:\n \"Use color token '{{suggestion}}' instead of custom property reference '{{raw}}'.\",\n preferClearToken:\n \"Use the '#clear' token instead of the 'transparent' keyword.\",\n preferCurrentToken:\n \"Use the '#current' token instead of the 'currentColor' keyword.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n /**\n * Whether rewriting to `suggestion` actually round-trips for `property`.\n *\n * Every rewrite this rule offers has to render to the same CSS as what it\n * replaces. Tasty expands `$name` and `#name` per property, not globally, so\n * a suggestion that is correct on `gap` can silently delete the declaration on\n * `fontFamily`. Suppress the report entirely rather than offer a broken fix —\n * the rule is autofixable and runs unattended in pre-commit hooks.\n */\n function roundTrips(property: string, suggestion: string): boolean {\n if (suggestion.startsWith('$')) {\n return !PROPERTIES_WITHOUT_CUSTOM_PROPERTY_EXPANSION.has(property);\n }\n\n if (suggestion.startsWith('#')) {\n return !PROPERTIES_WITHOUT_COLOR_TOKEN_EXPANSION.has(property);\n }\n\n // `(token, fallback)` auto-calc form — the token inside decides.\n if (suggestion.startsWith('(')) {\n const inner = suggestion.slice(1);\n\n return roundTrips(property, inner);\n }\n\n return true;\n }\n\n function checkValue(\n value: string,\n node: TSESTree.Node,\n property: string,\n ): void {\n // Pass 1: var(--x) / var(--x, fallback) → $x / (#x, fallback).\n // Records spans so standalone keywords inside var() aren't double-reported.\n const varSpans: Span[] = [];\n VAR_REGEX.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = VAR_REGEX.exec(value)) !== null) {\n const raw = match[0];\n const name = match[1];\n const fallback = match[2];\n const suggestion = suggestVarSyntax(name, fallback);\n const start = match.index;\n const end = start + raw.length;\n\n if (!roundTrips(property, suggestion)) continue;\n\n if (match.index !== undefined) {\n varSpans.push({\n start: match.index,\n end: match.index + raw.length,\n });\n }\n\n context.report({\n node,\n messageId: 'preferCustomPropertySyntax',\n data: { raw, suggestion },\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement: suggestion }],\n context.sourceCode,\n );\n },\n });\n }\n\n // Pass 2: $x-color → #x.\n COLOR_PROP_REGEX.lastIndex = 0;\n while ((match = COLOR_PROP_REGEX.exec(value)) !== null) {\n const raw = match[0];\n const name = match[1];\n const opacity = match[2] ?? '';\n const suggestion = `#${name}${opacity}`;\n const start = match.index;\n const end = start + raw.length;\n\n if (!roundTrips(property, suggestion)) continue;\n\n context.report({\n node,\n messageId: 'preferColorToken',\n data: { raw, suggestion },\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement: suggestion }],\n context.sourceCode,\n );\n },\n });\n }\n\n // Pass 3: standalone transparent / currentColor (skip inside var()).\n KEYWORD_REGEX.lastIndex = 0;\n while ((match = KEYWORD_REGEX.exec(value)) !== null) {\n const start = match.index;\n const end = start + match[0].length;\n if (isInsideSpans(start, end, varSpans)) continue;\n\n const lower = match[0].toLowerCase();\n const replacement = lower === 'transparent' ? '#clear' : '#current';\n\n if (!roundTrips(property, replacement)) continue;\n\n context.report({\n node,\n messageId:\n lower === 'transparent' ? 'preferClearToken' : 'preferCurrentToken',\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement }],\n context.sourceCode,\n );\n },\n });\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\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-Z@&$#]/.test(key)) continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkValue(str, prop.value, key);\n continue;\n }\n\n if (prop.value.type === 'ObjectExpression') {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateStr = getStringValue(stateProp.value);\n if (stateStr) {\n // State maps keep the outer property's expansion rules.\n checkValue(stateStr, stateProp.value, key);\n }\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;;AAgBA,MAAM,YAAY;AAUlB,MAAM,mBACJ;AACF,MAAM,gBAAgB;AAEtB,SAAS,kBAAkB,UAA0B;CACnD,MAAM,UAAU,SAAS,MAAM;CAC/B,MAAM,QAAQ,QAAQ,aAAa;AACnC,KAAI,UAAU,cAAe,QAAO;AACpC,KAAI,UAAU,eAAgB,QAAO;AACrC,QAAO;;AAGT,SAAS,iBAAiB,MAAc,UAA2B;AACjE,KAAI,KAAK,SAAS,SAAS,EAAE;EAE3B,MAAM,aAAa,IADD,KAAK,MAAM,GAAG,GAAiB;AAEjD,MAAI,SACF,QAAO,IAAI,WAAW,IAAI,kBAAkB,SAAS,CAAC;AAExD,SAAO;;CAGT,MAAM,YAAY,IAAI;AACtB,KAAI,SACF,QAAO,IAAI,UAAU,IAAI,kBAAkB,SAAS,CAAC;AAEvD,QAAO;;AAQT,SAAS,cAAc,OAAe,KAAa,OAAwB;AACzE,MAAK,MAAM,QAAQ,MACjB,KAAI,SAAS,KAAK,SAAS,OAAO,KAAK,IAAK,QAAO;AAErD,QAAO;;AAGT,4CAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,SAAS;EACT,MAAM,EACJ,aACE,yHACH;EACD,UAAU;GACR,4BACE;GACF,kBACE;GACF,kBACE;GACF,oBACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;;;;;;;;;;EAWrC,SAAS,WAAW,UAAkB,YAA6B;AACjE,OAAI,WAAW,WAAW,IAAI,CAC5B,QAAO,CAAC,6CAA6C,IAAI,SAAS;AAGpE,OAAI,WAAW,WAAW,IAAI,CAC5B,QAAO,CAAC,yCAAyC,IAAI,SAAS;AAIhE,OAAI,WAAW,WAAW,IAAI,CAG5B,QAAO,WAAW,UAFJ,WAAW,MAAM,EAAE,CAEC;AAGpC,UAAO;;EAGT,SAAS,WACP,OACA,MACA,UACM;GAGN,MAAM,WAAmB,EAAE;AAC3B,aAAU,YAAY;GACtB,IAAI;AACJ,WAAQ,QAAQ,UAAU,KAAK,MAAM,MAAM,MAAM;IAC/C,MAAM,MAAM,MAAM;IAClB,MAAM,OAAO,MAAM;IACnB,MAAM,WAAW,MAAM;IACvB,MAAM,aAAa,iBAAiB,MAAM,SAAS;IACnD,MAAM,QAAQ,MAAM;IACpB,MAAM,MAAM,QAAQ,IAAI;AAExB,QAAI,CAAC,WAAW,UAAU,WAAW,CAAE;AAEvC,QAAI,MAAM,UAAU,OAClB,UAAS,KAAK;KACZ,OAAO,MAAM;KACb,KAAK,MAAM,QAAQ,IAAI;KACxB,CAAC;AAGJ,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MAAE;MAAK;MAAY;KACzB,IAAI,OAAO;AACT,aAAO,qBACL,OACA,MACA,CAAC;OAAE;OAAO;OAAK,aAAa;OAAY,CAAC,EACzC,QAAQ,WACT;;KAEJ,CAAC;;AAIJ,oBAAiB,YAAY;AAC7B,WAAQ,QAAQ,iBAAiB,KAAK,MAAM,MAAM,MAAM;IACtD,MAAM,MAAM,MAAM;IAGlB,MAAM,aAAa,IAFN,MAAM,KACH,MAAM,MAAM;IAE5B,MAAM,QAAQ,MAAM;IACpB,MAAM,MAAM,QAAQ,IAAI;AAExB,QAAI,CAAC,WAAW,UAAU,WAAW,CAAE;AAEvC,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MAAE;MAAK;MAAY;KACzB,IAAI,OAAO;AACT,aAAO,qBACL,OACA,MACA,CAAC;OAAE;OAAO;OAAK,aAAa;OAAY,CAAC,EACzC,QAAQ,WACT;;KAEJ,CAAC;;AAIJ,iBAAc,YAAY;AAC1B,WAAQ,QAAQ,cAAc,KAAK,MAAM,MAAM,MAAM;IACnD,MAAM,QAAQ,MAAM;IACpB,MAAM,MAAM,QAAQ,MAAM,GAAG;AAC7B,QAAI,cAAc,OAAO,KAAK,SAAS,CAAE;IAEzC,MAAM,QAAQ,MAAM,GAAG,aAAa;IACpC,MAAM,cAAc,UAAU,gBAAgB,WAAW;AAEzD,QAAI,CAAC,WAAW,UAAU,YAAY,CAAE;AAExC,YAAQ,OAAO;KACb;KACA,WACE,UAAU,gBAAgB,qBAAqB;KACjD,IAAI,OAAO;AACT,aAAO,qBACL,OACA,MACA,CAAC;OAAE;OAAO;OAAK;OAAa,CAAC,EAC7B,QAAQ,WACT;;KAEJ,CAAC;;;EAIN,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAE/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,QAAQ,KAAM;AAClB,QAAI,aAAa,KAAK,IAAI,CAAE;IAE5B,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,QAAI,KAAK;AACP,gBAAW,KAAK,KAAK,OAAO,IAAI;AAChC;;AAGF,QAAI,KAAK,MAAM,SAAS,mBACtB,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;KACnC,MAAM,WAAW,eAAe,UAAU,MAAM;AAChD,SAAI,SAEF,YAAW,UAAU,UAAU,OAAO,IAAI;;;;AAOpD,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|
|
1
|
+
{"version":3,"file":"prefer-custom-property-syntax.js","names":[],"sources":["../../src/rules/prefer-custom-property-syntax.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { stringStyleValueListeners } from '../style-values.js';\nimport {\n PROPERTIES_WITHOUT_COLOR_TOKEN_EXPANSION,\n PROPERTIES_WITHOUT_CUSTOM_PROPERTY_EXPANSION,\n} from '../constants.js';\nimport { replaceInStringValue } from '../fix-utils.js';\nimport { maskValueLiterals } from '../value-words.js';\n\ntype MessageIds =\n | 'preferCustomPropertySyntax'\n | 'preferColorToken'\n | 'preferClearToken'\n | 'preferCurrentToken';\n\nconst VAR_REGEX = /var\\(\\s*--([a-zA-Z0-9_-]+)\\s*(?:,\\s*([^)]+))?\\s*\\)/g;\n// `$x-color` → `#x`. Negative lookbehind avoids `$$func` / `##transition`.\n// Optional opacity suffix.\n//\n// `-color` must END the identifier, hence the lookahead rather than `\\b`. `\\b`\n// matches between `color` and a following `-`, so `$purple-color-rgb` matched as\n// `$purple-color` and was rewritten to `#purple-rgb` — a property that does not\n// exist. That bit in practice: `var(--purple-color-rgb)` became `$purple-color-rgb`\n// on one --fix pass and `#purple-rgb` on the next, and the declaration was dropped\n// at computed-value time with no error anywhere.\nconst COLOR_PROP_REGEX =\n /(?<![$#])\\$([a-zA-Z][a-zA-Z0-9_-]*)-color(?![a-zA-Z0-9_-])(\\.[0-9]+|\\.\\$[a-zA-Z][a-zA-Z0-9_-]*)?/g;\nconst KEYWORD_REGEX = /(?<![\\w$#-])(transparent|currentColor)(?![\\w-])/gi;\n\nfunction normalizeFallback(fallback: string): string {\n const trimmed = fallback.trim();\n const lower = trimmed.toLowerCase();\n if (lower === 'transparent') return '#clear';\n if (lower === 'currentcolor') return '#current';\n return trimmed;\n}\n\nfunction suggestVarSyntax(name: string, fallback?: string): string {\n if (name.endsWith('-color')) {\n const tokenName = name.slice(0, -'-color'.length);\n const tastyToken = `#${tokenName}`;\n if (fallback) {\n return `(${tastyToken}, ${normalizeFallback(fallback)})`;\n }\n return tastyToken;\n }\n\n const propToken = `$${name}`;\n if (fallback) {\n return `(${propToken}, ${normalizeFallback(fallback)})`;\n }\n return propToken;\n}\n\ninterface Span {\n start: number;\n end: number;\n}\n\nfunction isInsideSpans(start: number, end: number, spans: Span[]): boolean {\n for (const span of spans) {\n if (start >= span.start && end <= span.end) return true;\n }\n return false;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'prefer-custom-property-syntax',\n meta: {\n type: 'suggestion',\n fixable: 'code',\n docs: {\n description:\n 'Prefer Tasty token syntax ($prop, #color, #clear, #current) over raw CSS (var(), $x-color, transparent, currentColor)',\n },\n messages: {\n preferCustomPropertySyntax:\n \"Use Tasty token syntax '{{suggestion}}' instead of '{{raw}}'.\",\n preferColorToken:\n \"Use color token '{{suggestion}}' instead of custom property reference '{{raw}}'.\",\n preferClearToken:\n \"Use the '#clear' token instead of the 'transparent' keyword.\",\n preferCurrentToken:\n \"Use the '#current' token instead of the 'currentColor' keyword.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n /**\n * Whether rewriting to `suggestion` actually round-trips for `property`.\n *\n * Every rewrite this rule offers has to render to the same CSS as what it\n * replaces. Tasty expands `$name` and `#name` per property, not globally, so\n * a suggestion that is correct on `gap` can silently delete the declaration on\n * `fontFamily`. Suppress the report entirely rather than offer a broken fix —\n * the rule is autofixable and runs unattended in pre-commit hooks.\n */\n function roundTrips(property: string, suggestion: string): boolean {\n if (suggestion.startsWith('$')) {\n return !PROPERTIES_WITHOUT_CUSTOM_PROPERTY_EXPANSION.has(property);\n }\n\n if (suggestion.startsWith('#')) {\n return !PROPERTIES_WITHOUT_COLOR_TOKEN_EXPANSION.has(property);\n }\n\n // `(token, fallback)` auto-calc form — the token inside decides.\n if (suggestion.startsWith('(')) {\n const inner = suggestion.slice(1);\n\n return roundTrips(property, inner);\n }\n\n return true;\n }\n\n function checkValue(\n value: string,\n node: TSESTree.Node,\n property: string,\n ): void {\n const masked = maskValueLiterals(value);\n // Pass 1: var(--x) / var(--x, fallback) → $x / (#x, fallback).\n // Records spans so standalone keywords inside var() aren't double-reported.\n const varSpans: Span[] = [];\n VAR_REGEX.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = VAR_REGEX.exec(masked)) !== null) {\n const raw = match[0];\n if (raw !== value.slice(match.index, match.index + raw.length))\n continue;\n const name = match[1];\n const fallback = match[2];\n const suggestion = suggestVarSyntax(name, fallback);\n const start = match.index;\n const end = start + raw.length;\n\n if (!roundTrips(property, suggestion)) continue;\n\n if (match.index !== undefined) {\n varSpans.push({\n start: match.index,\n end: match.index + raw.length,\n });\n }\n\n context.report({\n node,\n messageId: 'preferCustomPropertySyntax',\n data: { raw, suggestion },\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement: suggestion }],\n context.sourceCode,\n );\n },\n });\n }\n\n // Pass 2: $x-color → #x.\n COLOR_PROP_REGEX.lastIndex = 0;\n while ((match = COLOR_PROP_REGEX.exec(masked)) !== null) {\n const raw = match[0];\n const name = match[1];\n const opacity = match[2] ?? '';\n const suggestion = `#${name}${opacity}`;\n const start = match.index;\n const end = start + raw.length;\n\n if (!roundTrips(property, suggestion)) continue;\n\n context.report({\n node,\n messageId: 'preferColorToken',\n data: { raw, suggestion },\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement: suggestion }],\n context.sourceCode,\n );\n },\n });\n }\n\n // Pass 3: standalone transparent / currentColor (skip inside var()).\n KEYWORD_REGEX.lastIndex = 0;\n while ((match = KEYWORD_REGEX.exec(masked)) !== null) {\n const start = match.index;\n const end = start + match[0].length;\n if (isInsideSpans(start, end, varSpans)) continue;\n\n const lower = match[0].toLowerCase();\n const replacement = lower === 'transparent' ? '#clear' : '#current';\n\n if (!roundTrips(property, replacement)) continue;\n\n context.report({\n node,\n messageId:\n lower === 'transparent' ? 'preferClearToken' : 'preferCurrentToken',\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement }],\n context.sourceCode,\n );\n },\n });\n }\n }\n\n return stringStyleValueListeners(\n ctx,\n (value, node, property) => {\n if (!/^[#$]/.test(property)) checkValue(value, node, property);\n },\n { requireTastyJSX: true },\n );\n },\n});\n"],"mappings":"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"}
|