@tenphi/eslint-plugin-tasty 0.7.0 → 0.9.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 (56) hide show
  1. package/README.md +9 -3
  2. package/dist/configs.js +10 -5
  3. package/dist/configs.js.map +1 -1
  4. package/dist/constants.js +322 -3
  5. package/dist/constants.js.map +1 -1
  6. package/dist/fix-utils.js +92 -0
  7. package/dist/fix-utils.js.map +1 -0
  8. package/dist/index.js +11 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/rules/consistent-token-usage.js +59 -12
  11. package/dist/rules/consistent-token-usage.js.map +1 -1
  12. package/dist/rules/known-property.js +1 -1
  13. package/dist/rules/known-property.js.map +1 -1
  14. package/dist/rules/no-important.js +12 -4
  15. package/dist/rules/no-important.js.map +1 -1
  16. package/dist/rules/no-nested-selector.js +2 -2
  17. package/dist/rules/no-nested-selector.js.map +1 -1
  18. package/dist/rules/no-own-at-root.js +9 -1
  19. package/dist/rules/no-own-at-root.js.map +1 -1
  20. package/dist/rules/no-raw-color-values.js +23 -7
  21. package/dist/rules/no-raw-color-values.js.map +1 -1
  22. package/dist/rules/no-runtime-styles-mutation.js +8 -5
  23. package/dist/rules/no-runtime-styles-mutation.js.map +1 -1
  24. package/dist/rules/no-unknown-state-alias.js +1 -1
  25. package/dist/rules/no-unknown-state-alias.js.map +1 -1
  26. package/dist/rules/prefer-auto-calc.js +81 -0
  27. package/dist/rules/prefer-auto-calc.js.map +1 -0
  28. package/dist/rules/prefer-custom-property-syntax.js +151 -0
  29. package/dist/rules/prefer-custom-property-syntax.js.map +1 -0
  30. package/dist/rules/prefer-directional-shorthand.js +88 -0
  31. package/dist/rules/prefer-directional-shorthand.js.map +1 -0
  32. package/dist/rules/prefer-hide.js +58 -0
  33. package/dist/rules/prefer-hide.js.map +1 -0
  34. package/dist/rules/prefer-shorthand-property.js +5 -0
  35. package/dist/rules/prefer-shorthand-property.js.map +1 -1
  36. package/dist/rules/valid-color-token.js +1 -1
  37. package/dist/rules/valid-color-token.js.map +1 -1
  38. package/dist/rules/valid-custom-property.js +2 -2
  39. package/dist/rules/valid-custom-property.js.map +1 -1
  40. package/dist/rules/valid-custom-unit.js +1 -1
  41. package/dist/rules/valid-custom-unit.js.map +1 -1
  42. package/dist/rules/valid-default-state-order.js +106 -0
  43. package/dist/rules/valid-default-state-order.js.map +1 -0
  44. package/dist/rules/valid-directional-modifier.js +1 -1
  45. package/dist/rules/valid-directional-modifier.js.map +1 -1
  46. package/dist/rules/valid-preset.js +6 -3
  47. package/dist/rules/valid-preset.js.map +1 -1
  48. package/dist/rules/valid-radius-shape.js +18 -2
  49. package/dist/rules/valid-radius-shape.js.map +1 -1
  50. package/dist/rules/valid-recipe.js +1 -1
  51. package/dist/rules/valid-recipe.js.map +1 -1
  52. package/dist/rules/valid-transition.js +52 -5
  53. package/dist/rules/valid-transition.js.map +1 -1
  54. package/dist/rules/valid-value.js +1 -1
  55. package/dist/rules/valid-value.js.map +1 -1
  56. package/package.json +1 -1
