@tenphi/eslint-plugin-tasty 0.8.0 → 0.10.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 (38) hide show
  1. package/README.md +4 -3
  2. package/dist/configs.js +3 -2
  3. package/dist/configs.js.map +1 -1
  4. package/dist/constants.js +234 -15
  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 +3 -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/no-important.js +12 -4
  13. package/dist/rules/no-important.js.map +1 -1
  14. package/dist/rules/no-nested-selector.js +2 -2
  15. package/dist/rules/no-nested-selector.js.map +1 -1
  16. package/dist/rules/no-own-at-root.js +9 -1
  17. package/dist/rules/no-own-at-root.js.map +1 -1
  18. package/dist/rules/no-raw-color-values.js +23 -7
  19. package/dist/rules/no-raw-color-values.js.map +1 -1
  20. package/dist/rules/prefer-auto-calc.js +23 -1
  21. package/dist/rules/prefer-auto-calc.js.map +1 -1
  22. package/dist/rules/prefer-custom-property-syntax.js +79 -4
  23. package/dist/rules/prefer-custom-property-syntax.js.map +1 -1
  24. package/dist/rules/prefer-directional-shorthand.js +21 -8
  25. package/dist/rules/prefer-directional-shorthand.js.map +1 -1
  26. package/dist/rules/prefer-hide.js +15 -9
  27. package/dist/rules/prefer-hide.js.map +1 -1
  28. package/dist/rules/prefer-longhand-property.js +47 -0
  29. package/dist/rules/prefer-longhand-property.js.map +1 -0
  30. package/dist/rules/prefer-shorthand-property.js +5 -0
  31. package/dist/rules/prefer-shorthand-property.js.map +1 -1
  32. package/dist/rules/valid-default-state-order.js +36 -5
  33. package/dist/rules/valid-default-state-order.js.map +1 -1
  34. package/dist/rules/valid-radius-shape.js +18 -2
  35. package/dist/rules/valid-radius-shape.js.map +1 -1
  36. package/dist/rules/valid-transition.js +37 -5
  37. package/dist/rules/valid-transition.js.map +1 -1
  38. package/package.json +1 -1
@@ -1,7 +1,8 @@
1
1
  import { createRule } from "../create-rule.js";
2
- import { DIRECTIONAL_MODIFIERS } from "../constants.js";
2
+ import { DIRECTIONAL_BOX_IDENTITY } from "../constants.js";
3
3
  import { getKeyName, getStringValue } from "../utils.js";
4
4
  import { TastyContext, styleObjectListeners } from "../context.js";
5
+ import { replaceStringValue } from "../fix-utils.js";
5
6
 
6
7
  //#region src/rules/prefer-directional-shorthand.ts
