@tenphi/eslint-plugin-tasty 1.5.2 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +160 -3
  2. package/dist/configs.js +3 -1
  3. package/dist/configs.js.map +1 -1
  4. package/dist/constants.js +1 -1
  5. package/dist/context.js +51 -28
  6. package/dist/context.js.map +1 -1
  7. package/dist/index.d.ts +2 -1
  8. package/dist/index.js +4 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/rules/consistent-token-usage.js +88 -87
  11. package/dist/rules/consistent-token-usage.js.map +1 -1
  12. package/dist/rules/no-classname-prop.js +27 -0
  13. package/dist/rules/no-classname-prop.js.map +1 -0
  14. package/dist/rules/no-important.js +23 -28
  15. package/dist/rules/no-important.js.map +1 -1
  16. package/dist/rules/no-raw-color-values.js +40 -59
  17. package/dist/rules/no-raw-color-values.js.map +1 -1
  18. package/dist/rules/no-raw-motion-duration.js +6 -24
  19. package/dist/rules/no-raw-motion-duration.js.map +1 -1
  20. package/dist/rules/no-raw-transition-duration.js +7 -49
  21. package/dist/rules/no-raw-transition-duration.js.map +1 -1
  22. package/dist/rules/no-runtime-styles-mutation.js +2 -2
  23. package/dist/rules/no-runtime-styles-mutation.js.map +1 -1
  24. package/dist/rules/no-state-in-selector.js +1 -1
  25. package/dist/rules/no-state-in-selector.js.map +1 -1
  26. package/dist/rules/no-style-prop.js +27 -0
  27. package/dist/rules/no-style-prop.js.map +1 -0
  28. package/dist/rules/no-style-spread.js +2 -2
  29. package/dist/rules/no-style-spread.js.map +1 -1
  30. package/dist/rules/no-styles-prop.js +7 -5
  31. package/dist/rules/no-styles-prop.js.map +1 -1
  32. package/dist/rules/prefer-auto-calc.js +5 -27
  33. package/dist/rules/prefer-auto-calc.js.map +1 -1
  34. package/dist/rules/prefer-custom-property-syntax.js +12 -31
  35. package/dist/rules/prefer-custom-property-syntax.js.map +1 -1
  36. package/dist/rules/prefer-shorthand-property.js +2 -2
  37. package/dist/rules/prefer-shorthand-property.js.map +1 -1
  38. package/dist/rules/tasty-component-props.js +33 -0
  39. package/dist/rules/tasty-component-props.js.map +1 -0
  40. package/dist/rules/valid-boolean-property.js +6 -26
  41. package/dist/rules/valid-boolean-property.js.map +1 -1
  42. package/dist/rules/valid-color-token.js +12 -29
  43. package/dist/rules/valid-color-token.js.map +1 -1
  44. package/dist/rules/valid-custom-property.js +12 -28
  45. package/dist/rules/valid-custom-property.js.map +1 -1
  46. package/dist/rules/valid-custom-unit.js +10 -27
  47. package/dist/rules/valid-custom-unit.js.map +1 -1
  48. package/dist/rules/valid-directional-modifier.js +5 -27
  49. package/dist/rules/valid-directional-modifier.js.map +1 -1
  50. package/dist/rules/valid-preset.js +5 -29
  51. package/dist/rules/valid-preset.js.map +1 -1
  52. package/dist/rules/valid-radius-shape.js +5 -25
  53. package/dist/rules/valid-radius-shape.js.map +1 -1
  54. package/dist/rules/valid-recipe.js +5 -20
  55. package/dist/rules/valid-recipe.js.map +1 -1
  56. package/dist/rules/valid-transition.js +5 -25
  57. package/dist/rules/valid-transition.js.map +1 -1
  58. package/dist/rules/valid-value.js +5 -31
  59. package/dist/rules/valid-value.js.map +1 -1
  60. package/dist/style-values.js +79 -0
  61. package/dist/style-values.js.map +1 -0
  62. package/dist/type-aware-jsx.d.ts +9 -0
  63. package/dist/type-aware-jsx.js +275 -0
  64. package/dist/type-aware-jsx.js.map +1 -0
  65. package/dist/utils.js +1 -10
  66. package/dist/utils.js.map +1 -1
  67. package/dist/value-words.js +76 -0
  68. package/dist/value-words.js.map +1 -0
  69. package/package.json +6 -2
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-shorthand-property.js","names":[],"sources":["../../src/rules/prefer-shorthand-property.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport type { StyleContext } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport { SHORTHAND_MAPPING } from '../constants.js';\n\ntype MessageIds = 'preferShorthand' | 'preferShorthandExtending';\n\nconst CSS_WIDE_KEYWORDS = new Set([\n 'inherit',\n 'initial',\n 'unset',\n 'revert',\n 'revert-layer',\n]);\n\n/**\n * `font` resolves to `<value>, var(--font-sans, var(--font-sans-fallback))`,\n * so it cannot express a CSS-wide keyword — `font: 'inherit'` emits\n * `font-family: inherit, …`, which is invalid and drops the inherit.\n *\n * `preset` is the property that handles these: a CSS-wide keyword used as the\n * preset name short-circuits token lookup and is emitted verbatim across the\n * whole typography group, so `preset: 'inherit'` yields a real\n * `font-family: inherit`. Point at that instead of `font`.\n */\nfunction shorthandMapping(key: string, prop: TSESTree.Property) {\n const mapping = SHORTHAND_MAPPING[key];\n if (!mapping) return null;\n if (key !== 'fontFamily') return mapping;\n\n const value = getStringValue(prop.value)?.trim().toLowerCase();\n return value && CSS_WIDE_KEYWORDS.has(value)\n ? { ...mapping, property: 'preset', hint: `preset: '${value}'` }\n : mapping;\n}\n\nfunction tokenOverrideHint(\n key: string,\n property: string,\n styleCtx: StyleContext,\n isSubElement: boolean,\n): string {\n const usesTokensProp = styleCtx.type === 'tasty';\n const original = styleCtx.isExtending\n ? usesTokensProp\n ? `base component's`\n : `base style definition's`\n : 'original';\n const warning = styleCtx.isExtending\n ? ''\n : ` Replacing '${property}' can reset its other values.`;\n const defaultHint =\n usesTokensProp && isSubElement\n ? ' Declare the default on the component root so this sub-element inherits the token.'\n : '';\n const dimension = /^(min|max)(Width|Height|BlockSize|InlineSize)$/.exec(key);\n\n if (dimension) {\n const token = `$${key.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`)}`;\n const values =\n dimension[1] === 'min'\n ? `${token} auto initial`\n : `initial auto ${token}`;\n const override = usesTokensProp\n ? `set tokens={{ '${token}': '20x' }}`\n : `set the '${token.replace('$', '--')}' CSS custom property on the target element to a CSS value (e.g. '160px')`;\n\n return `${warning} To override only '${key}', expose a token with a default in the ${original} '${property}' (e.g. ${property}: '${values}'), keep its other values, and ${override}.${defaultHint}`;\n }\n\n const override = usesTokensProp\n ? `set it via the 'tokens' prop`\n : 'set its CSS custom property on the target element';\n\n if (property === 'preset') {\n return `${warning} For independent typography overrides, use the CSS value tokens referenced by a named preset in the ${original} styles, keep its other values, and ${override}. Keep preset names and modifiers static.${defaultHint}`;\n }\n\n if (key === 'scrollbarWidth' || key === 'scrollbarGutter') {\n return `${warning} For independent overrides, expose the value as a token with a default in the ${original} '${key}' longhand, keep its other values, and ${override}. '${property}' width and gutter modifiers are static.${defaultHint}`;\n }\n\n return `${warning} For independent overrides, expose the part you need to change as a token with a default in the ${original} '${property}', keep its other values, and ${override}.${defaultHint}`;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'prefer-shorthand-property',\n meta: {\n type: 'suggestion',\n fixable: 'code',\n docs: {\n description:\n 'Suggest tasty shorthand when a native CSS property with a tasty alternative is used',\n },\n messages: {\n preferShorthand:\n \"When defining the complete style, prefer tasty shorthand '{{alternative}}' instead of '{{native}}'.{{overrideHint}}\",\n preferShorthandExtending:\n \"'{{native}}' changes one part of the base styles. '{{alternative}}' would replace the whole '{{property}}'.{{overrideHint}}\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n const styleCtx = ctx.getStyleContext(node);\n if (!styleCtx) return;\n const isSubElement = ctx.isInsideSubElement(node);\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\n const mapping = shorthandMapping(key, prop);\n if (mapping) {\n const hint = mapping.hint;\n // An extension layer merges per key, so renaming the key replaces the\n // base component's whole `mapping.property` instead of patching the\n // one part written here — `paddingTop: '2x'` -> `padding: '2x top'`\n // drops the base's other three edges to 0. The token seam belongs in\n // the base component, which is another file and may not even be the\n // author's to edit, so this stays a report with no fix.\n if (styleCtx.isExtending) {\n // The token seam has to be added to the base component's own\n // definition, so the suggestion is only actionable when that file\n // belongs to this project. Over an imported base — a UI kit, say —\n // the author's remaining options are the longhand they already\n // wrote or a rewrite that would clobber the base, and a warning\n // recommending neither is just noise. `ownedSources` opts a\n // published-from-this-monorepo design system back in.\n if (!ctx.isOwnedComponent(styleCtx.baseComponent)) continue;\n\n context.report({\n node: prop.key,\n messageId: 'preferShorthandExtending',\n data: {\n native: key,\n alternative: hint,\n property: mapping.property,\n overrideHint: tokenOverrideHint(\n key,\n mapping.property,\n styleCtx,\n isSubElement,\n ),\n },\n });\n\n continue;\n }\n\n context.report({\n node: prop.key,\n messageId: 'preferShorthand',\n data: {\n native: key,\n alternative: hint,\n overrideHint: mapping.safeFix\n ? ''\n : tokenOverrideHint(\n key,\n mapping.property,\n styleCtx,\n isSubElement,\n ),\n },\n fix(fixer) {\n // Only auto-fix the carry-over subset where the value passes\n // through unchanged (e.g. backgroundColor → fill, borderRadius →\n // radius). Directional / min/max / border-* renames change\n // semantics and stay report-only.\n if (!mapping.safeFix) return null;\n return fixer.replaceText(prop.key, mapping.property);\n },\n });\n }\n }\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":"prefer-shorthand-property.js","names":[],"sources":["../../src/rules/prefer-shorthand-property.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport type { StyleContext } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport { SHORTHAND_MAPPING } from '../constants.js';\n\ntype MessageIds = 'preferShorthand' | 'preferShorthandExtending';\n\nconst CSS_WIDE_KEYWORDS = new Set([\n 'inherit',\n 'initial',\n 'unset',\n 'revert',\n 'revert-layer',\n]);\n\n/**\n * `font` resolves to `<value>, var(--font-sans, var(--font-sans-fallback))`,\n * so it cannot express a CSS-wide keyword — `font: 'inherit'` emits\n * `font-family: inherit, …`, which is invalid and drops the inherit.\n *\n * `preset` is the property that handles these: a CSS-wide keyword used as the\n * preset name short-circuits token lookup and is emitted verbatim across the\n * whole typography group, so `preset: 'inherit'` yields a real\n * `font-family: inherit`. Point at that instead of `font`.\n */\nfunction shorthandMapping(key: string, prop: TSESTree.Property) {\n const mapping = SHORTHAND_MAPPING[key];\n if (!mapping) return null;\n if (key !== 'fontFamily') return mapping;\n\n const value = getStringValue(prop.value)?.trim().toLowerCase();\n return value && CSS_WIDE_KEYWORDS.has(value)\n ? { ...mapping, property: 'preset', hint: `preset: '${value}'` }\n : mapping;\n}\n\nfunction tokenOverrideHint(\n key: string,\n property: string,\n styleCtx: StyleContext,\n isSubElement: boolean,\n): string {\n const usesTokensProp = styleCtx.type === 'tasty';\n const original = styleCtx.isExtending\n ? usesTokensProp\n ? `base component's`\n : `base style definition's`\n : 'original';\n const warning = styleCtx.isExtending\n ? ''\n : ` Replacing '${property}' can reset its other values.`;\n const defaultHint =\n usesTokensProp && isSubElement\n ? ' Declare the default on the component root so this sub-element inherits the token.'\n : '';\n const dimension = /^(min|max)(Width|Height|BlockSize|InlineSize)$/.exec(key);\n\n if (dimension) {\n const token = `$${key.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`)}`;\n const values =\n dimension[1] === 'min'\n ? `${token} auto initial`\n : `initial auto ${token}`;\n const override = usesTokensProp\n ? `set tokens={{ '${token}': '20x' }}`\n : `set the '${token.replace('$', '--')}' CSS custom property on the target element to a CSS value (e.g. '160px')`;\n\n return `${warning} To override only '${key}', expose a token with a default in the ${original} '${property}' (e.g. ${property}: '${values}'), keep its other values, and ${override}.${defaultHint}`;\n }\n\n const override = usesTokensProp\n ? `set it via the 'tokens' prop`\n : 'set its CSS custom property on the target element';\n\n if (property === 'preset') {\n return `${warning} For independent typography overrides, use the CSS value tokens referenced by a named preset in the ${original} styles, keep its other values, and ${override}. Keep preset names and modifiers static.${defaultHint}`;\n }\n\n if (key === 'scrollbarWidth' || key === 'scrollbarGutter') {\n return `${warning} For independent overrides, expose the value as a token with a default in the ${original} '${key}' longhand, keep its other values, and ${override}. '${property}' width and gutter modifiers are static.${defaultHint}`;\n }\n\n return `${warning} For independent overrides, expose the part you need to change as a token with a default in the ${original} '${property}', keep its other values, and ${override}.${defaultHint}`;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'prefer-shorthand-property',\n meta: {\n type: 'suggestion',\n fixable: 'code',\n docs: {\n description:\n 'Suggest tasty shorthand when a native CSS property with a tasty alternative is used',\n },\n messages: {\n preferShorthand:\n \"When defining the complete style, prefer tasty shorthand '{{alternative}}' instead of '{{native}}'.{{overrideHint}} If this longhand is intentional, explicitly disable tasty/prefer-shorthand-property on this line with a reason.\",\n preferShorthandExtending:\n \"'{{native}}' changes one part of the base styles. '{{alternative}}' would replace the whole '{{property}}'.{{overrideHint}} If this longhand is intentional, explicitly disable tasty/prefer-shorthand-property on this line with a reason.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n const styleCtx = ctx.getStyleContext(node);\n if (!styleCtx) return;\n const isSubElement = ctx.isInsideSubElement(node);\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\n const mapping = shorthandMapping(key, prop);\n if (mapping) {\n const hint = mapping.hint;\n // An extension layer merges per key, so renaming the key replaces the\n // base component's whole `mapping.property` instead of patching the\n // one part written here — `paddingTop: '2x'` -> `padding: '2x top'`\n // drops the base's other three edges to 0. The token seam belongs in\n // the base component, which is another file and may not even be the\n // author's to edit, so this stays a report with no fix.\n if (styleCtx.isExtending) {\n // The token seam has to be added to the base component's own\n // definition, so the suggestion is only actionable when that file\n // belongs to this project. Over an imported base — a UI kit, say —\n // the author's remaining options are the longhand they already\n // wrote or a rewrite that would clobber the base, and a warning\n // recommending neither is just noise. `ownedSources` opts a\n // published-from-this-monorepo design system back in.\n if (!ctx.isOwnedComponent(styleCtx.baseComponent)) continue;\n\n context.report({\n node: prop.key,\n messageId: 'preferShorthandExtending',\n data: {\n native: key,\n alternative: hint,\n property: mapping.property,\n overrideHint: tokenOverrideHint(\n key,\n mapping.property,\n styleCtx,\n isSubElement,\n ),\n },\n });\n\n continue;\n }\n\n context.report({\n node: prop.key,\n messageId: 'preferShorthand',\n data: {\n native: key,\n alternative: hint,\n overrideHint: mapping.safeFix\n ? ''\n : tokenOverrideHint(\n key,\n mapping.property,\n styleCtx,\n isSubElement,\n ),\n },\n fix(fixer) {\n // Only auto-fix the carry-over subset where the value passes\n // through unchanged (e.g. backgroundColor → fill, borderRadius →\n // radius). Directional / min/max / border-* renames change\n // semantics and stay report-only.\n if (!mapping.safeFix) return null;\n return fixer.replaceText(prop.key, mapping.property);\n },\n });\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import { getKeyName, unwrapExpression } from "../utils.js";
2
+
3
+ //#region src/rules/tasty-component-props.ts
4
+ /** Inspect explicit props and inline object spreads without evaluating spread bindings. */
5
+ function tastyComponentPropListeners(ctx, check) {
6
+ function visitSpread(node) {
7
+ node = unwrapExpression(node);
8
+ if (node.type !== "ObjectExpression") return;
9
+ for (const prop of node.properties) if (prop.type === "SpreadElement") visitSpread(prop.argument);
10
+ else {
11
+ const name = prop.computed ? getKeyName(unwrapExpression(prop.key)) : getKeyName(prop.key);
12
+ if (name !== null && (!prop.computed || unwrapExpression(prop.key).type === "Literal")) check(name, prop);
13
+ }
14
+ }
15
+ return {
16
+ ImportDeclaration(node) {
17
+ ctx.trackImport(node);
18
+ },
19
+ JSXOpeningElement(node) {
20
+ let name = node.name;
21
+ while (name.type === "JSXMemberExpression") name = name.object;
22
+ if (name.type !== "JSXIdentifier") return;
23
+ if (node.name.type === "JSXIdentifier" && !/^[A-Z]/.test(name.name)) return;
24
+ if (!ctx.isTastyJSXComponent(node, name.name)) return;
25
+ for (const attribute of node.attributes) if (attribute.type === "JSXSpreadAttribute") visitSpread(attribute.argument);
26
+ else if (attribute.name.type === "JSXIdentifier") check(attribute.name.name, attribute);
27
+ }
28
+ };
29
+ }
30
+
31
+ //#endregion
32
+ export { tastyComponentPropListeners };
33
+ //# sourceMappingURL=tasty-component-props.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tasty-component-props.js","names":[],"sources":["../../src/rules/tasty-component-props.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport type { TastyContext } from '../context.js';\nimport { getKeyName, unwrapExpression } from '../utils.js';\n\n/** Inspect explicit props and inline object spreads without evaluating spread bindings. */\nexport function tastyComponentPropListeners(\n ctx: TastyContext,\n check: (name: string, node: TSESTree.Node) => void,\n) {\n function visitSpread(node: TSESTree.Node): void {\n node = unwrapExpression(node);\n if (node.type !== 'ObjectExpression') return;\n for (const prop of node.properties) {\n if (prop.type === 'SpreadElement') {\n visitSpread(prop.argument);\n } else {\n const name = prop.computed\n ? getKeyName(unwrapExpression(prop.key))\n : getKeyName(prop.key);\n // A computed identifier is dynamic; a computed string literal is visible.\n if (\n name !== null &&\n (!prop.computed || unwrapExpression(prop.key).type === 'Literal')\n ) {\n check(name, prop);\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node: TSESTree.ImportDeclaration) {\n ctx.trackImport(node);\n },\n JSXOpeningElement(node: TSESTree.JSXOpeningElement) {\n let name = node.name;\n while (name.type === 'JSXMemberExpression') name = name.object;\n if (name.type !== 'JSXIdentifier') return;\n if (node.name.type === 'JSXIdentifier' && !/^[A-Z]/.test(name.name))\n return;\n if (!ctx.isTastyJSXComponent(node, name.name)) return;\n for (const attribute of node.attributes) {\n if (attribute.type === 'JSXSpreadAttribute')\n visitSpread(attribute.argument);\n else if (attribute.name.type === 'JSXIdentifier')\n check(attribute.name.name, attribute);\n }\n },\n };\n}\n"],"mappings":";;;;AAKA,SAAgB,4BACd,KACA,OACA;CACA,SAAS,YAAY,MAA2B;AAC9C,SAAO,iBAAiB,KAAK;AAC7B,MAAI,KAAK,SAAS,mBAAoB;AACtC,OAAK,MAAM,QAAQ,KAAK,WACtB,KAAI,KAAK,SAAS,gBAChB,aAAY,KAAK,SAAS;OACrB;GACL,MAAM,OAAO,KAAK,WACd,WAAW,iBAAiB,KAAK,IAAI,CAAC,GACtC,WAAW,KAAK,IAAI;AAExB,OACE,SAAS,SACR,CAAC,KAAK,YAAY,iBAAiB,KAAK,IAAI,CAAC,SAAS,WAEvD,OAAM,MAAM,KAAK;;;AAMzB,QAAO;EACL,kBAAkB,MAAkC;AAClD,OAAI,YAAY,KAAK;;EAEvB,kBAAkB,MAAkC;GAClD,IAAI,OAAO,KAAK;AAChB,UAAO,KAAK,SAAS,sBAAuB,QAAO,KAAK;AACxD,OAAI,KAAK,SAAS,gBAAiB;AACnC,OAAI,KAAK,KAAK,SAAS,mBAAmB,CAAC,SAAS,KAAK,KAAK,KAAK,CACjE;AACF,OAAI,CAAC,IAAI,oBAAoB,MAAM,KAAK,KAAK,CAAE;AAC/C,QAAK,MAAM,aAAa,KAAK,WAC3B,KAAI,UAAU,SAAS,qBACrB,aAAY,UAAU,SAAS;YACxB,UAAU,KAAK,SAAS,gBAC/B,OAAM,UAAU,KAAK,MAAM,UAAU;;EAG5C"}
@@ -1,7 +1,7 @@
1
1
  import { createRule } from "../create-rule.js";
2
2
  import { BOOLEAN_TRUE_PROPERTIES } from "../constants.js";
3
- import { getKeyName } from "../utils.js";
4
- import { TastyContext, styleObjectListeners } from "../context.js";
3
+ import { TastyContext } from "../context.js";
4
+ import { styleValueListeners } from "../style-values.js";
5
5
 
6
6
  //#region src/rules/valid-boolean-property.ts
7
7
  var valid_boolean_property_default = createRule({
@@ -25,30 +25,10 @@ var valid_boolean_property_default = createRule({
25
25
  }
26
26
  });
27
27
  }
28
- function handleStyleObject(node) {
29
- if (!ctx.isStyleObject(node)) return;
30
- for (const prop of node.properties) {
31
- if (prop.type !== "Property" || prop.computed) continue;
32
- const key = getKeyName(prop.key);
33
- if (key === null) continue;
34
- if (/^[A-Z@&$#]/.test(key)) continue;
35
- if (BOOLEAN_TRUE_PROPERTIES.has(key)) continue;
36
- if (prop.value.type === "Literal" && prop.value.value === true) {
37
- report(key, prop.value);
38
- continue;
39
- }
40
- if (prop.value.type === "ObjectExpression" && ctx.isStateMap(prop.value, prop)) for (const stateProp of prop.value.properties) {
41
- if (stateProp.type !== "Property") continue;
42
- if (stateProp.value.type === "Literal" && stateProp.value.value === true) report(key, stateProp.value);
43
- }
44
- }
45
- }
46
- return {
47
- ImportDeclaration(node) {
48
- ctx.trackImport(node);
49
- },
50
- ...styleObjectListeners(handleStyleObject)
51
- };
28
+ return styleValueListeners(ctx, (key, node) => {
29
+ if (/^[#$]/.test(key) || BOOLEAN_TRUE_PROPERTIES.has(key)) return;
30
+ if (node.type === "Literal" && node.value === true || node.type === "JSXAttribute") report(key, node);
31
+ });
52
32
  }
53
33
  });
54
34
 
@@ -1 +1 @@
1
- {"version":3,"file":"valid-boolean-property.js","names":[],"sources":["../../src/rules/valid-boolean-property.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName } from '../utils.js';\nimport { BOOLEAN_TRUE_PROPERTIES } from '../constants.js';\n\ntype MessageIds = 'invalidBooleanTrue';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-boolean-property',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Validate that true/false values are only used on properties that support them',\n },\n messages: {\n invalidBooleanTrue:\n \"Property '{{name}}' does not accept boolean true. Only these properties support it: {{allowed}}.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function report(name: string, node: TSESTree.Node): void {\n context.report({\n node,\n messageId: 'invalidBooleanTrue',\n data: { name, allowed: [...BOOLEAN_TRUE_PROPERTIES].join(', ') },\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\n // Skip sub-elements and special keys\n if (/^[A-Z@&$#]/.test(key)) continue;\n\n if (BOOLEAN_TRUE_PROPERTIES.has(key)) continue;\n\n // Check for true literal — false is always valid (tombstone)\n if (prop.value.type === 'Literal' && prop.value.value === true) {\n report(key, prop.value);\n continue;\n }\n\n // A state map holds values for the same property, so `true` is exactly\n // as unsupported in one as it is written directly — and rather more\n // likely, since a per-state default (`fill: { '': '#clear', hovered:\n // true }`) is the shape that reads as if it should work.\n //\n // Only genuine state maps: a sub-element object is a style object in its\n // own right and is visited by the listener, so descending into one here\n // would attribute its keys to the outer property.\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 if (\n stateProp.value.type === 'Literal' &&\n stateProp.value.value === true\n ) {\n report(key, stateProp.value);\n }\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;AAQA,qCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,iFACH;EACD,UAAU,EACR,oBACE,oGACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,OAAO,MAAc,MAA2B;AACvD,WAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM;KAAE;KAAM,SAAS,CAAC,GAAG,wBAAwB,CAAC,KAAK,KAAK;KAAE;IACjE,CAAC;;EAGJ,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;AAGlB,QAAI,aAAa,KAAK,IAAI,CAAE;AAE5B,QAAI,wBAAwB,IAAI,IAAI,CAAE;AAGtC,QAAI,KAAK,MAAM,SAAS,aAAa,KAAK,MAAM,UAAU,MAAM;AAC9D,YAAO,KAAK,KAAK,MAAM;AACvB;;AAWF,QACE,KAAK,MAAM,SAAS,sBACpB,IAAI,WAAW,KAAK,OAAO,KAAK,CAEhC,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;AACnC,SACE,UAAU,MAAM,SAAS,aACzB,UAAU,MAAM,UAAU,KAE1B,QAAO,KAAK,UAAU,MAAM;;;;AAOtC,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAGvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
1
+ {"version":3,"file":"valid-boolean-property.js","names":[],"sources":["../../src/rules/valid-boolean-property.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { styleValueListeners } from '../style-values.js';\nimport { BOOLEAN_TRUE_PROPERTIES } from '../constants.js';\n\ntype MessageIds = 'invalidBooleanTrue';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-boolean-property',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Validate that true/false values are only used on properties that support them',\n },\n messages: {\n invalidBooleanTrue:\n \"Property '{{name}}' does not accept boolean true. Only these properties support it: {{allowed}}.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function report(name: string, node: TSESTree.Node): void {\n context.report({\n node,\n messageId: 'invalidBooleanTrue',\n data: { name, allowed: [...BOOLEAN_TRUE_PROPERTIES].join(', ') },\n });\n }\n\n return styleValueListeners(ctx, (key, node) => {\n if (/^[#$]/.test(key) || BOOLEAN_TRUE_PROPERTIES.has(key)) return;\n if (\n (node.type === 'Literal' && node.value === true) ||\n node.type === 'JSXAttribute'\n ) {\n report(key, node);\n }\n });\n },\n});\n"],"mappings":";;;;;;AAQA,qCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,iFACH;EACD,UAAU,EACR,oBACE,oGACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,OAAO,MAAc,MAA2B;AACvD,WAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM;KAAE;KAAM,SAAS,CAAC,GAAG,wBAAwB,CAAC,KAAK,KAAK;KAAE;IACjE,CAAC;;AAGJ,SAAO,oBAAoB,MAAM,KAAK,SAAS;AAC7C,OAAI,QAAQ,KAAK,IAAI,IAAI,wBAAwB,IAAI,IAAI,CAAE;AAC3D,OACG,KAAK,SAAS,aAAa,KAAK,UAAU,QAC3C,KAAK,SAAS,eAEd,QAAO,KAAK,KAAK;IAEnB;;CAEL,CAAC"}
@@ -1,6 +1,8 @@
1
1
  import { createRule } from "../create-rule.js";
2
- import { getKeyName, getStringValue, isRawHexColor, validateColorTokenSyntax } from "../utils.js";
3
- import { TastyContext, styleObjectListeners } from "../context.js";
2
+ import { getKeyName, isRawHexColor, validateColorTokenSyntax } from "../utils.js";
3
+ import { TastyContext } from "../context.js";
4
+ import { stringStyleValueListeners } from "../style-values.js";
5
+ import { scanValueWords } from "../value-words.js";
4
6
 
5
7
  //#region src/rules/valid-color-token.ts
6
8
  var valid_color_token_default = createRule({
@@ -27,10 +29,10 @@ var valid_color_token_default = createRule({
27
29
  }
28
30
  }
29
31
  function checkColorTokensInValue(value, node) {
30
- const tokenRegex = /##?[a-zA-Z][a-zA-Z0-9-]*(?:\.\$?[a-zA-Z0-9-]+)?(?!\()/g;
31
- let match;
32
- while ((match = tokenRegex.exec(value)) !== null) {
33
- const token = match[0];
32
+ for (const word of scanValueWords(value)) {
33
+ const token = word.value;
34
+ if (!token.startsWith("#")) continue;
35
+ if (token.startsWith("##") && word.isFunction && value[word.offset + token.length] === "(") continue;
34
36
  if (isRawHexColor(token)) continue;
35
37
  const syntaxError = validateColorTokenSyntax(token);
36
38
  if (syntaxError) {
@@ -54,28 +56,9 @@ var valid_color_token_default = createRule({
54
56
  });
55
57
  }
56
58
  }
57
- function handleStyleObject(node) {
58
- if (!ctx.isStyleObject(node)) return;
59
- collectLocalTokens(node);
60
- for (const prop of node.properties) {
61
- if (prop.type !== "Property") continue;
62
- if (prop.value.type === "Literal") {
63
- const str = getStringValue(prop.value);
64
- if (str && str.includes("#")) checkColorTokensInValue(str, prop.value);
65
- }
66
- if (prop.value.type === "ObjectExpression") for (const stateProp of prop.value.properties) {
67
- if (stateProp.type !== "Property") continue;
68
- const str = getStringValue(stateProp.value);
69
- if (str && str.includes("#")) checkColorTokensInValue(str, stateProp.value);
70
- }
71
- }
72
- }
73
- return {
74
- ImportDeclaration(node) {
75
- ctx.trackImport(node);
76
- },
77
- ...styleObjectListeners(handleStyleObject),
78
- "Program:exit"() {
59
+ return stringStyleValueListeners(ctx, checkColorTokensInValue, {
60
+ onStyleObject: collectLocalTokens,
61
+ onProgramExit() {
79
62
  if (!Array.isArray(ctx.config.tokens) || ctx.config.tokens.length === 0) return;
80
63
  for (const { token, baseName, node } of pendingChecks) {
81
64
  if (fileColorTokens.has(baseName)) continue;
@@ -87,7 +70,7 @@ var valid_color_token_default = createRule({
87
70
  });
88
71
  }
89
72
  }
90
- };
73
+ });
91
74
  }
92
75
  });
93
76
 
@@ -1 +1 @@
1
- {"version":3,"file":"valid-color-token.js","names":[],"sources":["../../src/rules/valid-color-token.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport {\n getKeyName,\n getStringValue,\n validateColorTokenSyntax,\n isRawHexColor,\n} from '../utils.js';\n\ntype MessageIds = 'invalidSyntax' | 'unknownToken';\n\ninterface PendingExistenceCheck {\n token: string;\n baseName: string;\n node: TSESTree.Node;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'valid-color-token',\n meta: {\n type: 'problem',\n docs: {\n description: 'Validate color token syntax and existence',\n },\n messages: {\n invalidSyntax: \"Invalid color token '{{token}}': {{reason}}.\",\n unknownToken:\n \"Unknown color token '{{token}}'. Declare it as a '#name' key, or add it to 'tokens' in your tasty config.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n const fileColorTokens = new Set<string>();\n const pendingChecks: PendingExistenceCheck[] = [];\n\n function collectLocalTokens(node: TSESTree.ObjectExpression): void {\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n const key = getKeyName(prop.key);\n if (key && key.startsWith('#') && !key.startsWith('##')) {\n fileColorTokens.add(key);\n }\n }\n }\n\n function checkColorTokensInValue(value: string, node: TSESTree.Node): void {\n // `(?!\\()` keeps a `##name(...)` Tasty `@function` call from being read as a\n // bare color reference, which would demand the token be declared.\n const tokenRegex =\n /##?[a-zA-Z][a-zA-Z0-9-]*(?:\\.\\$?[a-zA-Z0-9-]+)?(?!\\()/g;\n let match;\n\n while ((match = tokenRegex.exec(value)) !== null) {\n const token = match[0];\n\n if (isRawHexColor(token)) continue;\n\n const syntaxError = validateColorTokenSyntax(token);\n if (syntaxError) {\n context.report({\n node,\n messageId: 'invalidSyntax',\n data: { token, reason: syntaxError },\n });\n continue;\n }\n\n if (ctx.config.tokens === false) continue;\n\n const baseName = token.startsWith('##')\n ? '#' + token.slice(2).split('.')[0]\n : '#' + token.slice(1).split('.')[0];\n\n if (baseName === '#current') continue;\n\n pendingChecks.push({ token, baseName, node });\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n collectLocalTokens(node);\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property') continue;\n\n if (prop.value.type === 'Literal') {\n const str = getStringValue(prop.value);\n if (str && str.includes('#')) {\n checkColorTokensInValue(str, prop.value);\n }\n }\n\n if (prop.value.type === 'ObjectExpression') {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const str = getStringValue(stateProp.value);\n if (str && str.includes('#')) {\n checkColorTokensInValue(str, stateProp.value);\n }\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n\n ...styleObjectListeners(handleStyleObject),\n\n 'Program:exit'() {\n if (\n !Array.isArray(ctx.config.tokens) ||\n ctx.config.tokens.length === 0\n ) {\n return;\n }\n\n for (const { token, baseName, node } of pendingChecks) {\n if (fileColorTokens.has(baseName)) continue;\n if (ctx.config.tokens.includes(baseName)) continue;\n\n context.report({\n node,\n messageId: 'unknownToken',\n data: { token },\n });\n }\n },\n };\n },\n});\n"],"mappings":";;;;;AAkBA,gCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,6CACd;EACD,UAAU;GACR,eAAe;GACf,cACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EACrC,MAAM,kCAAkB,IAAI,KAAa;EACzC,MAAM,gBAAyC,EAAE;EAEjD,SAAS,mBAAmB,MAAuC;AACjE,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAC/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,OAAO,IAAI,WAAW,IAAI,IAAI,CAAC,IAAI,WAAW,KAAK,CACrD,iBAAgB,IAAI,IAAI;;;EAK9B,SAAS,wBAAwB,OAAe,MAA2B;GAGzE,MAAM,aACJ;GACF,IAAI;AAEJ,WAAQ,QAAQ,WAAW,KAAK,MAAM,MAAM,MAAM;IAChD,MAAM,QAAQ,MAAM;AAEpB,QAAI,cAAc,MAAM,CAAE;IAE1B,MAAM,cAAc,yBAAyB,MAAM;AACnD,QAAI,aAAa;AACf,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM;OAAE;OAAO,QAAQ;OAAa;MACrC,CAAC;AACF;;AAGF,QAAI,IAAI,OAAO,WAAW,MAAO;IAEjC,MAAM,WAAW,MAAM,WAAW,KAAK,GACnC,MAAM,MAAM,MAAM,EAAE,CAAC,MAAM,IAAI,CAAC,KAChC,MAAM,MAAM,MAAM,EAAE,CAAC,MAAM,IAAI,CAAC;AAEpC,QAAI,aAAa,WAAY;AAE7B,kBAAc,KAAK;KAAE;KAAO;KAAU;KAAM,CAAC;;;EAIjD,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAC9B,sBAAmB,KAAK;AAExB,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,WAAY;AAE9B,QAAI,KAAK,MAAM,SAAS,WAAW;KACjC,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,SAAI,OAAO,IAAI,SAAS,IAAI,CAC1B,yBAAwB,KAAK,KAAK,MAAM;;AAI5C,QAAI,KAAK,MAAM,SAAS,mBACtB,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;KACnC,MAAM,MAAM,eAAe,UAAU,MAAM;AAC3C,SAAI,OAAO,IAAI,SAAS,IAAI,CAC1B,yBAAwB,KAAK,UAAU,MAAM;;;;AAOvD,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAGvB,GAAG,qBAAqB,kBAAkB;GAE1C,iBAAiB;AACf,QACE,CAAC,MAAM,QAAQ,IAAI,OAAO,OAAO,IACjC,IAAI,OAAO,OAAO,WAAW,EAE7B;AAGF,SAAK,MAAM,EAAE,OAAO,UAAU,UAAU,eAAe;AACrD,SAAI,gBAAgB,IAAI,SAAS,CAAE;AACnC,SAAI,IAAI,OAAO,OAAO,SAAS,SAAS,CAAE;AAE1C,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM,EAAE,OAAO;MAChB,CAAC;;;GAGP;;CAEJ,CAAC"}
1
+ {"version":3,"file":"valid-color-token.js","names":[],"sources":["../../src/rules/valid-color-token.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 getKeyName,\n validateColorTokenSyntax,\n isRawHexColor,\n} from '../utils.js';\nimport { scanValueWords } from '../value-words.js';\n\ntype MessageIds = 'invalidSyntax' | 'unknownToken';\n\ninterface PendingExistenceCheck {\n token: string;\n baseName: string;\n node: TSESTree.Node;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'valid-color-token',\n meta: {\n type: 'problem',\n docs: {\n description: 'Validate color token syntax and existence',\n },\n messages: {\n invalidSyntax: \"Invalid color token '{{token}}': {{reason}}.\",\n unknownToken:\n \"Unknown color token '{{token}}'. Declare it as a '#name' key, or add it to 'tokens' in your tasty config.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n const fileColorTokens = new Set<string>();\n const pendingChecks: PendingExistenceCheck[] = [];\n\n function collectLocalTokens(node: TSESTree.ObjectExpression): void {\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n const key = getKeyName(prop.key);\n if (key && key.startsWith('#') && !key.startsWith('##')) {\n fileColorTokens.add(key);\n }\n }\n }\n\n function checkColorTokensInValue(value: string, node: TSESTree.Node): void {\n for (const word of scanValueWords(value)) {\n const token = word.value;\n if (!token.startsWith('#')) continue;\n if (\n token.startsWith('##') &&\n word.isFunction &&\n value[word.offset + token.length] === '('\n )\n continue;\n\n if (isRawHexColor(token)) continue;\n\n const syntaxError = validateColorTokenSyntax(token);\n if (syntaxError) {\n context.report({\n node,\n messageId: 'invalidSyntax',\n data: { token, reason: syntaxError },\n });\n continue;\n }\n\n if (ctx.config.tokens === false) continue;\n\n const baseName = token.startsWith('##')\n ? '#' + token.slice(2).split('.')[0]\n : '#' + token.slice(1).split('.')[0];\n\n if (baseName === '#current') continue;\n\n pendingChecks.push({ token, baseName, node });\n }\n }\n\n return stringStyleValueListeners(ctx, checkColorTokensInValue, {\n onStyleObject: collectLocalTokens,\n onProgramExit() {\n if (\n !Array.isArray(ctx.config.tokens) ||\n ctx.config.tokens.length === 0\n ) {\n return;\n }\n\n for (const { token, baseName, node } of pendingChecks) {\n if (fileColorTokens.has(baseName)) continue;\n if (ctx.config.tokens.includes(baseName)) continue;\n\n context.report({\n node,\n messageId: 'unknownToken',\n data: { token },\n });\n }\n },\n });\n },\n});\n"],"mappings":";;;;;;;AAmBA,gCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,6CACd;EACD,UAAU;GACR,eAAe;GACf,cACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EACrC,MAAM,kCAAkB,IAAI,KAAa;EACzC,MAAM,gBAAyC,EAAE;EAEjD,SAAS,mBAAmB,MAAuC;AACjE,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAC/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,OAAO,IAAI,WAAW,IAAI,IAAI,CAAC,IAAI,WAAW,KAAK,CACrD,iBAAgB,IAAI,IAAI;;;EAK9B,SAAS,wBAAwB,OAAe,MAA2B;AACzE,QAAK,MAAM,QAAQ,eAAe,MAAM,EAAE;IACxC,MAAM,QAAQ,KAAK;AACnB,QAAI,CAAC,MAAM,WAAW,IAAI,CAAE;AAC5B,QACE,MAAM,WAAW,KAAK,IACtB,KAAK,cACL,MAAM,KAAK,SAAS,MAAM,YAAY,IAEtC;AAEF,QAAI,cAAc,MAAM,CAAE;IAE1B,MAAM,cAAc,yBAAyB,MAAM;AACnD,QAAI,aAAa;AACf,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM;OAAE;OAAO,QAAQ;OAAa;MACrC,CAAC;AACF;;AAGF,QAAI,IAAI,OAAO,WAAW,MAAO;IAEjC,MAAM,WAAW,MAAM,WAAW,KAAK,GACnC,MAAM,MAAM,MAAM,EAAE,CAAC,MAAM,IAAI,CAAC,KAChC,MAAM,MAAM,MAAM,EAAE,CAAC,MAAM,IAAI,CAAC;AAEpC,QAAI,aAAa,WAAY;AAE7B,kBAAc,KAAK;KAAE;KAAO;KAAU;KAAM,CAAC;;;AAIjD,SAAO,0BAA0B,KAAK,yBAAyB;GAC7D,eAAe;GACf,gBAAgB;AACd,QACE,CAAC,MAAM,QAAQ,IAAI,OAAO,OAAO,IACjC,IAAI,OAAO,OAAO,WAAW,EAE7B;AAGF,SAAK,MAAM,EAAE,OAAO,UAAU,UAAU,eAAe;AACrD,SAAI,gBAAgB,IAAI,SAAS,CAAE;AACnC,SAAI,IAAI,OAAO,OAAO,SAAS,SAAS,CAAE;AAE1C,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM,EAAE,OAAO;MAChB,CAAC;;;GAGP,CAAC;;CAEL,CAAC"}
@@ -1,9 +1,10 @@
1
1
  import { createRule } from "../create-rule.js";
2
- import { getKeyName, getStringValue } from "../utils.js";
3
- import { TastyContext, styleObjectListeners } from "../context.js";
2
+ import { getKeyName } from "../utils.js";
3
+ import { TastyContext } from "../context.js";
4
+ import { stringStyleValueListeners } from "../style-values.js";
5
+ import { scanValueWords } from "../value-words.js";
4
6
 
5
7
  //#region src/rules/valid-custom-property.ts
6
- const CUSTOM_PROP_REGEX = /\$\$?[a-zA-Z][a-zA-Z0-9-]*(?!\()/g;
7
8
  var valid_custom_property_default = createRule({
8
9
  name: "valid-custom-property",
9
10
  meta: {
@@ -29,10 +30,10 @@ var valid_custom_property_default = createRule({
29
30
  }
30
31
  function checkValue(value, node) {
31
32
  if (ctx.config.tokens === false) return;
32
- let match;
33
- CUSTOM_PROP_REGEX.lastIndex = 0;
34
- while ((match = CUSTOM_PROP_REGEX.exec(value)) !== null) {
35
- const token = match[0];
33
+ for (const word of scanValueWords(value)) {
34
+ const token = word.value.startsWith("#") && word.value.includes(".$") ? word.value.slice(word.value.indexOf(".$") + 1) : word.value;
35
+ if (!token.startsWith("$")) continue;
36
+ if (token.startsWith("$$") && word.isFunction && value[word.offset + word.value.length] === "(") continue;
36
37
  const baseName = token.startsWith("$$") ? "$" + token.slice(2) : token;
37
38
  pendingChecks.push({
38
39
  token,
@@ -41,26 +42,9 @@ var valid_custom_property_default = createRule({
41
42
  });
42
43
  }
43
44
  }
44
- function handleStyleObject(node) {
45
- if (!ctx.isStyleObject(node)) return;
46
- collectLocalProperties(node);
47
- for (const prop of node.properties) {
48
- if (prop.type !== "Property") continue;
49
- const str = getStringValue(prop.value);
50
- if (str && str.includes("$")) checkValue(str, prop.value);
51
- if (prop.value.type === "ObjectExpression") for (const stateProp of prop.value.properties) {
52
- if (stateProp.type !== "Property") continue;
53
- const stateStr = getStringValue(stateProp.value);
54
- if (stateStr && stateStr.includes("$")) checkValue(stateStr, stateProp.value);
55
- }
56
- }
57
- }
58
- return {
59
- ImportDeclaration(node) {
60
- ctx.trackImport(node);
61
- },
62
- ...styleObjectListeners(handleStyleObject),
63
- "Program:exit"() {
45
+ return stringStyleValueListeners(ctx, checkValue, {
46
+ onStyleObject: collectLocalProperties,
47
+ onProgramExit() {
64
48
  if (!Array.isArray(ctx.config.tokens) || ctx.config.tokens.length === 0) return;
65
49
  for (const { token, baseName, node } of pendingChecks) {
66
50
  if (fileCustomProperties.has(baseName)) continue;
@@ -72,7 +56,7 @@ var valid_custom_property_default = createRule({
72
56
  });
73
57
  }
74
58
  }
75
- };
59
+ });
76
60
  }
77
61
  });
78
62
 
@@ -1 +1 @@
1
- {"version":3,"file":"valid-custom-property.js","names":[],"sources":["../../src/rules/valid-custom-property.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';\n\ntype MessageIds = 'invalidSyntax' | 'unknownProperty';\n\n// `(?!\\()` keeps a `$$name(...)` Tasty `@function` call from being read as a bare\n// `$name` custom-property reference, which would demand the name be declared.\nconst CUSTOM_PROP_REGEX = /\\$\\$?[a-zA-Z][a-zA-Z0-9-]*(?!\\()/g;\n\ninterface PendingExistenceCheck {\n token: string;\n baseName: string;\n node: TSESTree.Node;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'valid-custom-property',\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Validate $name custom property references',\n },\n messages: {\n invalidSyntax:\n \"Invalid custom property '{{token}}'. Use '$name' with letters, digits, or hyphens (e.g. $accent-color).\",\n unknownProperty:\n \"Unknown custom property '{{token}}'. Declare it as a '$name' key in this styles object, or add it to your tasty config.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n const fileCustomProperties = new Set<string>();\n const pendingChecks: PendingExistenceCheck[] = [];\n\n function collectLocalProperties(node: TSESTree.ObjectExpression): void {\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n const key = getKeyName(prop.key);\n if (key && key.startsWith('$') && !key.startsWith('$$')) {\n fileCustomProperties.add(key);\n }\n }\n }\n\n function checkValue(value: string, node: TSESTree.Node): void {\n if (ctx.config.tokens === false) return;\n\n let match;\n CUSTOM_PROP_REGEX.lastIndex = 0;\n\n while ((match = CUSTOM_PROP_REGEX.exec(value)) !== null) {\n const token = match[0];\n const baseName = token.startsWith('$$') ? '$' + token.slice(2) : token;\n\n pendingChecks.push({ token, baseName, node });\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n collectLocalProperties(node);\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property') continue;\n\n const str = getStringValue(prop.value);\n if (str && str.includes('$')) {\n checkValue(str, prop.value);\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 && stateStr.includes('$')) {\n checkValue(stateStr, stateProp.value);\n }\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n\n ...styleObjectListeners(handleStyleObject),\n\n 'Program:exit'() {\n if (\n !Array.isArray(ctx.config.tokens) ||\n ctx.config.tokens.length === 0\n ) {\n return;\n }\n\n for (const { token, baseName, node } of pendingChecks) {\n if (fileCustomProperties.has(baseName)) continue;\n if (ctx.config.tokens.includes(baseName)) continue;\n\n context.report({\n node,\n messageId: 'unknownProperty',\n data: { token },\n });\n }\n },\n };\n },\n});\n"],"mappings":";;;;;AASA,MAAM,oBAAoB;AAQ1B,oCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,6CACd;EACD,UAAU;GACR,eACE;GACF,iBACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EACrC,MAAM,uCAAuB,IAAI,KAAa;EAC9C,MAAM,gBAAyC,EAAE;EAEjD,SAAS,uBAAuB,MAAuC;AACrE,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAC/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,OAAO,IAAI,WAAW,IAAI,IAAI,CAAC,IAAI,WAAW,KAAK,CACrD,sBAAqB,IAAI,IAAI;;;EAKnC,SAAS,WAAW,OAAe,MAA2B;AAC5D,OAAI,IAAI,OAAO,WAAW,MAAO;GAEjC,IAAI;AACJ,qBAAkB,YAAY;AAE9B,WAAQ,QAAQ,kBAAkB,KAAK,MAAM,MAAM,MAAM;IACvD,MAAM,QAAQ,MAAM;IACpB,MAAM,WAAW,MAAM,WAAW,KAAK,GAAG,MAAM,MAAM,MAAM,EAAE,GAAG;AAEjE,kBAAc,KAAK;KAAE;KAAO;KAAU;KAAM,CAAC;;;EAIjD,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAC9B,0BAAuB,KAAK;AAE5B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,WAAY;IAE9B,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,QAAI,OAAO,IAAI,SAAS,IAAI,CAC1B,YAAW,KAAK,KAAK,MAAM;AAG7B,QAAI,KAAK,MAAM,SAAS,mBACtB,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;KACnC,MAAM,WAAW,eAAe,UAAU,MAAM;AAChD,SAAI,YAAY,SAAS,SAAS,IAAI,CACpC,YAAW,UAAU,UAAU,MAAM;;;;AAO/C,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAGvB,GAAG,qBAAqB,kBAAkB;GAE1C,iBAAiB;AACf,QACE,CAAC,MAAM,QAAQ,IAAI,OAAO,OAAO,IACjC,IAAI,OAAO,OAAO,WAAW,EAE7B;AAGF,SAAK,MAAM,EAAE,OAAO,UAAU,UAAU,eAAe;AACrD,SAAI,qBAAqB,IAAI,SAAS,CAAE;AACxC,SAAI,IAAI,OAAO,OAAO,SAAS,SAAS,CAAE;AAE1C,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM,EAAE,OAAO;MAChB,CAAC;;;GAGP;;CAEJ,CAAC"}
1
+ {"version":3,"file":"valid-custom-property.js","names":[],"sources":["../../src/rules/valid-custom-property.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 { getKeyName } from '../utils.js';\nimport { scanValueWords } from '../value-words.js';\n\ntype MessageIds = 'invalidSyntax' | 'unknownProperty';\n\ninterface PendingExistenceCheck {\n token: string;\n baseName: string;\n node: TSESTree.Node;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'valid-custom-property',\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Validate $name custom property references',\n },\n messages: {\n invalidSyntax:\n \"Invalid custom property '{{token}}'. Use '$name' with letters, digits, or hyphens (e.g. $accent-color).\",\n unknownProperty:\n \"Unknown custom property '{{token}}'. Declare it as a '$name' key in this styles object, or add it to your tasty config.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n const fileCustomProperties = new Set<string>();\n const pendingChecks: PendingExistenceCheck[] = [];\n\n function collectLocalProperties(node: TSESTree.ObjectExpression): void {\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n const key = getKeyName(prop.key);\n if (key && key.startsWith('$') && !key.startsWith('$$')) {\n fileCustomProperties.add(key);\n }\n }\n }\n\n function checkValue(value: string, node: TSESTree.Node): void {\n if (ctx.config.tokens === false) return;\n\n for (const word of scanValueWords(value)) {\n // A dynamic color opacity is another custom-property reference.\n const token =\n word.value.startsWith('#') && word.value.includes('.$')\n ? word.value.slice(word.value.indexOf('.$') + 1)\n : word.value;\n if (!token.startsWith('$')) continue;\n if (\n token.startsWith('$$') &&\n word.isFunction &&\n value[word.offset + word.value.length] === '('\n )\n continue;\n const baseName = token.startsWith('$$') ? '$' + token.slice(2) : token;\n\n pendingChecks.push({ token, baseName, node });\n }\n }\n\n return stringStyleValueListeners(ctx, checkValue, {\n onStyleObject: collectLocalProperties,\n onProgramExit() {\n if (\n !Array.isArray(ctx.config.tokens) ||\n ctx.config.tokens.length === 0\n ) {\n return;\n }\n\n for (const { token, baseName, node } of pendingChecks) {\n if (fileCustomProperties.has(baseName)) continue;\n if (ctx.config.tokens.includes(baseName)) continue;\n\n context.report({\n node,\n messageId: 'unknownProperty',\n data: { token },\n });\n }\n },\n });\n },\n});\n"],"mappings":";;;;;;;AAeA,oCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,6CACd;EACD,UAAU;GACR,eACE;GACF,iBACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EACrC,MAAM,uCAAuB,IAAI,KAAa;EAC9C,MAAM,gBAAyC,EAAE;EAEjD,SAAS,uBAAuB,MAAuC;AACrE,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAC/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,OAAO,IAAI,WAAW,IAAI,IAAI,CAAC,IAAI,WAAW,KAAK,CACrD,sBAAqB,IAAI,IAAI;;;EAKnC,SAAS,WAAW,OAAe,MAA2B;AAC5D,OAAI,IAAI,OAAO,WAAW,MAAO;AAEjC,QAAK,MAAM,QAAQ,eAAe,MAAM,EAAE;IAExC,MAAM,QACJ,KAAK,MAAM,WAAW,IAAI,IAAI,KAAK,MAAM,SAAS,KAAK,GACnD,KAAK,MAAM,MAAM,KAAK,MAAM,QAAQ,KAAK,GAAG,EAAE,GAC9C,KAAK;AACX,QAAI,CAAC,MAAM,WAAW,IAAI,CAAE;AAC5B,QACE,MAAM,WAAW,KAAK,IACtB,KAAK,cACL,MAAM,KAAK,SAAS,KAAK,MAAM,YAAY,IAE3C;IACF,MAAM,WAAW,MAAM,WAAW,KAAK,GAAG,MAAM,MAAM,MAAM,EAAE,GAAG;AAEjE,kBAAc,KAAK;KAAE;KAAO;KAAU;KAAM,CAAC;;;AAIjD,SAAO,0BAA0B,KAAK,YAAY;GAChD,eAAe;GACf,gBAAgB;AACd,QACE,CAAC,MAAM,QAAQ,IAAI,OAAO,OAAO,IACjC,IAAI,OAAO,OAAO,WAAW,EAE7B;AAGF,SAAK,MAAM,EAAE,OAAO,UAAU,UAAU,eAAe;AACrD,SAAI,qBAAqB,IAAI,SAAS,CAAE;AACxC,SAAI,IAAI,OAAO,OAAO,SAAS,SAAS,CAAE;AAE1C,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM,EAAE,OAAO;MAChB,CAAC;;;GAGP,CAAC;;CAEL,CAAC"}
@@ -1,6 +1,8 @@
1
1
  import { createRule } from "../create-rule.js";
2
- import { extractCustomUnit, getStringValue, isValidUnit } from "../utils.js";
3
- import { TastyContext, styleObjectListeners } from "../context.js";
2
+ import { isValidUnit } from "../utils.js";
3
+ import { TastyContext } from "../context.js";
4
+ import { stringStyleValueListeners } from "../style-values.js";
5
+ import { scanValueWords } from "../value-words.js";
4
6
 
5
7
  //#region src/rules/valid-custom-unit.ts
6
8
  var valid_custom_unit_default = createRule({
@@ -16,40 +18,21 @@ var valid_custom_unit_default = createRule({
16
18
  const ctx = new TastyContext(context);
17
19
  function checkUnitsInValue(value, node) {
18
20
  if (ctx.config.units === false) return;
19
- const tokens = value.split(/\s+/);
20
- for (const token of tokens) {
21
- if (token.startsWith("#") || token.startsWith("$") || token.startsWith("@") || token.includes("(") || token.includes(")") || token === "true" || token === "false" || token === "none" || token === "auto" || token === "inherit" || token === "initial" || token === "unset" || token === "revert") continue;
22
- const unit = extractCustomUnit(token);
21
+ for (const word of scanValueWords(value)) {
22
+ const unit = word.value.match(/^[+-]?(?:\d*\.\d+|\d+\.?\d*)(?:e[+-]?\d+)?([a-z][a-z0-9]*)$/i)?.[1];
23
23
  if (unit && !isValidUnit(unit, ctx.config)) context.report({
24
24
  node,
25
25
  messageId: "unknownUnit",
26
26
  data: {
27
27
  unit,
28
- value: token
28
+ value: word.value
29
29
  }
30
30
  });
31
31
  }
32
32
  }
33
- function checkNode(node) {
34
- const str = getStringValue(node);
35
- if (str) checkUnitsInValue(str, node);
36
- }
37
- function handleStyleObject(node) {
38
- if (!ctx.isStyleObject(node)) return;
39
- for (const prop of node.properties) {
40
- if (prop.type !== "Property") continue;
41
- checkNode(prop.value);
42
- if (prop.value.type === "ObjectExpression") {
43
- for (const stateProp of prop.value.properties) if (stateProp.type === "Property") checkNode(stateProp.value);
44
- }
45
- }
46
- }
47
- return {
48
- ImportDeclaration(node) {
49
- ctx.trackImport(node);
50
- },
51
- ...styleObjectListeners(handleStyleObject)
52
- };
33
+ return stringStyleValueListeners(ctx, (value, node) => {
34
+ checkUnitsInValue(value, node);
35
+ });
53
36
  }
54
37
  });
55
38
 
@@ -1 +1 @@
1
- {"version":3,"file":"valid-custom-unit.js","names":[],"sources":["../../src/rules/valid-custom-unit.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getStringValue, extractCustomUnit, isValidUnit } from '../utils.js';\n\ntype MessageIds = 'unknownUnit';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-custom-unit',\n meta: {\n type: 'problem',\n docs: {\n description: 'Validate that custom units in style values are recognized',\n },\n messages: {\n unknownUnit:\n \"Unknown unit '{{unit}}' in '{{value}}'. Use a built-in unit (x, r, cr, bw, ow, lh, sf) or a CSS unit, or add '{{unit}}' to 'units' in your tasty config.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkUnitsInValue(value: string, node: TSESTree.Node): void {\n if (ctx.config.units === false) return;\n\n // Split by spaces and check each token\n const tokens = value.split(/\\s+/);\n for (const token of tokens) {\n // Skip function calls, colors, modifiers, special values\n if (\n token.startsWith('#') ||\n token.startsWith('$') ||\n token.startsWith('@') ||\n token.includes('(') ||\n token.includes(')') ||\n token === 'true' ||\n token === 'false' ||\n token === 'none' ||\n token === 'auto' ||\n token === 'inherit' ||\n token === 'initial' ||\n token === 'unset' ||\n token === 'revert'\n ) {\n continue;\n }\n\n const unit = extractCustomUnit(token);\n if (unit && !isValidUnit(unit, ctx.config)) {\n context.report({\n node,\n messageId: 'unknownUnit',\n data: { unit, value: token },\n });\n }\n }\n }\n\n function checkNode(node: TSESTree.Node): void {\n const str = getStringValue(node);\n if (str) {\n checkUnitsInValue(str, node);\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') continue;\n\n checkNode(prop.value);\n\n if (prop.value.type === 'ObjectExpression') {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type === 'Property') {\n checkNode(stateProp.value);\n }\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;AAOA,gCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,6DACd;EACD,UAAU,EACR,aACE,4JACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,kBAAkB,OAAe,MAA2B;AACnE,OAAI,IAAI,OAAO,UAAU,MAAO;GAGhC,MAAM,SAAS,MAAM,MAAM,MAAM;AACjC,QAAK,MAAM,SAAS,QAAQ;AAE1B,QACE,MAAM,WAAW,IAAI,IACrB,MAAM,WAAW,IAAI,IACrB,MAAM,WAAW,IAAI,IACrB,MAAM,SAAS,IAAI,IACnB,MAAM,SAAS,IAAI,IACnB,UAAU,UACV,UAAU,WACV,UAAU,UACV,UAAU,UACV,UAAU,aACV,UAAU,aACV,UAAU,WACV,UAAU,SAEV;IAGF,MAAM,OAAO,kBAAkB,MAAM;AACrC,QAAI,QAAQ,CAAC,YAAY,MAAM,IAAI,OAAO,CACxC,SAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MAAE;MAAM,OAAO;MAAO;KAC7B,CAAC;;;EAKR,SAAS,UAAU,MAA2B;GAC5C,MAAM,MAAM,eAAe,KAAK;AAChC,OAAI,IACF,mBAAkB,KAAK,KAAK;;EAIhC,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,WAAY;AAE9B,cAAU,KAAK,MAAM;AAErB,QAAI,KAAK,MAAM,SAAS,oBACtB;UAAK,MAAM,aAAa,KAAK,MAAM,WACjC,KAAI,UAAU,SAAS,WACrB,WAAU,UAAU,MAAM;;;;AAOpC,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
1
+ {"version":3,"file":"valid-custom-unit.js","names":[],"sources":["../../src/rules/valid-custom-unit.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 { isValidUnit } from '../utils.js';\nimport { scanValueWords } from '../value-words.js';\n\ntype MessageIds = 'unknownUnit';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-custom-unit',\n meta: {\n type: 'problem',\n docs: {\n description: 'Validate that custom units in style values are recognized',\n },\n messages: {\n unknownUnit:\n \"Unknown unit '{{unit}}' in '{{value}}'. Use a built-in unit (x, r, cr, bw, ow, lh, sf) or a CSS unit, or add '{{unit}}' to 'units' in your tasty config.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkUnitsInValue(value: string, node: TSESTree.Node): void {\n if (ctx.config.units === false) return;\n\n for (const word of scanValueWords(value)) {\n const match = word.value.match(\n /^[+-]?(?:\\d*\\.\\d+|\\d+\\.?\\d*)(?:e[+-]?\\d+)?([a-z][a-z0-9]*)$/i,\n );\n const unit = match?.[1];\n if (unit && !isValidUnit(unit, ctx.config)) {\n context.report({\n node,\n messageId: 'unknownUnit',\n data: { unit, value: word.value },\n });\n }\n }\n }\n\n return stringStyleValueListeners(ctx, (value, node) => {\n checkUnitsInValue(value, node);\n });\n },\n});\n"],"mappings":";;;;;;;AASA,gCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,6DACd;EACD,UAAU,EACR,aACE,4JACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,kBAAkB,OAAe,MAA2B;AACnE,OAAI,IAAI,OAAO,UAAU,MAAO;AAEhC,QAAK,MAAM,QAAQ,eAAe,MAAM,EAAE;IAIxC,MAAM,OAHQ,KAAK,MAAM,MACvB,+DACD,GACoB;AACrB,QAAI,QAAQ,CAAC,YAAY,MAAM,IAAI,OAAO,CACxC,SAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MAAE;MAAM,OAAO,KAAK;MAAO;KAClC,CAAC;;;AAKR,SAAO,0BAA0B,MAAM,OAAO,SAAS;AACrD,qBAAkB,OAAO,KAAK;IAC9B;;CAEL,CAAC"}
@@ -1,7 +1,7 @@
1
1
  import { createRule } from "../create-rule.js";
2
2
  import { DIRECTIONAL_MODIFIERS, DOCK_PROPERTIES, SINGLE_VALUE_DIRECTIONAL_PROPERTIES } from "../constants.js";
3
- import { getKeyName, getStringValue } from "../utils.js";
4
- import { TastyContext, styleObjectListeners } from "../context.js";
3
+ import { TastyContext } from "../context.js";
4
+ import { stringStyleValueListeners } from "../style-values.js";
5
5
  import { parseValue } from "../parsers/value-parser.js";
6
6
 
7
7
  //#region src/rules/valid-directional-modifier.ts
@@ -118,31 +118,9 @@ var valid_directional_modifier_default = createRule({
118
118
  });
119
119
  }
120
120
  }
121
- function handleStyleObject(node) {
122
- if (!ctx.isStyleObject(node)) return;
123
- for (const prop of node.properties) {
124
- if (prop.type !== "Property" || prop.computed) continue;
125
- const key = getKeyName(prop.key);
126
- if (key === null) continue;
127
- if (key === "$" || key.startsWith("@") || key.startsWith("$") || key.startsWith("#") || key.startsWith("&") || /^[A-Z]/.test(key)) continue;
128
- const str = getStringValue(prop.value);
129
- if (str) {
130
- checkValue(key, str, prop.value);
131
- continue;
132
- }
133
- if (prop.value.type === "ObjectExpression" && ctx.isStateMap(prop.value, prop)) for (const stateProp of prop.value.properties) {
134
- if (stateProp.type !== "Property") continue;
135
- const stateStr = getStringValue(stateProp.value);
136
- if (stateStr) checkValue(key, stateStr, stateProp.value);
137
- }
138
- }
139
- }
140
- return {
141
- ImportDeclaration(node) {
142
- ctx.trackImport(node);
143
- },
144
- ...styleObjectListeners(handleStyleObject)
145
- };
121
+ return stringStyleValueListeners(ctx, (value, node, property) => {
122
+ checkValue(property, value, node);
123
+ });
146
124
  }
147
125
  });
148
126
 
@@ -1 +1 @@
1
- {"version":3,"file":"valid-directional-modifier.js","names":[],"sources":["../../src/rules/valid-directional-modifier.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 {\n DIRECTIONAL_MODIFIERS,\n DOCK_PROPERTIES,\n SINGLE_VALUE_DIRECTIONAL_PROPERTIES,\n} from '../constants.js';\nimport { parseValue } from '../parsers/index.js';\nimport type { ValueToken } from '../parsers/index.js';\n\ntype MessageIds = 'invalidDirectionalModifier' | 'tooManyValues';\n\n/**\n * Every direction word the rule recognises, across physical and logical\n * vocabularies. A word here is read as a *modifier*; whether it is accepted is\n * then decided per property by `DIRECTIONAL_MODIFIERS`.\n *\n * `start` and `end` are ordinary CSS *values* for a long tail of properties\n * (`textAlign: 'start'`, `justifyContent: 'end'`), which is exactly what the\n * `DIRECTIONAL_MODIFIERS` gate in `checkValue` exists to keep out: only the\n * properties with a directional vocabulary are ever inspected. Inside that set\n * no property takes `start`/`end` as a value, so reading them as modifiers is\n * safe — and it is what turns `padding: '1x start'` (a physical property given\n * a logical edge, which the handler drops) into a report.\n */\nconst ALL_DIRECTIONS = new Set([\n 'top',\n 'right',\n 'bottom',\n 'left',\n 'top-left',\n 'top-right',\n 'bottom-left',\n 'bottom-right',\n 'start',\n 'end',\n]);\n\n/**\n * Token types that count as a *value* rather than a modifier.\n *\n * Deliberately excludes `keyword`: direction words, `solid`, `inherit` and every\n * other bare word classify as keywords, and a CSS-wide keyword alongside a\n * direction (`padding: 'inherit top'`) is legal while carrying no value of its own.\n *\n * Also excludes colors: a `fade` group carries a width plus up to two mask\n * colors, so colors must not count toward the one-value limit.\n */\nconst VALUE_TOKEN_TYPES = new Set<ValueToken['type']>([\n 'custom-unit',\n 'css-unit',\n 'number',\n 'custom-prop',\n 'custom-prop-ref',\n 'css-function',\n 'tasty-function',\n 'group-expr',\n]);\n\n/** The source text of a token. `keyword` tokens carry `value` rather than `raw`. */\nfunction tokenText(token: ValueToken): string {\n if ('raw' in token && typeof token.raw === 'string') return token.raw;\n if (token.type === 'keyword') return token.value;\n\n return '';\n}\n\nexport default createRule<[], MessageIds>({\n name: 'valid-directional-modifier',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Validate directional modifiers: only on properties that support them, and one value per group',\n },\n messages: {\n invalidDirectionalModifier:\n \"Modifier '{{modifier}}' is not valid for '{{property}}'. Accepted: {{accepted}}.\",\n tooManyValues:\n 'A group that names directions takes {{limit}}, but \\'{{group}}\\' has {{count}}. Values and modifiers are parsed into separate buckets, so their order is not recoverable — use comma-separated groups for per-side values, e.g. \"2x top, 4x right\".',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkValue(\n property: string,\n value: string,\n node: TSESTree.Node,\n ): void {\n const allowedMods = DIRECTIONAL_MODIFIERS[property];\n // Only properties with a directional vocabulary are checked.\n //\n // This gate looks like it makes the \"wrong property\" case unreachable, and\n // it does — but removing it is not the fix. Direction words are ordinary CSS\n // *values* for a long tail of properties: `verticalAlign: 'bottom'`,\n // `textAlign: 'left'`, `transformOrigin: 'top center'`,\n // `backgroundPosition`, `objectPosition`, `float`, `clear`,\n // `scrollSnapAlign`, and `transition` (whose value names properties, which\n // can themselves be `left`/`top`). Checking every property reported all of\n // those as errors across a real design system. Catching `fill: '#purple\n // top'` is not worth an allowlist of every property that takes a\n // positional keyword.\n if (!allowedMods) return;\n\n // `parseValue` splits on commas (groups) and spaced slashes (parts), which\n // is what makes a per-group arity check possible at all.\n const { groups } = parseValue(value, {\n skipUnitValidation: true,\n skipFuncValidation: true,\n });\n\n for (const group of groups) {\n const tokens = group.parts.flatMap((part) => part.tokens);\n const directions: string[] = [];\n let valueCount = 0;\n let hasSpanModifier = false;\n\n for (const token of tokens) {\n if (VALUE_TOKEN_TYPES.has(token.type)) {\n valueCount++;\n continue;\n }\n\n const word = tokenText(token);\n\n if (ALL_DIRECTIONS.has(word)) {\n directions.push(word);\n } else if (word === 'dock' && DOCK_PROPERTIES.has(property)) {\n hasSpanModifier = true;\n }\n }\n\n if (directions.length === 0) continue;\n\n let reportedUnknown = false;\n for (const direction of directions) {\n if (!allowedMods.has(direction)) {\n context.report({\n node,\n messageId: 'invalidDirectionalModifier',\n data: {\n property,\n modifier: direction,\n accepted: [...allowedMods].join(', '),\n },\n });\n reportedUnknown = true;\n }\n }\n\n // Don't pile an arity complaint on top of an already-invalid modifier.\n if (reportedUnknown) continue;\n\n if (!SINGLE_VALUE_DIRECTIONAL_PROPERTIES.has(property)) continue;\n\n // Values and modifiers are bucketed separately by the parser, so the\n // pairing a reader infers from source order does not survive parsing: a\n // group naming directions carries one value, applied to every direction it\n // names. `dock` is the exception — its second value insets the sides the\n // dock spans.\n const limit = hasSpanModifier ? 2 : 1;\n\n if (valueCount > limit) {\n context.report({\n node,\n messageId: 'tooManyValues',\n data: {\n group: tokens.map(tokenText).filter(Boolean).join(' '),\n count: `${valueCount}`,\n limit: hasSpanModifier ? 'at most two values' : 'a single value',\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\n // Sub-elements, at-rules, token declarations, nested selectors, and the\n // `$` affix (which holds a selector, not a value) are not style values.\n if (\n key === '$' ||\n key.startsWith('@') ||\n key.startsWith('$') ||\n key.startsWith('#') ||\n key.startsWith('&') ||\n /^[A-Z]/.test(key)\n ) {\n continue;\n }\n\n // Direct value\n const str = getStringValue(prop.value);\n if (str) {\n checkValue(key, str, prop.value);\n continue;\n }\n\n // State map\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 checkValue(key, stateStr, stateProp.value);\n }\n }\n }\n }\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":"valid-directional-modifier.js","names":[],"sources":["../../src/rules/valid-directional-modifier.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 DIRECTIONAL_MODIFIERS,\n DOCK_PROPERTIES,\n SINGLE_VALUE_DIRECTIONAL_PROPERTIES,\n} from '../constants.js';\nimport { parseValue } from '../parsers/index.js';\nimport type { ValueToken } from '../parsers/index.js';\n\ntype MessageIds = 'invalidDirectionalModifier' | 'tooManyValues';\n\n/**\n * Every direction word the rule recognises, across physical and logical\n * vocabularies. A word here is read as a *modifier*; whether it is accepted is\n * then decided per property by `DIRECTIONAL_MODIFIERS`.\n *\n * `start` and `end` are ordinary CSS *values* for a long tail of properties\n * (`textAlign: 'start'`, `justifyContent: 'end'`), which is exactly what the\n * `DIRECTIONAL_MODIFIERS` gate in `checkValue` exists to keep out: only the\n * properties with a directional vocabulary are ever inspected. Inside that set\n * no property takes `start`/`end` as a value, so reading them as modifiers is\n * safe — and it is what turns `padding: '1x start'` (a physical property given\n * a logical edge, which the handler drops) into a report.\n */\nconst ALL_DIRECTIONS = new Set([\n 'top',\n 'right',\n 'bottom',\n 'left',\n 'top-left',\n 'top-right',\n 'bottom-left',\n 'bottom-right',\n 'start',\n 'end',\n]);\n\n/**\n * Token types that count as a *value* rather than a modifier.\n *\n * Deliberately excludes `keyword`: direction words, `solid`, `inherit` and every\n * other bare word classify as keywords, and a CSS-wide keyword alongside a\n * direction (`padding: 'inherit top'`) is legal while carrying no value of its own.\n *\n * Also excludes colors: a `fade` group carries a width plus up to two mask\n * colors, so colors must not count toward the one-value limit.\n */\nconst VALUE_TOKEN_TYPES = new Set<ValueToken['type']>([\n 'custom-unit',\n 'css-unit',\n 'number',\n 'custom-prop',\n 'custom-prop-ref',\n 'css-function',\n 'tasty-function',\n 'group-expr',\n]);\n\n/** The source text of a token. `keyword` tokens carry `value` rather than `raw`. */\nfunction tokenText(token: ValueToken): string {\n if ('raw' in token && typeof token.raw === 'string') return token.raw;\n if (token.type === 'keyword') return token.value;\n\n return '';\n}\n\nexport default createRule<[], MessageIds>({\n name: 'valid-directional-modifier',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Validate directional modifiers: only on properties that support them, and one value per group',\n },\n messages: {\n invalidDirectionalModifier:\n \"Modifier '{{modifier}}' is not valid for '{{property}}'. Accepted: {{accepted}}.\",\n tooManyValues:\n 'A group that names directions takes {{limit}}, but \\'{{group}}\\' has {{count}}. Values and modifiers are parsed into separate buckets, so their order is not recoverable — use comma-separated groups for per-side values, e.g. \"2x top, 4x right\".',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkValue(\n property: string,\n value: string,\n node: TSESTree.Node,\n ): void {\n const allowedMods = DIRECTIONAL_MODIFIERS[property];\n // Only properties with a directional vocabulary are checked.\n //\n // This gate looks like it makes the \"wrong property\" case unreachable, and\n // it does — but removing it is not the fix. Direction words are ordinary CSS\n // *values* for a long tail of properties: `verticalAlign: 'bottom'`,\n // `textAlign: 'left'`, `transformOrigin: 'top center'`,\n // `backgroundPosition`, `objectPosition`, `float`, `clear`,\n // `scrollSnapAlign`, and `transition` (whose value names properties, which\n // can themselves be `left`/`top`). Checking every property reported all of\n // those as errors across a real design system. Catching `fill: '#purple\n // top'` is not worth an allowlist of every property that takes a\n // positional keyword.\n if (!allowedMods) return;\n\n // `parseValue` splits on commas (groups) and spaced slashes (parts), which\n // is what makes a per-group arity check possible at all.\n const { groups } = parseValue(value, {\n skipUnitValidation: true,\n skipFuncValidation: true,\n });\n\n for (const group of groups) {\n const tokens = group.parts.flatMap((part) => part.tokens);\n const directions: string[] = [];\n let valueCount = 0;\n let hasSpanModifier = false;\n\n for (const token of tokens) {\n if (VALUE_TOKEN_TYPES.has(token.type)) {\n valueCount++;\n continue;\n }\n\n const word = tokenText(token);\n\n if (ALL_DIRECTIONS.has(word)) {\n directions.push(word);\n } else if (word === 'dock' && DOCK_PROPERTIES.has(property)) {\n hasSpanModifier = true;\n }\n }\n\n if (directions.length === 0) continue;\n\n let reportedUnknown = false;\n for (const direction of directions) {\n if (!allowedMods.has(direction)) {\n context.report({\n node,\n messageId: 'invalidDirectionalModifier',\n data: {\n property,\n modifier: direction,\n accepted: [...allowedMods].join(', '),\n },\n });\n reportedUnknown = true;\n }\n }\n\n // Don't pile an arity complaint on top of an already-invalid modifier.\n if (reportedUnknown) continue;\n\n if (!SINGLE_VALUE_DIRECTIONAL_PROPERTIES.has(property)) continue;\n\n // Values and modifiers are bucketed separately by the parser, so the\n // pairing a reader infers from source order does not survive parsing: a\n // group naming directions carries one value, applied to every direction it\n // names. `dock` is the exception — its second value insets the sides the\n // dock spans.\n const limit = hasSpanModifier ? 2 : 1;\n\n if (valueCount > limit) {\n context.report({\n node,\n messageId: 'tooManyValues',\n data: {\n group: tokens.map(tokenText).filter(Boolean).join(' '),\n count: `${valueCount}`,\n limit: hasSpanModifier ? 'at most two values' : 'a single value',\n },\n });\n }\n }\n }\n\n return stringStyleValueListeners(ctx, (value, node, property) => {\n checkValue(property, value, node);\n });\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA2BA,MAAM,iBAAiB,IAAI,IAAI;CAC7B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;;;;;;AAYF,MAAM,oBAAoB,IAAI,IAAwB;CACpD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;AAGF,SAAS,UAAU,OAA2B;AAC5C,KAAI,SAAS,SAAS,OAAO,MAAM,QAAQ,SAAU,QAAO,MAAM;AAClE,KAAI,MAAM,SAAS,UAAW,QAAO,MAAM;AAE3C,QAAO;;AAGT,yCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,iGACH;EACD,UAAU;GACR,4BACE;GACF,eACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,WACP,UACA,OACA,MACM;GACN,MAAM,cAAc,sBAAsB;AAa1C,OAAI,CAAC,YAAa;GAIlB,MAAM,EAAE,WAAW,WAAW,OAAO;IACnC,oBAAoB;IACpB,oBAAoB;IACrB,CAAC;AAEF,QAAK,MAAM,SAAS,QAAQ;IAC1B,MAAM,SAAS,MAAM,MAAM,SAAS,SAAS,KAAK,OAAO;IACzD,MAAM,aAAuB,EAAE;IAC/B,IAAI,aAAa;IACjB,IAAI,kBAAkB;AAEtB,SAAK,MAAM,SAAS,QAAQ;AAC1B,SAAI,kBAAkB,IAAI,MAAM,KAAK,EAAE;AACrC;AACA;;KAGF,MAAM,OAAO,UAAU,MAAM;AAE7B,SAAI,eAAe,IAAI,KAAK,CAC1B,YAAW,KAAK,KAAK;cACZ,SAAS,UAAU,gBAAgB,IAAI,SAAS,CACzD,mBAAkB;;AAItB,QAAI,WAAW,WAAW,EAAG;IAE7B,IAAI,kBAAkB;AACtB,SAAK,MAAM,aAAa,WACtB,KAAI,CAAC,YAAY,IAAI,UAAU,EAAE;AAC/B,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM;OACJ;OACA,UAAU;OACV,UAAU,CAAC,GAAG,YAAY,CAAC,KAAK,KAAK;OACtC;MACF,CAAC;AACF,uBAAkB;;AAKtB,QAAI,gBAAiB;AAErB,QAAI,CAAC,oCAAoC,IAAI,SAAS,CAAE;AASxD,QAAI,cAFU,kBAAkB,IAAI,GAGlC,SAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MACJ,OAAO,OAAO,IAAI,UAAU,CAAC,OAAO,QAAQ,CAAC,KAAK,IAAI;MACtD,OAAO,GAAG;MACV,OAAO,kBAAkB,uBAAuB;MACjD;KACF,CAAC;;;AAKR,SAAO,0BAA0B,MAAM,OAAO,MAAM,aAAa;AAC/D,cAAW,UAAU,OAAO,KAAK;IACjC;;CAEL,CAAC"}
@@ -1,7 +1,7 @@
1
1
  import { createRule } from "../create-rule.js";
2
2
  import { PRESET_MODIFIERS } from "../constants.js";
3
- import { getKeyName, getStringValue } from "../utils.js";
4
- import { TastyContext, styleObjectListeners } from "../context.js";
3
+ import { TastyContext } from "../context.js";
4
+ import { stringStyleValueListeners } from "../style-values.js";
5
5
 
6
6
  //#region src/rules/valid-preset.ts
7
7
  var valid_preset_default = createRule({
@@ -52,33 +52,9 @@ var valid_preset_default = createRule({
52
52
  });
53
53
  }
54
54
  }
55
- function checkObject(node) {
56
- if (!ctx.isStyleObject(node)) return;
57
- for (const prop of node.properties) {
58
- if (prop.type !== "Property" || prop.computed) continue;
59
- if (getKeyName(prop.key) !== "preset") continue;
60
- const str = getStringValue(prop.value);
61
- if (str) {
62
- checkPresetValue(str, prop.value);
63
- continue;
64
- }
65
- if (prop.value.type === "Literal" && prop.value.value === true) continue;
66
- if (prop.value.type === "ObjectExpression") for (const stateProp of prop.value.properties) {
67
- if (stateProp.type !== "Property") continue;
68
- const stateStr = getStringValue(stateProp.value);
69
- if (stateStr) checkPresetValue(stateStr, stateProp.value);
70
- }
71
- }
72
- }
73
- function handleStyleObject(node) {
74
- checkObject(node);
75
- }
76
- return {
77
- ImportDeclaration(node) {
78
- ctx.trackImport(node);
79
- },
80
- ...styleObjectListeners(handleStyleObject)
81
- };
55
+ return stringStyleValueListeners(ctx, (value, node, property) => {
56
+ if (property === "preset") checkPresetValue(value, node);
57
+ });
82
58
  }
83
59
  });
84
60
 
@@ -1 +1 @@
1
- {"version":3,"file":"valid-preset.js","names":[],"sources":["../../src/rules/valid-preset.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 { PRESET_MODIFIERS } from '../constants.js';\n\ntype MessageIds = 'unknownPreset' | 'unknownModifier';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-preset',\n meta: {\n type: 'problem',\n docs: {\n description: 'Validate preset property values against config',\n },\n messages: {\n unknownPreset:\n \"Unknown preset '{{name}}'. Add it to 'presets' in your tasty config, or use a configured preset.\",\n unknownModifier:\n \"Unknown preset modifier '{{modifier}}'. Valid modifiers: {{valid}}.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n const CSS_GLOBAL_KEYWORDS = new Set([\n 'inherit',\n 'initial',\n 'unset',\n 'revert',\n 'revert-layer',\n ]);\n\n function checkPresetValue(value: string, node: TSESTree.Node): void {\n const segments = value.trim().split(/\\s*\\/\\s*/);\n if (segments.length === 0) return;\n\n const nameSegment = segments[0].trim();\n const modSegment = segments[1]?.trim();\n\n if (!nameSegment) return;\n if (CSS_GLOBAL_KEYWORDS.has(nameSegment)) return;\n\n const nameTokens = nameSegment.split(/\\s+/).filter(Boolean);\n // Mod-only shorthand: every token in the name segment is a recognized\n // modifier (e.g. preset=\"bold\", preset=\"bold italic\").\n const isModOnlyShorthand =\n nameTokens.length > 0 &&\n nameTokens.every((t) => PRESET_MODIFIERS.has(t));\n\n if (!isModOnlyShorthand) {\n const presetName = nameTokens[0];\n if (\n presetName &&\n ctx.config.presets.length > 0 &&\n !ctx.config.presets.includes(presetName)\n ) {\n context.report({\n node,\n messageId: 'unknownPreset',\n data: { name: presetName },\n });\n }\n }\n\n if (modSegment) {\n for (const mod of modSegment.split(/\\s+/).filter(Boolean)) {\n if (!PRESET_MODIFIERS.has(mod)) {\n context.report({\n node,\n messageId: 'unknownModifier',\n data: {\n modifier: mod,\n valid: [...PRESET_MODIFIERS].join(', '),\n },\n });\n }\n }\n }\n }\n\n function checkObject(node: TSESTree.ObjectExpression): void {\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 !== 'preset') continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkPresetValue(str, prop.value);\n continue;\n }\n\n if (prop.value.type === 'Literal' && prop.value.value === true) {\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 checkPresetValue(stateStr, stateProp.value);\n }\n }\n }\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n checkObject(node);\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;AAQA,2BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,kDACd;EACD,UAAU;GACR,eACE;GACF,iBACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,MAAM,sBAAsB,IAAI,IAAI;GAClC;GACA;GACA;GACA;GACA;GACD,CAAC;EAEF,SAAS,iBAAiB,OAAe,MAA2B;GAClE,MAAM,WAAW,MAAM,MAAM,CAAC,MAAM,WAAW;AAC/C,OAAI,SAAS,WAAW,EAAG;GAE3B,MAAM,cAAc,SAAS,GAAG,MAAM;GACtC,MAAM,aAAa,SAAS,IAAI,MAAM;AAEtC,OAAI,CAAC,YAAa;AAClB,OAAI,oBAAoB,IAAI,YAAY,CAAE;GAE1C,MAAM,aAAa,YAAY,MAAM,MAAM,CAAC,OAAO,QAAQ;AAO3D,OAAI,EAHF,WAAW,SAAS,KACpB,WAAW,OAAO,MAAM,iBAAiB,IAAI,EAAE,CAAC,GAEzB;IACvB,MAAM,aAAa,WAAW;AAC9B,QACE,cACA,IAAI,OAAO,QAAQ,SAAS,KAC5B,CAAC,IAAI,OAAO,QAAQ,SAAS,WAAW,CAExC,SAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM,EAAE,MAAM,YAAY;KAC3B,CAAC;;AAIN,OAAI,YACF;SAAK,MAAM,OAAO,WAAW,MAAM,MAAM,CAAC,OAAO,QAAQ,CACvD,KAAI,CAAC,iBAAiB,IAAI,IAAI,CAC5B,SAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MACJ,UAAU;MACV,OAAO,CAAC,GAAG,iBAAiB,CAAC,KAAK,KAAK;MACxC;KACF,CAAC;;;EAMV,SAAS,YAAY,MAAuC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;AAG/C,QADY,WAAW,KAAK,IAAI,KACpB,SAAU;IAEtB,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,QAAI,KAAK;AACP,sBAAiB,KAAK,KAAK,MAAM;AACjC;;AAGF,QAAI,KAAK,MAAM,SAAS,aAAa,KAAK,MAAM,UAAU,KACxD;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,SACF,kBAAiB,UAAU,UAAU,MAAM;;;;EAOrD,SAAS,kBAAkB,MAAiC;AAC1D,eAAY,KAAK;;AAGnB,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAGvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
1
+ {"version":3,"file":"valid-preset.js","names":[],"sources":["../../src/rules/valid-preset.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 { PRESET_MODIFIERS } from '../constants.js';\n\ntype MessageIds = 'unknownPreset' | 'unknownModifier';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-preset',\n meta: {\n type: 'problem',\n docs: {\n description: 'Validate preset property values against config',\n },\n messages: {\n unknownPreset:\n \"Unknown preset '{{name}}'. Add it to 'presets' in your tasty config, or use a configured preset.\",\n unknownModifier:\n \"Unknown preset modifier '{{modifier}}'. Valid modifiers: {{valid}}.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n const CSS_GLOBAL_KEYWORDS = new Set([\n 'inherit',\n 'initial',\n 'unset',\n 'revert',\n 'revert-layer',\n ]);\n\n function checkPresetValue(value: string, node: TSESTree.Node): void {\n const segments = value.trim().split(/\\s*\\/\\s*/);\n if (segments.length === 0) return;\n\n const nameSegment = segments[0].trim();\n const modSegment = segments[1]?.trim();\n\n if (!nameSegment) return;\n if (CSS_GLOBAL_KEYWORDS.has(nameSegment)) return;\n\n const nameTokens = nameSegment.split(/\\s+/).filter(Boolean);\n // Mod-only shorthand: every token in the name segment is a recognized\n // modifier (e.g. preset=\"bold\", preset=\"bold italic\").\n const isModOnlyShorthand =\n nameTokens.length > 0 &&\n nameTokens.every((t) => PRESET_MODIFIERS.has(t));\n\n if (!isModOnlyShorthand) {\n const presetName = nameTokens[0];\n if (\n presetName &&\n ctx.config.presets.length > 0 &&\n !ctx.config.presets.includes(presetName)\n ) {\n context.report({\n node,\n messageId: 'unknownPreset',\n data: { name: presetName },\n });\n }\n }\n\n if (modSegment) {\n for (const mod of modSegment.split(/\\s+/).filter(Boolean)) {\n if (!PRESET_MODIFIERS.has(mod)) {\n context.report({\n node,\n messageId: 'unknownModifier',\n data: {\n modifier: mod,\n valid: [...PRESET_MODIFIERS].join(', '),\n },\n });\n }\n }\n }\n }\n\n return stringStyleValueListeners(ctx, (value, node, property) => {\n if (property === 'preset') checkPresetValue(value, node);\n });\n },\n});\n"],"mappings":";;;;;;AAQA,2BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,kDACd;EACD,UAAU;GACR,eACE;GACF,iBACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,MAAM,sBAAsB,IAAI,IAAI;GAClC;GACA;GACA;GACA;GACA;GACD,CAAC;EAEF,SAAS,iBAAiB,OAAe,MAA2B;GAClE,MAAM,WAAW,MAAM,MAAM,CAAC,MAAM,WAAW;AAC/C,OAAI,SAAS,WAAW,EAAG;GAE3B,MAAM,cAAc,SAAS,GAAG,MAAM;GACtC,MAAM,aAAa,SAAS,IAAI,MAAM;AAEtC,OAAI,CAAC,YAAa;AAClB,OAAI,oBAAoB,IAAI,YAAY,CAAE;GAE1C,MAAM,aAAa,YAAY,MAAM,MAAM,CAAC,OAAO,QAAQ;AAO3D,OAAI,EAHF,WAAW,SAAS,KACpB,WAAW,OAAO,MAAM,iBAAiB,IAAI,EAAE,CAAC,GAEzB;IACvB,MAAM,aAAa,WAAW;AAC9B,QACE,cACA,IAAI,OAAO,QAAQ,SAAS,KAC5B,CAAC,IAAI,OAAO,QAAQ,SAAS,WAAW,CAExC,SAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM,EAAE,MAAM,YAAY;KAC3B,CAAC;;AAIN,OAAI,YACF;SAAK,MAAM,OAAO,WAAW,MAAM,MAAM,CAAC,OAAO,QAAQ,CACvD,KAAI,CAAC,iBAAiB,IAAI,IAAI,CAC5B,SAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MACJ,UAAU;MACV,OAAO,CAAC,GAAG,iBAAiB,CAAC,KAAK,KAAK;MACxC;KACF,CAAC;;;AAMV,SAAO,0BAA0B,MAAM,OAAO,MAAM,aAAa;AAC/D,OAAI,aAAa,SAAU,kBAAiB,OAAO,KAAK;IACxD;;CAEL,CAAC"}