@@ -0,0 +1,92 @@
1
+ import { getStringValue } from "./utils.js";
2
+
3
+ //#region src/fix-utils.ts
4
+ /**
5
+ * Source range of a string literal's content, excluding the surrounding
6
+ * quotes/backticks. Returns null for non-string nodes (or template literals
7
+ * with expressions, whose cooked value is not a simple substring of source).
8
+ */
9
+ function getStringContentRange(node) {
10
+ if (node.type === "Literal" && typeof node.value === "string") return {
11
+ start: node.range[0] + 1,
12
+ end: node.range[1] - 1
13
+ };
14
+ if (node.type === "TemplateLiteral" && node.expressions.length === 0) return {
15
+ start: node.range[0] + 1,
16
+ end: node.range[1] - 1
17
+ };
18
+ return null;
19
+ }
20
+ /**
21
+ * Build non-overlapping range fixes inside a string literal's content.
22
+ *
23
+ * `edits` are offsets within the value string the rule already scanned (the
24
+ * cooked value returned by `getStringValue`). They are mapped to source ranges
25
+ * inside the literal content.
26
+ *
27
+ * Escape guard: when the raw source content does not equal the cooked value
28
+ * (i.e. the literal contains escape sequences), character offsets would drift,
29
+ * so we bail out and emit no fix. Tasty style values rarely contain escapes,
30
+ * so this loses almost nothing while staying correct.
31
+ */
32
+ function replaceInStringValue(fixer, valueNode, edits, sourceCode) {
33
+ const contentRange = getStringContentRange(valueNode);
34
+ if (!contentRange) return null;
35
+ const rawContent = sourceCode.getText(valueNode).slice(1, -1);
36
+ const cooked = getStringValue(valueNode);
37
+ if (cooked === null || rawContent !== cooked) return null;
38
+ const sorted = [...edits].sort((a, b) => a.start - b.start);
39
+ for (let i = 1; i < sorted.length; i++) if (sorted[i].start < sorted[i - 1].end) return null;
40
+ return sorted.map((edit) => fixer.replaceTextRange([contentRange.start + edit.start, contentRange.start + edit.end], edit.replacement));
41
+ }
42
+ /**
43
+ * Replace the entire content of a string literal (keeping the surrounding
44
+ * quotes/backticks intact). Safe for replacements that contain no quote
45
+ * characters (true for all Tasty token rewrites).
46
+ */
47
+ function replaceStringValue(fixer, valueNode, newValue) {
48
+ const contentRange = getStringContentRange(valueNode);
49
+ if (!contentRange) return null;
50
+ return fixer.replaceTextRange([contentRange.start, contentRange.end], newValue);
51
+ }
52
+ /**
53
+ * Remove an object property along with an adjacent comma so the result is
54
+ * valid syntax. Prefers removing the comma/whitespace after the property;
55
+ * for a trailing property, removes the comma/whitespace before it instead.
56
+ */
57
+ function removePropertyWithComma(fixer, prop, sourceCode) {
58
+ const objectNode = prop.parent;
59
+ if (!objectNode || objectNode.type !== "ObjectExpression") return null;
60
+ const props = objectNode.properties;
61
+ const idx = props.indexOf(prop);
62
+ if (idx === -1) return null;
63
+ if (idx < props.length - 1) {
64
+ const nextStart = props[idx + 1].range[0];
65
+ return fixer.removeRange([prop.range[0], nextStart]);
66
+ }
67
+ const removeStart = idx > 0 ? props[idx - 1].range[1] : objectNode.range[0] + 1;
68
+ return fixer.removeRange([removeStart, prop.range[1]]);
69
+ }
70
+ /**
71
+ * Move a property to `targetIndex` within its parent object (indexed among
72
+ * `Property` members only). Rebuilds the content between the braces with the
73
+ * properties joined by `, ` — acceptable for state maps, which are simple and
74
+ * rarely carry inline comments. Returns null when the property is already at
75
+ * the target index.
76
+ */
77
+ function movePropertyToIndex(fixer, prop, targetIndex, sourceCode) {
78
+ const objectNode = prop.parent;
79
+ if (!objectNode || objectNode.type !== "ObjectExpression") return null;
80
+ const props = objectNode.properties.filter((p) => p.type === "Property");
81
+ const idx = props.indexOf(prop);
82
+ if (idx === -1 || idx === targetIndex) return null;
83
+ const reordered = [...props];
84
+ const [moved] = reordered.splice(idx, 1);
85
+ reordered.splice(targetIndex, 0, moved);
86
+ const newText = reordered.map((p) => sourceCode.getText(p)).join(", ");
87
+ return fixer.replaceTextRange([objectNode.range[0] + 1, objectNode.range[1] - 1], ` ${newText} `);
88
+ }
89
+
90
+ //#endregion
91
+ export { movePropertyToIndex, removePropertyWithComma, replaceInStringValue, replaceStringValue };
92
+ //# sourceMappingURL=fix-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fix-utils.js","names":[],"sources":["../src/fix-utils.ts"],"sourcesContent":["import type { TSESLint, TSESTree } from '@typescript-eslint/utils';\nimport { getStringValue } from './utils.js';\n\n/**\n * Source range of a string literal's content, excluding the surrounding\n * quotes/backticks. Returns null for non-string nodes (or template literals\n * with expressions, whose cooked value is not a simple substring of source).\n */\nexport function getStringContentRange(\n node: TSESTree.Node,\n): { start: number; end: number } | null {\n if (node.type === 'Literal' && typeof node.value === 'string') {\n return { start: node.range[0] + 1, end: node.range[1] - 1 };\n }\n if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {\n return { start: node.range[0] + 1, end: node.range[1] - 1 };\n }\n return null;\n}\n\nexport interface StringEdit {\n /** Offset within the (cooked) value string where the edit starts. */\n start: number;\n /** Offset where the edit ends (exclusive). */\n end: number;\n /** Replacement text. */\n replacement: string;\n}\n\n/**\n * Build non-overlapping range fixes inside a string literal's content.\n *\n * `edits` are offsets within the value string the rule already scanned (the\n * cooked value returned by `getStringValue`). They are mapped to source ranges\n * inside the literal content.\n *\n * Escape guard: when the raw source content does not equal the cooked value\n * (i.e. the literal contains escape sequences), character offsets would drift,\n * so we bail out and emit no fix. Tasty style values rarely contain escapes,\n * so this loses almost nothing while staying correct.\n */\nexport function replaceInStringValue(\n fixer: TSESLint.RuleFixer,\n valueNode: TSESTree.Node,\n edits: StringEdit[],\n sourceCode: TSESLint.SourceCode,\n): TSESLint.RuleFix[] | null {\n const contentRange = getStringContentRange(valueNode);\n if (!contentRange) return null;\n\n const rawContent = sourceCode.getText(valueNode).slice(1, -1);\n const cooked = getStringValue(valueNode);\n if (cooked === null || rawContent !== cooked) return null;\n\n const sorted = [...edits].sort((a, b) => a.start - b.start);\n for (let i = 1; i < sorted.length; i++) {\n if (sorted[i].start < sorted[i - 1].end) return null;\n }\n\n return sorted.map((edit) =>\n fixer.replaceTextRange(\n [contentRange.start + edit.start, contentRange.start + edit.end],\n edit.replacement,\n ),\n );\n}\n\n/**\n * Replace the entire content of a string literal (keeping the surrounding\n * quotes/backticks intact). Safe for replacements that contain no quote\n * characters (true for all Tasty token rewrites).\n */\nexport function replaceStringValue(\n fixer: TSESLint.RuleFixer,\n valueNode: TSESTree.Node,\n newValue: string,\n): TSESLint.RuleFix | null {\n const contentRange = getStringContentRange(valueNode);\n if (!contentRange) return null;\n return fixer.replaceTextRange(\n [contentRange.start, contentRange.end],\n newValue,\n );\n}\n\n/**\n * Remove an object property along with an adjacent comma so the result is\n * valid syntax. Prefers removing the comma/whitespace after the property;\n * for a trailing property, removes the comma/whitespace before it instead.\n */\nexport function removePropertyWithComma(\n fixer: TSESLint.RuleFixer,\n prop: TSESTree.Property,\n sourceCode: TSESLint.SourceCode,\n): TSESLint.RuleFix | null {\n const objectNode = prop.parent;\n if (!objectNode || objectNode.type !== 'ObjectExpression') return null;\n\n const props = objectNode.properties;\n const idx = props.indexOf(prop);\n if (idx === -1) return null;\n\n void sourceCode;\n\n if (idx < props.length - 1) {\n const nextStart = props[idx + 1].range[0];\n return fixer.removeRange([prop.range[0], nextStart]);\n }\n\n const removeStart =\n idx > 0 ? props[idx - 1].range[1] : objectNode.range[0] + 1;\n return fixer.removeRange([removeStart, prop.range[1]]);\n}\n\n/**\n * Move a property to `targetIndex` within its parent object (indexed among\n * `Property` members only). Rebuilds the content between the braces with the\n * properties joined by `, ` — acceptable for state maps, which are simple and\n * rarely carry inline comments. Returns null when the property is already at\n * the target index.\n */\nexport function movePropertyToIndex(\n fixer: TSESLint.RuleFixer,\n prop: TSESTree.Property,\n targetIndex: number,\n sourceCode: TSESLint.SourceCode,\n): TSESLint.RuleFix | null {\n const objectNode = prop.parent;\n if (!objectNode || objectNode.type !== 'ObjectExpression') return null;\n\n const props = objectNode.properties.filter(\n (p): p is TSESTree.Property => p.type === 'Property',\n );\n const idx = props.indexOf(prop);\n if (idx === -1 || idx === targetIndex) return null;\n\n const reordered = [...props];\n const [moved] = reordered.splice(idx, 1);\n reordered.splice(targetIndex, 0, moved);\n\n const newText = reordered.map((p) => sourceCode.getText(p)).join(', ');\n\n return fixer.replaceTextRange(\n [objectNode.range[0] + 1, objectNode.range[1] - 1],\n ` ${newText} `,\n );\n}\n"],"mappings":";;;;;;;;AAQA,SAAgB,sBACd,MACuC;AACvC,KAAI,KAAK,SAAS,aAAa,OAAO,KAAK,UAAU,SACnD,QAAO;EAAE,OAAO,KAAK,MAAM,KAAK;EAAG,KAAK,KAAK,MAAM,KAAK;EAAG;AAE7D,KAAI,KAAK,SAAS,qBAAqB,KAAK,YAAY,WAAW,EACjE,QAAO;EAAE,OAAO,KAAK,MAAM,KAAK;EAAG,KAAK,KAAK,MAAM,KAAK;EAAG;AAE7D,QAAO;;;;;;;;;;;;;;AAwBT,SAAgB,qBACd,OACA,WACA,OACA,YAC2B;CAC3B,MAAM,eAAe,sBAAsB,UAAU;AACrD,KAAI,CAAC,aAAc,QAAO;CAE1B,MAAM,aAAa,WAAW,QAAQ,UAAU,CAAC,MAAM,GAAG,GAAG;CAC7D,MAAM,SAAS,eAAe,UAAU;AACxC,KAAI,WAAW,QAAQ,eAAe,OAAQ,QAAO;CAErD,MAAM,SAAS,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM;AAC3D,MAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,IACjC,KAAI,OAAO,GAAG,QAAQ,OAAO,IAAI,GAAG,IAAK,QAAO;AAGlD,QAAO,OAAO,KAAK,SACjB,MAAM,iBACJ,CAAC,aAAa,QAAQ,KAAK,OAAO,aAAa,QAAQ,KAAK,IAAI,EAChE,KAAK,YACN,CACF;;;;;;;AAQH,SAAgB,mBACd,OACA,WACA,UACyB;CACzB,MAAM,eAAe,sBAAsB,UAAU;AACrD,KAAI,CAAC,aAAc,QAAO;AAC1B,QAAO,MAAM,iBACX,CAAC,aAAa,OAAO,aAAa,IAAI,EACtC,SACD;;;;;;;AAQH,SAAgB,wBACd,OACA,MACA,YACyB;CACzB,MAAM,aAAa,KAAK;AACxB,KAAI,CAAC,cAAc,WAAW,SAAS,mBAAoB,QAAO;CAElE,MAAM,QAAQ,WAAW;CACzB,MAAM,MAAM,MAAM,QAAQ,KAAK;AAC/B,KAAI,QAAQ,GAAI,QAAO;AAIvB,KAAI,MAAM,MAAM,SAAS,GAAG;EAC1B,MAAM,YAAY,MAAM,MAAM,GAAG,MAAM;AACvC,SAAO,MAAM,YAAY,CAAC,KAAK,MAAM,IAAI,UAAU,CAAC;;CAGtD,MAAM,cACJ,MAAM,IAAI,MAAM,MAAM,GAAG,MAAM,KAAK,WAAW,MAAM,KAAK;AAC5D,QAAO,MAAM,YAAY,CAAC,aAAa,KAAK,MAAM,GAAG,CAAC;;;;;;;;;AAUxD,SAAgB,oBACd,OACA,MACA,aACA,YACyB;CACzB,MAAM,aAAa,KAAK;AACxB,KAAI,CAAC,cAAc,WAAW,SAAS,mBAAoB,QAAO;CAElE,MAAM,QAAQ,WAAW,WAAW,QACjC,MAA8B,EAAE,SAAS,WAC3C;CACD,MAAM,MAAM,MAAM,QAAQ,KAAK;AAC/B,KAAI,QAAQ,MAAM,QAAQ,YAAa,QAAO;CAE9C,MAAM,YAAY,CAAC,GAAG,MAAM;CAC5B,MAAM,CAAC,SAAS,UAAU,OAAO,KAAK,EAAE;AACxC,WAAU,OAAO,aAAa,GAAG,MAAM;CAEvC,MAAM,UAAU,UAAU,KAAK,MAAM,WAAW,QAAQ,EAAE,CAAC,CAAC,KAAK,KAAK;AAEtE,QAAO,MAAM,iBACX,CAAC,WAAW,MAAM,KAAK,GAAG,WAAW,MAAM,KAAK,EAAE,EAClD,IAAI,QAAQ,GACb"}
package/dist/index.js CHANGED
@@ -26,6 +26,11 @@ import consistent_token_usage_default from "./rules/consistent-token-usage.js";
26
26
  import no_runtime_styles_mutation_default from "./rules/no-runtime-styles-mutation.js";
27
27
  import valid_state_definition_default from "./rules/valid-state-definition.js";
28
28
  import no_own_at_root_default from "./rules/no-own-at-root.js";
29
+ import valid_default_state_order_default from "./rules/valid-default-state-order.js";
30
+ import prefer_auto_calc_default from "./rules/prefer-auto-calc.js";
31
+ import prefer_custom_property_syntax_default from "./rules/prefer-custom-property-syntax.js";
32
+ import prefer_hide_default from "./rules/prefer-hide.js";
33
+ import prefer_directional_shorthand_default from "./rules/prefer-directional-shorthand.js";
29
34
  import { recommended, strict } from "./configs.js";
30
35
 
31
36
  //#region src/index.ts
@@ -62,7 +67,12 @@ const plugin = {
62
67
  "consistent-token-usage": consistent_token_usage_default,
63
68
  "no-runtime-styles-mutation": no_runtime_styles_mutation_default,
64
69
  "valid-state-definition": valid_state_definition_default,
65
- "no-own-at-root": no_own_at_root_default
70
+ "no-own-at-root": no_own_at_root_default,
71
+ "valid-default-state-order": valid_default_state_order_default,
72
+ "prefer-auto-calc": prefer_auto_calc_default,
73
+ "prefer-custom-property-syntax": prefer_custom_property_syntax_default,
74
+ "prefer-hide": prefer_hide_default,
75
+ "prefer-directional-shorthand": prefer_directional_shorthand_default
66
76
  },