7
8
  const SIDES = [
@@ -15,21 +16,30 @@ function isZeroLike(token) {
15
16
  if (trimmed === "0") return true;
16
17
  return /^0+(?:\.0+)?(?:px|em|rem|%|x|r|cr|bw|ow|lh|sf)?$/i.test(trimmed);
17
18
  }
18
- function suggestDirectional(value) {
19
+ /**
20
+ * Whether `token` is the placeholder value for an unset side, i.e. dropping it
21
+ * from the shorthand leaves the emitted CSS unchanged.
22
+ */
23
+ function isIdentity(token, identity) {
24
+ if (identity === "auto") return token.trim().toLowerCase() === "auto";
25
+ return isZeroLike(token);
26
+ }
27
+ function suggestDirectional(value, identity) {
19
28
  const tokens = value.trim().split(/\s+/);
20
29
  if (tokens.length !== 4) return null;
21
- const nonZero = tokens.map((token, index) => ({
30
+ const meaningful = tokens.map((token, index) => ({
22
31
  token,
23
32
  side: SIDES[index]
24
- })).filter(({ token }) => !isZeroLike(token));
25
- if (nonZero.length !== 1) return null;
26
- const { token, side } = nonZero[0];
33
+ })).filter(({ token }) => !isIdentity(token, identity));
34
+ if (meaningful.length !== 1) return null;
35
+ const { token, side } = meaningful[0];
27
36
  return `${token} ${side}`;
28
37
  }
29
38
  var prefer_directional_shorthand_default = createRule({
30
39
  name: "prefer-directional-shorthand",
31
40
  meta: {
32
41
  type: "suggestion",
42
+ fixable: "code",
33
43
  docs: { description: "Suggest Tasty directional shorthand instead of 4-value CSS box syntax with zero placeholders" },
34
44
  messages: { preferDirectionalShorthand: "'{{property}}' value '{{raw}}' only sets one side. Use the directional shorthand '{{suggestion}}' (e.g. '1x bottom') instead." },
35
45
  schema: []
@@ -38,7 +48,7 @@ var prefer_directional_shorthand_default = createRule({
38
48
  create(context) {
39
49
  const ctx = new TastyContext(context);
40
50
  function checkValue(property, value, node) {
41
- const suggestion = suggestDirectional(value);
51
+ const suggestion = suggestDirectional(value, DIRECTIONAL_BOX_IDENTITY[property]);
42
52
  if (!suggestion) return;
43
53
  context.report({
44
54
  node,
@@ -47,6 +57,9 @@ var prefer_directional_shorthand_default = createRule({
47
57
  property,
48
58
  raw: value,
49
59
  suggestion
60
+ },
61
+ fix(fixer) {
62
+ return replaceStringValue(fixer, node, suggestion);
50
63
  }
51
64
  });
52
65
  }
@@ -56,7 +69,7 @@ var prefer_directional_shorthand_default = createRule({
56
69
  if (prop.type !== "Property" || prop.computed) continue;
57
70
  const key = getKeyName(prop.key);
58
71
  if (key === null) continue;
59
- if (!(key in DIRECTIONAL_MODIFIERS)) continue;
72
+ if (!(key in DIRECTIONAL_BOX_IDENTITY)) continue;
60
73
  const str = getStringValue(prop.value);
61
74
  if (str) {
62
75
  checkValue(key, str, prop.value);
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-directional-shorthand.js","names":[],"sources":["../../src/rules/prefer-directional-shorthand.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 { DIRECTIONAL_MODIFIERS } from '../constants.js';\n\ntype MessageIds = 'preferDirectionalShorthand';\n\nconst SIDES = ['top', 'right', 'bottom', 'left'] as const;\n\nfunction isZeroLike(token: string): boolean {\n const trimmed = token.trim();\n if (trimmed === '0') return true;\n return /^0+(?:\\.0+)?(?:px|em|rem|%|x|r|cr|bw|ow|lh|sf)?$/i.test(trimmed);\n}\n\nfunction suggestDirectional(value: string): string | null {\n const tokens = value.trim().split(/\\s+/);\n if (tokens.length !== 4) return null;\n\n const nonZero = tokens\n .map((token, index) => ({ token, side: SIDES[index] }))\n .filter(({ token }) => !isZeroLike(token));\n\n if (nonZero.length !== 1) return null;\n\n const { token, side } = nonZero[0];\n return `${token} ${side}`;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'prefer-directional-shorthand',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Suggest Tasty directional shorthand instead of 4-value CSS box syntax with zero placeholders',\n },\n messages: {\n preferDirectionalShorthand:\n \"'{{property}}' value '{{raw}}' only sets one side. Use the directional shorthand '{{suggestion}}' (e.g. '1x bottom') instead.\",\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 suggestion = suggestDirectional(value);\n if (!suggestion) return;\n\n context.report({\n node,\n messageId: 'preferDirectionalShorthand',\n data: { property, raw: value, suggestion },\n });\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n if (!(key in DIRECTIONAL_MODIFIERS)) 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,QAAQ;CAAC;CAAO;CAAS;CAAU;CAAO;AAEhD,SAAS,WAAW,OAAwB;CAC1C,MAAM,UAAU,MAAM,MAAM;AAC5B,KAAI,YAAY,IAAK,QAAO;AAC5B,QAAO,oDAAoD,KAAK,QAAQ;;AAG1E,SAAS,mBAAmB,OAA8B;CACxD,MAAM,SAAS,MAAM,MAAM,CAAC,MAAM,MAAM;AACxC,KAAI,OAAO,WAAW,EAAG,QAAO;CAEhC,MAAM,UAAU,OACb,KAAK,OAAO,WAAW;EAAE;EAAO,MAAM,MAAM;EAAQ,EAAE,CACtD,QAAQ,EAAE,YAAY,CAAC,WAAW,MAAM,CAAC;AAE5C,KAAI,QAAQ,WAAW,EAAG,QAAO;CAEjC,MAAM,EAAE,OAAO,SAAS,QAAQ;AAChC,QAAO,GAAG,MAAM,GAAG;;AAGrB,2CAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,gGACH;EACD,UAAU,EACR,4BACE,iIACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,WACP,UACA,OACA,MACM;GACN,MAAM,aAAa,mBAAmB,MAAM;AAC5C,OAAI,CAAC,WAAY;AAEjB,WAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM;KAAE;KAAU,KAAK;KAAO;KAAY;IAC3C,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;AAClB,QAAI,EAAE,OAAO,uBAAwB;IAErC,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":"prefer-directional-shorthand.js","names":[],"sources":["../../src/rules/prefer-directional-shorthand.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 { DIRECTIONAL_BOX_IDENTITY } from '../constants.js';\nimport { replaceStringValue } from '../fix-utils.js';\n\ntype MessageIds = 'preferDirectionalShorthand';\n\nconst SIDES = ['top', 'right', 'bottom', 'left'] as const;\n\nfunction isZeroLike(token: string): boolean {\n const trimmed = token.trim();\n if (trimmed === '0') return true;\n return /^0+(?:\\.0+)?(?:px|em|rem|%|x|r|cr|bw|ow|lh|sf)?$/i.test(trimmed);\n}\n\n/**\n * Whether `token` is the placeholder value for an unset side, i.e. dropping it\n * from the shorthand leaves the emitted CSS unchanged.\n */\nfunction isIdentity(token: string, identity: string): boolean {\n if (identity === 'auto') return token.trim().toLowerCase() === 'auto';\n return isZeroLike(token);\n}\n\nfunction suggestDirectional(value: string, identity: string): string | null {\n const tokens = value.trim().split(/\\s+/);\n if (tokens.length !== 4) return null;\n\n const meaningful = tokens\n .map((token, index) => ({ token, side: SIDES[index] }))\n .filter(({ token }) => !isIdentity(token, identity));\n\n // Only collapse when exactly one side carries a real value — otherwise the\n // directional shorthand would drop the others.\n if (meaningful.length !== 1) return null;\n\n const { token, side } = meaningful[0];\n return `${token} ${side}`;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'prefer-directional-shorthand',\n meta: {\n type: 'suggestion',\n fixable: 'code',\n docs: {\n description:\n 'Suggest Tasty directional shorthand instead of 4-value CSS box syntax with zero placeholders',\n },\n messages: {\n preferDirectionalShorthand:\n \"'{{property}}' value '{{raw}}' only sets one side. Use the directional shorthand '{{suggestion}}' (e.g. '1x bottom') instead.\",\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 suggestion = suggestDirectional(\n value,\n DIRECTIONAL_BOX_IDENTITY[property],\n );\n if (!suggestion) return;\n\n context.report({\n node,\n messageId: 'preferDirectionalShorthand',\n data: { property, raw: value, suggestion },\n fix(fixer) {\n return replaceStringValue(fixer, node, suggestion);\n },\n });\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n if (!(key in DIRECTIONAL_BOX_IDENTITY)) 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":";;;;;;;AASA,MAAM,QAAQ;CAAC;CAAO;CAAS;CAAU;CAAO;AAEhD,SAAS,WAAW,OAAwB;CAC1C,MAAM,UAAU,MAAM,MAAM;AAC5B,KAAI,YAAY,IAAK,QAAO;AAC5B,QAAO,oDAAoD,KAAK,QAAQ;;;;;;AAO1E,SAAS,WAAW,OAAe,UAA2B;AAC5D,KAAI,aAAa,OAAQ,QAAO,MAAM,MAAM,CAAC,aAAa,KAAK;AAC/D,QAAO,WAAW,MAAM;;AAG1B,SAAS,mBAAmB,OAAe,UAAiC;CAC1E,MAAM,SAAS,MAAM,MAAM,CAAC,MAAM,MAAM;AACxC,KAAI,OAAO,WAAW,EAAG,QAAO;CAEhC,MAAM,aAAa,OAChB,KAAK,OAAO,WAAW;EAAE;EAAO,MAAM,MAAM;EAAQ,EAAE,CACtD,QAAQ,EAAE,YAAY,CAAC,WAAW,OAAO,SAAS,CAAC;AAItD,KAAI,WAAW,WAAW,EAAG,QAAO;CAEpC,MAAM,EAAE,OAAO,SAAS,WAAW;AACnC,QAAO,GAAG,MAAM,GAAG;;AAGrB,2CAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,SAAS;EACT,MAAM,EACJ,aACE,gGACH;EACD,UAAU,EACR,4BACE,iIACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,WACP,UACA,OACA,MACM;GACN,MAAM,aAAa,mBACjB,OACA,yBAAyB,UAC1B;AACD,OAAI,CAAC,WAAY;AAEjB,WAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM;KAAE;KAAU,KAAK;KAAO;KAAY;IAC1C,IAAI,OAAO;AACT,YAAO,mBAAmB,OAAO,MAAM,WAAW;;IAErD,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;AAClB,QAAI,EAAE,OAAO,0BAA2B;IAExC,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"}
@@ -3,10 +3,14 @@ import { getKeyName, getStringValue } from "../utils.js";
3
3
  import { TastyContext, styleObjectListeners } from "../context.js";
4
4
 
5
5
  //#region src/rules/prefer-hide.ts
6
+ function isNone(value) {
7
+ return value.trim().toLowerCase() === "none";
8
+ }
6
9
  var prefer_hide_default = createRule({
7
10
  name: "prefer-hide",
8
11
  meta: {
9
12
  type: "suggestion",
13
+ fixable: "code",
10
14
  docs: { description: "Suggest hide: true instead of display: 'none' for Tasty's display/hide priority handling" },
11
15
  messages: { preferHide: "Use 'hide: true' instead of \"display: 'none'\" so it composes with Tasty's display/hide priority handling." },
12
16
  schema: []
@@ -14,13 +18,6 @@ var prefer_hide_default = createRule({
14
18
  defaultOptions: [],
15
19
  create(context) {
16
20
  const ctx = new TastyContext(context);
17
- function checkDisplayValue(value, node) {
18
- if (value.trim().toLowerCase() !== "none") return;
19
- context.report({
20
- node,
21
- messageId: "preferHide"
22
- });
23
- }
24
21
  function handleStyleObject(node) {
25
22
  if (!ctx.isStyleObject(node)) return;
26
23
  for (const prop of node.properties) {
@@ -28,13 +25,22 @@ var prefer_hide_default = createRule({
28
25
  if (getKeyName(prop.key) !== "display") continue;
29
26
  const str = getStringValue(prop.value);
30
27
  if (str) {
31
- checkDisplayValue(str, prop.value);
28
+ if (isNone(str)) context.report({
29
+ node: prop.value,
30
+ messageId: "preferHide",
31
+ fix(fixer) {
32
+ return [fixer.replaceText(prop.key, "hide"), fixer.replaceText(prop.value, "true")];
33
+ }
34
+ });
32
35
  continue;
33
36
  }
34
37
  if (prop.value.type === "ObjectExpression") for (const stateProp of prop.value.properties) {
35
38
  if (stateProp.type !== "Property") continue;
36
39
  const stateStr = getStringValue(stateProp.value);
37
- if (stateStr) checkDisplayValue(stateStr, stateProp.value);
40
+ if (stateStr && isNone(stateStr)) context.report({
41
+ node: stateProp.value,
42
+ messageId: "preferHide"
43
+ });
38
44
  }
39
45
  }
40
46
  }
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-hide.js","names":[],"sources":["../../src/rules/prefer-hide.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 = 'preferHide';\n\nexport default createRule<[], MessageIds>({\n name: 'prefer-hide',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n \"Suggest hide: true instead of display: 'none' for Tasty's display/hide priority handling\",\n },\n messages: {\n preferHide:\n \"Use 'hide: true' instead of \\\"display: 'none'\\\" so it composes with Tasty's display/hide priority handling.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkDisplayValue(value: string, node: TSESTree.Node): void {\n if (value.trim().toLowerCase() !== 'none') return;\n\n context.report({\n node,\n messageId: 'preferHide',\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 !== 'display') continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkDisplayValue(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 checkDisplayValue(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,0BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,4FACH;EACD,UAAU,EACR,YACE,+GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,kBAAkB,OAAe,MAA2B;AACnE,OAAI,MAAM,MAAM,CAAC,aAAa,KAAK,OAAQ;AAE3C,WAAQ,OAAO;IACb;IACA,WAAW;IACZ,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;AAG/C,QADY,WAAW,KAAK,IAAI,KACpB,UAAW;IAEvB,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,QAAI,KAAK;AACP,uBAAkB,KAAK,KAAK,MAAM;AAClC;;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,mBAAkB,UAAU,UAAU,MAAM;;;;AAOtD,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
1
+ {"version":3,"file":"prefer-hide.js","names":[],"sources":["../../src/rules/prefer-hide.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 = 'preferHide';\n\nfunction isNone(value: string): boolean {\n return value.trim().toLowerCase() === 'none';\n}\n\nexport default createRule<[], MessageIds>({\n name: 'prefer-hide',\n meta: {\n type: 'suggestion',\n fixable: 'code',\n docs: {\n description:\n \"Suggest hide: true instead of display: 'none' for Tasty's display/hide priority handling\",\n },\n messages: {\n preferHide:\n \"Use 'hide: true' instead of \\\"display: 'none'\\\" so it composes with Tasty's display/hide priority handling.\",\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 !== 'display') continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n if (isNone(str)) {\n context.report({\n node: prop.value,\n messageId: 'preferHide',\n fix(fixer) {\n return [\n fixer.replaceText(prop.key, 'hide'),\n fixer.replaceText(prop.value, 'true'),\n ];\n },\n });\n }\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 && isNone(stateStr)) {\n context.report({\n node: stateProp.value,\n messageId: 'preferHide',\n });\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,SAAS,OAAO,OAAwB;AACtC,QAAO,MAAM,MAAM,CAAC,aAAa,KAAK;;AAGxC,0BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,SAAS;EACT,MAAM,EACJ,aACE,4FACH;EACD,UAAU,EACR,YACE,+GACH;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;AAG/C,QADY,WAAW,KAAK,IAAI,KACpB,UAAW;IAEvB,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,QAAI,KAAK;AACP,SAAI,OAAO,IAAI,CACb,SAAQ,OAAO;MACb,MAAM,KAAK;MACX,WAAW;MACX,IAAI,OAAO;AACT,cAAO,CACL,MAAM,YAAY,KAAK,KAAK,OAAO,EACnC,MAAM,YAAY,KAAK,OAAO,OAAO,CACtC;;MAEJ,CAAC;AAEJ;;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,YAAY,OAAO,SAAS,CAC9B,SAAQ,OAAO;MACb,MAAM,UAAU;MAChB,WAAW;MACZ,CAAC;;;;AAOZ,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
@@ -0,0 +1,47 @@
1
+ import { createRule } from "../create-rule.js";
2
+ import { LONGHAND_MAPPING } from "../constants.js";
3
+ import { getKeyName } from "../utils.js";
4
+ import { TastyContext, styleObjectListeners } from "../context.js";
5
+
6
+ //#region src/rules/prefer-longhand-property.ts
7
+ var prefer_longhand_property_default = createRule({
8
+ name: "prefer-longhand-property",
9
+ meta: {
10
+ type: "problem",
11
+ docs: { description: "Disallow lossy CSS shorthands whose longhands are the preferred Tasty form" },
12
+ messages: { preferLonghand: "'{{shorthand}}' resets the components you omit to non-initial values. Use {{alternative}} instead." },
13
+ schema: []
14
+ },
15
+ defaultOptions: [],
16
+ create(context) {
17
+ const ctx = new TastyContext(context);
18
+ function handleStyleObject(node) {
19
+ if (!ctx.isStyleObject(node)) return;
20
+ for (const prop of node.properties) {
21
+ if (prop.type !== "Property" || prop.computed) continue;
22
+ const key = getKeyName(prop.key);
23
+ if (key === null) continue;
24
+ const mapping = LONGHAND_MAPPING[key];
25
+ if (!mapping) continue;
26
+ context.report({
27
+ node: prop.key,
28
+ messageId: "preferLonghand",
29
+ data: {
30
+ shorthand: key,
31
+ alternative: mapping.hint
32
+ }
33
+ });
34
+ }
35
+ }
36
+ return {
37
+ ImportDeclaration(node) {
38
+ ctx.trackImport(node);
39
+ },
40
+ ...styleObjectListeners(handleStyleObject)
41
+ };
42
+ }
43
+ });
44
+
45
+ //#endregion
46
+ export { prefer_longhand_property_default as default };
47
+ //# sourceMappingURL=prefer-longhand-property.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer-longhand-property.js","names":[],"sources":["../../src/rules/prefer-longhand-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 { LONGHAND_MAPPING } from '../constants.js';\n\ntype MessageIds = 'preferLonghand';\n\nexport default createRule<[], MessageIds>({\n name: 'prefer-longhand-property',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Disallow lossy CSS shorthands whose longhands are the preferred Tasty form',\n },\n messages: {\n preferLonghand:\n \"'{{shorthand}}' resets the components you omit to non-initial values. Use {{alternative}} 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 const mapping = LONGHAND_MAPPING[key];\n if (!mapping) continue;\n\n // Report-only: splitting the shorthand requires knowing which of the\n // three components the author actually meant to set.\n context.report({\n node: prop.key,\n messageId: 'preferLonghand',\n data: { shorthand: key, alternative: mapping.hint },\n });\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;AAQA,uCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,8EACH;EACD,UAAU,EACR,gBACE,sGACH;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;IAElB,MAAM,UAAU,iBAAiB;AACjC,QAAI,CAAC,QAAS;AAId,YAAQ,OAAO;KACb,MAAM,KAAK;KACX,WAAW;KACX,MAAM;MAAE,WAAW;MAAK,aAAa,QAAQ;MAAM;KACpD,CAAC;;;AAIN,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
@@ -8,6 +8,7 @@ var prefer_shorthand_property_default = createRule({
8
8
  name: "prefer-shorthand-property",
9
9
  meta: {
10
10
  type: "suggestion",
11
+ fixable: "code",
11
12
  docs: { description: "Suggest tasty shorthand when a native CSS property with a tasty alternative is used" },
12
13
  messages: { preferShorthand: "Prefer tasty shorthand '{{alternative}}' instead of '{{native}}'." },
13
14
  schema: []
@@ -28,6 +29,10 @@ var prefer_shorthand_property_default = createRule({
28
29
  data: {
29
30
  native: key,
30
31
  alternative: mapping.hint
32
+ },
33
+ fix(fixer) {
34
+ if (!mapping.safeFix) return null;
35
+ return fixer.replaceText(prop.key, mapping.property);
31
36
  }
32
37
  });
33
38
  }
@@ -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 { getKeyName } from '../utils.js';\nimport { SHORTHAND_MAPPING } from '../constants.js';\n\ntype MessageIds = 'preferShorthand';\n\nexport default createRule<[], MessageIds>({\n name: 'prefer-shorthand-property',\n meta: {\n type: 'suggestion',\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 \"Prefer tasty shorthand '{{alternative}}' instead of '{{native}}'.\",\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 const mapping = SHORTHAND_MAPPING[key];\n if (mapping) {\n context.report({\n node: prop.key,\n messageId: 'preferShorthand',\n data: { native: key, alternative: mapping.hint },\n });\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;AAQA,wCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,uFACH;EACD,UAAU,EACR,iBACE,qEACH;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;IAElB,MAAM,UAAU,kBAAkB;AAClC,QAAI,QACF,SAAQ,OAAO;KACb,MAAM,KAAK;KACX,WAAW;KACX,MAAM;MAAE,QAAQ;MAAK,aAAa,QAAQ;MAAM;KACjD,CAAC;;;AAKR,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
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 { getKeyName } from '../utils.js';\nimport { SHORTHAND_MAPPING } from '../constants.js';\n\ntype MessageIds = 'preferShorthand';\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 \"Prefer tasty shorthand '{{alternative}}' instead of '{{native}}'.\",\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 const mapping = SHORTHAND_MAPPING[key];\n if (mapping) {\n context.report({\n node: prop.key,\n messageId: 'preferShorthand',\n data: { native: key, alternative: mapping.hint },\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":";;;;;;AAQA,wCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,SAAS;EACT,MAAM,EACJ,aACE,uFACH;EACD,UAAU,EACR,iBACE,qEACH;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;IAElB,MAAM,UAAU,kBAAkB;AAClC,QAAI,QACF,SAAQ,OAAO;KACb,MAAM,KAAK;KACX,WAAW;KACX,MAAM;MAAE,QAAQ;MAAK,aAAa,QAAQ;MAAM;KAChD,IAAI,OAAO;AAKT,UAAI,CAAC,QAAQ,QAAS,QAAO;AAC7B,aAAO,MAAM,YAAY,KAAK,KAAK,QAAQ,SAAS;;KAEvD,CAAC;;;AAKR,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
@@ -1,6 +1,7 @@
1
1
  import { createRule } from "../create-rule.js";
2
2
  import { getKeyName } from "../utils.js";
3
3
  import { TastyContext, styleObjectListeners } from "../context.js";
4
+ import { movePropertyToIndex, removePropertyWithComma } from "../fix-utils.js";
4
5
 
5
6
  //#region src/rules/valid-default-state-order.ts
6
7
  function getStateKeyName(key) {
@@ -12,9 +13,11 @@ var valid_default_state_order_default = createRule({
12
13
  name: "valid-default-state-order",
13
14
  meta: {
14
15
  type: "suggestion",
15
- docs: { description: "Warn when the default ('') state is misplaced or redundant relative to the '_' fallback floor" },
16
+ fixable: "code",
17
+ docs: { description: "Warn when the default ('') state or '_' fallback floor is misplaced or redundant" },
16
18
  messages: {
17
- misplacedDefaultState: "Default state ('') in '{{property}}' must be the first key in the state map. Move it before other states — otherwise it overrides them at runtime.",
19
+ misplacedDefaultState: "Default state ('') in '{{property}}' must be the first key in the state map (or right after the '_' floor when present). Move it before other states — otherwise it overrides them at runtime.",
20
+ misplacedFloorState: "Fallback floor ('_') in '{{property}}' must be the first key in the state map, above the '' default. Move it to the top — it is the lowest-priority cascade layer.",
18
21
  redundantDefaultState: "Default state ('') in '{{property}}' is redundant when '_' is the only other state. Remove '' — '_' already provides the always-on fallback value."
19
22
  },
20
23
  schema: []
@@ -39,14 +42,42 @@ var valid_default_state_order_default = createRule({
39
42
  context.report({
40
43
  node: stateKeys[defaultIndex].node.key,
41
44
  messageId: "redundantDefaultState",
42
- data: { property }
45
+ data: { property },
46
+ fix(fixer) {
47
+ return removePropertyWithComma(fixer, stateKeys[defaultIndex].node, context.sourceCode);
48
+ }
43
49
  });
44
50
  return;
45
51
  }
46
- if (defaultIndex > 0) context.report({
52
+ if (floorIndex !== -1 && floorIndex !== 0) {
53
+ context.report({
54
+ node: stateKeys[floorIndex].node.key,
55
+ messageId: "misplacedFloorState",
56
+ data: { property },
57
+ fix(fixer) {
58
+ return movePropertyToIndex(fixer, stateKeys[floorIndex].node, 0, context.sourceCode);
59
+ }
60
+ });
61
+ return;
62
+ }
63
+ if (floorIndex === 0 && defaultIndex !== -1 && defaultIndex !== 1) {
64
+ context.report({
65
+ node: stateKeys[defaultIndex].node.key,
66
+ messageId: "misplacedDefaultState",
67
+ data: { property },
68
+ fix(fixer) {
69
+ return movePropertyToIndex(fixer, stateKeys[defaultIndex].node, 1, context.sourceCode);
70
+ }
71
+ });
72
+ return;
73
+ }
74
+ if (floorIndex === -1 && defaultIndex > 0) context.report({
47
75
  node: stateKeys[defaultIndex].node.key,
48
76
  messageId: "misplacedDefaultState",
49
- data: { property }
77
+ data: { property },
78
+ fix(fixer) {
79
+ return movePropertyToIndex(fixer, stateKeys[defaultIndex].node, 0, context.sourceCode);
80
+ }
50
81
  });
51
82
  }
52
83
  function handleStyleObject(node) {
@@ -1 +1 @@
1
- {"version":3,"file":"valid-default-state-order.js","names":[],"sources":["../../src/rules/valid-default-state-order.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 = 'misplacedDefaultState' | 'redundantDefaultState';\n\nfunction getStateKeyName(key: TSESTree.Node): string | null {\n if (key.type === 'Identifier') return key.name;\n if (key.type === 'Literal' && typeof key.value === 'string') return key.value;\n return null;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'valid-default-state-order',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n \"Warn when the default ('') state is misplaced or redundant relative to the '_' fallback floor\",\n },\n messages: {\n misplacedDefaultState:\n \"Default state ('') in '{{property}}' must be the first key in the state map. Move it before other states — otherwise it overrides them at runtime.\",\n redundantDefaultState:\n \"Default state ('') in '{{property}}' is redundant when '_' is the only other state. Remove '' — '_' already provides the always-on fallback value.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkStateMap(\n property: string,\n stateMap: TSESTree.ObjectExpression,\n ): void {\n const stateKeys: {\n key: string;\n node: TSESTree.Property;\n }[] = [];\n\n for (const prop of stateMap.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n const stateKey = getStateKeyName(prop.key);\n if (stateKey === null) continue;\n stateKeys.push({ key: stateKey, node: prop });\n }\n\n const defaultIndex = stateKeys.findIndex((entry) => entry.key === '');\n const floorIndex = stateKeys.findIndex((entry) => entry.key === '_');\n\n if (defaultIndex !== -1 && floorIndex !== -1 && stateKeys.length === 2) {\n context.report({\n node: stateKeys[defaultIndex].node.key,\n messageId: 'redundantDefaultState',\n data: { property },\n });\n return;\n }\n\n if (defaultIndex > 0) {\n context.report({\n node: stateKeys[defaultIndex].node.key,\n messageId: 'misplacedDefaultState',\n data: { property },\n });\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n if (/^[A-Z@&$#]/.test(key)) continue;\n\n if (prop.value.type !== 'ObjectExpression') continue;\n if (!ctx.isStateMap(prop.value, prop)) continue;\n\n checkStateMap(key, prop.value);\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;AAOA,SAAS,gBAAgB,KAAmC;AAC1D,KAAI,IAAI,SAAS,aAAc,QAAO,IAAI;AAC1C,KAAI,IAAI,SAAS,aAAa,OAAO,IAAI,UAAU,SAAU,QAAO,IAAI;AACxE,QAAO;;AAGT,wCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,iGACH;EACD,UAAU;GACR,uBACE;GACF,uBACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,cACP,UACA,UACM;GACN,MAAM,YAGA,EAAE;AAER,QAAK,MAAM,QAAQ,SAAS,YAAY;AACtC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAC/C,MAAM,WAAW,gBAAgB,KAAK,IAAI;AAC1C,QAAI,aAAa,KAAM;AACvB,cAAU,KAAK;KAAE,KAAK;KAAU,MAAM;KAAM,CAAC;;GAG/C,MAAM,eAAe,UAAU,WAAW,UAAU,MAAM,QAAQ,GAAG;GACrE,MAAM,aAAa,UAAU,WAAW,UAAU,MAAM,QAAQ,IAAI;AAEpE,OAAI,iBAAiB,MAAM,eAAe,MAAM,UAAU,WAAW,GAAG;AACtE,YAAQ,OAAO;KACb,MAAM,UAAU,cAAc,KAAK;KACnC,WAAW;KACX,MAAM,EAAE,UAAU;KACnB,CAAC;AACF;;AAGF,OAAI,eAAe,EACjB,SAAQ,OAAO;IACb,MAAM,UAAU,cAAc,KAAK;IACnC,WAAW;IACX,MAAM,EAAE,UAAU;IACnB,CAAC;;EAIN,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAE/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,QAAQ,KAAM;AAClB,QAAI,aAAa,KAAK,IAAI,CAAE;AAE5B,QAAI,KAAK,MAAM,SAAS,mBAAoB;AAC5C,QAAI,CAAC,IAAI,WAAW,KAAK,OAAO,KAAK,CAAE;AAEvC,kBAAc,KAAK,KAAK,MAAM;;;AAIlC,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
1
+ {"version":3,"file":"valid-default-state-order.js","names":[],"sources":["../../src/rules/valid-default-state-order.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 { removePropertyWithComma, movePropertyToIndex } from '../fix-utils.js';\n\ntype MessageIds =\n | 'misplacedDefaultState'\n | 'misplacedFloorState'\n | 'redundantDefaultState';\n\nfunction getStateKeyName(key: TSESTree.Node): string | null {\n if (key.type === 'Identifier') return key.name;\n if (key.type === 'Literal' && typeof key.value === 'string') return key.value;\n return null;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'valid-default-state-order',\n meta: {\n type: 'suggestion',\n fixable: 'code',\n docs: {\n description:\n \"Warn when the default ('') state or '_' fallback floor is misplaced or redundant\",\n },\n messages: {\n misplacedDefaultState:\n \"Default state ('') in '{{property}}' must be the first key in the state map (or right after the '_' floor when present). Move it before other states — otherwise it overrides them at runtime.\",\n misplacedFloorState:\n \"Fallback floor ('_') in '{{property}}' must be the first key in the state map, above the '' default. Move it to the top — it is the lowest-priority cascade layer.\",\n redundantDefaultState:\n \"Default state ('') in '{{property}}' is redundant when '_' is the only other state. Remove '' — '_' already provides the always-on fallback value.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkStateMap(\n property: string,\n stateMap: TSESTree.ObjectExpression,\n ): void {\n const stateKeys: {\n key: string;\n node: TSESTree.Property;\n }[] = [];\n\n for (const prop of stateMap.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n const stateKey = getStateKeyName(prop.key);\n if (stateKey === null) continue;\n stateKeys.push({ key: stateKey, node: prop });\n }\n\n const defaultIndex = stateKeys.findIndex((entry) => entry.key === '');\n const floorIndex = stateKeys.findIndex((entry) => entry.key === '_');\n\n // 1. Both '' and '_' present with no other states → '' is redundant.\n if (defaultIndex !== -1 && floorIndex !== -1 && stateKeys.length === 2) {\n context.report({\n node: stateKeys[defaultIndex].node.key,\n messageId: 'redundantDefaultState',\n data: { property },\n fix(fixer) {\n return removePropertyWithComma(\n fixer,\n stateKeys[defaultIndex].node,\n context.sourceCode,\n );\n },\n });\n return;\n }\n\n // 2. '_' present but not first → move it to the top. Re-evaluated next\n // pass so a misplaced '' is handled afterwards.\n if (floorIndex !== -1 && floorIndex !== 0) {\n context.report({\n node: stateKeys[floorIndex].node.key,\n messageId: 'misplacedFloorState',\n data: { property },\n fix(fixer) {\n return movePropertyToIndex(\n fixer,\n stateKeys[floorIndex].node,\n 0,\n context.sourceCode,\n );\n },\n });\n return;\n }\n\n // 3. '_' at top, '' present but not right after it → move '' to index 1.\n if (floorIndex === 0 && defaultIndex !== -1 && defaultIndex !== 1) {\n context.report({\n node: stateKeys[defaultIndex].node.key,\n messageId: 'misplacedDefaultState',\n data: { property },\n fix(fixer) {\n return movePropertyToIndex(\n fixer,\n stateKeys[defaultIndex].node,\n 1,\n context.sourceCode,\n );\n },\n });\n return;\n }\n\n // 4. No '_', '' not first → move '' to the top.\n if (floorIndex === -1 && defaultIndex > 0) {\n context.report({\n node: stateKeys[defaultIndex].node.key,\n messageId: 'misplacedDefaultState',\n data: { property },\n fix(fixer) {\n return movePropertyToIndex(\n fixer,\n stateKeys[defaultIndex].node,\n 0,\n context.sourceCode,\n );\n },\n });\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n if (/^[A-Z@&$#]/.test(key)) continue;\n\n if (prop.value.type !== 'ObjectExpression') continue;\n if (!ctx.isStateMap(prop.value, prop)) continue;\n\n checkStateMap(key, prop.value);\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;AAWA,SAAS,gBAAgB,KAAmC;AAC1D,KAAI,IAAI,SAAS,aAAc,QAAO,IAAI;AAC1C,KAAI,IAAI,SAAS,aAAa,OAAO,IAAI,UAAU,SAAU,QAAO,IAAI;AACxE,QAAO;;AAGT,wCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,SAAS;EACT,MAAM,EACJ,aACE,oFACH;EACD,UAAU;GACR,uBACE;GACF,qBACE;GACF,uBACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,cACP,UACA,UACM;GACN,MAAM,YAGA,EAAE;AAER,QAAK,MAAM,QAAQ,SAAS,YAAY;AACtC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAC/C,MAAM,WAAW,gBAAgB,KAAK,IAAI;AAC1C,QAAI,aAAa,KAAM;AACvB,cAAU,KAAK;KAAE,KAAK;KAAU,MAAM;KAAM,CAAC;;GAG/C,MAAM,eAAe,UAAU,WAAW,UAAU,MAAM,QAAQ,GAAG;GACrE,MAAM,aAAa,UAAU,WAAW,UAAU,MAAM,QAAQ,IAAI;AAGpE,OAAI,iBAAiB,MAAM,eAAe,MAAM,UAAU,WAAW,GAAG;AACtE,YAAQ,OAAO;KACb,MAAM,UAAU,cAAc,KAAK;KACnC,WAAW;KACX,MAAM,EAAE,UAAU;KAClB,IAAI,OAAO;AACT,aAAO,wBACL,OACA,UAAU,cAAc,MACxB,QAAQ,WACT;;KAEJ,CAAC;AACF;;AAKF,OAAI,eAAe,MAAM,eAAe,GAAG;AACzC,YAAQ,OAAO;KACb,MAAM,UAAU,YAAY,KAAK;KACjC,WAAW;KACX,MAAM,EAAE,UAAU;KAClB,IAAI,OAAO;AACT,aAAO,oBACL,OACA,UAAU,YAAY,MACtB,GACA,QAAQ,WACT;;KAEJ,CAAC;AACF;;AAIF,OAAI,eAAe,KAAK,iBAAiB,MAAM,iBAAiB,GAAG;AACjE,YAAQ,OAAO;KACb,MAAM,UAAU,cAAc,KAAK;KACnC,WAAW;KACX,MAAM,EAAE,UAAU;KAClB,IAAI,OAAO;AACT,aAAO,oBACL,OACA,UAAU,cAAc,MACxB,GACA,QAAQ,WACT;;KAEJ,CAAC;AACF;;AAIF,OAAI,eAAe,MAAM,eAAe,EACtC,SAAQ,OAAO;IACb,MAAM,UAAU,cAAc,KAAK;IACnC,WAAW;IACX,MAAM,EAAE,UAAU;IAClB,IAAI,OAAO;AACT,YAAO,oBACL,OACA,UAAU,cAAc,MACxB,GACA,QAAQ,WACT;;IAEJ,CAAC;;EAIN,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAE/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,QAAQ,KAAM;AAClB,QAAI,aAAa,KAAK,IAAI,CAAE;AAE5B,QAAI,KAAK,MAAM,SAAS,mBAAoB;AAC5C,QAAI,CAAC,IAAI,WAAW,KAAK,OAAO,KAAK,CAAE;AAEvC,kBAAc,KAAK,KAAK,MAAM;;;AAIlC,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
@@ -2,6 +2,7 @@ import { createRule } from "../create-rule.js";
2
2
  import { RADIUS_SHAPES } from "../constants.js";
3
3
  import { getKeyName, getStringValue } from "../utils.js";
4
4
  import { TastyContext, styleObjectListeners } from "../context.js";
5
+ import { replaceStringValue } from "../fix-utils.js";
5
6
 
6
7
  //#region src/rules/valid-radius-shape.ts
7
8
  const SHAPE_LIKE = /^[a-z]+$/;
@@ -9,8 +10,12 @@ var valid_radius_shape_default = createRule({
9
10
  name: "valid-radius-shape",
10
11
  meta: {
11
12
  type: "problem",
13
+ hasSuggestions: true,
12
14
  docs: { description: "Validate special shape keywords used with the radius property" },
13
- messages: { unknownShape: "Unknown radius shape '{{shape}}'. Valid shapes: {{valid}}." },
15
+ messages: {
16
+ unknownShape: "Unknown radius shape '{{shape}}'. Valid shapes: {{valid}}.",
17
+ replaceWithShape: "Replace '{{shape}}' with '{{suggestion}}'"
18
+ },
14
19
  schema: []
15
20
  },
16
21
  defaultOptions: [],
@@ -34,13 +39,24 @@ var valid_radius_shape_default = createRule({
34
39
  ]).has(trimmed)) return;
35
40
  const suggestion = findClosestShape(trimmed);
36
41
  const validList = [...RADIUS_SHAPES].join(", ");
42
+ const suggest = suggestion ? [{
43
+ messageId: "replaceWithShape",
44
+ data: {
45
+ shape: trimmed,
46
+ suggestion
47
+ },
48
+ fix(fixer) {
49
+ return replaceStringValue(fixer, node, suggestion);
50
+ }
51
+ }] : void 0;
37
52
  context.report({
38
53
  node,
39
54
  messageId: "unknownShape",
40
55
  data: {
41
56
  shape: trimmed,
42
57
  valid: validList + (suggestion ? `. Did you mean '${suggestion}'?` : "")
43
- }
58
+ },
59
+ suggest
44
60
  });
45
61
  }
46
62
  function findClosestShape(input) {
@@ -1 +1 @@
1
- {"version":3,"file":"valid-radius-shape.js","names":[],"sources":["../../src/rules/valid-radius-shape.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 { RADIUS_SHAPES } from '../constants.js';\n\ntype MessageIds = 'unknownShape';\n\nconst SHAPE_LIKE = /^[a-z]+$/;\n\nexport default createRule<[], MessageIds>({\n name: 'valid-radius-shape',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Validate special shape keywords used with the radius property',\n },\n messages: {\n unknownShape:\n \"Unknown radius shape '{{shape}}'. Valid shapes: {{valid}}.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkRadiusValue(value: string, node: TSESTree.Node): void {\n const trimmed = value.trim();\n // Only check single-word values that look like keywords\n if (!SHAPE_LIKE.test(trimmed)) return;\n\n // Known valid keywords\n if (RADIUS_SHAPES.has(trimmed)) return;\n if (trimmed === 'true' || trimmed === 'false') return;\n if (trimmed === 'none' || trimmed === 'inherit' || trimmed === 'initial')\n return;\n\n // Check if it's a directional modifier (handled elsewhere)\n const directions = new Set([\n 'top',\n 'right',\n 'bottom',\n 'left',\n 'top-left',\n 'top-right',\n 'bottom-left',\n 'bottom-right',\n ]);\n if (directions.has(trimmed)) return;\n\n // It looks like they tried to use a shape keyword\n const suggestion = findClosestShape(trimmed);\n const validList = [...RADIUS_SHAPES].join(', ');\n\n context.report({\n node,\n messageId: 'unknownShape',\n data: {\n shape: trimmed,\n valid:\n validList + (suggestion ? `. Did you mean '${suggestion}'?` : ''),\n },\n });\n }\n\n function findClosestShape(input: string): string | null {\n for (const shape of RADIUS_SHAPES) {\n if (\n shape.startsWith(input.slice(0, 3)) ||\n input.startsWith(shape.slice(0, 3))\n ) {\n return shape;\n }\n }\n return null;\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 !== 'radius') continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkRadiusValue(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 checkRadiusValue(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,aAAa;AAEnB,iCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,iEACH;EACD,UAAU,EACR,cACE,8DACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,iBAAiB,OAAe,MAA2B;GAClE,MAAM,UAAU,MAAM,MAAM;AAE5B,OAAI,CAAC,WAAW,KAAK,QAAQ,CAAE;AAG/B,OAAI,cAAc,IAAI,QAAQ,CAAE;AAChC,OAAI,YAAY,UAAU,YAAY,QAAS;AAC/C,OAAI,YAAY,UAAU,YAAY,aAAa,YAAY,UAC7D;AAaF,OAVmB,IAAI,IAAI;IACzB;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACD,CAAC,CACa,IAAI,QAAQ,CAAE;GAG7B,MAAM,aAAa,iBAAiB,QAAQ;GAC5C,MAAM,YAAY,CAAC,GAAG,cAAc,CAAC,KAAK,KAAK;AAE/C,WAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM;KACJ,OAAO;KACP,OACE,aAAa,aAAa,mBAAmB,WAAW,MAAM;KACjE;IACF,CAAC;;EAGJ,SAAS,iBAAiB,OAA8B;AACtD,QAAK,MAAM,SAAS,cAClB,KACE,MAAM,WAAW,MAAM,MAAM,GAAG,EAAE,CAAC,IACnC,MAAM,WAAW,MAAM,MAAM,GAAG,EAAE,CAAC,CAEnC,QAAO;AAGX,UAAO;;EAGT,SAAS,kBAAkB,MAAiC;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,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;;;;AAOrD,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
1
+ {"version":3,"file":"valid-radius-shape.js","names":[],"sources":["../../src/rules/valid-radius-shape.ts"],"sourcesContent":["import type { TSESLint, TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport { RADIUS_SHAPES } from '../constants.js';\nimport { replaceStringValue } from '../fix-utils.js';\n\ntype MessageIds = 'unknownShape' | 'replaceWithShape';\n\nconst SHAPE_LIKE = /^[a-z]+$/;\n\nexport default createRule<[], MessageIds>({\n name: 'valid-radius-shape',\n meta: {\n type: 'problem',\n hasSuggestions: true,\n docs: {\n description:\n 'Validate special shape keywords used with the radius property',\n },\n messages: {\n unknownShape:\n \"Unknown radius shape '{{shape}}'. Valid shapes: {{valid}}.\",\n replaceWithShape: \"Replace '{{shape}}' with '{{suggestion}}'\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkRadiusValue(value: string, node: TSESTree.Node): void {\n const trimmed = value.trim();\n // Only check single-word values that look like keywords\n if (!SHAPE_LIKE.test(trimmed)) return;\n\n // Known valid keywords\n if (RADIUS_SHAPES.has(trimmed)) return;\n if (trimmed === 'true' || trimmed === 'false') return;\n if (trimmed === 'none' || trimmed === 'inherit' || trimmed === 'initial')\n return;\n\n // Check if it's a directional modifier (handled elsewhere)\n const directions = new Set([\n 'top',\n 'right',\n 'bottom',\n 'left',\n 'top-left',\n 'top-right',\n 'bottom-left',\n 'bottom-right',\n ]);\n if (directions.has(trimmed)) return;\n\n // It looks like they tried to use a shape keyword\n const suggestion = findClosestShape(trimmed);\n const validList = [...RADIUS_SHAPES].join(', ');\n\n const suggest = suggestion\n ? [\n {\n messageId: 'replaceWithShape' as const,\n data: { shape: trimmed, suggestion },\n fix(fixer: TSESLint.RuleFixer) {\n return replaceStringValue(fixer, node, suggestion);\n },\n },\n ]\n : undefined;\n\n context.report({\n node,\n messageId: 'unknownShape',\n data: {\n shape: trimmed,\n valid:\n validList + (suggestion ? `. Did you mean '${suggestion}'?` : ''),\n },\n suggest,\n });\n }\n\n function findClosestShape(input: string): string | null {\n for (const shape of RADIUS_SHAPES) {\n if (\n shape.startsWith(input.slice(0, 3)) ||\n input.startsWith(shape.slice(0, 3))\n ) {\n return shape;\n }\n }\n return null;\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 !== 'radius') continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkRadiusValue(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 checkRadiusValue(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":";;;;;;;AASA,MAAM,aAAa;AAEnB,iCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,gBAAgB;EAChB,MAAM,EACJ,aACE,iEACH;EACD,UAAU;GACR,cACE;GACF,kBAAkB;GACnB;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,iBAAiB,OAAe,MAA2B;GAClE,MAAM,UAAU,MAAM,MAAM;AAE5B,OAAI,CAAC,WAAW,KAAK,QAAQ,CAAE;AAG/B,OAAI,cAAc,IAAI,QAAQ,CAAE;AAChC,OAAI,YAAY,UAAU,YAAY,QAAS;AAC/C,OAAI,YAAY,UAAU,YAAY,aAAa,YAAY,UAC7D;AAaF,OAVmB,IAAI,IAAI;IACzB;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACD,CAAC,CACa,IAAI,QAAQ,CAAE;GAG7B,MAAM,aAAa,iBAAiB,QAAQ;GAC5C,MAAM,YAAY,CAAC,GAAG,cAAc,CAAC,KAAK,KAAK;GAE/C,MAAM,UAAU,aACZ,CACE;IACE,WAAW;IACX,MAAM;KAAE,OAAO;KAAS;KAAY;IACpC,IAAI,OAA2B;AAC7B,YAAO,mBAAmB,OAAO,MAAM,WAAW;;IAErD,CACF,GACD;AAEJ,WAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM;KACJ,OAAO;KACP,OACE,aAAa,aAAa,mBAAmB,WAAW,MAAM;KACjE;IACD;IACD,CAAC;;EAGJ,SAAS,iBAAiB,OAA8B;AACtD,QAAK,MAAM,SAAS,cAClB,KACE,MAAM,WAAW,MAAM,MAAM,GAAG,EAAE,CAAC,IACnC,MAAM,WAAW,MAAM,MAAM,GAAG,EAAE,CAAC,CAEnC,QAAO;AAGX,UAAO;;EAGT,SAAS,kBAAkB,MAAiC;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,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;;;;AAOrD,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
@@ -2,16 +2,19 @@ import { createRule } from "../create-rule.js";
2
2
  import { KNOWN_CSS_PROPERTIES, SEMANTIC_TRANSITIONS, TRANSITION_SEMANTIC_MAPPING } from "../constants.js";
3
3
  import { getKeyName, getStringValue } from "../utils.js";
4
4
  import { TastyContext, styleObjectListeners } from "../context.js";
5
+ import { replaceInStringValue } from "../fix-utils.js";
5
6
 
6
7
  //#region src/rules/valid-transition.ts
7
8
  var valid_transition_default = createRule({
8
9
  name: "valid-transition",
9
10
  meta: {
10
11
  type: "suggestion",
12
+ hasSuggestions: true,
11
13
  docs: { description: "Validate transition property values use valid semantic transition names" },
12
14
  messages: {
13
15
  unknownTransition: "Unknown transition name '{{name}}'. Use a semantic name ({{known}}) or a CSS property name.",
14
- preferSemanticTransition: "Transition '{{native}}' has a Tasty semantic equivalent — use '{{semantic}}' instead."
16
+ preferSemanticTransition: "Transition '{{native}}' has a Tasty semantic equivalent — use '{{semantic}}' instead.",
17
+ useSemantic: "Use '{{semantic}}' instead of '{{native}}'"
15
18
  },
16
19
  schema: []
17
20
  },
@@ -20,22 +23,50 @@ var valid_transition_default = createRule({
20
23
  const ctx = new TastyContext(context);
21
24
  function checkTransitionValue(value, node) {
22
25
  const groups = value.split(",");
26
+ let offset = 0;
23
27
  for (const group of groups) {
24
28
  const parts = group.trim().split(/\s+/);
25
- if (parts.length === 0) continue;
29
+ if (parts.length === 0 || parts[0] === "") {
30
+ offset += group.length + 1;
31
+ continue;
32
+ }
26
33
  const name = parts[0];
27
- if (name.startsWith("$$")) continue;
28
- if (name.startsWith("##")) continue;
34
+ if (name.startsWith("$$")) {
35
+ offset += group.length + 1;
36
+ continue;
37
+ }
38
+ if (name.startsWith("##")) {
39
+ offset += group.length + 1;
40
+ continue;
41
+ }
29
42
  const semantic = TRANSITION_SEMANTIC_MAPPING[name];
30
43
  if (semantic) {
44
+ const nameStartInGroup = group.indexOf(name);
45
+ const start = offset + nameStartInGroup;
46
+ const end = start + name.length;
31
47
  context.report({
32
48
  node,
33
49
  messageId: "preferSemanticTransition",
34
50
  data: {
35
51
  native: name,
36
52
  semantic
37
- }
53
+ },
54
+ suggest: [{
55
+ messageId: "useSemantic",
56
+ data: {
57
+ native: name,
58
+ semantic
59
+ },
60
+ fix(fixer) {
61
+ return replaceInStringValue(fixer, node, [{
62
+ start,
63
+ end,
64
+ replacement: semantic
65
+ }], context.sourceCode);
66
+ }
67
+ }]
38
68
  });
69
+ offset += group.length + 1;
39
70
  continue;
40
71
  }
41
72
  if (!SEMANTIC_TRANSITIONS.has(name) && !KNOWN_CSS_PROPERTIES.has(name) && name !== "all" && name !== "none") context.report({
@@ -46,6 +77,7 @@ var valid_transition_default = createRule({
46
77
  known: [...SEMANTIC_TRANSITIONS].join(", ")
47
78
  }
48
79
  });
80
+ offset += group.length + 1;
49
81
  }
50
82
  }
51
83
  function handleStyleObject(node) {
@@ -1 +1 @@
1
- {"version":3,"file":"valid-transition.js","names":[],"sources":["../../src/rules/valid-transition.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 SEMANTIC_TRANSITIONS,\n KNOWN_CSS_PROPERTIES,\n TRANSITION_SEMANTIC_MAPPING,\n} from '../constants.js';\n\ntype MessageIds = 'unknownTransition' | 'preferSemanticTransition';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-transition',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Validate transition property values use valid semantic transition names',\n },\n messages: {\n unknownTransition:\n \"Unknown transition name '{{name}}'. Use a semantic name ({{known}}) or a CSS property name.\",\n preferSemanticTransition:\n \"Transition '{{native}}' has a Tasty semantic equivalent — use '{{semantic}}' instead.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkTransitionValue(value: string, node: TSESTree.Node): void {\n const groups = value.split(',');\n\n for (const group of groups) {\n const parts = group.trim().split(/\\s+/);\n if (parts.length === 0) continue;\n\n const name = parts[0];\n\n // $$ prefix is always valid (custom property reference: $$name -> --name)\n if (name.startsWith('$$')) continue;\n\n // ## prefix is always valid (color property reference: ##name -> --name-color)\n if (name.startsWith('##')) continue;\n\n const semantic = TRANSITION_SEMANTIC_MAPPING[name];\n if (semantic) {\n context.report({\n node,\n messageId: 'preferSemanticTransition',\n data: { native: name, semantic },\n });\n continue;\n }\n\n if (\n !SEMANTIC_TRANSITIONS.has(name) &&\n !KNOWN_CSS_PROPERTIES.has(name) &&\n name !== 'all' &&\n name !== 'none'\n ) {\n context.report({\n node,\n messageId: 'unknownTransition',\n data: {\n name,\n known: [...SEMANTIC_TRANSITIONS].join(', '),\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 !== 'transition') continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkTransitionValue(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 checkTransitionValue(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":";;;;;;AAYA,+BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,2EACH;EACD,UAAU;GACR,mBACE;GACF,0BACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,qBAAqB,OAAe,MAA2B;GACtE,MAAM,SAAS,MAAM,MAAM,IAAI;AAE/B,QAAK,MAAM,SAAS,QAAQ;IAC1B,MAAM,QAAQ,MAAM,MAAM,CAAC,MAAM,MAAM;AACvC,QAAI,MAAM,WAAW,EAAG;IAExB,MAAM,OAAO,MAAM;AAGnB,QAAI,KAAK,WAAW,KAAK,CAAE;AAG3B,QAAI,KAAK,WAAW,KAAK,CAAE;IAE3B,MAAM,WAAW,4BAA4B;AAC7C,QAAI,UAAU;AACZ,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM;OAAE,QAAQ;OAAM;OAAU;MACjC,CAAC;AACF;;AAGF,QACE,CAAC,qBAAqB,IAAI,KAAK,IAC/B,CAAC,qBAAqB,IAAI,KAAK,IAC/B,SAAS,SACT,SAAS,OAET,SAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MACJ;MACA,OAAO,CAAC,GAAG,qBAAqB,CAAC,KAAK,KAAK;MAC5C;KACF,CAAC;;;EAKR,SAAS,kBAAkB,MAAiC;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,aAAc;IAE1B,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,QAAI,KAAK;AACP,0BAAqB,KAAK,KAAK,MAAM;AACrC;;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,sBAAqB,UAAU,UAAU,MAAM;;;;AAOzD,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
1
+ {"version":3,"file":"valid-transition.js","names":[],"sources":["../../src/rules/valid-transition.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 SEMANTIC_TRANSITIONS,\n KNOWN_CSS_PROPERTIES,\n TRANSITION_SEMANTIC_MAPPING,\n} from '../constants.js';\nimport { replaceInStringValue } from '../fix-utils.js';\n\ntype MessageIds =\n | 'unknownTransition'\n | 'preferSemanticTransition'\n | 'useSemantic';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-transition',\n meta: {\n type: 'suggestion',\n hasSuggestions: true,\n docs: {\n description:\n 'Validate transition property values use valid semantic transition names',\n },\n messages: {\n unknownTransition:\n \"Unknown transition name '{{name}}'. Use a semantic name ({{known}}) or a CSS property name.\",\n preferSemanticTransition:\n \"Transition '{{native}}' has a Tasty semantic equivalent — use '{{semantic}}' instead.\",\n useSemantic: \"Use '{{semantic}}' instead of '{{native}}'\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkTransitionValue(value: string, node: TSESTree.Node): void {\n const groups = value.split(',');\n let offset = 0;\n\n for (const group of groups) {\n const parts = group.trim().split(/\\s+/);\n if (parts.length === 0 || parts[0] === '') {\n offset += group.length + 1;\n continue;\n }\n\n const name = parts[0];\n\n // $$ prefix is always valid (custom property reference: $$name -> --name)\n if (name.startsWith('$$')) {\n offset += group.length + 1;\n continue;\n }\n\n // ## prefix is always valid (color property reference: ##name -> --name-color)\n if (name.startsWith('##')) {\n offset += group.length + 1;\n continue;\n }\n\n const semantic = TRANSITION_SEMANTIC_MAPPING[name];\n if (semantic) {\n const nameStartInGroup = group.indexOf(name);\n const start = offset + nameStartInGroup;\n const end = start + name.length;\n context.report({\n node,\n messageId: 'preferSemanticTransition',\n data: { native: name, semantic },\n suggest: [\n {\n messageId: 'useSemantic',\n data: { native: name, semantic },\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement: semantic }],\n context.sourceCode,\n );\n },\n },\n ],\n });\n offset += group.length + 1;\n continue;\n }\n\n if (\n !SEMANTIC_TRANSITIONS.has(name) &&\n !KNOWN_CSS_PROPERTIES.has(name) &&\n name !== 'all' &&\n name !== 'none'\n ) {\n context.report({\n node,\n messageId: 'unknownTransition',\n data: {\n name,\n known: [...SEMANTIC_TRANSITIONS].join(', '),\n },\n });\n }\n\n offset += group.length + 1;\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 !== 'transition') continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkTransitionValue(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 checkTransitionValue(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":";;;;;;;AAgBA,+BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,gBAAgB;EAChB,MAAM,EACJ,aACE,2EACH;EACD,UAAU;GACR,mBACE;GACF,0BACE;GACF,aAAa;GACd;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,qBAAqB,OAAe,MAA2B;GACtE,MAAM,SAAS,MAAM,MAAM,IAAI;GAC/B,IAAI,SAAS;AAEb,QAAK,MAAM,SAAS,QAAQ;IAC1B,MAAM,QAAQ,MAAM,MAAM,CAAC,MAAM,MAAM;AACvC,QAAI,MAAM,WAAW,KAAK,MAAM,OAAO,IAAI;AACzC,eAAU,MAAM,SAAS;AACzB;;IAGF,MAAM,OAAO,MAAM;AAGnB,QAAI,KAAK,WAAW,KAAK,EAAE;AACzB,eAAU,MAAM,SAAS;AACzB;;AAIF,QAAI,KAAK,WAAW,KAAK,EAAE;AACzB,eAAU,MAAM,SAAS;AACzB;;IAGF,MAAM,WAAW,4BAA4B;AAC7C,QAAI,UAAU;KACZ,MAAM,mBAAmB,MAAM,QAAQ,KAAK;KAC5C,MAAM,QAAQ,SAAS;KACvB,MAAM,MAAM,QAAQ,KAAK;AACzB,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM;OAAE,QAAQ;OAAM;OAAU;MAChC,SAAS,CACP;OACE,WAAW;OACX,MAAM;QAAE,QAAQ;QAAM;QAAU;OAChC,IAAI,OAAO;AACT,eAAO,qBACL,OACA,MACA,CAAC;SAAE;SAAO;SAAK,aAAa;SAAU,CAAC,EACvC,QAAQ,WACT;;OAEJ,CACF;MACF,CAAC;AACF,eAAU,MAAM,SAAS;AACzB;;AAGF,QACE,CAAC,qBAAqB,IAAI,KAAK,IAC/B,CAAC,qBAAqB,IAAI,KAAK,IAC/B,SAAS,SACT,SAAS,OAET,SAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MACJ;MACA,OAAO,CAAC,GAAG,qBAAqB,CAAC,KAAK,KAAK;MAC5C;KACF,CAAC;AAGJ,cAAU,MAAM,SAAS;;;EAI7B,SAAS,kBAAkB,MAAiC;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,aAAc;IAE1B,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,QAAI,KAAK;AACP,0BAAqB,KAAK,KAAK,MAAM;AACrC;;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,sBAAqB,UAAU,UAAU,MAAM;;;;AAOzD,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenphi/eslint-plugin-tasty",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "ESLint plugin for validating tasty() and tastyStatic() style objects",
5
5
  "type": "module",
6
6
  "exports": {