67
77
  configs: {
68
78
  recommended: {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["rules.knownProperty","rules.validValue","rules.validColorToken","rules.validCustomUnit","rules.validStateKey","rules.validStylesStructure","rules.noNestedSelector","rules.validCustomProperty","rules.validPreset","rules.validRecipe","rules.validBooleanProperty","rules.validDirectionalModifier","rules.validRadiusShape","rules.noImportant","rules.validSubElement","rules.noNestedStateMap","rules.staticNoDynamicValues","rules.staticValidSelector","rules.preferShorthandProperty","rules.validTransition","rules.requireDefaultState","rules.noUnknownStateAlias","rules.noRawColorValues","rules.noStylesProp","rules.consistentTokenUsage","rules.noRuntimeStylesMutation","rules.validStateDefinition","rules.noOwnAtRoot"],"sources":["../src/index.ts"],"sourcesContent":["import type { TSESLint } from '@typescript-eslint/utils';\nimport * as rules from './rules/index.js';\nimport { recommended, strict } from './configs.js';\n\nconst ruleMap: Record<string, TSESLint.RuleModule<string, unknown[]>> = {\n 'known-property': rules.knownProperty,\n 'valid-value': rules.validValue,\n 'valid-color-token': rules.validColorToken,\n 'valid-custom-unit': rules.validCustomUnit,\n 'valid-state-key': rules.validStateKey,\n 'valid-styles-structure': rules.validStylesStructure,\n 'no-nested-selector': rules.noNestedSelector,\n 'valid-custom-property': rules.validCustomProperty,\n 'valid-preset': rules.validPreset,\n 'valid-recipe': rules.validRecipe,\n 'valid-boolean-property': rules.validBooleanProperty,\n 'valid-directional-modifier': rules.validDirectionalModifier,\n 'valid-radius-shape': rules.validRadiusShape,\n 'no-important': rules.noImportant,\n 'valid-sub-element': rules.validSubElement,\n 'no-nested-state-map': rules.noNestedStateMap,\n 'static-no-dynamic-values': rules.staticNoDynamicValues,\n 'static-valid-selector': rules.staticValidSelector,\n 'prefer-shorthand-property': rules.preferShorthandProperty,\n 'valid-transition': rules.validTransition,\n 'require-default-state': rules.requireDefaultState,\n 'no-unknown-state-alias': rules.noUnknownStateAlias,\n 'no-raw-color-values': rules.noRawColorValues,\n 'no-styles-prop': rules.noStylesProp,\n 'consistent-token-usage': rules.consistentTokenUsage,\n 'no-runtime-styles-mutation': rules.noRuntimeStylesMutation,\n 'valid-state-definition': rules.validStateDefinition,\n 'no-own-at-root': rules.noOwnAtRoot,\n};\n\nconst plugin = {\n meta: {\n name: '@tenphi/eslint-plugin-tasty',\n version: '0.1.0',\n },\n rules: ruleMap,\n configs: {\n recommended: {\n plugins: {\n get tasty() {\n return plugin;\n },\n },\n rules: recommended,\n },\n strict: {\n plugins: {\n get tasty() {\n return plugin;\n },\n },\n rules: strict,\n },\n },\n} satisfies TSESLint.FlatConfig.Plugin & {\n configs: Record<string, TSESLint.FlatConfig.Config>;\n};\n\nexport default plugin;\n\nexport { recommended, strict } from './configs.js';\nexport type { TastyValidationConfig, ResolvedConfig } from './types.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,MAAM,SAAS;CACb,MAAM;EACJ,MAAM;EACN,SAAS;EACV;CACD,OApCsE;EACtE,kBAAkBA;EAClB,eAAeC;EACf,qBAAqBC;EACrB,qBAAqBC;EACrB,mBAAmBC;EACnB,0BAA0BC;EAC1B,sBAAsBC;EACtB,yBAAyBC;EACzB,gBAAgBC;EAChB,gBAAgBC;EAChB,0BAA0BC;EAC1B,8BAA8BC;EAC9B,sBAAsBC;EACtB,gBAAgBC;EAChB,qBAAqBC;EACrB,uBAAuBC;EACvB,4BAA4BC;EAC5B,yBAAyBC;EACzB,6BAA6BC;EAC7B,oBAAoBC;EACpB,yBAAyBC;EACzB,0BAA0BC;EAC1B,uBAAuBC;EACvB,kBAAkBC;EAClB,0BAA0BC;EAC1B,8BAA8BC;EAC9B,0BAA0BC;EAC1B,kBAAkBC;EACnB;CAQC,SAAS;EACP,aAAa;GACX,SAAS,EACP,IAAI,QAAQ;AACV,WAAO;MAEV;GACD,OAAO;GACR;EACD,QAAQ;GACN,SAAS,EACP,IAAI,QAAQ;AACV,WAAO;MAEV;GACD,OAAO;GACR;EACF;CACF"}
1
+ {"version":3,"file":"index.js","names":["rules.knownProperty","rules.validValue","rules.validColorToken","rules.validCustomUnit","rules.validStateKey","rules.validStylesStructure","rules.noNestedSelector","rules.validCustomProperty","rules.validPreset","rules.validRecipe","rules.validBooleanProperty","rules.validDirectionalModifier","rules.validRadiusShape","rules.noImportant","rules.validSubElement","rules.noNestedStateMap","rules.staticNoDynamicValues","rules.staticValidSelector","rules.preferShorthandProperty","rules.validTransition","rules.requireDefaultState","rules.noUnknownStateAlias","rules.noRawColorValues","rules.noStylesProp","rules.consistentTokenUsage","rules.noRuntimeStylesMutation","rules.validStateDefinition","rules.noOwnAtRoot","rules.validDefaultStateOrder","rules.preferAutoCalc","rules.preferCustomPropertySyntax","rules.preferHide","rules.preferDirectionalShorthand"],"sources":["../src/index.ts"],"sourcesContent":["import type { TSESLint } from '@typescript-eslint/utils';\nimport * as rules from './rules/index.js';\nimport { recommended, strict } from './configs.js';\n\nconst ruleMap: Record<string, TSESLint.RuleModule<string, unknown[]>> = {\n 'known-property': rules.knownProperty,\n 'valid-value': rules.validValue,\n 'valid-color-token': rules.validColorToken,\n 'valid-custom-unit': rules.validCustomUnit,\n 'valid-state-key': rules.validStateKey,\n 'valid-styles-structure': rules.validStylesStructure,\n 'no-nested-selector': rules.noNestedSelector,\n 'valid-custom-property': rules.validCustomProperty,\n 'valid-preset': rules.validPreset,\n 'valid-recipe': rules.validRecipe,\n 'valid-boolean-property': rules.validBooleanProperty,\n 'valid-directional-modifier': rules.validDirectionalModifier,\n 'valid-radius-shape': rules.validRadiusShape,\n 'no-important': rules.noImportant,\n 'valid-sub-element': rules.validSubElement,\n 'no-nested-state-map': rules.noNestedStateMap,\n 'static-no-dynamic-values': rules.staticNoDynamicValues,\n 'static-valid-selector': rules.staticValidSelector,\n 'prefer-shorthand-property': rules.preferShorthandProperty,\n 'valid-transition': rules.validTransition,\n 'require-default-state': rules.requireDefaultState,\n 'no-unknown-state-alias': rules.noUnknownStateAlias,\n 'no-raw-color-values': rules.noRawColorValues,\n 'no-styles-prop': rules.noStylesProp,\n 'consistent-token-usage': rules.consistentTokenUsage,\n 'no-runtime-styles-mutation': rules.noRuntimeStylesMutation,\n 'valid-state-definition': rules.validStateDefinition,\n 'no-own-at-root': rules.noOwnAtRoot,\n 'valid-default-state-order': rules.validDefaultStateOrder,\n 'prefer-auto-calc': rules.preferAutoCalc,\n 'prefer-custom-property-syntax': rules.preferCustomPropertySyntax,\n 'prefer-hide': rules.preferHide,\n 'prefer-directional-shorthand': rules.preferDirectionalShorthand,\n};\n\nconst plugin = {\n meta: {\n name: '@tenphi/eslint-plugin-tasty',\n version: '0.1.0',\n },\n rules: ruleMap,\n configs: {\n recommended: {\n plugins: {\n get tasty() {\n return plugin;\n },\n },\n rules: recommended,\n },\n strict: {\n plugins: {\n get tasty() {\n return plugin;\n },\n },\n rules: strict,\n },\n },\n} satisfies TSESLint.FlatConfig.Plugin & {\n configs: Record<string, TSESLint.FlatConfig.Config>;\n};\n\nexport default plugin;\n\nexport { recommended, strict } from './configs.js';\nexport type { TastyValidationConfig, ResolvedConfig } from './types.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,MAAM,SAAS;CACb,MAAM;EACJ,MAAM;EACN,SAAS;EACV;CACD,OAzCsE;EACtE,kBAAkBA;EAClB,eAAeC;EACf,qBAAqBC;EACrB,qBAAqBC;EACrB,mBAAmBC;EACnB,0BAA0BC;EAC1B,sBAAsBC;EACtB,yBAAyBC;EACzB,gBAAgBC;EAChB,gBAAgBC;EAChB,0BAA0BC;EAC1B,8BAA8BC;EAC9B,sBAAsBC;EACtB,gBAAgBC;EAChB,qBAAqBC;EACrB,uBAAuBC;EACvB,4BAA4BC;EAC5B,yBAAyBC;EACzB,6BAA6BC;EAC7B,oBAAoBC;EACpB,yBAAyBC;EACzB,0BAA0BC;EAC1B,uBAAuBC;EACvB,kBAAkBC;EAClB,0BAA0BC;EAC1B,8BAA8BC;EAC9B,0BAA0BC;EAC1B,kBAAkBC;EAClB,6BAA6BC;EAC7B,oBAAoBC;EACpB,iCAAiCC;EACjC,eAAeC;EACf,gCAAgCC;EACjC;CAQC,SAAS;EACP,aAAa;GACX,SAAS,EACP,IAAI,QAAQ;AACV,WAAO;MAEV;GACD,OAAO;GACR;EACD,QAAQ;GACN,SAAS,EACP,IAAI,QAAQ;AACV,WAAO;MAEV;GACD,OAAO;GACR;EACF;CACF"}
@@ -1,6 +1,7 @@
1
1
  import { createRule } from "../create-rule.js";
2
2
  import { getKeyName, getStringValue } from "../utils.js";
3
3
  import { TastyContext, styleObjectListeners } from "../context.js";
4
+ import { replaceInStringValue, replaceStringValue } from "../fix-utils.js";
4
5
 
5
6
  //#region src/rules/consistent-token-usage.ts
6
7
  const PX_TO_UNIT = {
@@ -17,8 +18,12 @@ var consistent_token_usage_default = createRule({
17
18
  name: "consistent-token-usage",
18
19
  meta: {
19
20
  type: "suggestion",
21
+ hasSuggestions: true,
20
22
  docs: { description: "Suggest using design tokens and custom units instead of raw CSS values" },
21
- messages: { preferToken: "Consider using '{{suggestion}}' instead of '{{raw}}'." },
23
+ messages: {
24
+ preferToken: "Consider using '{{suggestion}}' instead of '{{raw}}'.",
25
+ replaceWithToken: "Replace '{{raw}}' with '{{suggestion}}'"
26
+ },
22
27
  schema: []
23
28
  },
24
29
  defaultOptions: [],
@@ -27,13 +32,24 @@ var consistent_token_usage_default = createRule({
27
32
  function checkValue(property, value, node) {
28
33
  const trimmed = value.trim();
29
34
  if (trimmed in PX_TO_UNIT) {
35
+ const suggestion = PX_TO_UNIT[trimmed];
30
36
  context.report({
31
37
  node,
32
38
  messageId: "preferToken",
33
39
  data: {
34
- suggestion: PX_TO_UNIT[trimmed],
40
+ suggestion,
35
41
  raw: trimmed
36
- }
42
+ },
43
+ suggest: [{
44
+ messageId: "replaceWithToken",
45
+ data: {
46
+ raw: trimmed,
47
+ suggestion
48
+ },
49
+ fix(fixer) {
50
+ return replaceStringValue(fixer, node, suggestion);
51
+ }
52
+ }]
37
53
  });
38
54
  return;
39
55
  }
@@ -44,18 +60,49 @@ var consistent_token_usage_default = createRule({
44
60
  data: {
45
61
  suggestion: "1r",
46
62
  raw: "6px"
47
- }
63
+ },
64
+ suggest: [{
65
+ messageId: "replaceWithToken",
66
+ data: {
67
+ raw: "6px",
68
+ suggestion: "1r"
69
+ },
70
+ fix(fixer) {
71
+ return replaceStringValue(fixer, node, "1r");
72
+ }
73
+ }]
48
74
  });
49
75
  return;
50
76
  }
51
- if (property === "border" && trimmed.includes("1px")) context.report({
52
- node,
53
- messageId: "preferToken",
54
- data: {
55
- suggestion: "1bw",
56
- raw: "1px"
57
- }
58
- });
77
+ if (property === "border" && trimmed.includes("1px")) {
78
+ const edits = [];
79
+ const re = /\b1px\b/g;
80
+ let match;
81
+ while ((match = re.exec(value)) !== null) edits.push({
82
+ start: match.index,
83
+ end: match.index + match[0].length,
84
+ replacement: "1bw"
85
+ });
86
+ if (edits.length === 0) return;
87
+ context.report({
88
+ node,
89
+ messageId: "preferToken",
90
+ data: {
91
+ suggestion: "1bw",
92
+ raw: "1px"
93
+ },
94
+ suggest: [{
95
+ messageId: "replaceWithToken",
96
+ data: {
97
+ raw: "1px",
98
+ suggestion: "1bw"
99
+ },
100
+ fix(fixer) {
101
+ return replaceInStringValue(fixer, node, edits, context.sourceCode);
102
+ }
103
+ }]
104
+ });
105
+ }
59
106
  }
60
107
  function handleStyleObject(node) {
61
108
  if (!ctx.isStyleObject(node)) return;
@@ -1 +1 @@
1
- {"version":3,"file":"consistent-token-usage.js","names":[],"sources":["../../src/rules/consistent-token-usage.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 = 'preferToken';\n\nconst PX_TO_UNIT: Record<string, string> = {\n '8px': '1x',\n '16px': '2x',\n '24px': '3x',\n '32px': '4x',\n '40px': '5x',\n '48px': '6x',\n '56px': '7x',\n '64px': '8x',\n};\n\nexport default createRule<[], MessageIds>({\n name: 'consistent-token-usage',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Suggest using design tokens and custom units instead of raw CSS values',\n },\n messages: {\n preferToken: \"Consider using '{{suggestion}}' instead of '{{raw}}'.\",\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 trimmed = value.trim();\n\n // Check pixel values that map to gap multiples\n if (trimmed in PX_TO_UNIT) {\n context.report({\n node,\n messageId: 'preferToken',\n data: { suggestion: PX_TO_UNIT[trimmed], raw: trimmed },\n });\n return;\n }\n\n // Check 6px in radius context\n if (property === 'radius' && trimmed === '6px') {\n context.report({\n node,\n messageId: 'preferToken',\n data: { suggestion: '1r', raw: '6px' },\n });\n return;\n }\n\n // Check 1px in border context\n if (property === 'border' && trimmed.includes('1px')) {\n context.report({\n node,\n messageId: 'preferToken',\n data: { suggestion: '1bw', raw: '1px' },\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 const str = getStringValue(prop.value);\n if (str) {\n checkValue(key, str, prop.value);\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 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":";;;;;AAOA,MAAM,aAAqC;CACzC,OAAO;CACP,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACT;AAED,qCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,0EACH;EACD,UAAU,EACR,aAAa,yDACd;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,WACP,UACA,OACA,MACM;GACN,MAAM,UAAU,MAAM,MAAM;AAG5B,OAAI,WAAW,YAAY;AACzB,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MAAE,YAAY,WAAW;MAAU,KAAK;MAAS;KACxD,CAAC;AACF;;AAIF,OAAI,aAAa,YAAY,YAAY,OAAO;AAC9C,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MAAE,YAAY;MAAM,KAAK;MAAO;KACvC,CAAC;AACF;;AAIF,OAAI,aAAa,YAAY,QAAQ,SAAS,MAAM,CAClD,SAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM;KAAE,YAAY;KAAO,KAAK;KAAO;IACxC,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;IAElB,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,QAAI,KAAK;AACP,gBAAW,KAAK,KAAK,KAAK,MAAM;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,SACF,YAAW,KAAK,UAAU,UAAU,MAAM;;;;AAOpD,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
1
+ {"version":3,"file":"consistent-token-usage.js","names":[],"sources":["../../src/rules/consistent-token-usage.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 { replaceStringValue, replaceInStringValue } from '../fix-utils.js';\n\ntype MessageIds = 'preferToken' | 'replaceWithToken';\n\nconst PX_TO_UNIT: Record<string, string> = {\n '8px': '1x',\n '16px': '2x',\n '24px': '3x',\n '32px': '4x',\n '40px': '5x',\n '48px': '6x',\n '56px': '7x',\n '64px': '8x',\n};\n\nexport default createRule<[], MessageIds>({\n name: 'consistent-token-usage',\n meta: {\n type: 'suggestion',\n hasSuggestions: true,\n docs: {\n description:\n 'Suggest using design tokens and custom units instead of raw CSS values',\n },\n messages: {\n preferToken: \"Consider using '{{suggestion}}' instead of '{{raw}}'.\",\n replaceWithToken: \"Replace '{{raw}}' with '{{suggestion}}'\",\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 trimmed = value.trim();\n\n // Check pixel values that map to gap multiples\n if (trimmed in PX_TO_UNIT) {\n const suggestion = PX_TO_UNIT[trimmed];\n context.report({\n node,\n messageId: 'preferToken',\n data: { suggestion, raw: trimmed },\n suggest: [\n {\n messageId: 'replaceWithToken',\n data: { raw: trimmed, suggestion },\n fix(fixer) {\n return replaceStringValue(fixer, node, suggestion);\n },\n },\n ],\n });\n return;\n }\n\n // Check 6px in radius context\n if (property === 'radius' && trimmed === '6px') {\n context.report({\n node,\n messageId: 'preferToken',\n data: { suggestion: '1r', raw: '6px' },\n suggest: [\n {\n messageId: 'replaceWithToken',\n data: { raw: '6px', suggestion: '1r' },\n fix(fixer) {\n return replaceStringValue(fixer, node, '1r');\n },\n },\n ],\n });\n return;\n }\n\n // Check 1px in border context\n if (property === 'border' && trimmed.includes('1px')) {\n const edits: { start: number; end: number; replacement: string }[] = [];\n const re = /\\b1px\\b/g;\n let match: RegExpExecArray | null;\n while ((match = re.exec(value)) !== null) {\n edits.push({\n start: match.index,\n end: match.index + match[0].length,\n replacement: '1bw',\n });\n }\n if (edits.length === 0) return;\n context.report({\n node,\n messageId: 'preferToken',\n data: { suggestion: '1bw', raw: '1px' },\n suggest: [\n {\n messageId: 'replaceWithToken',\n data: { raw: '1px', suggestion: '1bw' },\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n edits,\n context.sourceCode,\n );\n },\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 const str = getStringValue(prop.value);\n if (str) {\n checkValue(key, str, prop.value);\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 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":";;;;;;AAQA,MAAM,aAAqC;CACzC,OAAO;CACP,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACT;AAED,qCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,gBAAgB;EAChB,MAAM,EACJ,aACE,0EACH;EACD,UAAU;GACR,aAAa;GACb,kBAAkB;GACnB;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,WACP,UACA,OACA,MACM;GACN,MAAM,UAAU,MAAM,MAAM;AAG5B,OAAI,WAAW,YAAY;IACzB,MAAM,aAAa,WAAW;AAC9B,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MAAE;MAAY,KAAK;MAAS;KAClC,SAAS,CACP;MACE,WAAW;MACX,MAAM;OAAE,KAAK;OAAS;OAAY;MAClC,IAAI,OAAO;AACT,cAAO,mBAAmB,OAAO,MAAM,WAAW;;MAErD,CACF;KACF,CAAC;AACF;;AAIF,OAAI,aAAa,YAAY,YAAY,OAAO;AAC9C,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MAAE,YAAY;MAAM,KAAK;MAAO;KACtC,SAAS,CACP;MACE,WAAW;MACX,MAAM;OAAE,KAAK;OAAO,YAAY;OAAM;MACtC,IAAI,OAAO;AACT,cAAO,mBAAmB,OAAO,MAAM,KAAK;;MAE/C,CACF;KACF,CAAC;AACF;;AAIF,OAAI,aAAa,YAAY,QAAQ,SAAS,MAAM,EAAE;IACpD,MAAM,QAA+D,EAAE;IACvE,MAAM,KAAK;IACX,IAAI;AACJ,YAAQ,QAAQ,GAAG,KAAK,MAAM,MAAM,KAClC,OAAM,KAAK;KACT,OAAO,MAAM;KACb,KAAK,MAAM,QAAQ,MAAM,GAAG;KAC5B,aAAa;KACd,CAAC;AAEJ,QAAI,MAAM,WAAW,EAAG;AACxB,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MAAE,YAAY;MAAO,KAAK;MAAO;KACvC,SAAS,CACP;MACE,WAAW;MACX,MAAM;OAAE,KAAK;OAAO,YAAY;OAAO;MACvC,IAAI,OAAO;AACT,cAAO,qBACL,OACA,MACA,OACA,QAAQ,WACT;;MAEJ,CACF;KACF,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;IAElB,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,QAAI,KAAK;AACP,gBAAW,KAAK,KAAK,KAAK,MAAM;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,SACF,YAAW,KAAK,UAAU,UAAU,MAAM;;;;AAOpD,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
@@ -9,7 +9,7 @@ var known_property_default = createRule({
9
9
  meta: {
10
10
  type: "problem",
11
11
  docs: { description: "Warn when a style property name is not recognized as a valid tasty or CSS property" },
12
- messages: { unknownProperty: "Unknown style property '{{name}}'." },
12
+ messages: { unknownProperty: "Unknown style property '{{name}}'. Check the spelling or use a valid Tasty/CSS property." },
13
13
  schema: []
14
14
  },
15
15
  defaultOptions: [],
@@ -1 +1 @@
1
- {"version":3,"file":"known-property.js","names":[],"sources":["../../src/rules/known-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 {\n KNOWN_TASTY_PROPERTIES,\n KNOWN_CSS_PROPERTIES,\n SPECIAL_STYLE_KEYS,\n SHORTHAND_MAPPING,\n} from '../constants.js';\n\ntype MessageIds = 'unknownProperty';\n\nexport default createRule<[], MessageIds>({\n name: 'known-property',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Warn when a style property name is not recognized as a valid tasty or CSS property',\n },\n messages: {\n unknownProperty: \"Unknown style property '{{name}}'.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\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-element keys (uppercase start)\n if (/^[A-Z]/.test(key)) continue;\n\n // Nested selectors (handled by no-nested-selector)\n if (key.startsWith('&')) continue;\n\n // Special @ keys\n if (key.startsWith('@')) continue;\n\n // Custom CSS property definitions\n if (key.startsWith('$')) continue;\n\n // Color token definitions\n if (key.startsWith('#')) continue;\n\n // CSS custom properties (--foo)\n if (key.startsWith('--')) continue;\n\n // Vendor-prefixed properties (-webkit-*, -moz-*, etc.)\n if (key.startsWith('-')) continue;\n\n // Known tasty property\n if (KNOWN_TASTY_PROPERTIES.has(key)) continue;\n\n // Known CSS property\n if (KNOWN_CSS_PROPERTIES.has(key)) continue;\n\n // Special style keys\n if (SPECIAL_STYLE_KEYS.has(key)) continue;\n\n // Shorthand mappings (reported by prefer-shorthand-property)\n if (key in SHORTHAND_MAPPING) continue;\n\n // Custom styles from config\n if (ctx.config.styles.includes(key)) continue;\n\n // Check if the parent is a state map — state keys are not properties\n if (\n prop.value.type === 'ObjectExpression' &&\n node.parent?.type === 'Property'\n ) {\n const parentProp = node.parent as TSESTree.Property;\n if (!parentProp.computed && ctx.isStateMap(node, parentProp)) {\n continue;\n }\n }\n\n context.report({\n node: prop.key,\n messageId: 'unknownProperty',\n data: { name: key },\n });\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;AAaA,6BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,sFACH;EACD,UAAU,EACR,iBAAiB,sCAClB;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,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,SAAS,KAAK,IAAI,CAAE;AAGxB,QAAI,IAAI,WAAW,IAAI,CAAE;AAGzB,QAAI,IAAI,WAAW,IAAI,CAAE;AAGzB,QAAI,IAAI,WAAW,IAAI,CAAE;AAGzB,QAAI,IAAI,WAAW,IAAI,CAAE;AAGzB,QAAI,IAAI,WAAW,KAAK,CAAE;AAG1B,QAAI,IAAI,WAAW,IAAI,CAAE;AAGzB,QAAI,uBAAuB,IAAI,IAAI,CAAE;AAGrC,QAAI,qBAAqB,IAAI,IAAI,CAAE;AAGnC,QAAI,mBAAmB,IAAI,IAAI,CAAE;AAGjC,QAAI,OAAO,kBAAmB;AAG9B,QAAI,IAAI,OAAO,OAAO,SAAS,IAAI,CAAE;AAGrC,QACE,KAAK,MAAM,SAAS,sBACpB,KAAK,QAAQ,SAAS,YACtB;KACA,MAAM,aAAa,KAAK;AACxB,SAAI,CAAC,WAAW,YAAY,IAAI,WAAW,MAAM,WAAW,CAC1D;;AAIJ,YAAQ,OAAO;KACb,MAAM,KAAK;KACX,WAAW;KACX,MAAM,EAAE,MAAM,KAAK;KACpB,CAAC;;;AAIN,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
1
+ {"version":3,"file":"known-property.js","names":[],"sources":["../../src/rules/known-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 {\n KNOWN_TASTY_PROPERTIES,\n KNOWN_CSS_PROPERTIES,\n SPECIAL_STYLE_KEYS,\n SHORTHAND_MAPPING,\n} from '../constants.js';\n\ntype MessageIds = 'unknownProperty';\n\nexport default createRule<[], MessageIds>({\n name: 'known-property',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Warn when a style property name is not recognized as a valid tasty or CSS property',\n },\n messages: {\n unknownProperty:\n \"Unknown style property '{{name}}'. Check the spelling or use a valid Tasty/CSS property.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\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-element keys (uppercase start)\n if (/^[A-Z]/.test(key)) continue;\n\n // Nested selectors (handled by no-nested-selector)\n if (key.startsWith('&')) continue;\n\n // Special @ keys\n if (key.startsWith('@')) continue;\n\n // Custom CSS property definitions\n if (key.startsWith('$')) continue;\n\n // Color token definitions\n if (key.startsWith('#')) continue;\n\n // CSS custom properties (--foo)\n if (key.startsWith('--')) continue;\n\n // Vendor-prefixed properties (-webkit-*, -moz-*, etc.)\n if (key.startsWith('-')) continue;\n\n // Known tasty property\n if (KNOWN_TASTY_PROPERTIES.has(key)) continue;\n\n // Known CSS property\n if (KNOWN_CSS_PROPERTIES.has(key)) continue;\n\n // Special style keys\n if (SPECIAL_STYLE_KEYS.has(key)) continue;\n\n // Shorthand mappings (reported by prefer-shorthand-property)\n if (key in SHORTHAND_MAPPING) continue;\n\n // Custom styles from config\n if (ctx.config.styles.includes(key)) continue;\n\n // Check if the parent is a state map — state keys are not properties\n if (\n prop.value.type === 'ObjectExpression' &&\n node.parent?.type === 'Property'\n ) {\n const parentProp = node.parent as TSESTree.Property;\n if (!parentProp.computed && ctx.isStateMap(node, parentProp)) {\n continue;\n }\n }\n\n context.report({\n node: prop.key,\n messageId: 'unknownProperty',\n data: { name: key },\n });\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;AAaA,6BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,sFACH;EACD,UAAU,EACR,iBACE,4FACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,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,SAAS,KAAK,IAAI,CAAE;AAGxB,QAAI,IAAI,WAAW,IAAI,CAAE;AAGzB,QAAI,IAAI,WAAW,IAAI,CAAE;AAGzB,QAAI,IAAI,WAAW,IAAI,CAAE;AAGzB,QAAI,IAAI,WAAW,IAAI,CAAE;AAGzB,QAAI,IAAI,WAAW,KAAK,CAAE;AAG1B,QAAI,IAAI,WAAW,IAAI,CAAE;AAGzB,QAAI,uBAAuB,IAAI,IAAI,CAAE;AAGrC,QAAI,qBAAqB,IAAI,IAAI,CAAE;AAGnC,QAAI,mBAAmB,IAAI,IAAI,CAAE;AAGjC,QAAI,OAAO,kBAAmB;AAG9B,QAAI,IAAI,OAAO,OAAO,SAAS,IAAI,CAAE;AAGrC,QACE,KAAK,MAAM,SAAS,sBACpB,KAAK,QAAQ,SAAS,YACtB;KACA,MAAM,aAAa,KAAK;AACxB,SAAI,CAAC,WAAW,YAAY,IAAI,WAAW,MAAM,WAAW,CAC1D;;AAIJ,YAAQ,OAAO;KACb,MAAM,KAAK;KACX,WAAW;KACX,MAAM,EAAE,MAAM,KAAK;KACpB,CAAC;;;AAIN,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
@@ -1,12 +1,14 @@
1
1
  import { createRule } from "../create-rule.js";
2
2
  import { getStringValue } from "../utils.js";
3
3
  import { TastyContext, styleObjectListeners } from "../context.js";
4
+ import { replaceStringValue } from "../fix-utils.js";
4
5
 
5
6
  //#region src/rules/no-important.ts
6
7
  var no_important_default = createRule({
7
8
  name: "no-important",
8
9
  meta: {
9
10
  type: "problem",
11
+ fixable: "code",
10
12
  docs: { description: "Disallow !important in tasty style values" },
11
13
  messages: { noImportant: "Do not use !important in tasty styles. The tasty system manages specificity via doubled selectors and state ordering." },
12
14
  schema: []
@@ -16,10 +18,16 @@ var no_important_default = createRule({
16
18
  const ctx = new TastyContext(context);
17
19
  function checkNode(node) {
18
20
  const str = getStringValue(node);
19
- if (str && str.includes("!important")) context.report({
20
- node,
21
- messageId: "noImportant"
22
- });
21
+ if (str && str.includes("!important")) {
22
+ const stripped = str.replace(/\s*!important\s*/g, "").trim();
23
+ context.report({
24
+ node,
25
+ messageId: "noImportant",
26
+ fix(fixer) {
27
+ return replaceStringValue(fixer, node, stripped);
28
+ }
29
+ });
30
+ }
23
31
  }
24
32
  function handleStyleObject(node) {
25
33
  if (!ctx.isStyleObject(node)) return;
@@ -1 +1 @@
1
- {"version":3,"file":"no-important.js","names":[],"sources":["../../src/rules/no-important.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getStringValue } from '../utils.js';\n\ntype MessageIds = 'noImportant';\n\nexport default createRule<[], MessageIds>({\n name: 'no-important',\n meta: {\n type: 'problem',\n docs: {\n description: 'Disallow !important in tasty style values',\n },\n messages: {\n noImportant:\n 'Do not use !important in tasty styles. The tasty system manages specificity via doubled selectors and state ordering.',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkNode(node: TSESTree.Node): void {\n const str = getStringValue(node);\n if (str && str.includes('!important')) {\n context.report({ node, messageId: 'noImportant' });\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,2BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,6CACd;EACD,UAAU,EACR,aACE,yHACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,UAAU,MAA2B;GAC5C,MAAM,MAAM,eAAe,KAAK;AAChC,OAAI,OAAO,IAAI,SAAS,aAAa,CACnC,SAAQ,OAAO;IAAE;IAAM,WAAW;IAAe,CAAC;;EAItD,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":"no-important.js","names":[],"sources":["../../src/rules/no-important.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getStringValue } from '../utils.js';\nimport { replaceStringValue } from '../fix-utils.js';\n\ntype MessageIds = 'noImportant';\n\nexport default createRule<[], MessageIds>({\n name: 'no-important',\n meta: {\n type: 'problem',\n fixable: 'code',\n docs: {\n description: 'Disallow !important in tasty style values',\n },\n messages: {\n noImportant:\n 'Do not use !important in tasty styles. The tasty system manages specificity via doubled selectors and state ordering.',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkNode(node: TSESTree.Node): void {\n const str = getStringValue(node);\n if (str && str.includes('!important')) {\n const stripped = str.replace(/\\s*!important\\s*/g, '').trim();\n context.report({\n node,\n messageId: 'noImportant',\n fix(fixer) {\n return replaceStringValue(fixer, node, stripped);\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') 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":";;;;;;AAQA,2BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,SAAS;EACT,MAAM,EACJ,aAAa,6CACd;EACD,UAAU,EACR,aACE,yHACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,UAAU,MAA2B;GAC5C,MAAM,MAAM,eAAe,KAAK;AAChC,OAAI,OAAO,IAAI,SAAS,aAAa,EAAE;IACrC,MAAM,WAAW,IAAI,QAAQ,qBAAqB,GAAG,CAAC,MAAM;AAC5D,YAAQ,OAAO;KACb;KACA,WAAW;KACX,IAAI,OAAO;AACT,aAAO,mBAAmB,OAAO,MAAM,SAAS;;KAEnD,CAAC;;;EAIN,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"}
@@ -6,8 +6,8 @@ import { TastyContext, styleObjectListeners } from "../context.js";
6
6
  var no_nested_selector_default = createRule({
7
7
  name: "no-nested-selector",
8
8
  meta: {
9
- type: "suggestion",
10
- docs: { description: "Discourage &-prefixed nested selectors in favor of sub-element styling" },
9
+ type: "problem",
10
+ docs: { description: "Disallow &-prefixed nested selectors in favor of sub-element styling" },
11
11
  messages: { noNestedSelector: "Avoid nested selectors ('{{key}}'). Use sub-element styling with capitalized keys and data-element attributes instead." },
12
12
  schema: []
13
13
  },
@@ -1 +1 @@
1
- {"version":3,"file":"no-nested-selector.js","names":[],"sources":["../../src/rules/no-nested-selector.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';\n\ntype MessageIds = 'noNestedSelector';\n\nexport default createRule<[], MessageIds>({\n name: 'no-nested-selector',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Discourage &-prefixed nested selectors in favor of sub-element styling',\n },\n messages: {\n noNestedSelector:\n \"Avoid nested selectors ('{{key}}'). Use sub-element styling with capitalized keys and data-element attributes instead.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\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 if (key.startsWith('&') && !key.startsWith('&::')) {\n context.report({\n node: prop.key,\n messageId: 'noNestedSelector',\n data: { key },\n });\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;AAOA,iCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,0EACH;EACD,UAAU,EACR,kBACE,0HACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,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;AAElB,QAAI,IAAI,WAAW,IAAI,IAAI,CAAC,IAAI,WAAW,MAAM,CAC/C,SAAQ,OAAO;KACb,MAAM,KAAK;KACX,WAAW;KACX,MAAM,EAAE,KAAK;KACd,CAAC;;;AAKR,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAGvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
1
+ {"version":3,"file":"no-nested-selector.js","names":[],"sources":["../../src/rules/no-nested-selector.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';\n\ntype MessageIds = 'noNestedSelector';\n\nexport default createRule<[], MessageIds>({\n name: 'no-nested-selector',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Disallow &-prefixed nested selectors in favor of sub-element styling',\n },\n messages: {\n noNestedSelector:\n \"Avoid nested selectors ('{{key}}'). Use sub-element styling with capitalized keys and data-element attributes instead.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\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 if (key.startsWith('&') && !key.startsWith('&::')) {\n context.report({\n node: prop.key,\n messageId: 'noNestedSelector',\n data: { key },\n });\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;AAOA,iCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,wEACH;EACD,UAAU,EACR,kBACE,0HACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,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;AAElB,QAAI,IAAI,WAAW,IAAI,IAAI,CAAC,IAAI,WAAW,MAAM,CAC/C,SAAQ,OAAO;KACb,MAAM,KAAK;KACX,WAAW;KACX,MAAM,EAAE,KAAK;KACd,CAAC;;;AAKR,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAGvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
@@ -2,12 +2,14 @@ import { createRule } from "../create-rule.js";
2
2
  import { getKeyName, getStringValue } from "../utils.js";
3
3
  import { TastyContext, styleObjectListeners } from "../context.js";
4
4
  import { parseStateKey } from "../parsers/state-key-parser.js";
5
+ import { replaceStringValue } from "../fix-utils.js";
5
6
 
6
7
  //#region src/rules/no-own-at-root.ts
7
8
  var no_own_at_root_default = createRule({
8
9
  name: "no-own-at-root",
9
10
  meta: {
10
11
  type: "suggestion",
12
+ fixable: "code",
11
13
  docs: { description: "Warn when @own() is used outside sub-element styles where it is redundant" },
12
14
  messages: { ownAtRoot: "@own() is redundant outside sub-element styles. Use the inner selector directly instead (e.g. :hover instead of @own(:hover))." },
13
15
  schema: []
@@ -38,7 +40,13 @@ var no_own_at_root_default = createRule({
38
40
  if (stateKey === null || stateKey === "" || stateKey === "_") continue;
39
41
  if (parseStateKey(stateKey).hasOwn) context.report({
40
42
  node: stateProp.key,
41
- messageId: "ownAtRoot"
43
+ messageId: "ownAtRoot",
44
+ fix(fixer) {
45
+ if (!stateKey.startsWith("@own(") || !stateKey.endsWith(")")) return null;
46
+ const inner = stateKey.slice(5, -1);
47
+ if (!inner) return null;
48
+ return replaceStringValue(fixer, stateProp.key, inner);
49
+ }
42
50
  });
43
51
  }
44
52
  }
@@ -1 +1 @@
1
- {"version":3,"file":"no-own-at-root.js","names":[],"sources":["../../src/rules/no-own-at-root.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 { parseStateKey } from '../parsers/state-key-parser.js';\n\ntype MessageIds = 'ownAtRoot';\n\nexport default createRule<[], MessageIds>({\n name: 'no-own-at-root',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Warn when @own() is used outside sub-element styles where it is redundant',\n },\n messages: {\n ownAtRoot:\n '@own() is redundant outside sub-element styles. Use the inner selector directly instead (e.g. :hover instead of @own(:hover)).',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function isInsideSubElement(node: TSESTree.Node): boolean {\n let current: TSESTree.Node | undefined = node.parent;\n while (current) {\n if (\n current.type === 'Property' &&\n !current.computed &&\n current.key.type === 'Identifier' &&\n /^[A-Z]/.test(current.key.name)\n ) {\n return true;\n }\n current = current.parent;\n }\n return false;\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n const insideSubElement = isInsideSubElement(node);\n\n if (insideSubElement) 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 if (/^[A-Z]/.test(key) || key.startsWith('@') || key.startsWith('&'))\n continue;\n\n if (prop.value.type !== 'ObjectExpression') continue;\n\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n\n const stateKey = !stateProp.computed\n ? getKeyName(stateProp.key)\n : getStringValue(stateProp.key);\n if (stateKey === null || stateKey === '' || stateKey === '_')\n continue;\n\n const result = parseStateKey(stateKey);\n\n if (result.hasOwn) {\n context.report({\n node: stateProp.key,\n messageId: 'ownAtRoot',\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,6BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,6EACH;EACD,UAAU,EACR,WACE,kIACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,mBAAmB,MAA8B;GACxD,IAAI,UAAqC,KAAK;AAC9C,UAAO,SAAS;AACd,QACE,QAAQ,SAAS,cACjB,CAAC,QAAQ,YACT,QAAQ,IAAI,SAAS,gBACrB,SAAS,KAAK,QAAQ,IAAI,KAAK,CAE/B,QAAO;AAET,cAAU,QAAQ;;AAEpB,UAAO;;EAGT,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAI9B,OAFyB,mBAAmB,KAAK,CAE3B;AAEtB,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAE/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,QAAQ,KAAM;AAElB,QAAI,SAAS,KAAK,IAAI,IAAI,IAAI,WAAW,IAAI,IAAI,IAAI,WAAW,IAAI,CAClE;AAEF,QAAI,KAAK,MAAM,SAAS,mBAAoB;AAE5C,SAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;KAEnC,MAAM,WAAW,CAAC,UAAU,WACxB,WAAW,UAAU,IAAI,GACzB,eAAe,UAAU,IAAI;AACjC,SAAI,aAAa,QAAQ,aAAa,MAAM,aAAa,IACvD;AAIF,SAFe,cAAc,SAAS,CAE3B,OACT,SAAQ,OAAO;MACb,MAAM,UAAU;MAChB,WAAW;MACZ,CAAC;;;;AAMV,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAGvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
1
+ {"version":3,"file":"no-own-at-root.js","names":[],"sources":["../../src/rules/no-own-at-root.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 { parseStateKey } from '../parsers/state-key-parser.js';\nimport { replaceStringValue } from '../fix-utils.js';\n\ntype MessageIds = 'ownAtRoot';\n\nexport default createRule<[], MessageIds>({\n name: 'no-own-at-root',\n meta: {\n type: 'suggestion',\n fixable: 'code',\n docs: {\n description:\n 'Warn when @own() is used outside sub-element styles where it is redundant',\n },\n messages: {\n ownAtRoot:\n '@own() is redundant outside sub-element styles. Use the inner selector directly instead (e.g. :hover instead of @own(:hover)).',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function isInsideSubElement(node: TSESTree.Node): boolean {\n let current: TSESTree.Node | undefined = node.parent;\n while (current) {\n if (\n current.type === 'Property' &&\n !current.computed &&\n current.key.type === 'Identifier' &&\n /^[A-Z]/.test(current.key.name)\n ) {\n return true;\n }\n current = current.parent;\n }\n return false;\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n const insideSubElement = isInsideSubElement(node);\n\n if (insideSubElement) 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 if (/^[A-Z]/.test(key) || key.startsWith('@') || key.startsWith('&'))\n continue;\n\n if (prop.value.type !== 'ObjectExpression') continue;\n\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n\n const stateKey = !stateProp.computed\n ? getKeyName(stateProp.key)\n : getStringValue(stateProp.key);\n if (stateKey === null || stateKey === '' || stateKey === '_')\n continue;\n\n const result = parseStateKey(stateKey);\n\n if (result.hasOwn) {\n context.report({\n node: stateProp.key,\n messageId: 'ownAtRoot',\n fix(fixer) {\n if (!stateKey.startsWith('@own(') || !stateKey.endsWith(')')) {\n return null;\n }\n const inner = stateKey.slice(5, -1);\n if (!inner) return null;\n return replaceStringValue(fixer, stateProp.key, inner);\n },\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":";;;;;;;AASA,6BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,SAAS;EACT,MAAM,EACJ,aACE,6EACH;EACD,UAAU,EACR,WACE,kIACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,mBAAmB,MAA8B;GACxD,IAAI,UAAqC,KAAK;AAC9C,UAAO,SAAS;AACd,QACE,QAAQ,SAAS,cACjB,CAAC,QAAQ,YACT,QAAQ,IAAI,SAAS,gBACrB,SAAS,KAAK,QAAQ,IAAI,KAAK,CAE/B,QAAO;AAET,cAAU,QAAQ;;AAEpB,UAAO;;EAGT,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAI9B,OAFyB,mBAAmB,KAAK,CAE3B;AAEtB,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAE/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,QAAQ,KAAM;AAElB,QAAI,SAAS,KAAK,IAAI,IAAI,IAAI,WAAW,IAAI,IAAI,IAAI,WAAW,IAAI,CAClE;AAEF,QAAI,KAAK,MAAM,SAAS,mBAAoB;AAE5C,SAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;KAEnC,MAAM,WAAW,CAAC,UAAU,WACxB,WAAW,UAAU,IAAI,GACzB,eAAe,UAAU,IAAI;AACjC,SAAI,aAAa,QAAQ,aAAa,MAAM,aAAa,IACvD;AAIF,SAFe,cAAc,SAAS,CAE3B,OACT,SAAQ,OAAO;MACb,MAAM,UAAU;MAChB,WAAW;MACX,IAAI,OAAO;AACT,WAAI,CAAC,SAAS,WAAW,QAAQ,IAAI,CAAC,SAAS,SAAS,IAAI,CAC1D,QAAO;OAET,MAAM,QAAQ,SAAS,MAAM,GAAG,GAAG;AACnC,WAAI,CAAC,MAAO,QAAO;AACnB,cAAO,mBAAmB,OAAO,UAAU,KAAK,MAAM;;MAEzD,CAAC;;;;AAMV,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAGvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
@@ -1,18 +1,21 @@
1
1
  import { createRule } from "../create-rule.js";
2
- import { getStringValue } from "../utils.js";
2
+ import { COLOR_BEARING_PROPERTIES, NAMED_CSS_COLORS } from "../constants.js";
3
+ import { getKeyName, getStringValue } from "../utils.js";
3
4
  import { TastyContext, styleObjectListeners } from "../context.js";
4
5
 
5
6
  //#region src/rules/no-raw-color-values.ts
6
7
  const HEX_COLOR_REGEX = /#([0-9a-fA-F]{3,8})\b/g;
7
- const COLOR_FUNC_REGEX = /\b(rgb|rgba|hsl|hsla)\s*\(/gi;
8
+ const COLOR_FUNC_REGEX = /\b(rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|okhsl|okhsv|okhst|color|device-cmyk|light-dark)\s*\(/gi;
9
+ const NAMED_COLOR_REGEX = new RegExp(`\\b(${[...NAMED_CSS_COLORS].sort((a, b) => b.length - a.length).join("|")})\\b`, "gi");
8
10
  var no_raw_color_values_default = createRule({
9
11
  name: "no-raw-color-values",
10
12
  meta: {
11
13
  type: "suggestion",
12
- docs: { description: "Suggest using color tokens instead of raw hex/rgb values" },
14
+ docs: { description: "Suggest using color tokens instead of raw hex/rgb/okhsl/named colors" },
13
15
  messages: {
14
16
  rawHexColor: "Use a color token instead of raw hex color '{{value}}'.",
15
- rawColorFunction: "Use a color token instead of raw {{func}}() color."
17
+ rawColorFunction: "Use a color token instead of raw {{func}}() color.",
18
+ rawNamedColor: "Use a color token instead of raw named color '{{name}}'."
16
19
  },
17
20
  schema: []
18
21
  },
@@ -34,7 +37,7 @@ var no_raw_color_values_default = createRule({
34
37
  }
35
38
  return false;
36
39
  }
37
- function checkValue(value, node) {
40
+ function checkValue(value, node, propertyKey) {
38
41
  if (isInTokenDefinition(node)) return;
39
42
  HEX_COLOR_REGEX.lastIndex = 0;
40
43
  let match;
@@ -57,17 +60,30 @@ var no_raw_color_values_default = createRule({
57
60
  messageId: "rawColorFunction",
58
61
  data: { func: match[1] }
59
62
  });
63
+ if (propertyKey && COLOR_BEARING_PROPERTIES.has(propertyKey)) {
64
+ NAMED_COLOR_REGEX.lastIndex = 0;
65
+ while ((match = NAMED_COLOR_REGEX.exec(value)) !== null) {
66
+ const prev = match.index > 0 ? value[match.index - 1] : "";
67
+ if (prev === "#" || prev === "$") continue;
68
+ context.report({
69
+ node,
70
+ messageId: "rawNamedColor",
71
+ data: { name: match[1] }
72
+ });
73
+ }
74
+ }
60
75
  }
61
76
  function handleStyleObject(node) {
62
77
  if (!ctx.isStyleObject(node)) return;
63
78
  for (const prop of node.properties) {
64
79
  if (prop.type !== "Property") continue;
80
+ const key = getKeyName(prop.key);
65
81
  const str = getStringValue(prop.value);
66
- if (str) checkValue(str, prop.value);
82
+ if (str) checkValue(str, prop.value, key);
67
83
  if (prop.value.type === "ObjectExpression") for (const stateProp of prop.value.properties) {
68
84
  if (stateProp.type !== "Property") continue;
69
85
  const stateStr = getStringValue(stateProp.value);
70
- if (stateStr) checkValue(stateStr, stateProp.value);
86
+ if (stateStr) checkValue(stateStr, stateProp.value, key);
71
87
  }
72
88
  }
73
89
  }
@@ -1 +1 @@
1
- {"version":3,"file":"no-raw-color-values.js","names":[],"sources":["../../src/rules/no-raw-color-values.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getStringValue } from '../utils.js';\n\ntype MessageIds = 'rawHexColor' | 'rawColorFunction';\n\nconst HEX_COLOR_REGEX = /#([0-9a-fA-F]{3,8})\\b/g;\nconst COLOR_FUNC_REGEX = /\\b(rgb|rgba|hsl|hsla)\\s*\\(/gi;\n\nexport default createRule<[], MessageIds>({\n name: 'no-raw-color-values',\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Suggest using color tokens instead of raw hex/rgb values',\n },\n messages: {\n rawHexColor: \"Use a color token instead of raw hex color '{{value}}'.\",\n rawColorFunction: 'Use a color token instead of raw {{func}}() color.',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function isInTokenDefinition(node: TSESTree.Node): boolean {\n // Check if this is inside a :root or token-defining context\n let current: TSESTree.Node | undefined = node;\n while (current) {\n if (current.type === 'CallExpression') {\n const imp = ctx.isTastyCall(current);\n if (imp && imp.importedName === 'tastyStatic') {\n const firstArg = current.arguments[0];\n const selectorStr = getStringValue(firstArg);\n if (selectorStr === ':root') return true;\n }\n break;\n }\n current = current.parent;\n }\n return false;\n }\n\n function checkValue(value: string, node: TSESTree.Node): void {\n if (isInTokenDefinition(node)) return;\n\n // Check hex colors\n HEX_COLOR_REGEX.lastIndex = 0;\n let match;\n while ((match = HEX_COLOR_REGEX.exec(value)) !== null) {\n const hex = match[1];\n if ([3, 4, 6, 8].includes(hex.length)) {\n context.report({\n node,\n messageId: 'rawHexColor',\n data: { value: match[0] },\n });\n }\n }\n\n // Check color functions\n COLOR_FUNC_REGEX.lastIndex = 0;\n while ((match = COLOR_FUNC_REGEX.exec(value)) !== null) {\n context.report({\n node,\n messageId: 'rawColorFunction',\n data: { func: match[1] },\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') continue;\n\n const str = getStringValue(prop.value);\n if (str) checkValue(str, prop.value);\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) checkValue(stateStr, stateProp.value);\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;AAOA,MAAM,kBAAkB;AACxB,MAAM,mBAAmB;AAEzB,kCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,4DACd;EACD,UAAU;GACR,aAAa;GACb,kBAAkB;GACnB;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,oBAAoB,MAA8B;GAEzD,IAAI,UAAqC;AACzC,UAAO,SAAS;AACd,QAAI,QAAQ,SAAS,kBAAkB;KACrC,MAAM,MAAM,IAAI,YAAY,QAAQ;AACpC,SAAI,OAAO,IAAI,iBAAiB,eAAe;MAC7C,MAAM,WAAW,QAAQ,UAAU;AAEnC,UADoB,eAAe,SAAS,KACxB,QAAS,QAAO;;AAEtC;;AAEF,cAAU,QAAQ;;AAEpB,UAAO;;EAGT,SAAS,WAAW,OAAe,MAA2B;AAC5D,OAAI,oBAAoB,KAAK,CAAE;AAG/B,mBAAgB,YAAY;GAC5B,IAAI;AACJ,WAAQ,QAAQ,gBAAgB,KAAK,MAAM,MAAM,MAAM;IACrD,MAAM,MAAM,MAAM;AAClB,QAAI;KAAC;KAAG;KAAG;KAAG;KAAE,CAAC,SAAS,IAAI,OAAO,CACnC,SAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM,EAAE,OAAO,MAAM,IAAI;KAC1B,CAAC;;AAKN,oBAAiB,YAAY;AAC7B,WAAQ,QAAQ,iBAAiB,KAAK,MAAM,MAAM,KAChD,SAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM,EAAE,MAAM,MAAM,IAAI;IACzB,CAAC;;EAIN,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,WAAY;IAE9B,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,QAAI,IAAK,YAAW,KAAK,KAAK,MAAM;AAEpC,QAAI,KAAK,MAAM,SAAS,mBACtB,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;KACnC,MAAM,WAAW,eAAe,UAAU,MAAM;AAChD,SAAI,SAAU,YAAW,UAAU,UAAU,MAAM;;;;AAM3D,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
1
+ {"version":3,"file":"no-raw-color-values.js","names":[],"sources":["../../src/rules/no-raw-color-values.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getStringValue, getKeyName } from '../utils.js';\nimport { COLOR_BEARING_PROPERTIES, NAMED_CSS_COLORS } from '../constants.js';\n\ntype MessageIds = 'rawHexColor' | 'rawColorFunction' | 'rawNamedColor';\n\nconst HEX_COLOR_REGEX = /#([0-9a-fA-F]{3,8})\\b/g;\nconst COLOR_FUNC_REGEX =\n /\\b(rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|okhsl|okhsv|okhst|color|device-cmyk|light-dark)\\s*\\(/gi;\nconst NAMED_COLOR_REGEX = new RegExp(\n `\\\\b(${[...NAMED_CSS_COLORS].sort((a, b) => b.length - a.length).join('|')})\\\\b`,\n 'gi',\n);\n\nexport default createRule<[], MessageIds>({\n name: 'no-raw-color-values',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Suggest using color tokens instead of raw hex/rgb/okhsl/named colors',\n },\n messages: {\n rawHexColor: \"Use a color token instead of raw hex color '{{value}}'.\",\n rawColorFunction: 'Use a color token instead of raw {{func}}() color.',\n rawNamedColor: \"Use a color token instead of raw named color '{{name}}'.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function isInTokenDefinition(node: TSESTree.Node): boolean {\n // Check if this is inside a :root or token-defining context\n let current: TSESTree.Node | undefined = node;\n while (current) {\n if (current.type === 'CallExpression') {\n const imp = ctx.isTastyCall(current);\n if (imp && imp.importedName === 'tastyStatic') {\n const firstArg = current.arguments[0];\n const selectorStr = getStringValue(firstArg);\n if (selectorStr === ':root') return true;\n }\n break;\n }\n current = current.parent;\n }\n return false;\n }\n\n function checkValue(\n value: string,\n node: TSESTree.Node,\n propertyKey?: string | null,\n ): void {\n if (isInTokenDefinition(node)) return;\n\n // Check hex colors\n HEX_COLOR_REGEX.lastIndex = 0;\n let match;\n while ((match = HEX_COLOR_REGEX.exec(value)) !== null) {\n const hex = match[1];\n if ([3, 4, 6, 8].includes(hex.length)) {\n context.report({\n node,\n messageId: 'rawHexColor',\n data: { value: match[0] },\n });\n }\n }\n\n // Check color functions (rgb, hsl, okhsl, okhst, oklch, …)\n COLOR_FUNC_REGEX.lastIndex = 0;\n while ((match = COLOR_FUNC_REGEX.exec(value)) !== null) {\n context.report({\n node,\n messageId: 'rawColorFunction',\n data: { func: match[1] },\n });\n }\n\n // Check named CSS colors — scoped to color-bearing properties to avoid\n // false positives on string properties like `content` or `cursor`. Skip\n // matches that are part of a `#token` or `$prop` reference (e.g. `#purple`,\n // `$red`) — those are Tasty tokens/custom properties, not raw colors.\n if (propertyKey && COLOR_BEARING_PROPERTIES.has(propertyKey)) {\n NAMED_COLOR_REGEX.lastIndex = 0;\n while ((match = NAMED_COLOR_REGEX.exec(value)) !== null) {\n const prev = match.index > 0 ? value[match.index - 1] : '';\n if (prev === '#' || prev === '$') continue;\n context.report({\n node,\n messageId: 'rawNamedColor',\n data: { name: match[1] },\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') continue;\n const key = getKeyName(prop.key);\n\n const str = getStringValue(prop.value);\n if (str) checkValue(str, prop.value, key);\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) checkValue(stateStr, stateProp.value, key);\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;AAQA,MAAM,kBAAkB;AACxB,MAAM,mBACJ;AACF,MAAM,oBAAoB,IAAI,OAC5B,OAAO,CAAC,GAAG,iBAAiB,CAAC,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC,OAC3E,KACD;AAED,kCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,wEACH;EACD,UAAU;GACR,aAAa;GACb,kBAAkB;GAClB,eAAe;GAChB;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,oBAAoB,MAA8B;GAEzD,IAAI,UAAqC;AACzC,UAAO,SAAS;AACd,QAAI,QAAQ,SAAS,kBAAkB;KACrC,MAAM,MAAM,IAAI,YAAY,QAAQ;AACpC,SAAI,OAAO,IAAI,iBAAiB,eAAe;MAC7C,MAAM,WAAW,QAAQ,UAAU;AAEnC,UADoB,eAAe,SAAS,KACxB,QAAS,QAAO;;AAEtC;;AAEF,cAAU,QAAQ;;AAEpB,UAAO;;EAGT,SAAS,WACP,OACA,MACA,aACM;AACN,OAAI,oBAAoB,KAAK,CAAE;AAG/B,mBAAgB,YAAY;GAC5B,IAAI;AACJ,WAAQ,QAAQ,gBAAgB,KAAK,MAAM,MAAM,MAAM;IACrD,MAAM,MAAM,MAAM;AAClB,QAAI;KAAC;KAAG;KAAG;KAAG;KAAE,CAAC,SAAS,IAAI,OAAO,CACnC,SAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM,EAAE,OAAO,MAAM,IAAI;KAC1B,CAAC;;AAKN,oBAAiB,YAAY;AAC7B,WAAQ,QAAQ,iBAAiB,KAAK,MAAM,MAAM,KAChD,SAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM,EAAE,MAAM,MAAM,IAAI;IACzB,CAAC;AAOJ,OAAI,eAAe,yBAAyB,IAAI,YAAY,EAAE;AAC5D,sBAAkB,YAAY;AAC9B,YAAQ,QAAQ,kBAAkB,KAAK,MAAM,MAAM,MAAM;KACvD,MAAM,OAAO,MAAM,QAAQ,IAAI,MAAM,MAAM,QAAQ,KAAK;AACxD,SAAI,SAAS,OAAO,SAAS,IAAK;AAClC,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM,EAAE,MAAM,MAAM,IAAI;MACzB,CAAC;;;;EAKR,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,WAAY;IAC9B,MAAM,MAAM,WAAW,KAAK,IAAI;IAEhC,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,QAAI,IAAK,YAAW,KAAK,KAAK,OAAO,IAAI;AAEzC,QAAI,KAAK,MAAM,SAAS,mBACtB,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;KACnC,MAAM,WAAW,eAAe,UAAU,MAAM;AAChD,SAAI,SAAU,YAAW,UAAU,UAAU,OAAO,IAAI;;;;AAMhE,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
@@ -8,30 +8,33 @@ var no_runtime_styles_mutation_default = createRule({
8
8
  meta: {
9
9
  type: "suggestion",
10
10
  docs: { description: "Warn when style objects contain runtime-computed values" },
11
- messages: { dynamicStyleValue: "Style values should be static. Use modifiers (mods prop), tokens, or CSS custom properties for dynamic behavior." },
11
+ messages: { dynamicStyleValue: "Style value for '{{property}}' should be static. Use the 'mods' prop, tokens, or CSS custom properties for dynamic behavior." },
12
12
  schema: []
13
13
  },
14
14
  defaultOptions: [],
15
15
  create(context) {
16
16
  const ctx = new TastyContext(context);
17
- function checkProperties(node) {
17
+ function checkProperties(node, parentProperty = null) {
18
18
  for (const prop of node.properties) {
19
19
  if (prop.type === "SpreadElement") {
20
20
  context.report({
21
21
  node: prop,
22
- messageId: "dynamicStyleValue"
22
+ messageId: "dynamicStyleValue",
23
+ data: { property: parentProperty ?? "(spread)" }
23
24
  });
24
25
  continue;
25
26
  }
26
27
  if (prop.type !== "Property") continue;
28
+ const key = !prop.computed && prop.key.type === "Identifier" ? prop.key.name : parentProperty;
27
29
  if (!prop.computed && prop.key.type === "Identifier" && /^[A-Z]/.test(prop.key.name)) {
28
- if (prop.value.type === "ObjectExpression") checkProperties(prop.value);
30
+ if (prop.value.type === "ObjectExpression") checkProperties(prop.value, key);
29
31
  continue;
30
32
  }
31
33
  if (!prop.computed && prop.key.type === "Identifier" && prop.key.name.startsWith("@")) continue;
32
34
  if (!isStaticValue(prop.value)) context.report({
33
35
  node: prop.value,
34
- messageId: "dynamicStyleValue"
36
+ messageId: "dynamicStyleValue",
37
+ data: { property: key ?? "(unknown)" }
35
38
  });
36
39
  }
37
40
  }