@tenphi/eslint-plugin-tasty 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -192,9 +192,12 @@ for typing shared signatures.
192
192
  | `tasty/valid-default-state-order` | warn | Misplaced default (`''`) or redundant `''` when only `_` is present |
193
193
  | `tasty/prefer-shorthand-property` | warn | Use Tasty shorthand instead of native CSS properties (`backgroundColor` → `fill`, `paddingBlock` → `blockPadding`, etc.). In an extension layer the rewrite is report-only and points at a token in the base component, and over a base you cannot edit it is skipped (see `ownedSources`) |
194
194
  | `tasty/no-raw-color-values` | warn | Raw hex/rgb/`okhsl`/`okhst`/`oklch`/named colors instead of `#color` tokens |
195
- | `tasty/no-raw-transition-duration` | warn | Hardcoded `transition` duration (`fill 0.2s`) instead of a duration token or tasty's default timing. Suggests each `$…-transition` / `$…duration` token in your config, or dropping the duration; a *delay* is left alone |
195
+ | `tasty/no-raw-transition-duration` | warn | Hardcoded `transition` duration (`fill 0.2s`) in Tasty styles or a local `tasty()` component's `transition` prop. Suggests a configured duration token or the implicit per-name timing; zero and delays are left alone |
196
+ | `tasty/no-runtime-styles-mutation` | warn | JavaScript variables, calls, conditionals, computed keys, or interpolated templates in Tasty style values; use states and tokens instead |
197
+ | `tasty/no-style-spread` | warn | Object or array spreads inside runtime Tasty styles; suppress an intentional spread on its line with an ESLint comment and a reason |
196
198
  | `tasty/consistent-token-usage` | warn | Raw px values when custom units or tokens exist |
197
199
  | `tasty/prefer-auto-calc` | warn | `calc(...)` instead of Tasty auto-calc `(...)` |
200
+ | `tasty/prefer-state-negation` | warn | Top-level `:not(...)` in state keys instead of the `!` prefix (autofixable; nested CSS selectors stay intact) |
198
201
  | `tasty/prefer-custom-property-syntax` | warn | `var(--prop)` / `$x-color` / `transparent` / `currentColor` instead of `$prop` / `#color` / `#clear` / `#current` |
199
202
  | `tasty/prefer-hide` | warn | `display: 'none'` instead of `hide: true` |
200
203
  | `tasty/prefer-directional-shorthand` | warn | 4-value `margin`/`padding`/`inset`/`radius`/`fade`/`border` with placeholder positions instead of directional form |
@@ -208,7 +211,69 @@ for typing shared signatures.
208
211
  | `tasty/valid-state-definition` | warn | Invalid state definition values in `configure()` or `tasty.config` |
209
212
  | `tasty/no-unknown-state-alias` | warn | Unknown `@name` state aliases |
210
213
  | `tasty/no-styles-prop` | warn | Direct `styles` prop usage |
211
- | `tasty/no-runtime-styles-mutation` | warn | Dynamic values in style objects |
214
+
215
+ ### Static values and intentional spreads
216
+
217
+ The recommended rules flag JavaScript-computed style values, including
218
+ identifiers, conditionals, function calls, computed keys, and template
219
+ interpolation. Use a Tasty state map with `mods` for discrete states, or a token
220
+ for a value that changes per instance. Literal CSS strings, token references,
221
+ and state maps are accepted.
222
+
223
+ Spreads in runtime style objects get a separate warning because they hide which
224
+ properties are defined and can make overrides hard to review. If a spread is
225
+ necessary, suppress just that line and explain why:
226
+
227
+ ```js
228
+ const Card = tasty({
229
+ styles: {
230
+ // eslint-disable-next-line tasty/no-style-spread -- generated defaults are shared by all cards
231
+ ...sharedStyles,
232
+ fill: { '': '#surface', active: '#primary' },
233
+ },
234
+ });
235
+ ```
236
+
237
+ `tastyStatic()` still rejects spreads and dynamic expressions as errors because
238
+ its build-time extractor cannot evaluate them. An ESLint suppression does not
239
+ make a spread valid there. To make warnings fail CI until addressed or explicitly
240
+ suppressed, run ESLint with `--max-warnings=0`.
241
+
242
+ ### Optional motion duration rule
243
+
244
+ Enable `tasty/no-raw-motion-duration` as a warning if your design system uses
245
+ tokens for animation timing and explicit CSS durations:
246
+
247
+ ```js
248
+ import tasty from '@tenphi/eslint-plugin-tasty';
249
+
250
+ export default [
251
+ tasty.configs.recommended,
252
+ { rules: { 'tasty/no-raw-motion-duration': 'warn' } },
253
+ ];
254
+ ```
255
+
256
+ It checks `animation`, `animationDuration`, and `transitionDuration` in Tasty
257
+ style objects. It suggests configured `$…duration` tokens when
258
+ available. It never suggests removing a duration, since these properties do not
259
+ inherit Tasty's implicit transition timing. Zero, delays, and values based on
260
+ tokens are left alone. Lengths and widths remain covered by the separate
261
+ `consistent-token-usage` rule; valid custom units such as `x` and `bw` are fine.
262
+
263
+ ### Animation and transition shorthands
264
+
265
+ Use Tasty's semantic `transition` value when defining a complete transition,
266
+ and the native CSS `animation` shorthand when defining a complete animation.
267
+ Tasty already supports both: `transition` uses semantic names and duration
268
+ tokens, while `animation` accepts tokens and tracks local keyframes. No
269
+ additional animation style handler is needed. Keep longhands such as
270
+ `transitionDuration`, `animationDuration`, and `animationTimeline` when changing
271
+ one part of an existing effect in a state or component extension.
272
+
273
+ `prefer-shorthand-property` deliberately leaves these motion longhands alone.
274
+ Replacing one with a shorthand can reset the other parts of the effect, including
275
+ an animation's timeline and range. The duration rules above still encourage
276
+ tokens wherever a duration is written.
212
277
 
213
278
  ## Logical styles
214
279
 
package/dist/configs.js CHANGED
@@ -24,8 +24,11 @@ const recommended = {
24
24
  "tasty/prefer-shorthand-property": "warn",
25
25
  "tasty/no-raw-color-values": "warn",
26
26
  "tasty/no-raw-transition-duration": "warn",
27
+ "tasty/no-runtime-styles-mutation": "warn",
28
+ "tasty/no-style-spread": "warn",
27
29
  "tasty/consistent-token-usage": "warn",
28
30
  "tasty/prefer-auto-calc": "warn",
31
+ "tasty/prefer-state-negation": "warn",
29
32
  "tasty/prefer-custom-property-syntax": "warn",
30
33
  "tasty/prefer-hide": "warn",
31
34
  "tasty/prefer-directional-shorthand": "warn",
@@ -36,7 +39,6 @@ const strict = {
36
39
  "tasty/valid-custom-property": "warn",
37
40
  "tasty/no-unknown-state-alias": "warn",
38
41
  "tasty/no-styles-prop": "warn",
39
- "tasty/no-runtime-styles-mutation": "warn",
40
42
  "tasty/valid-state-definition": "warn"
41
43
  };
42
44
 
@@ -1 +1 @@
1
- {"version":3,"file":"configs.js","names":[],"sources":["../src/configs.ts"],"sourcesContent":["import type { TSESLint } from '@typescript-eslint/utils';\n\nexport const recommended: TSESLint.SharedConfig.RulesRecord = {\n 'tasty/known-property': 'warn',\n 'tasty/valid-value': 'error',\n 'tasty/valid-color-token': 'error',\n 'tasty/valid-custom-unit': 'error',\n 'tasty/valid-boolean-property': 'error',\n 'tasty/valid-state-key': 'error',\n 'tasty/valid-styles-structure': 'error',\n 'tasty/no-nested-state-map': 'error',\n 'tasty/no-important': 'error',\n 'tasty/valid-sub-element': 'error',\n 'tasty/valid-directional-modifier': 'error',\n 'tasty/valid-radius-shape': 'error',\n 'tasty/no-nested-selector': 'error',\n 'tasty/static-no-dynamic-values': 'error',\n 'tasty/static-valid-selector': 'error',\n 'tasty/valid-preset': 'error',\n 'tasty/valid-recipe': 'error',\n 'tasty/valid-transition': 'warn',\n 'tasty/require-default-state': 'error',\n 'tasty/no-own-at-root': 'warn',\n 'tasty/valid-default-state-order': 'warn',\n 'tasty/prefer-shorthand-property': 'warn',\n 'tasty/no-raw-color-values': 'warn',\n 'tasty/no-raw-transition-duration': 'warn',\n 'tasty/consistent-token-usage': 'warn',\n 'tasty/prefer-auto-calc': 'warn',\n 'tasty/prefer-custom-property-syntax': 'warn',\n 'tasty/prefer-hide': 'warn',\n 'tasty/prefer-directional-shorthand': 'warn',\n 'tasty/prefer-longhand-property': 'error',\n};\n\nexport const strict: TSESLint.SharedConfig.RulesRecord = {\n ...recommended,\n 'tasty/valid-custom-property': 'warn',\n 'tasty/no-unknown-state-alias': 'warn',\n 'tasty/no-styles-prop': 'warn',\n 'tasty/no-runtime-styles-mutation': 'warn',\n 'tasty/valid-state-definition': 'warn',\n};\n"],"mappings":";AAEA,MAAa,cAAiD;CAC5D,wBAAwB;CACxB,qBAAqB;CACrB,2BAA2B;CAC3B,2BAA2B;CAC3B,gCAAgC;CAChC,yBAAyB;CACzB,gCAAgC;CAChC,6BAA6B;CAC7B,sBAAsB;CACtB,2BAA2B;CAC3B,oCAAoC;CACpC,4BAA4B;CAC5B,4BAA4B;CAC5B,kCAAkC;CAClC,+BAA+B;CAC/B,sBAAsB;CACtB,sBAAsB;CACtB,0BAA0B;CAC1B,+BAA+B;CAC/B,wBAAwB;CACxB,mCAAmC;CACnC,mCAAmC;CACnC,6BAA6B;CAC7B,oCAAoC;CACpC,gCAAgC;CAChC,0BAA0B;CAC1B,uCAAuC;CACvC,qBAAqB;CACrB,sCAAsC;CACtC,kCAAkC;CACnC;AAED,MAAa,SAA4C;CACvD,GAAG;CACH,+BAA+B;CAC/B,gCAAgC;CAChC,wBAAwB;CACxB,oCAAoC;CACpC,gCAAgC;CACjC"}
1
+ {"version":3,"file":"configs.js","names":[],"sources":["../src/configs.ts"],"sourcesContent":["import type { TSESLint } from '@typescript-eslint/utils';\n\nexport const recommended: TSESLint.SharedConfig.RulesRecord = {\n 'tasty/known-property': 'warn',\n 'tasty/valid-value': 'error',\n 'tasty/valid-color-token': 'error',\n 'tasty/valid-custom-unit': 'error',\n 'tasty/valid-boolean-property': 'error',\n 'tasty/valid-state-key': 'error',\n 'tasty/valid-styles-structure': 'error',\n 'tasty/no-nested-state-map': 'error',\n 'tasty/no-important': 'error',\n 'tasty/valid-sub-element': 'error',\n 'tasty/valid-directional-modifier': 'error',\n 'tasty/valid-radius-shape': 'error',\n 'tasty/no-nested-selector': 'error',\n 'tasty/static-no-dynamic-values': 'error',\n 'tasty/static-valid-selector': 'error',\n 'tasty/valid-preset': 'error',\n 'tasty/valid-recipe': 'error',\n 'tasty/valid-transition': 'warn',\n 'tasty/require-default-state': 'error',\n 'tasty/no-own-at-root': 'warn',\n 'tasty/valid-default-state-order': 'warn',\n 'tasty/prefer-shorthand-property': 'warn',\n 'tasty/no-raw-color-values': 'warn',\n 'tasty/no-raw-transition-duration': 'warn',\n 'tasty/no-runtime-styles-mutation': 'warn',\n 'tasty/no-style-spread': 'warn',\n 'tasty/consistent-token-usage': 'warn',\n 'tasty/prefer-auto-calc': 'warn',\n 'tasty/prefer-state-negation': 'warn',\n 'tasty/prefer-custom-property-syntax': 'warn',\n 'tasty/prefer-hide': 'warn',\n 'tasty/prefer-directional-shorthand': 'warn',\n 'tasty/prefer-longhand-property': 'error',\n};\n\nexport const strict: TSESLint.SharedConfig.RulesRecord = {\n ...recommended,\n 'tasty/valid-custom-property': 'warn',\n 'tasty/no-unknown-state-alias': 'warn',\n 'tasty/no-styles-prop': 'warn',\n 'tasty/valid-state-definition': 'warn',\n};\n"],"mappings":";AAEA,MAAa,cAAiD;CAC5D,wBAAwB;CACxB,qBAAqB;CACrB,2BAA2B;CAC3B,2BAA2B;CAC3B,gCAAgC;CAChC,yBAAyB;CACzB,gCAAgC;CAChC,6BAA6B;CAC7B,sBAAsB;CACtB,2BAA2B;CAC3B,oCAAoC;CACpC,4BAA4B;CAC5B,4BAA4B;CAC5B,kCAAkC;CAClC,+BAA+B;CAC/B,sBAAsB;CACtB,sBAAsB;CACtB,0BAA0B;CAC1B,+BAA+B;CAC/B,wBAAwB;CACxB,mCAAmC;CACnC,mCAAmC;CACnC,6BAA6B;CAC7B,oCAAoC;CACpC,oCAAoC;CACpC,yBAAyB;CACzB,gCAAgC;CAChC,0BAA0B;CAC1B,+BAA+B;CAC/B,uCAAuC;CACvC,qBAAqB;CACrB,sCAAsC;CACtC,kCAAkC;CACnC;AAED,MAAa,SAA4C;CACvD,GAAG;CACH,+BAA+B;CAC/B,gCAAgC;CAChC,wBAAwB;CACxB,gCAAgC;CACjC"}
package/dist/index.js CHANGED
@@ -22,13 +22,16 @@ import require_default_state_default from "./rules/require-default-state.js";
22
22
  import no_unknown_state_alias_default from "./rules/no-unknown-state-alias.js";
23
23
  import no_raw_color_values_default from "./rules/no-raw-color-values.js";
24
24
  import no_raw_transition_duration_default from "./rules/no-raw-transition-duration.js";
25
+ import no_raw_motion_duration_default from "./rules/no-raw-motion-duration.js";
25
26
  import no_styles_prop_default from "./rules/no-styles-prop.js";
26
27
  import consistent_token_usage_default from "./rules/consistent-token-usage.js";
27
28
  import no_runtime_styles_mutation_default from "./rules/no-runtime-styles-mutation.js";
29
+ import no_style_spread_default from "./rules/no-style-spread.js";
28
30
  import valid_state_definition_default from "./rules/valid-state-definition.js";
29
31
  import no_own_at_root_default from "./rules/no-own-at-root.js";
30
32
  import valid_default_state_order_default from "./rules/valid-default-state-order.js";
31
33
  import prefer_auto_calc_default from "./rules/prefer-auto-calc.js";
34
+ import prefer_state_negation_default from "./rules/prefer-state-negation.js";
32
35
  import prefer_custom_property_syntax_default from "./rules/prefer-custom-property-syntax.js";
33
36
  import prefer_hide_default from "./rules/prefer-hide.js";
34
37
  import prefer_directional_shorthand_default from "./rules/prefer-directional-shorthand.js";
@@ -62,13 +65,16 @@ const ruleMap = {
62
65
  "no-unknown-state-alias": no_unknown_state_alias_default,
63
66
  "no-raw-color-values": no_raw_color_values_default,
64
67
  "no-raw-transition-duration": no_raw_transition_duration_default,
68
+ "no-raw-motion-duration": no_raw_motion_duration_default,
65
69
  "no-styles-prop": no_styles_prop_default,
66
70
  "consistent-token-usage": consistent_token_usage_default,
67
71
  "no-runtime-styles-mutation": no_runtime_styles_mutation_default,
72
+ "no-style-spread": no_style_spread_default,
68
73
  "valid-state-definition": valid_state_definition_default,
69
74
  "no-own-at-root": no_own_at_root_default,
70
75
  "valid-default-state-order": valid_default_state_order_default,
71
76
  "prefer-auto-calc": prefer_auto_calc_default,
77
+ "prefer-state-negation": prefer_state_negation_default,
72
78
  "prefer-custom-property-syntax": prefer_custom_property_syntax_default,
73
79
  "prefer-hide": prefer_hide_default,
74
80
  "prefer-directional-shorthand": prefer_directional_shorthand_default,
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.noRawTransitionDuration","rules.noStylesProp","rules.consistentTokenUsage","rules.noRuntimeStylesMutation","rules.validStateDefinition","rules.noOwnAtRoot","rules.validDefaultStateOrder","rules.preferAutoCalc","rules.preferCustomPropertySyntax","rules.preferHide","rules.preferDirectionalShorthand","rules.preferLonghandProperty"],"sources":["../src/index.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport 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-raw-transition-duration': rules.noRawTransitionDuration,\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 'prefer-longhand-property': rules.preferLonghandProperty,\n};\n\n// Read from package.json so `meta.version` cannot drift from the released\n// version, as the previous hardcoded literal had. `../package.json` resolves to\n// the package root from both `src/` and the unbundled `dist/`.\nconst { version } = createRequire(import.meta.url)('../package.json') as {\n version: string;\n};\n\nconst plugin = {\n meta: {\n name: '@tenphi/eslint-plugin-tasty',\n version,\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 {\n TastyValidationConfig,\n ResolvedConfig,\n StyleFunctionConfig,\n} from './types.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAKA,MAAM,UAAkE;CACtE,kBAAkBA;CAClB,eAAeC;CACf,qBAAqBC;CACrB,qBAAqBC;CACrB,mBAAmBC;CACnB,0BAA0BC;CAC1B,sBAAsBC;CACtB,yBAAyBC;CACzB,gBAAgBC;CAChB,gBAAgBC;CAChB,0BAA0BC;CAC1B,8BAA8BC;CAC9B,sBAAsBC;CACtB,gBAAgBC;CAChB,qBAAqBC;CACrB,uBAAuBC;CACvB,4BAA4BC;CAC5B,yBAAyBC;CACzB,6BAA6BC;CAC7B,oBAAoBC;CACpB,yBAAyBC;CACzB,0BAA0BC;CAC1B,uBAAuBC;CACvB,8BAA8BC;CAC9B,kBAAkBC;CAClB,0BAA0BC;CAC1B,8BAA8BC;CAC9B,0BAA0BC;CAC1B,kBAAkBC;CAClB,6BAA6BC;CAC7B,oBAAoBC;CACpB,iCAAiCC;CACjC,eAAeC;CACf,gCAAgCC;CAChC,4BAA4BC;CAC7B;AAKD,MAAM,EAAE,YAAY,cAAc,OAAO,KAAK,IAAI,CAAC,kBAAkB;AAIrE,MAAM,SAAS;CACb,MAAM;EACJ,MAAM;EACN;EACD;CACD,OAAO;CACP,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.noRawTransitionDuration","rules.noRawMotionDuration","rules.noStylesProp","rules.consistentTokenUsage","rules.noRuntimeStylesMutation","rules.noStyleSpread","rules.validStateDefinition","rules.noOwnAtRoot","rules.validDefaultStateOrder","rules.preferAutoCalc","rules.preferStateNegation","rules.preferCustomPropertySyntax","rules.preferHide","rules.preferDirectionalShorthand","rules.preferLonghandProperty"],"sources":["../src/index.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport 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-raw-transition-duration': rules.noRawTransitionDuration,\n 'no-raw-motion-duration': rules.noRawMotionDuration,\n 'no-styles-prop': rules.noStylesProp,\n 'consistent-token-usage': rules.consistentTokenUsage,\n 'no-runtime-styles-mutation': rules.noRuntimeStylesMutation,\n 'no-style-spread': rules.noStyleSpread,\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-state-negation': rules.preferStateNegation,\n 'prefer-custom-property-syntax': rules.preferCustomPropertySyntax,\n 'prefer-hide': rules.preferHide,\n 'prefer-directional-shorthand': rules.preferDirectionalShorthand,\n 'prefer-longhand-property': rules.preferLonghandProperty,\n};\n\n// Read from package.json so `meta.version` cannot drift from the released\n// version, as the previous hardcoded literal had. `../package.json` resolves to\n// the package root from both `src/` and the unbundled `dist/`.\nconst { version } = createRequire(import.meta.url)('../package.json') as {\n version: string;\n};\n\nconst plugin = {\n meta: {\n name: '@tenphi/eslint-plugin-tasty',\n version,\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 {\n TastyValidationConfig,\n ResolvedConfig,\n StyleFunctionConfig,\n} from './types.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAKA,MAAM,UAAkE;CACtE,kBAAkBA;CAClB,eAAeC;CACf,qBAAqBC;CACrB,qBAAqBC;CACrB,mBAAmBC;CACnB,0BAA0BC;CAC1B,sBAAsBC;CACtB,yBAAyBC;CACzB,gBAAgBC;CAChB,gBAAgBC;CAChB,0BAA0BC;CAC1B,8BAA8BC;CAC9B,sBAAsBC;CACtB,gBAAgBC;CAChB,qBAAqBC;CACrB,uBAAuBC;CACvB,4BAA4BC;CAC5B,yBAAyBC;CACzB,6BAA6BC;CAC7B,oBAAoBC;CACpB,yBAAyBC;CACzB,0BAA0BC;CAC1B,uBAAuBC;CACvB,8BAA8BC;CAC9B,0BAA0BC;CAC1B,kBAAkBC;CAClB,0BAA0BC;CAC1B,8BAA8BC;CAC9B,mBAAmBC;CACnB,0BAA0BC;CAC1B,kBAAkBC;CAClB,6BAA6BC;CAC7B,oBAAoBC;CACpB,yBAAyBC;CACzB,iCAAiCC;CACjC,eAAeC;CACf,gCAAgCC;CAChC,4BAA4BC;CAC7B;AAKD,MAAM,EAAE,YAAY,cAAc,OAAO,KAAK,IAAI,CAAC,kBAAkB;AAIrE,MAAM,SAAS;CACb,MAAM;EACJ,MAAM;EACN;EACD;CACD,OAAO;CACP,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,4 +1,5 @@
1
1
  import { KNOWN_PSEUDO_CLASSES } from "../constants.js";
2
+ import { checkBracketBalance } from "./utils.js";
2
3
 
3
4
  //#region src/parsers/state-key-parser.ts
4
5
  /**
@@ -461,6 +462,29 @@ function findTopLevelComma(str) {
461
462
  return -1;
462
463
  }
463
464
  /**
465
+ * Rewrite :not() state atoms, leaving raw CSS inside other functions alone.
466
+ * A selector list/compound must stay inside :is(): negating individual parts
467
+ * would change which elements match. Only a single pseudo can be unwrapped.
468
+ */
469
+ function getStateNegationEdits(stateKey) {
470
+ if (!stateKey.includes(":not(")) return [];
471
+ if (checkBracketBalance(stateKey)) return [];
472
+ if (parseStateKey(stateKey).errors.length > 0) return [];
473
+ const { tokens } = tokenize(stateKey);
474
+ return tokens.filter((token) => token.type === "STATE" && token.value.startsWith(":not(")).flatMap((token) => {
475
+ if (checkBracketBalance(token.value)) return [];
476
+ const inner = token.value.slice(5, -1).trim();
477
+ if (!inner) return [];
478
+ const { tokens: innerTokens, errors } = tokenize(inner);
479
+ const singlePseudo = errors.length === 0 && innerTokens.length === 1 && innerTokens[0].type === "STATE" && innerTokens[0].value === inner && inner.startsWith(":") && parseStateKey(inner).errors.length === 0 && !inner.startsWith(":not(") && !inner.startsWith(":is(") && !inner.startsWith(":where(") && !(inner.startsWith(":has(") && /[>+~]\s*\)$/.test(inner));
480
+ return [{
481
+ start: token.offset,
482
+ end: token.offset + token.length,
483
+ replacement: singlePseudo ? `!${inner}` : `!:is(${inner})`
484
+ }];
485
+ });
486
+ }
487
+ /**
464
488
  * Validate a state key string and return detailed errors.
465
489
  */
466
490
  function parseStateKey(stateKey, opts = {}) {
@@ -507,5 +531,5 @@ function validateStateDefinition(value, opts = {}) {
507
531
  }
508
532
 
509
533
  //#endregion
510
- export { parseStateKey, validateStateDefinition };
534
+ export { getStateNegationEdits, parseStateKey, validateStateDefinition };
511
535
  //# sourceMappingURL=state-key-parser.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"state-key-parser.js","names":[],"sources":["../../src/parsers/state-key-parser.ts"],"sourcesContent":["import { KNOWN_PSEUDO_CLASSES } from '../constants.js';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface StateKeyError {\n message: string;\n offset: number;\n length: number;\n}\n\nexport interface StateKeyResult {\n errors: StateKeyError[];\n hasOwn: boolean;\n referencedAliases: string[];\n}\n\nexport interface StateKeyParserOptions {\n knownAliases?: string[];\n}\n\n// ============================================================================\n// Tokenizer\n// ============================================================================\n\ntype TokenType = 'AND' | 'OR' | 'NOT' | 'XOR' | 'LPAREN' | 'RPAREN' | 'STATE';\n\ninterface Token {\n type: TokenType;\n value: string;\n offset: number;\n length: number;\n}\n\n/**\n * Pattern for tokenizing state notation.\n * Matches operators, parentheses, @-prefixed states, value mods, boolean mods,\n * pseudo-classes with functions (including :is/:has/:not/:where with nesting),\n * class selectors, and attribute selectors.\n */\nconst STATE_TOKEN_PATTERN =\n /([&|!^])|([()])|(@media:[a-z]+)|(@media\\([^)]*\\))|(@supports\\([^()]*(?:\\([^)]*\\))?[^)]*\\))|(@root\\([^()]*(?:\\([^()]*(?:\\([^)]*\\))?[^)]*\\))*[^)]*\\))|(@parent\\([^()]*(?:\\([^()]*(?:\\([^)]*\\))?[^)]*\\))*[^)]*\\))|(@own\\([^()]*(?:\\([^()]*(?:\\([^)]*\\))?[^)]*\\))*[^)]*\\))|(@\\([^()]*(?:\\([^)]*\\))?[^)]*\\))|(@starting)|(@[A-Za-z][A-Za-z0-9-]*)|([a-z][a-z0-9-]*(?:\\^=|\\$=|\\*=|=)(?:\"[^\"]*\"|'[^']*'|[^\\s&|!^()]+))|([a-z][a-z0-9-]+)|(:(?:is|has|not|where)\\([^()]*(?:\\([^()]*(?:\\([^)]*\\))?[^)]*\\))*[^)]*\\))|(:[-a-z][a-z0-9-]*(?:\\([^)]+\\))?)|(\\.[a-z][a-z0-9-]+)|(\\[[^\\]]+\\])/gi;\n\nfunction tokenize(stateKey: string): {\n tokens: Token[];\n errors: StateKeyError[];\n} {\n const tokens: Token[] = [];\n const errors: StateKeyError[] = [];\n\n // Replace commas with | outside of parentheses\n const normalized = replaceCommasOutsideParens(stateKey);\n\n const covered = new Set<number>();\n\n STATE_TOKEN_PATTERN.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = STATE_TOKEN_PATTERN.exec(normalized)) !== null) {\n const fullMatch = match[0];\n const offset = match.index;\n\n for (let i = offset; i < offset + fullMatch.length; i++) {\n covered.add(i);\n }\n\n if (match[1]) {\n switch (fullMatch) {\n case '&':\n tokens.push({ type: 'AND', value: '&', offset, length: 1 });\n break;\n case '|':\n tokens.push({ type: 'OR', value: '|', offset, length: 1 });\n break;\n case '!':\n tokens.push({ type: 'NOT', value: '!', offset, length: 1 });\n break;\n case '^':\n tokens.push({ type: 'XOR', value: '^', offset, length: 1 });\n break;\n }\n } else if (match[2]) {\n if (fullMatch === '(') {\n tokens.push({ type: 'LPAREN', value: '(', offset, length: 1 });\n } else {\n tokens.push({ type: 'RPAREN', value: ')', offset, length: 1 });\n }\n } else {\n tokens.push({\n type: 'STATE',\n value: fullMatch,\n offset,\n length: fullMatch.length,\n });\n }\n }\n\n // Check for uncovered characters (unrecognized tokens)\n const uncovered: { ch: string; pos: number }[] = [];\n for (let i = 0; i < stateKey.length; i++) {\n const ch = stateKey[i];\n if (ch === ' ' || ch === '\\t' || ch === ',') continue;\n if (!covered.has(i)) {\n uncovered.push({ ch, pos: i });\n }\n }\n\n if (uncovered.length > 0) {\n const chars = [...new Set(uncovered.map((u) => u.ch))].join('');\n errors.push({\n message: `Unrecognized characters '${chars}' in state key '${stateKey}'.`,\n offset: uncovered[0].pos,\n length: 1,\n });\n }\n\n return { tokens, errors };\n}\n\nfunction replaceCommasOutsideParens(str: string): string {\n let result = '';\n let depth = 0;\n\n for (const char of str) {\n if (char === '(') {\n depth++;\n result += char;\n } else if (char === ')') {\n depth--;\n result += char;\n } else if (char === ',' && depth === 0) {\n result += '|';\n } else {\n result += char;\n }\n }\n\n return result;\n}\n\n// ============================================================================\n// Validator\n// ============================================================================\n\nconst MAX_XOR_CHAIN_LENGTH = 4;\n\nconst DIMENSION_SHORTHANDS = new Set(['w', 'h', 'is', 'bs']);\nconst DIMENSION_FULL = new Set([\n 'width',\n 'height',\n 'inline-size',\n 'block-size',\n]);\n\nclass StateKeyValidator {\n private errors: StateKeyError[] = [];\n private hasOwn = false;\n private referencedAliases: string[] = [];\n private tokens: Token[];\n private pos = 0;\n private opts: StateKeyParserOptions;\n private insideOwn = false;\n\n constructor(\n tokens: Token[],\n tokenErrors: StateKeyError[],\n opts: StateKeyParserOptions,\n ) {\n this.tokens = tokens;\n this.errors = [...tokenErrors];\n this.opts = opts;\n }\n\n validate(): StateKeyResult {\n if (this.tokens.length > 0) {\n this.parseExpression();\n }\n\n return {\n errors: this.errors,\n hasOwn: this.hasOwn,\n referencedAliases: this.referencedAliases,\n };\n }\n\n private current(): Token | undefined {\n return this.tokens[this.pos];\n }\n\n private advance(): Token | undefined {\n return this.tokens[this.pos++];\n }\n\n private match(type: TokenType): boolean {\n if (this.current()?.type === type) {\n this.advance();\n return true;\n }\n return false;\n }\n\n private parseExpression(): void {\n this.parseAnd();\n }\n\n private parseAnd(): void {\n this.parseOr();\n while (this.current()?.type === 'AND') {\n this.advance();\n if (!this.current() || this.current()?.type === 'AND') {\n const prev = this.tokens[this.pos - 1];\n this.errors.push({\n message: \"Expected expression after '&' operator.\",\n offset: prev.offset,\n length: prev.length,\n });\n return;\n }\n this.parseOr();\n }\n }\n\n private parseOr(): void {\n this.parseXor();\n while (this.current()?.type === 'OR') {\n this.advance();\n if (!this.current() || this.current()?.type === 'OR') {\n const prev = this.tokens[this.pos - 1];\n this.errors.push({\n message: \"Expected expression after '|' operator.\",\n offset: prev.offset,\n length: prev.length,\n });\n return;\n }\n this.parseXor();\n }\n }\n\n private parseXor(): void {\n this.parseUnary();\n let operandCount = 1;\n\n while (this.current()?.type === 'XOR') {\n this.advance();\n operandCount++;\n if (operandCount > MAX_XOR_CHAIN_LENGTH) {\n const prev = this.tokens[this.pos - 1];\n this.errors.push({\n message: `XOR chain with ${operandCount} operands produces ${Math.pow(2, operandCount - 1)} OR branches. Consider breaking into smaller expressions.`,\n offset: prev.offset,\n length: prev.length,\n });\n }\n this.parseUnary();\n }\n }\n\n private parseUnary(): void {\n if (this.match('NOT')) {\n if (\n !this.current() ||\n this.current()?.type === 'AND' ||\n this.current()?.type === 'OR' ||\n this.current()?.type === 'XOR'\n ) {\n const prev = this.tokens[this.pos - 1];\n this.errors.push({\n message: \"Expected expression after '!' operator.\",\n offset: prev.offset,\n length: prev.length,\n });\n return;\n }\n this.parseUnary();\n return;\n }\n this.parsePrimary();\n }\n\n private parsePrimary(): void {\n if (this.match('LPAREN')) {\n this.parseExpression();\n if (!this.match('RPAREN')) {\n this.errors.push({\n message: \"Missing closing ')' in state expression.\",\n offset: this.tokens[this.pos - 1]?.offset ?? 0,\n length: 1,\n });\n }\n return;\n }\n\n const token = this.current();\n if (token?.type === 'STATE') {\n this.advance();\n this.validateStateToken(token);\n return;\n }\n\n // Unexpected token or end\n if (token) {\n this.errors.push({\n message: `Unexpected token '${token.value}'.`,\n offset: token.offset,\n length: token.length,\n });\n this.advance();\n }\n }\n\n private validateStateToken(token: Token): void {\n const value = token.value;\n\n // @starting\n if (value === '@starting') return;\n\n // @media:type\n if (value.startsWith('@media:')) {\n const mediaType = value.slice(7);\n const validTypes = new Set(['print', 'screen', 'all', 'speech']);\n if (!validTypes.has(mediaType)) {\n this.errors.push({\n message: `Unknown media type '${mediaType}'. Valid: print, screen, all, speech.`,\n offset: token.offset,\n length: token.length,\n });\n }\n return;\n }\n\n // @media(...)\n if (value.startsWith('@media(')) {\n this.validateMediaQuery(value, token);\n return;\n }\n\n // @supports(...)\n if (value.startsWith('@supports(')) {\n this.validateSupportsQuery(value, token);\n return;\n }\n\n // @root(...)\n if (value.startsWith('@root(')) {\n this.validateInnerStateExpression(value, 6, token);\n return;\n }\n\n // @parent(...)\n if (value.startsWith('@parent(')) {\n this.validateParentState(value, token);\n return;\n }\n\n // @own(...)\n if (value.startsWith('@own(')) {\n this.hasOwn = true;\n if (this.insideOwn) {\n this.errors.push({\n message: 'Nested @own() is not allowed.',\n offset: token.offset,\n length: token.length,\n });\n return;\n }\n const prevInsideOwn = this.insideOwn;\n this.insideOwn = true;\n this.validateInnerStateExpression(value, 5, token);\n this.insideOwn = prevInsideOwn;\n return;\n }\n\n // @(...) container query\n if (value.startsWith('@(')) {\n this.validateContainerQuery(value, token);\n return;\n }\n\n // @alias predefined state\n if (value.startsWith('@') && /^@[A-Za-z][A-Za-z0-9-]*$/.test(value)) {\n this.referencedAliases.push(value);\n return;\n }\n\n // Pseudo-class/pseudo-element\n if (value.startsWith(':')) {\n this.validatePseudoClass(value, token);\n return;\n }\n\n // Class selector\n if (value.startsWith('.')) return;\n\n // Attribute selector\n if (value.startsWith('[')) {\n this.validateAttributeSelector(value, token);\n return;\n }\n\n // Value modifier (e.g., theme=danger)\n if (value.includes('=')) {\n this.validateValueModifier(value, token);\n return;\n }\n\n // Boolean modifier (e.g., hovered, disabled)\n if (/^[a-z][a-z0-9-]+$/i.test(value)) return;\n\n this.errors.push({\n message: `Unrecognized state token '${value}'.`,\n offset: token.offset,\n length: token.length,\n });\n }\n\n private validateMediaQuery(raw: string, token: Token): void {\n const content = raw.slice(7, -1);\n if (!content.trim()) {\n this.errors.push({\n message: 'Empty @media() query.',\n offset: token.offset,\n length: token.length,\n });\n return;\n }\n\n // Expand dimension shorthands for validation\n const expanded = expandDimensionShorthands(content.trim());\n\n // Feature query (contains ':' but no comparison operators)\n if (\n expanded.includes(':') &&\n !expanded.includes('<') &&\n !expanded.includes('>') &&\n !expanded.includes('=')\n ) {\n return;\n }\n\n // Boolean feature query (no comparison operators)\n if (\n !expanded.includes('<') &&\n !expanded.includes('>') &&\n !expanded.includes('=')\n ) {\n return;\n }\n\n // Dimension query — validate dimension names\n this.validateDimensionCondition(expanded, token);\n }\n\n private validateDimensionCondition(condition: string, token: Token): void {\n // Range syntax: 600px <= width < 1200px\n const rangeMatch = condition.match(\n /^(.+?)\\s*(<=|<)\\s*(\\S+)\\s*(<=|<)\\s*(.+)$/,\n );\n if (rangeMatch) {\n const dim = rangeMatch[3];\n if (!DIMENSION_FULL.has(dim) && !DIMENSION_SHORTHANDS.has(dim)) {\n this.errors.push({\n message: `Unknown dimension '${dim}' in media/container query. Valid: width, height, inline-size, block-size (or w, h, is, bs).`,\n offset: token.offset,\n length: token.length,\n });\n }\n return;\n }\n\n // Simple comparison: width < 768px\n const simpleMatch = condition.match(/^(\\S+)\\s*(<=|>=|<|>|=)\\s*(.+)$/);\n if (simpleMatch) {\n const dim = simpleMatch[1];\n if (DIMENSION_FULL.has(dim) || DIMENSION_SHORTHANDS.has(dim)) {\n return;\n }\n }\n\n // Reversed: 768px > width\n const reversedMatch = condition.match(/^(.+?)\\s*(<=|>=|<|>|=)\\s*(\\S+)$/);\n if (reversedMatch) {\n const dim = reversedMatch[3];\n if (DIMENSION_FULL.has(dim) || DIMENSION_SHORTHANDS.has(dim)) {\n return;\n }\n }\n }\n\n private validateSupportsQuery(raw: string, token: Token): void {\n const content = raw.slice(10, -1);\n if (!content.trim()) {\n this.errors.push({\n message: 'Empty @supports() query.',\n offset: token.offset,\n length: token.length,\n });\n }\n }\n\n private validateInnerStateExpression(\n raw: string,\n prefixLen: number,\n _token: Token,\n ): void {\n const content = raw.slice(prefixLen, -1);\n if (!content.trim()) return;\n\n const innerResult = parseStateKey(content, this.opts);\n this.errors.push(...innerResult.errors);\n if (innerResult.hasOwn) this.hasOwn = true;\n this.referencedAliases.push(...innerResult.referencedAliases);\n }\n\n private validateParentState(raw: string, token: Token): void {\n const content = raw.slice(8, -1);\n if (!content.trim()) {\n this.errors.push({\n message: 'Empty @parent() state.',\n offset: token.offset,\n length: token.length,\n });\n return;\n }\n\n let condition = content.trim();\n\n // Check for direct parent combinator: @parent(hovered, >)\n const lastCommaIdx = condition.lastIndexOf(',');\n if (lastCommaIdx !== -1) {\n const afterComma = condition.slice(lastCommaIdx + 1).trim();\n if (afterComma === '>') {\n condition = condition.slice(0, lastCommaIdx).trim();\n }\n }\n\n const innerResult = parseStateKey(condition, this.opts);\n this.errors.push(...innerResult.errors);\n this.referencedAliases.push(...innerResult.referencedAliases);\n }\n\n private validateContainerQuery(raw: string, token: Token): void {\n const content = raw.slice(2, -1);\n if (!content.trim()) {\n this.errors.push({\n message: 'Empty container query.',\n offset: token.offset,\n length: token.length,\n });\n return;\n }\n\n // Named container: @(layout, w < 600px)\n const commaIdx = findTopLevelComma(content);\n let condition: string;\n\n if (commaIdx !== -1) {\n condition = content.slice(commaIdx + 1).trim();\n } else {\n condition = content.trim();\n }\n\n // Style query: @($variant=primary) — skip\n if (condition.startsWith('$')) return;\n\n // Function-like: scroll-state(...) — skip\n if (/^[a-zA-Z][\\w-]*\\s*\\(/.test(condition)) return;\n\n // Dimension query\n const expanded = expandDimensionShorthands(condition);\n if (\n expanded.includes('<') ||\n expanded.includes('>') ||\n expanded.includes('=')\n ) {\n this.validateDimensionCondition(expanded, token);\n }\n }\n\n private validatePseudoClass(value: string, token: Token): void {\n // :is(), :has(), :not(), :where() — structural pseudo-classes\n const enhancedMatch = /^:(is|has|not|where)\\(/.exec(value);\n if (enhancedMatch) return;\n\n // Functional pseudo-classes like :nth-child(2n+1)\n const funcMatch = /^(:[a-z-]+)\\(/.exec(value);\n if (funcMatch) {\n const baseName = funcMatch[1];\n if (!KNOWN_PSEUDO_CLASSES.has(baseName)) {\n this.errors.push({\n message: `Unknown pseudo-class '${baseName}'.`,\n offset: token.offset,\n length: token.length,\n });\n }\n return;\n }\n\n // Simple pseudo-class: :hover, :focus, etc.\n if (!KNOWN_PSEUDO_CLASSES.has(value)) {\n this.errors.push({\n message: `Unknown pseudo-class '${value}'.`,\n offset: token.offset,\n length: token.length,\n });\n }\n }\n\n private validateAttributeSelector(value: string, token: Token): void {\n if (!value.startsWith('[') || !value.endsWith(']')) {\n this.errors.push({\n message: `Malformed attribute selector '${value}'.`,\n offset: token.offset,\n length: token.length,\n });\n }\n }\n\n private validateValueModifier(value: string, token: Token): void {\n const opMatch = value.match(/^([a-z][a-z0-9-]*)(\\^=|\\$=|\\*=|=)(.+)$/i);\n if (!opMatch) {\n this.errors.push({\n message: `Invalid value modifier syntax '${value}'.`,\n offset: token.offset,\n length: token.length,\n });\n }\n }\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction expandDimensionShorthands(condition: string): string {\n return condition\n .replace(/\\bw\\b/g, 'width')\n .replace(/\\bh\\b/g, 'height')\n .replace(/\\bis\\b/g, 'inline-size')\n .replace(/\\bbs\\b/g, 'block-size');\n}\n\nfunction findTopLevelComma(str: string): number {\n let depth = 0;\n for (let i = 0; i < str.length; i++) {\n const ch = str[i];\n if (ch === '(') depth++;\n else if (ch === ')') depth = Math.max(0, depth - 1);\n else if (ch === ',' && depth === 0) return i;\n }\n return -1;\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Validate a state key string and return detailed errors.\n */\nexport function parseStateKey(\n stateKey: string,\n opts: StateKeyParserOptions = {},\n): StateKeyResult {\n if (!stateKey || !stateKey.trim()) {\n return { errors: [], hasOwn: false, referencedAliases: [] };\n }\n\n const trimmed = stateKey.trim();\n\n // The standalone `_` key is the map-wide fallback floor. It always applies\n // and must be used on its own — it carries no state logic to validate.\n if (trimmed === '_') {\n return { errors: [], hasOwn: false, referencedAliases: [] };\n }\n\n // A `_` combined with state logic (`_ & hovered`, `_ | x`) is a misuse: the\n // fallback floor can only be used on its own. Tasty ignores such keys at\n // runtime and warns; flag them here so the mistake surfaces in the editor.\n if (isMisusedFallbackKey(trimmed)) {\n return {\n errors: [\n {\n message:\n 'The fallback floor \"_\" cannot be combined with other state ' +\n 'logic. Use \"_\" on its own as a map-wide fallback floor.',\n offset: 0,\n length: stateKey.length,\n },\n ],\n hasOwn: false,\n referencedAliases: [],\n };\n }\n\n const { tokens, errors: tokenErrors } = tokenize(trimmed);\n const validator = new StateKeyValidator(tokens, tokenErrors, opts);\n return validator.validate();\n}\n\n/**\n * Detect a misused standalone `_` fallback key: a key that is not exactly\n * `_` but contains `_` as a standalone atom among state-logic operators\n * (`&`, `|`, `^`, `,`) or grouping parens. A `_` embedded in a longer token\n * (e.g. a modifier named `_private`) is not a misuse.\n */\nfunction isMisusedFallbackKey(stateKey: string): boolean {\n if (stateKey === '_' || !stateKey.includes('_')) return false;\n return /(?:^|[\\s&|^,(])_(?:[\\s&|^,)]|$)/.test(stateKey);\n}\n\n/**\n * Validate a state definition value (the RHS of a state alias).\n * State definitions should be valid state expressions like\n * '@media(w < 768px)', '@root(theme=dark)', etc.\n */\nexport function validateStateDefinition(\n value: string,\n opts: StateKeyParserOptions = {},\n): StateKeyResult {\n return parseStateKey(value, opts);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"state-key-parser.js","names":[],"sources":["../../src/parsers/state-key-parser.ts"],"sourcesContent":["import { KNOWN_PSEUDO_CLASSES } from '../constants.js';\nimport type { StringEdit } from '../fix-utils.js';\nimport { checkBracketBalance } from './utils.js';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface StateKeyError {\n message: string;\n offset: number;\n length: number;\n}\n\nexport interface StateKeyResult {\n errors: StateKeyError[];\n hasOwn: boolean;\n referencedAliases: string[];\n}\n\nexport interface StateKeyParserOptions {\n knownAliases?: string[];\n}\n\n// ============================================================================\n// Tokenizer\n// ============================================================================\n\ntype TokenType = 'AND' | 'OR' | 'NOT' | 'XOR' | 'LPAREN' | 'RPAREN' | 'STATE';\n\ninterface Token {\n type: TokenType;\n value: string;\n offset: number;\n length: number;\n}\n\n/**\n * Pattern for tokenizing state notation.\n * Matches operators, parentheses, @-prefixed states, value mods, boolean mods,\n * pseudo-classes with functions (including :is/:has/:not/:where with nesting),\n * class selectors, and attribute selectors.\n */\nconst STATE_TOKEN_PATTERN =\n /([&|!^])|([()])|(@media:[a-z]+)|(@media\\([^)]*\\))|(@supports\\([^()]*(?:\\([^)]*\\))?[^)]*\\))|(@root\\([^()]*(?:\\([^()]*(?:\\([^)]*\\))?[^)]*\\))*[^)]*\\))|(@parent\\([^()]*(?:\\([^()]*(?:\\([^)]*\\))?[^)]*\\))*[^)]*\\))|(@own\\([^()]*(?:\\([^()]*(?:\\([^)]*\\))?[^)]*\\))*[^)]*\\))|(@\\([^()]*(?:\\([^)]*\\))?[^)]*\\))|(@starting)|(@[A-Za-z][A-Za-z0-9-]*)|([a-z][a-z0-9-]*(?:\\^=|\\$=|\\*=|=)(?:\"[^\"]*\"|'[^']*'|[^\\s&|!^()]+))|([a-z][a-z0-9-]+)|(:(?:is|has|not|where)\\([^()]*(?:\\([^()]*(?:\\([^)]*\\))?[^)]*\\))*[^)]*\\))|(:[-a-z][a-z0-9-]*(?:\\([^)]+\\))?)|(\\.[a-z][a-z0-9-]+)|(\\[[^\\]]+\\])/gi;\n\nfunction tokenize(stateKey: string): {\n tokens: Token[];\n errors: StateKeyError[];\n} {\n const tokens: Token[] = [];\n const errors: StateKeyError[] = [];\n\n // Replace commas with | outside of parentheses\n const normalized = replaceCommasOutsideParens(stateKey);\n\n const covered = new Set<number>();\n\n STATE_TOKEN_PATTERN.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = STATE_TOKEN_PATTERN.exec(normalized)) !== null) {\n const fullMatch = match[0];\n const offset = match.index;\n\n for (let i = offset; i < offset + fullMatch.length; i++) {\n covered.add(i);\n }\n\n if (match[1]) {\n switch (fullMatch) {\n case '&':\n tokens.push({ type: 'AND', value: '&', offset, length: 1 });\n break;\n case '|':\n tokens.push({ type: 'OR', value: '|', offset, length: 1 });\n break;\n case '!':\n tokens.push({ type: 'NOT', value: '!', offset, length: 1 });\n break;\n case '^':\n tokens.push({ type: 'XOR', value: '^', offset, length: 1 });\n break;\n }\n } else if (match[2]) {\n if (fullMatch === '(') {\n tokens.push({ type: 'LPAREN', value: '(', offset, length: 1 });\n } else {\n tokens.push({ type: 'RPAREN', value: ')', offset, length: 1 });\n }\n } else {\n tokens.push({\n type: 'STATE',\n value: fullMatch,\n offset,\n length: fullMatch.length,\n });\n }\n }\n\n // Check for uncovered characters (unrecognized tokens)\n const uncovered: { ch: string; pos: number }[] = [];\n for (let i = 0; i < stateKey.length; i++) {\n const ch = stateKey[i];\n if (ch === ' ' || ch === '\\t' || ch === ',') continue;\n if (!covered.has(i)) {\n uncovered.push({ ch, pos: i });\n }\n }\n\n if (uncovered.length > 0) {\n const chars = [...new Set(uncovered.map((u) => u.ch))].join('');\n errors.push({\n message: `Unrecognized characters '${chars}' in state key '${stateKey}'.`,\n offset: uncovered[0].pos,\n length: 1,\n });\n }\n\n return { tokens, errors };\n}\n\nfunction replaceCommasOutsideParens(str: string): string {\n let result = '';\n let depth = 0;\n\n for (const char of str) {\n if (char === '(') {\n depth++;\n result += char;\n } else if (char === ')') {\n depth--;\n result += char;\n } else if (char === ',' && depth === 0) {\n result += '|';\n } else {\n result += char;\n }\n }\n\n return result;\n}\n\n// ============================================================================\n// Validator\n// ============================================================================\n\nconst MAX_XOR_CHAIN_LENGTH = 4;\n\nconst DIMENSION_SHORTHANDS = new Set(['w', 'h', 'is', 'bs']);\nconst DIMENSION_FULL = new Set([\n 'width',\n 'height',\n 'inline-size',\n 'block-size',\n]);\n\nclass StateKeyValidator {\n private errors: StateKeyError[] = [];\n private hasOwn = false;\n private referencedAliases: string[] = [];\n private tokens: Token[];\n private pos = 0;\n private opts: StateKeyParserOptions;\n private insideOwn = false;\n\n constructor(\n tokens: Token[],\n tokenErrors: StateKeyError[],\n opts: StateKeyParserOptions,\n ) {\n this.tokens = tokens;\n this.errors = [...tokenErrors];\n this.opts = opts;\n }\n\n validate(): StateKeyResult {\n if (this.tokens.length > 0) {\n this.parseExpression();\n }\n\n return {\n errors: this.errors,\n hasOwn: this.hasOwn,\n referencedAliases: this.referencedAliases,\n };\n }\n\n private current(): Token | undefined {\n return this.tokens[this.pos];\n }\n\n private advance(): Token | undefined {\n return this.tokens[this.pos++];\n }\n\n private match(type: TokenType): boolean {\n if (this.current()?.type === type) {\n this.advance();\n return true;\n }\n return false;\n }\n\n private parseExpression(): void {\n this.parseAnd();\n }\n\n private parseAnd(): void {\n this.parseOr();\n while (this.current()?.type === 'AND') {\n this.advance();\n if (!this.current() || this.current()?.type === 'AND') {\n const prev = this.tokens[this.pos - 1];\n this.errors.push({\n message: \"Expected expression after '&' operator.\",\n offset: prev.offset,\n length: prev.length,\n });\n return;\n }\n this.parseOr();\n }\n }\n\n private parseOr(): void {\n this.parseXor();\n while (this.current()?.type === 'OR') {\n this.advance();\n if (!this.current() || this.current()?.type === 'OR') {\n const prev = this.tokens[this.pos - 1];\n this.errors.push({\n message: \"Expected expression after '|' operator.\",\n offset: prev.offset,\n length: prev.length,\n });\n return;\n }\n this.parseXor();\n }\n }\n\n private parseXor(): void {\n this.parseUnary();\n let operandCount = 1;\n\n while (this.current()?.type === 'XOR') {\n this.advance();\n operandCount++;\n if (operandCount > MAX_XOR_CHAIN_LENGTH) {\n const prev = this.tokens[this.pos - 1];\n this.errors.push({\n message: `XOR chain with ${operandCount} operands produces ${Math.pow(2, operandCount - 1)} OR branches. Consider breaking into smaller expressions.`,\n offset: prev.offset,\n length: prev.length,\n });\n }\n this.parseUnary();\n }\n }\n\n private parseUnary(): void {\n if (this.match('NOT')) {\n if (\n !this.current() ||\n this.current()?.type === 'AND' ||\n this.current()?.type === 'OR' ||\n this.current()?.type === 'XOR'\n ) {\n const prev = this.tokens[this.pos - 1];\n this.errors.push({\n message: \"Expected expression after '!' operator.\",\n offset: prev.offset,\n length: prev.length,\n });\n return;\n }\n this.parseUnary();\n return;\n }\n this.parsePrimary();\n }\n\n private parsePrimary(): void {\n if (this.match('LPAREN')) {\n this.parseExpression();\n if (!this.match('RPAREN')) {\n this.errors.push({\n message: \"Missing closing ')' in state expression.\",\n offset: this.tokens[this.pos - 1]?.offset ?? 0,\n length: 1,\n });\n }\n return;\n }\n\n const token = this.current();\n if (token?.type === 'STATE') {\n this.advance();\n this.validateStateToken(token);\n return;\n }\n\n // Unexpected token or end\n if (token) {\n this.errors.push({\n message: `Unexpected token '${token.value}'.`,\n offset: token.offset,\n length: token.length,\n });\n this.advance();\n }\n }\n\n private validateStateToken(token: Token): void {\n const value = token.value;\n\n // @starting\n if (value === '@starting') return;\n\n // @media:type\n if (value.startsWith('@media:')) {\n const mediaType = value.slice(7);\n const validTypes = new Set(['print', 'screen', 'all', 'speech']);\n if (!validTypes.has(mediaType)) {\n this.errors.push({\n message: `Unknown media type '${mediaType}'. Valid: print, screen, all, speech.`,\n offset: token.offset,\n length: token.length,\n });\n }\n return;\n }\n\n // @media(...)\n if (value.startsWith('@media(')) {\n this.validateMediaQuery(value, token);\n return;\n }\n\n // @supports(...)\n if (value.startsWith('@supports(')) {\n this.validateSupportsQuery(value, token);\n return;\n }\n\n // @root(...)\n if (value.startsWith('@root(')) {\n this.validateInnerStateExpression(value, 6, token);\n return;\n }\n\n // @parent(...)\n if (value.startsWith('@parent(')) {\n this.validateParentState(value, token);\n return;\n }\n\n // @own(...)\n if (value.startsWith('@own(')) {\n this.hasOwn = true;\n if (this.insideOwn) {\n this.errors.push({\n message: 'Nested @own() is not allowed.',\n offset: token.offset,\n length: token.length,\n });\n return;\n }\n const prevInsideOwn = this.insideOwn;\n this.insideOwn = true;\n this.validateInnerStateExpression(value, 5, token);\n this.insideOwn = prevInsideOwn;\n return;\n }\n\n // @(...) container query\n if (value.startsWith('@(')) {\n this.validateContainerQuery(value, token);\n return;\n }\n\n // @alias predefined state\n if (value.startsWith('@') && /^@[A-Za-z][A-Za-z0-9-]*$/.test(value)) {\n this.referencedAliases.push(value);\n return;\n }\n\n // Pseudo-class/pseudo-element\n if (value.startsWith(':')) {\n this.validatePseudoClass(value, token);\n return;\n }\n\n // Class selector\n if (value.startsWith('.')) return;\n\n // Attribute selector\n if (value.startsWith('[')) {\n this.validateAttributeSelector(value, token);\n return;\n }\n\n // Value modifier (e.g., theme=danger)\n if (value.includes('=')) {\n this.validateValueModifier(value, token);\n return;\n }\n\n // Boolean modifier (e.g., hovered, disabled)\n if (/^[a-z][a-z0-9-]+$/i.test(value)) return;\n\n this.errors.push({\n message: `Unrecognized state token '${value}'.`,\n offset: token.offset,\n length: token.length,\n });\n }\n\n private validateMediaQuery(raw: string, token: Token): void {\n const content = raw.slice(7, -1);\n if (!content.trim()) {\n this.errors.push({\n message: 'Empty @media() query.',\n offset: token.offset,\n length: token.length,\n });\n return;\n }\n\n // Expand dimension shorthands for validation\n const expanded = expandDimensionShorthands(content.trim());\n\n // Feature query (contains ':' but no comparison operators)\n if (\n expanded.includes(':') &&\n !expanded.includes('<') &&\n !expanded.includes('>') &&\n !expanded.includes('=')\n ) {\n return;\n }\n\n // Boolean feature query (no comparison operators)\n if (\n !expanded.includes('<') &&\n !expanded.includes('>') &&\n !expanded.includes('=')\n ) {\n return;\n }\n\n // Dimension query — validate dimension names\n this.validateDimensionCondition(expanded, token);\n }\n\n private validateDimensionCondition(condition: string, token: Token): void {\n // Range syntax: 600px <= width < 1200px\n const rangeMatch = condition.match(\n /^(.+?)\\s*(<=|<)\\s*(\\S+)\\s*(<=|<)\\s*(.+)$/,\n );\n if (rangeMatch) {\n const dim = rangeMatch[3];\n if (!DIMENSION_FULL.has(dim) && !DIMENSION_SHORTHANDS.has(dim)) {\n this.errors.push({\n message: `Unknown dimension '${dim}' in media/container query. Valid: width, height, inline-size, block-size (or w, h, is, bs).`,\n offset: token.offset,\n length: token.length,\n });\n }\n return;\n }\n\n // Simple comparison: width < 768px\n const simpleMatch = condition.match(/^(\\S+)\\s*(<=|>=|<|>|=)\\s*(.+)$/);\n if (simpleMatch) {\n const dim = simpleMatch[1];\n if (DIMENSION_FULL.has(dim) || DIMENSION_SHORTHANDS.has(dim)) {\n return;\n }\n }\n\n // Reversed: 768px > width\n const reversedMatch = condition.match(/^(.+?)\\s*(<=|>=|<|>|=)\\s*(\\S+)$/);\n if (reversedMatch) {\n const dim = reversedMatch[3];\n if (DIMENSION_FULL.has(dim) || DIMENSION_SHORTHANDS.has(dim)) {\n return;\n }\n }\n }\n\n private validateSupportsQuery(raw: string, token: Token): void {\n const content = raw.slice(10, -1);\n if (!content.trim()) {\n this.errors.push({\n message: 'Empty @supports() query.',\n offset: token.offset,\n length: token.length,\n });\n }\n }\n\n private validateInnerStateExpression(\n raw: string,\n prefixLen: number,\n _token: Token,\n ): void {\n const content = raw.slice(prefixLen, -1);\n if (!content.trim()) return;\n\n const innerResult = parseStateKey(content, this.opts);\n this.errors.push(...innerResult.errors);\n if (innerResult.hasOwn) this.hasOwn = true;\n this.referencedAliases.push(...innerResult.referencedAliases);\n }\n\n private validateParentState(raw: string, token: Token): void {\n const content = raw.slice(8, -1);\n if (!content.trim()) {\n this.errors.push({\n message: 'Empty @parent() state.',\n offset: token.offset,\n length: token.length,\n });\n return;\n }\n\n let condition = content.trim();\n\n // Check for direct parent combinator: @parent(hovered, >)\n const lastCommaIdx = condition.lastIndexOf(',');\n if (lastCommaIdx !== -1) {\n const afterComma = condition.slice(lastCommaIdx + 1).trim();\n if (afterComma === '>') {\n condition = condition.slice(0, lastCommaIdx).trim();\n }\n }\n\n const innerResult = parseStateKey(condition, this.opts);\n this.errors.push(...innerResult.errors);\n this.referencedAliases.push(...innerResult.referencedAliases);\n }\n\n private validateContainerQuery(raw: string, token: Token): void {\n const content = raw.slice(2, -1);\n if (!content.trim()) {\n this.errors.push({\n message: 'Empty container query.',\n offset: token.offset,\n length: token.length,\n });\n return;\n }\n\n // Named container: @(layout, w < 600px)\n const commaIdx = findTopLevelComma(content);\n let condition: string;\n\n if (commaIdx !== -1) {\n condition = content.slice(commaIdx + 1).trim();\n } else {\n condition = content.trim();\n }\n\n // Style query: @($variant=primary) — skip\n if (condition.startsWith('$')) return;\n\n // Function-like: scroll-state(...) — skip\n if (/^[a-zA-Z][\\w-]*\\s*\\(/.test(condition)) return;\n\n // Dimension query\n const expanded = expandDimensionShorthands(condition);\n if (\n expanded.includes('<') ||\n expanded.includes('>') ||\n expanded.includes('=')\n ) {\n this.validateDimensionCondition(expanded, token);\n }\n }\n\n private validatePseudoClass(value: string, token: Token): void {\n // :is(), :has(), :not(), :where() — structural pseudo-classes\n const enhancedMatch = /^:(is|has|not|where)\\(/.exec(value);\n if (enhancedMatch) return;\n\n // Functional pseudo-classes like :nth-child(2n+1)\n const funcMatch = /^(:[a-z-]+)\\(/.exec(value);\n if (funcMatch) {\n const baseName = funcMatch[1];\n if (!KNOWN_PSEUDO_CLASSES.has(baseName)) {\n this.errors.push({\n message: `Unknown pseudo-class '${baseName}'.`,\n offset: token.offset,\n length: token.length,\n });\n }\n return;\n }\n\n // Simple pseudo-class: :hover, :focus, etc.\n if (!KNOWN_PSEUDO_CLASSES.has(value)) {\n this.errors.push({\n message: `Unknown pseudo-class '${value}'.`,\n offset: token.offset,\n length: token.length,\n });\n }\n }\n\n private validateAttributeSelector(value: string, token: Token): void {\n if (!value.startsWith('[') || !value.endsWith(']')) {\n this.errors.push({\n message: `Malformed attribute selector '${value}'.`,\n offset: token.offset,\n length: token.length,\n });\n }\n }\n\n private validateValueModifier(value: string, token: Token): void {\n const opMatch = value.match(/^([a-z][a-z0-9-]*)(\\^=|\\$=|\\*=|=)(.+)$/i);\n if (!opMatch) {\n this.errors.push({\n message: `Invalid value modifier syntax '${value}'.`,\n offset: token.offset,\n length: token.length,\n });\n }\n }\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction expandDimensionShorthands(condition: string): string {\n return condition\n .replace(/\\bw\\b/g, 'width')\n .replace(/\\bh\\b/g, 'height')\n .replace(/\\bis\\b/g, 'inline-size')\n .replace(/\\bbs\\b/g, 'block-size');\n}\n\nfunction findTopLevelComma(str: string): number {\n let depth = 0;\n for (let i = 0; i < str.length; i++) {\n const ch = str[i];\n if (ch === '(') depth++;\n else if (ch === ')') depth = Math.max(0, depth - 1);\n else if (ch === ',' && depth === 0) return i;\n }\n return -1;\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Rewrite :not() state atoms, leaving raw CSS inside other functions alone.\n * A selector list/compound must stay inside :is(): negating individual parts\n * would change which elements match. Only a single pseudo can be unwrapped.\n */\nexport function getStateNegationEdits(stateKey: string): StringEdit[] {\n if (!stateKey.includes(':not(')) return [];\n if (checkBracketBalance(stateKey)) return [];\n if (parseStateKey(stateKey).errors.length > 0) return [];\n\n const { tokens } = tokenize(stateKey);\n return tokens\n .filter(\n (token) => token.type === 'STATE' && token.value.startsWith(':not('),\n )\n .flatMap((token) => {\n if (checkBracketBalance(token.value)) return [];\n const inner = token.value.slice(5, -1).trim();\n if (!inner) return [];\n\n const { tokens: innerTokens, errors } = tokenize(inner);\n const singlePseudo =\n errors.length === 0 &&\n innerTokens.length === 1 &&\n innerTokens[0].type === 'STATE' &&\n innerTokens[0].value === inner &&\n inner.startsWith(':') &&\n parseStateKey(inner).errors.length === 0 &&\n !inner.startsWith(':not(') &&\n // Keep forgiving selector-list wrappers, even for invalid branches.\n !inner.startsWith(':is(') &&\n !inner.startsWith(':where(') &&\n // Promoting :has() to a state atom enables Tasty's trailing-combinator\n // completion. Preserve the original raw CSS instead in that case.\n !(inner.startsWith(':has(') && /[>+~]\\s*\\)$/.test(inner));\n\n return [\n {\n start: token.offset,\n end: token.offset + token.length,\n replacement: singlePseudo ? `!${inner}` : `!:is(${inner})`,\n },\n ];\n });\n}\n\n/**\n * Validate a state key string and return detailed errors.\n */\nexport function parseStateKey(\n stateKey: string,\n opts: StateKeyParserOptions = {},\n): StateKeyResult {\n if (!stateKey || !stateKey.trim()) {\n return { errors: [], hasOwn: false, referencedAliases: [] };\n }\n\n const trimmed = stateKey.trim();\n\n // The standalone `_` key is the map-wide fallback floor. It always applies\n // and must be used on its own — it carries no state logic to validate.\n if (trimmed === '_') {\n return { errors: [], hasOwn: false, referencedAliases: [] };\n }\n\n // A `_` combined with state logic (`_ & hovered`, `_ | x`) is a misuse: the\n // fallback floor can only be used on its own. Tasty ignores such keys at\n // runtime and warns; flag them here so the mistake surfaces in the editor.\n if (isMisusedFallbackKey(trimmed)) {\n return {\n errors: [\n {\n message:\n 'The fallback floor \"_\" cannot be combined with other state ' +\n 'logic. Use \"_\" on its own as a map-wide fallback floor.',\n offset: 0,\n length: stateKey.length,\n },\n ],\n hasOwn: false,\n referencedAliases: [],\n };\n }\n\n const { tokens, errors: tokenErrors } = tokenize(trimmed);\n const validator = new StateKeyValidator(tokens, tokenErrors, opts);\n return validator.validate();\n}\n\n/**\n * Detect a misused standalone `_` fallback key: a key that is not exactly\n * `_` but contains `_` as a standalone atom among state-logic operators\n * (`&`, `|`, `^`, `,`) or grouping parens. A `_` embedded in a longer token\n * (e.g. a modifier named `_private`) is not a misuse.\n */\nfunction isMisusedFallbackKey(stateKey: string): boolean {\n if (stateKey === '_' || !stateKey.includes('_')) return false;\n return /(?:^|[\\s&|^,(])_(?:[\\s&|^,)]|$)/.test(stateKey);\n}\n\n/**\n * Validate a state definition value (the RHS of a state alias).\n * State definitions should be valid state expressions like\n * '@media(w < 768px)', '@root(theme=dark)', etc.\n */\nexport function validateStateDefinition(\n value: string,\n opts: StateKeyParserOptions = {},\n): StateKeyResult {\n return parseStateKey(value, opts);\n}\n"],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ //#region src/rules/motion-duration-utils.ts
2
+ /** Numeric CSS time values. A zero duration intentionally disables motion. */
3
+ const TIME_LITERAL = /^(?:\d+(?:\.\d+)?|\.\d+)(?:ms|s)$/i;
4
+ const TIME_IN_EXPRESSION = /(?:^|[^\w.])((?:\d+(?:\.\d+)?|\.\d+)(?:ms|s))(?![\w-])/gi;
5
+ const TIME_EXPRESSION = /^(?:\(|calc\(|min\(|max\(|clamp\()/i;
6
+ const DURATION_TOKEN = /(?:^\$transition$|-transition$|duration)/i;
7
+ function isTimeLiteral(value) {
8
+ return TIME_LITERAL.test(value);
9
+ }
10
+ function isZeroTime(value) {
11
+ return value === "0" || isTimeLiteral(value) && Number.parseFloat(value) === 0;
12
+ }
13
+ function isNonzeroTime(value) {
14
+ return isTimeLiteral(value) && Number.parseFloat(value) !== 0;
15
+ }
16
+ function hasRawTimeInExpression(value) {
17
+ if (!isTimeExpression(value) || /\$|\bvar\(/i.test(value)) return false;
18
+ for (const match of value.matchAll(TIME_IN_EXPRESSION)) if (Number.parseFloat(match[1]) !== 0) return true;
19
+ return false;
20
+ }
21
+ function isTimeExpression(value) {
22
+ return TIME_EXPRESSION.test(value);
23
+ }
24
+ function getDurationTokens(tokens) {
25
+ return Array.isArray(tokens) ? tokens.filter((token) => token.startsWith("$") && DURATION_TOKEN.test(token)) : [];
26
+ }
27
+
28
+ //#endregion
29
+ export { getDurationTokens, hasRawTimeInExpression, isNonzeroTime, isTimeExpression, isTimeLiteral, isZeroTime };
30
+ //# sourceMappingURL=motion-duration-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"motion-duration-utils.js","names":[],"sources":["../../src/rules/motion-duration-utils.ts"],"sourcesContent":["/** Numeric CSS time values. A zero duration intentionally disables motion. */\nconst TIME_LITERAL = /^(?:\\d+(?:\\.\\d+)?|\\.\\d+)(?:ms|s)$/i;\nconst TIME_IN_EXPRESSION =\n /(?:^|[^\\w.])((?:\\d+(?:\\.\\d+)?|\\.\\d+)(?:ms|s))(?![\\w-])/gi;\nconst TIME_EXPRESSION = /^(?:\\(|calc\\(|min\\(|max\\(|clamp\\()/i;\nconst DURATION_TOKEN = /(?:^\\$transition$|-transition$|duration)/i;\n\nexport function isTimeLiteral(value: string): boolean {\n return TIME_LITERAL.test(value);\n}\n\nexport function isZeroTime(value: string): boolean {\n return (\n value === '0' || (isTimeLiteral(value) && Number.parseFloat(value) === 0)\n );\n}\n\nexport function isNonzeroTime(value: string): boolean {\n return isTimeLiteral(value) && Number.parseFloat(value) !== 0;\n}\n\nexport function hasRawTimeInExpression(value: string): boolean {\n // A variable-based expression already derives its timing from a token.\n if (!isTimeExpression(value) || /\\$|\\bvar\\(/i.test(value)) return false;\n\n for (const match of value.matchAll(TIME_IN_EXPRESSION)) {\n if (Number.parseFloat(match[1]) !== 0) return true;\n }\n return false;\n}\n\nexport function isTimeExpression(value: string): boolean {\n return TIME_EXPRESSION.test(value);\n}\n\nexport function getDurationTokens(\n tokens: false | string[] | undefined,\n): string[] {\n return Array.isArray(tokens)\n ? tokens.filter(\n (token) => token.startsWith('$') && DURATION_TOKEN.test(token),\n )\n : [];\n}\n"],"mappings":";;AACA,MAAM,eAAe;AACrB,MAAM,qBACJ;AACF,MAAM,kBAAkB;AACxB,MAAM,iBAAiB;AAEvB,SAAgB,cAAc,OAAwB;AACpD,QAAO,aAAa,KAAK,MAAM;;AAGjC,SAAgB,WAAW,OAAwB;AACjD,QACE,UAAU,OAAQ,cAAc,MAAM,IAAI,OAAO,WAAW,MAAM,KAAK;;AAI3E,SAAgB,cAAc,OAAwB;AACpD,QAAO,cAAc,MAAM,IAAI,OAAO,WAAW,MAAM,KAAK;;AAG9D,SAAgB,uBAAuB,OAAwB;AAE7D,KAAI,CAAC,iBAAiB,MAAM,IAAI,cAAc,KAAK,MAAM,CAAE,QAAO;AAElE,MAAK,MAAM,SAAS,MAAM,SAAS,mBAAmB,CACpD,KAAI,OAAO,WAAW,MAAM,GAAG,KAAK,EAAG,QAAO;AAEhD,QAAO;;AAGT,SAAgB,iBAAiB,OAAwB;AACvD,QAAO,gBAAgB,KAAK,MAAM;;AAGpC,SAAgB,kBACd,QACU;AACV,QAAO,MAAM,QAAQ,OAAO,GACxB,OAAO,QACJ,UAAU,MAAM,WAAW,IAAI,IAAI,eAAe,KAAK,MAAM,CAC/D,GACD,EAAE"}
@@ -0,0 +1,105 @@
1
+ import { createRule } from "../create-rule.js";
2
+ import { getKeyName, getStringValue } from "../utils.js";
3
+ import { TastyContext, styleObjectListeners } from "../context.js";
4
+ import { scanTokens } from "../parsers/utils.js";
5
+ import { replaceInStringValue } from "../fix-utils.js";
6
+ import { getDurationTokens, hasRawTimeInExpression, isNonzeroTime, isTimeExpression, isTimeLiteral, isZeroTime } from "./motion-duration-utils.js";
7
+
8
+ //#region src/rules/no-raw-motion-duration.ts
9
+ const PROPERTIES = new Set([
10
+ "animation",
11
+ "animationDuration",
12
+ "transitionDuration"
13
+ ]);
14
+ var no_raw_motion_duration_default = createRule({
15
+ name: "no-raw-motion-duration",
16
+ meta: {
17
+ type: "suggestion",
18
+ hasSuggestions: true,
19
+ docs: { description: "Suggest motion tokens for animation and explicit CSS duration properties" },
20
+ messages: {
21
+ rawMotionDuration: "{{property}} duration '{{duration}}' is hardcoded. Use a duration token.",
22
+ useDurationToken: "Replace '{{duration}}' with '{{token}}'"
23
+ },
24
+ schema: []
25
+ },
26
+ defaultOptions: [],
27
+ create(context) {
28
+ const ctx = new TastyContext(context);
29
+ const durationTokens = getDurationTokens(ctx.config.tokens);
30
+ function checkDuration(token, property, node) {
31
+ const value = token.value;
32
+ if (!isNonzeroTime(value) && !hasRawTimeInExpression(value)) return;
33
+ const candidates = property === "transitionDuration" ? durationTokens : durationTokens.filter((name) => /animation|duration/i.test(name));
34
+ const isLiteral = isTimeLiteral(value);
35
+ context.report({
36
+ node,
37
+ messageId: "rawMotionDuration",
38
+ data: {
39
+ property,
40
+ duration: value
41
+ },
42
+ suggest: isLiteral ? candidates.map((candidate) => ({
43
+ messageId: "useDurationToken",
44
+ data: {
45
+ duration: value,
46
+ token: candidate
47
+ },
48
+ fix: (fixer) => replaceInStringValue(fixer, node, [{
49
+ start: token.offset,
50
+ end: token.offset + value.length,
51
+ replacement: candidate
52
+ }], context.sourceCode)
53
+ })) : []
54
+ });
55
+ }
56
+ function checkGroup(group, property, node) {
57
+ for (const token of group) {
58
+ if (property === "animation" && (/^\$[\w-]+$/.test(token.value) || /^var\(/i.test(token.value) || isZeroTime(token.value) || isTimeExpression(token.value) && !hasRawTimeInExpression(token.value))) return;
59
+ if (isTimeLiteral(token.value)) {
60
+ checkDuration(token, property, node);
61
+ if (property === "animation") return;
62
+ } else if (hasRawTimeInExpression(token.value)) {
63
+ checkDuration(token, property, node);
64
+ if (property === "animation") return;
65
+ }
66
+ }
67
+ }
68
+ function checkValue(value, property, node) {
69
+ let group = [];
70
+ for (const token of scanTokens(value)) {
71
+ if (token.value) group.push(token);
72
+ if (token.isComma) {
73
+ checkGroup(group, property, node);
74
+ group = [];
75
+ }
76
+ }
77
+ checkGroup(group, property, node);
78
+ }
79
+ function handleStyleObject(node) {
80
+ if (!ctx.isStyleObject(node)) return;
81
+ for (const prop of node.properties) {
82
+ if (prop.type !== "Property" || prop.computed) continue;
83
+ const key = getKeyName(prop.key);
84
+ if (!key || !PROPERTIES.has(key)) continue;
85
+ const value = getStringValue(prop.value);
86
+ if (value) checkValue(value, key, prop.value);
87
+ else if (prop.value.type === "ObjectExpression" && ctx.isStateMap(prop.value, prop)) for (const stateProp of prop.value.properties) {
88
+ if (stateProp.type !== "Property") continue;
89
+ const stateValue = getStringValue(stateProp.value);
90
+ if (stateValue) checkValue(stateValue, key, stateProp.value);
91
+ }
92
+ }
93
+ }
94
+ return {
95
+ ImportDeclaration(node) {
96
+ ctx.trackImport(node);
97
+ },
98
+ ...styleObjectListeners(handleStyleObject)
99
+ };
100
+ }
101
+ });
102
+
103
+ //#endregion
104
+ export { no_raw_motion_duration_default as default };
105
+ //# sourceMappingURL=no-raw-motion-duration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-raw-motion-duration.js","names":[],"sources":["../../src/rules/no-raw-motion-duration.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { replaceInStringValue } from '../fix-utils.js';\nimport { scanTokens, type ScannedToken } from '../parsers/utils.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport {\n getDurationTokens,\n hasRawTimeInExpression,\n isNonzeroTime,\n isTimeExpression,\n isTimeLiteral,\n isZeroTime,\n} from './motion-duration-utils.js';\n\ntype MessageIds = 'rawMotionDuration' | 'useDurationToken';\n\nconst PROPERTIES = new Set([\n 'animation',\n 'animationDuration',\n 'transitionDuration',\n]);\n\nexport default createRule<[], MessageIds>({\n name: 'no-raw-motion-duration',\n meta: {\n type: 'suggestion',\n hasSuggestions: true,\n docs: {\n description:\n 'Suggest motion tokens for animation and explicit CSS duration properties',\n },\n messages: {\n rawMotionDuration:\n \"{{property}} duration '{{duration}}' is hardcoded. Use a duration token.\",\n useDurationToken: \"Replace '{{duration}}' with '{{token}}'\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n const durationTokens = getDurationTokens(ctx.config.tokens);\n\n function checkDuration(\n token: ScannedToken,\n property: string,\n node: TSESTree.Node,\n ): void {\n const value = token.value;\n if (!isNonzeroTime(value) && !hasRawTimeInExpression(value)) return;\n\n // Animation has no implicit timing token. Only names that read as an\n // animation or generic duration are useful suggestions for that property.\n const candidates =\n property === 'transitionDuration'\n ? durationTokens\n : durationTokens.filter((name) => /animation|duration/i.test(name));\n const isLiteral = isTimeLiteral(value);\n context.report({\n node,\n messageId: 'rawMotionDuration',\n data: { property, duration: value },\n suggest: isLiteral\n ? candidates.map((candidate) => ({\n messageId: 'useDurationToken' as const,\n data: { duration: value, token: candidate },\n fix: (fixer: Parameters<typeof replaceInStringValue>[0]) =>\n replaceInStringValue(\n fixer,\n node,\n [\n {\n start: token.offset,\n end: token.offset + value.length,\n replacement: candidate,\n },\n ],\n context.sourceCode,\n ),\n }))\n : [],\n });\n }\n\n function checkGroup(\n group: ScannedToken[],\n property: string,\n node: TSESTree.Node,\n ): void {\n for (const token of group) {\n // The first time in an animation shorthand is its duration; a second\n // time is a delay. A variable can also be a duration, so stop there\n // rather than misreporting a later literal delay.\n if (\n property === 'animation' &&\n (/^\\$[\\w-]+$/.test(token.value) ||\n /^var\\(/i.test(token.value) ||\n isZeroTime(token.value) ||\n (isTimeExpression(token.value) &&\n !hasRawTimeInExpression(token.value)))\n ) {\n return;\n }\n if (isTimeLiteral(token.value)) {\n checkDuration(token, property, node);\n if (property === 'animation') return;\n } else if (hasRawTimeInExpression(token.value)) {\n checkDuration(token, property, node);\n if (property === 'animation') return;\n }\n }\n }\n\n function checkValue(\n value: string,\n property: string,\n node: TSESTree.Node,\n ): void {\n let group: ScannedToken[] = [];\n for (const token of scanTokens(value)) {\n if (token.value) group.push(token);\n if (token.isComma) {\n checkGroup(group, property, node);\n group = [];\n }\n }\n checkGroup(group, property, node);\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression): void {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n const key = getKeyName(prop.key);\n if (!key || !PROPERTIES.has(key)) continue;\n\n const value = getStringValue(prop.value);\n if (value) {\n checkValue(value, key, prop.value);\n } else if (\n prop.value.type === 'ObjectExpression' &&\n ctx.isStateMap(prop.value, prop)\n ) {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateValue = getStringValue(stateProp.value);\n if (stateValue) checkValue(stateValue, key, 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":";;;;;;;;AAiBA,MAAM,aAAa,IAAI,IAAI;CACzB;CACA;CACA;CACD,CAAC;AAEF,qCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,gBAAgB;EAChB,MAAM,EACJ,aACE,4EACH;EACD,UAAU;GACR,mBACE;GACF,kBAAkB;GACnB;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EACrC,MAAM,iBAAiB,kBAAkB,IAAI,OAAO,OAAO;EAE3D,SAAS,cACP,OACA,UACA,MACM;GACN,MAAM,QAAQ,MAAM;AACpB,OAAI,CAAC,cAAc,MAAM,IAAI,CAAC,uBAAuB,MAAM,CAAE;GAI7D,MAAM,aACJ,aAAa,uBACT,iBACA,eAAe,QAAQ,SAAS,sBAAsB,KAAK,KAAK,CAAC;GACvE,MAAM,YAAY,cAAc,MAAM;AACtC,WAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM;KAAE;KAAU,UAAU;KAAO;IACnC,SAAS,YACL,WAAW,KAAK,eAAe;KAC7B,WAAW;KACX,MAAM;MAAE,UAAU;MAAO,OAAO;MAAW;KAC3C,MAAM,UACJ,qBACE,OACA,MACA,CACE;MACE,OAAO,MAAM;MACb,KAAK,MAAM,SAAS,MAAM;MAC1B,aAAa;MACd,CACF,EACD,QAAQ,WACT;KACJ,EAAE,GACH,EAAE;IACP,CAAC;;EAGJ,SAAS,WACP,OACA,UACA,MACM;AACN,QAAK,MAAM,SAAS,OAAO;AAIzB,QACE,aAAa,gBACZ,aAAa,KAAK,MAAM,MAAM,IAC7B,UAAU,KAAK,MAAM,MAAM,IAC3B,WAAW,MAAM,MAAM,IACtB,iBAAiB,MAAM,MAAM,IAC5B,CAAC,uBAAuB,MAAM,MAAM,EAExC;AAEF,QAAI,cAAc,MAAM,MAAM,EAAE;AAC9B,mBAAc,OAAO,UAAU,KAAK;AACpC,SAAI,aAAa,YAAa;eACrB,uBAAuB,MAAM,MAAM,EAAE;AAC9C,mBAAc,OAAO,UAAU,KAAK;AACpC,SAAI,aAAa,YAAa;;;;EAKpC,SAAS,WACP,OACA,UACA,MACM;GACN,IAAI,QAAwB,EAAE;AAC9B,QAAK,MAAM,SAAS,WAAW,MAAM,EAAE;AACrC,QAAI,MAAM,MAAO,OAAM,KAAK,MAAM;AAClC,QAAI,MAAM,SAAS;AACjB,gBAAW,OAAO,UAAU,KAAK;AACjC,aAAQ,EAAE;;;AAGd,cAAW,OAAO,UAAU,KAAK;;EAGnC,SAAS,kBAAkB,MAAuC;AAChE,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAC/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,CAAC,OAAO,CAAC,WAAW,IAAI,IAAI,CAAE;IAElC,MAAM,QAAQ,eAAe,KAAK,MAAM;AACxC,QAAI,MACF,YAAW,OAAO,KAAK,KAAK,MAAM;aAElC,KAAK,MAAM,SAAS,sBACpB,IAAI,WAAW,KAAK,OAAO,KAAK,CAEhC,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;KACnC,MAAM,aAAa,eAAe,UAAU,MAAM;AAClD,SAAI,WAAY,YAAW,YAAY,KAAK,UAAU,MAAM;;;;AAMpE,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
@@ -1,47 +1,29 @@
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 { scanTokens } from "../parsers/utils.js";
4
5
  import { replaceInStringValue } from "../fix-utils.js";
6
+ import { getDurationTokens, hasRawTimeInExpression, isNonzeroTime, isTimeLiteral } from "./motion-duration-utils.js";
5
7
 
6
8
  //#region src/rules/no-raw-transition-duration.ts
7
9
  /**
8
- * A CSS time literal: `0.2s`, `200ms`, `.15s`, `0`.
9
- *
10
- * A bare `0` counts because it is a duration in the timing slot — and the one
11
- * spelling of "no transition" a design system would rather express as a state
12
- * than as a hardcoded zero.
13
- *
14
- * Deliberately narrow. `$fast-transition`, `var(--transition)`, `(0.2s * 2)`
15
- * and `calc(…)` all reach the same slot and none is a raw literal: the first
16
- * two are already tokens, and the arithmetic forms are somebody deriving a
17
- * duration from one, which is not what this rule is about. A unitless non-zero
18
- * (`fill 200`) is invalid CSS rather than a hardcoded token, so it is left to
19
- * `valid-value`.
20
- */
21
- const TIME_LITERAL = /^(?:\d*\.?\d+m?s|0)$/i;
22
- /**
23
10
  * A plausible transition name: a semantic name, a CSS property, or a `$$name` /
24
11
  * `##name` / `--name` custom-property reference.
25
12
  *
26
- * This is the guard that makes the naive comma split safe. A value can carry a
27
- * multi-argument easing — `transition: 'fill cubic-bezier(0.4, 0, 0.2, 1) 1s'`
28
- * — whose commas are not group separators, so the last "group" comes out as
29
- * `1) 1s`. Requiring the name slot to look like a name drops that fragment
30
- * instead of reporting a duration for a property called `1)`.
13
+ * This excludes malformed groups from duration advice.
31
14
  */
32
15
  const TRANSITION_NAME = /^(?:\$\$|##|--)?[a-zA-Z][\w-]*$/;
33
- /**
34
- * Config `tokens` entries that name a duration.
35
- *
36
- * Tasty resolves an omitted duration to `var(--<name>-transition,
37
- * var(--transition))`, so `-transition` is the naming convention the runtime
38
- * itself imposes; `duration` is the other spelling design systems reach for.
39
- */
40
- const DURATION_TOKEN = /(?:^\$transition$|-transition$|duration)/i;
16
+ const NON_TRANSITION_NAMES = new Set([
17
+ "none",
18
+ "inherit",
19
+ "initial",
20
+ "unset",
21
+ "revert",
22
+ "revert-layer"
23
+ ]);
41
24
  /** The default timing tasty substitutes for a group whose duration is omitted. */
42
25
  function defaultTimingHint(name) {
43
- if (!/^[a-zA-Z][\w-]*$/.test(name)) return "$transition";
44
- return `$${name}-transition (falling back to $transition)`;
26
+ return `$${name.startsWith("$$") ? name.slice(2) : name.startsWith("##") ? `${name.slice(2)}-color` : name.startsWith("--") ? name.slice(2) : name}-transition (falling back to $transition)`;
45
27
  }
46
28
  var no_raw_transition_duration_default = createRule({
47
29
  name: "no-raw-transition-duration",
@@ -51,6 +33,7 @@ var no_raw_transition_duration_default = createRule({
51
33
  docs: { description: "Suggest a duration token, or tasty's default timing, instead of a hardcoded transition duration" },
52
34
  messages: {
53
35
  rawTransitionDuration: "Transition duration '{{duration}}' is hardcoded. Use a duration token{{available}}, or omit it to inherit {{fallback}}.",
36
+ rawTransitionDurationExpression: "Transition duration '{{duration}}' contains a hardcoded time. Derive it from a duration token instead.",
54
37
  useDurationToken: "Replace '{{duration}}' with '{{token}}'",
55
38
  useDefaultDuration: "Remove '{{duration}}' to inherit the default transition timing"
56
39
  },
@@ -60,55 +43,78 @@ var no_raw_transition_duration_default = createRule({
60
43
  create(context) {
61
44
  const ctx = new TastyContext(context);
62
45
  /** `$`-prefixed config tokens whose name reads as a duration. */
63
- const durationTokens = Array.isArray(ctx.config.tokens) ? ctx.config.tokens.filter((token) => token.startsWith("$") && DURATION_TOKEN.test(token)) : [];
64
- function checkTransitionValue(value, node) {
65
- let offset = 0;
66
- for (const group of value.split(",")) {
67
- const [name, timing] = group.trim().split(/\s+/).filter(Boolean);
68
- if (!name || !timing || !TRANSITION_NAME.test(name) || !TIME_LITERAL.test(timing)) {
69
- offset += group.length + 1;
70
- continue;
71
- }
72
- const nameEnd = group.indexOf(name) + name.length;
73
- const startInGroup = group.indexOf(timing, nameEnd);
74
- if (startInGroup === -1) {
75
- offset += group.length + 1;
76
- continue;
77
- }
78
- const start = offset + startInGroup;
79
- const end = start + timing.length;
80
- const removeStart = offset + nameEnd;
46
+ const durationTokens = getDurationTokens(ctx.config.tokens);
47
+ function checkGroup(tokens, node) {
48
+ const [name, timing] = tokens;
49
+ if (!name || !timing || !TRANSITION_NAME.test(name.value)) return;
50
+ if (NON_TRANSITION_NAMES.has(name.value)) return;
51
+ const isLiteral = isTimeLiteral(timing.value);
52
+ if (isLiteral && !isNonzeroTime(timing.value)) return;
53
+ if (!isLiteral && !hasRawTimeInExpression(timing.value)) return;
54
+ if (!isLiteral) {
81
55
  context.report({
82
56
  node,
83
- messageId: "rawTransitionDuration",
57
+ messageId: "rawTransitionDurationExpression",
58
+ data: { duration: timing.value }
59
+ });
60
+ return;
61
+ }
62
+ const start = timing.offset;
63
+ const end = start + timing.value.length;
64
+ const removeStart = name.offset + name.value.length;
65
+ context.report({
66
+ node,
67
+ messageId: "rawTransitionDuration",
68
+ data: {
69
+ duration: timing.value,
70
+ available: durationTokens.length ? ` (${durationTokens.join(", ")})` : "",
71
+ fallback: defaultTimingHint(name.value)
72
+ },
73
+ suggest: [...durationTokens.map((token) => ({
74
+ messageId: "useDurationToken",
84
75
  data: {
85
- duration: timing,
86
- available: durationTokens.length ? ` (${durationTokens.join(", ")})` : "",
87
- fallback: defaultTimingHint(name)
76
+ duration: timing.value,
77
+ token
88
78
  },
89
- suggest: [...durationTokens.map((token) => ({
90
- messageId: "useDurationToken",
91
- data: {
92
- duration: timing,
93
- token
94
- },
95
- fix: (fixer) => replaceInStringValue(fixer, node, [{
96
- start,
97
- end,
98
- replacement: token
99
- }], context.sourceCode)
100
- })), {
101
- messageId: "useDefaultDuration",
102
- data: { duration: timing },
103
- fix: (fixer) => replaceInStringValue(fixer, node, [{
104
- start: removeStart,
105
- end,
106
- replacement: ""
107
- }], context.sourceCode)
108
- }]
79
+ fix: (fixer) => replaceInStringValue(fixer, node, [{
80
+ start,
81
+ end,
82
+ replacement: token
83
+ }], context.sourceCode)
84
+ })), {
85
+ messageId: "useDefaultDuration",
86
+ data: { duration: timing.value },
87
+ fix: (fixer) => replaceInStringValue(fixer, node, [{
88
+ start: removeStart,
89
+ end,
90
+ replacement: ""
91
+ }], context.sourceCode)
92
+ }]
93
+ });
94
+ }
95
+ function checkTransitionValue(value, node) {
96
+ let group = [];
97
+ for (const token of scanTokens(value)) {
98
+ if (token.value) group.push(token);
99
+ if (token.isComma) {
100
+ checkGroup(group, node);
101
+ group = [];
102
+ }
103
+ }
104
+ checkGroup(group, node);
105
+ }
106
+ function isLocalTastyComponent(node, name) {
107
+ let scope = context.sourceCode.getScope(node);
108
+ while (scope) {
109
+ const variable = scope.set.get(name);
110
+ if (variable) return variable.defs.some((definition) => {
111
+ if (definition.type !== "Variable") return false;
112
+ const declaration = definition.node;
113
+ return declaration.id.type === "Identifier" && declaration.parent.type === "VariableDeclaration" && declaration.parent.kind === "const" && declaration.init?.type === "CallExpression" && ctx.isTastyCall(declaration.init)?.importedName === "tasty";
109
114
  });
110
- offset += group.length + 1;
115
+ scope = scope.upper;
111
116
  }
117
+ return false;
112
118
  }
113
119
  function handleStyleObject(node) {
114
120
  if (!ctx.isStyleObject(node)) return;
@@ -120,7 +126,7 @@ var no_raw_transition_duration_default = createRule({
120
126
  checkTransitionValue(str, prop.value);
121
127
  continue;
122
128
  }
123
- if (prop.value.type === "ObjectExpression") for (const stateProp of prop.value.properties) {
129
+ if (prop.value.type === "ObjectExpression" && ctx.isStateMap(prop.value, prop)) for (const stateProp of prop.value.properties) {
124
130
  if (stateProp.type !== "Property") continue;
125
131
  const stateStr = getStringValue(stateProp.value);
126
132
  if (stateStr) checkTransitionValue(stateStr, stateProp.value);
@@ -131,6 +137,15 @@ var no_raw_transition_duration_default = createRule({
131
137
  ImportDeclaration(node) {
132
138
  ctx.trackImport(node);
133
139
  },
140
+ JSXAttribute(node) {
141
+ if (node.name.type !== "JSXIdentifier" || node.name.name !== "transition") return;
142
+ const opening = node.parent;
143
+ if (opening?.type !== "JSXOpeningElement" || opening.name.type !== "JSXIdentifier" || !isLocalTastyComponent(node, opening.name.name)) return;
144
+ const valueNode = node.value?.type === "JSXExpressionContainer" ? node.value.expression : node.value;
145
+ if (!valueNode) return;
146
+ const value = getStringValue(valueNode);
147
+ if (value) checkTransitionValue(value, valueNode);
148
+ },
134
149
  ...styleObjectListeners(handleStyleObject)
135
150
  };
136
151
  }
@@ -1 +1 @@
1
- {"version":3,"file":"no-raw-transition-duration.js","names":[],"sources":["../../src/rules/no-raw-transition-duration.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport { replaceInStringValue } from '../fix-utils.js';\n\ntype MessageIds =\n | 'rawTransitionDuration'\n | 'useDurationToken'\n | 'useDefaultDuration';\n\n/**\n * A CSS time literal: `0.2s`, `200ms`, `.15s`, `0`.\n *\n * A bare `0` counts because it is a duration in the timing slot — and the one\n * spelling of \"no transition\" a design system would rather express as a state\n * than as a hardcoded zero.\n *\n * Deliberately narrow. `$fast-transition`, `var(--transition)`, `(0.2s * 2)`\n * and `calc(…)` all reach the same slot and none is a raw literal: the first\n * two are already tokens, and the arithmetic forms are somebody deriving a\n * duration from one, which is not what this rule is about. A unitless non-zero\n * (`fill 200`) is invalid CSS rather than a hardcoded token, so it is left to\n * `valid-value`.\n */\nconst TIME_LITERAL = /^(?:\\d*\\.?\\d+m?s|0)$/i;\n\n/**\n * A plausible transition name: a semantic name, a CSS property, or a `$$name` /\n * `##name` / `--name` custom-property reference.\n *\n * This is the guard that makes the naive comma split safe. A value can carry a\n * multi-argument easing — `transition: 'fill cubic-bezier(0.4, 0, 0.2, 1) 1s'`\n * — whose commas are not group separators, so the last \"group\" comes out as\n * `1) 1s`. Requiring the name slot to look like a name drops that fragment\n * instead of reporting a duration for a property called `1)`.\n */\nconst TRANSITION_NAME = /^(?:\\$\\$|##|--)?[a-zA-Z][\\w-]*$/;\n\n/**\n * Config `tokens` entries that name a duration.\n *\n * Tasty resolves an omitted duration to `var(--<name>-transition,\n * var(--transition))`, so `-transition` is the naming convention the runtime\n * itself imposes; `duration` is the other spelling design systems reach for.\n */\nconst DURATION_TOKEN = /(?:^\\$transition$|-transition$|duration)/i;\n\n/** The default timing tasty substitutes for a group whose duration is omitted. */\nfunction defaultTimingHint(name: string): string {\n // A custom-property reference names no semantic group, so there is no\n // per-name timing token to point at — only the global one.\n if (!/^[a-zA-Z][\\w-]*$/.test(name)) return '$transition';\n\n return `$${name}-transition (falling back to $transition)`;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'no-raw-transition-duration',\n meta: {\n type: 'suggestion',\n hasSuggestions: true,\n docs: {\n description:\n \"Suggest a duration token, or tasty's default timing, instead of a hardcoded transition duration\",\n },\n messages: {\n rawTransitionDuration:\n \"Transition duration '{{duration}}' is hardcoded. Use a duration token{{available}}, or omit it to inherit {{fallback}}.\",\n useDurationToken: \"Replace '{{duration}}' with '{{token}}'\",\n useDefaultDuration:\n \"Remove '{{duration}}' to inherit the default transition timing\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n /** `$`-prefixed config tokens whose name reads as a duration. */\n const durationTokens = Array.isArray(ctx.config.tokens)\n ? ctx.config.tokens.filter(\n (token) => token.startsWith('$') && DURATION_TOKEN.test(token),\n )\n : [];\n\n function checkTransitionValue(value: string, node: TSESTree.Node): void {\n let offset = 0;\n\n for (const group of value.split(',')) {\n const tokens = group.trim().split(/\\s+/).filter(Boolean);\n const [name, timing] = tokens;\n\n // Only the second token is inspected, which is exactly the slot tasty\n // reads as the duration. A later time value is a *delay*\n // (`fill ease-in 0.1s`), and the advice here would not hold for one:\n // omitting a delay means \"no delay\", a real rendering change, whereas\n // omitting a duration falls back to the design system's own timing.\n if (\n !name ||\n !timing ||\n !TRANSITION_NAME.test(name) ||\n !TIME_LITERAL.test(timing)\n ) {\n offset += group.length + 1;\n continue;\n }\n\n // Search from the end of the name so a duration that repeats the name's\n // own text cannot match the wrong occurrence.\n const nameEnd = group.indexOf(name) + name.length;\n const startInGroup = group.indexOf(timing, nameEnd);\n if (startInGroup === -1) {\n offset += group.length + 1;\n continue;\n }\n\n const start = offset + startInGroup;\n const end = start + timing.length;\n // Removal swallows the separating whitespace too, so dropping the\n // duration does not leave `fill ease-in` behind.\n const removeStart = offset + nameEnd;\n\n context.report({\n node,\n messageId: 'rawTransitionDuration',\n data: {\n duration: timing,\n available: durationTokens.length\n ? ` (${durationTokens.join(', ')})`\n : '',\n fallback: defaultTimingHint(name),\n },\n suggest: [\n // Config tokens first: naming the project's own duration is better\n // advice than deleting the author's intent, when one exists.\n ...durationTokens.map((token) => ({\n messageId: 'useDurationToken' as const,\n data: { duration: timing, token },\n fix: (fixer: Parameters<typeof replaceInStringValue>[0]) =>\n replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement: token }],\n context.sourceCode,\n ),\n })),\n {\n messageId: 'useDefaultDuration' as const,\n data: { duration: timing },\n fix: (fixer: Parameters<typeof replaceInStringValue>[0]) =>\n replaceInStringValue(\n fixer,\n node,\n [{ start: removeStart, end, replacement: '' }],\n context.sourceCode,\n ),\n },\n ],\n });\n\n 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":";;;;;;;;;;;;;;;;;;;;AAyBA,MAAM,eAAe;;;;;;;;;;;AAYrB,MAAM,kBAAkB;;;;;;;;AASxB,MAAM,iBAAiB;;AAGvB,SAAS,kBAAkB,MAAsB;AAG/C,KAAI,CAAC,mBAAmB,KAAK,KAAK,CAAE,QAAO;AAE3C,QAAO,IAAI,KAAK;;AAGlB,yCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,gBAAgB;EAChB,MAAM,EACJ,aACE,mGACH;EACD,UAAU;GACR,uBACE;GACF,kBAAkB;GAClB,oBACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;;EAGrC,MAAM,iBAAiB,MAAM,QAAQ,IAAI,OAAO,OAAO,GACnD,IAAI,OAAO,OAAO,QACf,UAAU,MAAM,WAAW,IAAI,IAAI,eAAe,KAAK,MAAM,CAC/D,GACD,EAAE;EAEN,SAAS,qBAAqB,OAAe,MAA2B;GACtE,IAAI,SAAS;AAEb,QAAK,MAAM,SAAS,MAAM,MAAM,IAAI,EAAE;IAEpC,MAAM,CAAC,MAAM,UADE,MAAM,MAAM,CAAC,MAAM,MAAM,CAAC,OAAO,QAAQ;AAQxD,QACE,CAAC,QACD,CAAC,UACD,CAAC,gBAAgB,KAAK,KAAK,IAC3B,CAAC,aAAa,KAAK,OAAO,EAC1B;AACA,eAAU,MAAM,SAAS;AACzB;;IAKF,MAAM,UAAU,MAAM,QAAQ,KAAK,GAAG,KAAK;IAC3C,MAAM,eAAe,MAAM,QAAQ,QAAQ,QAAQ;AACnD,QAAI,iBAAiB,IAAI;AACvB,eAAU,MAAM,SAAS;AACzB;;IAGF,MAAM,QAAQ,SAAS;IACvB,MAAM,MAAM,QAAQ,OAAO;IAG3B,MAAM,cAAc,SAAS;AAE7B,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MACJ,UAAU;MACV,WAAW,eAAe,SACtB,KAAK,eAAe,KAAK,KAAK,CAAC,KAC/B;MACJ,UAAU,kBAAkB,KAAK;MAClC;KACD,SAAS,CAGP,GAAG,eAAe,KAAK,WAAW;MAChC,WAAW;MACX,MAAM;OAAE,UAAU;OAAQ;OAAO;MACjC,MAAM,UACJ,qBACE,OACA,MACA,CAAC;OAAE;OAAO;OAAK,aAAa;OAAO,CAAC,EACpC,QAAQ,WACT;MACJ,EAAE,EACH;MACE,WAAW;MACX,MAAM,EAAE,UAAU,QAAQ;MAC1B,MAAM,UACJ,qBACE,OACA,MACA,CAAC;OAAE,OAAO;OAAa;OAAK,aAAa;OAAI,CAAC,EAC9C,QAAQ,WACT;MACJ,CACF;KACF,CAAC;AAEF,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"}
1
+ {"version":3,"file":"no-raw-transition-duration.js","names":[],"sources":["../../src/rules/no-raw-transition-duration.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport { replaceInStringValue } from '../fix-utils.js';\nimport { scanTokens, type ScannedToken } from '../parsers/utils.js';\nimport {\n getDurationTokens,\n hasRawTimeInExpression,\n isNonzeroTime,\n isTimeLiteral,\n} from './motion-duration-utils.js';\n\ntype MessageIds =\n | 'rawTransitionDuration'\n | 'rawTransitionDurationExpression'\n | 'useDurationToken'\n | 'useDefaultDuration';\n\n/**\n * A plausible transition name: a semantic name, a CSS property, or a `$$name` /\n * `##name` / `--name` custom-property reference.\n *\n * This excludes malformed groups from duration advice.\n */\nconst TRANSITION_NAME = /^(?:\\$\\$|##|--)?[a-zA-Z][\\w-]*$/;\nconst NON_TRANSITION_NAMES = new Set([\n 'none',\n 'inherit',\n 'initial',\n 'unset',\n 'revert',\n 'revert-layer',\n]);\n\n/** The default timing tasty substitutes for a group whose duration is omitted. */\nfunction defaultTimingHint(name: string): string {\n // The parser turns $$name into --name and ##name into --name-color before\n // the transition handler selects its per-property timing variable.\n const property = name.startsWith('$$')\n ? name.slice(2)\n : name.startsWith('##')\n ? `${name.slice(2)}-color`\n : name.startsWith('--')\n ? name.slice(2)\n : name;\n return `$${property}-transition (falling back to $transition)`;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'no-raw-transition-duration',\n meta: {\n type: 'suggestion',\n hasSuggestions: true,\n docs: {\n description:\n \"Suggest a duration token, or tasty's default timing, instead of a hardcoded transition duration\",\n },\n messages: {\n rawTransitionDuration:\n \"Transition duration '{{duration}}' is hardcoded. Use a duration token{{available}}, or omit it to inherit {{fallback}}.\",\n rawTransitionDurationExpression:\n \"Transition duration '{{duration}}' contains a hardcoded time. Derive it from a duration token instead.\",\n useDurationToken: \"Replace '{{duration}}' with '{{token}}'\",\n useDefaultDuration:\n \"Remove '{{duration}}' to inherit the default transition timing\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n /** `$`-prefixed config tokens whose name reads as a duration. */\n const durationTokens = getDurationTokens(ctx.config.tokens);\n\n function checkGroup(tokens: ScannedToken[], node: TSESTree.Node): void {\n const [name, timing] = tokens;\n\n // Only the second token is the duration slot. A later time value is a\n // delay, for which removing the value has different semantics.\n if (!name || !timing || !TRANSITION_NAME.test(name.value)) return;\n if (NON_TRANSITION_NAMES.has(name.value)) return;\n\n const isLiteral = isTimeLiteral(timing.value);\n if (isLiteral && !isNonzeroTime(timing.value)) return;\n if (!isLiteral && !hasRawTimeInExpression(timing.value)) return;\n\n if (!isLiteral) {\n // Replacing or dropping a whole expression can change its behavior in\n // ways the rule cannot infer, so offer a report without a quick fix.\n context.report({\n node,\n messageId: 'rawTransitionDurationExpression',\n data: { duration: timing.value },\n });\n return;\n }\n\n const start = timing.offset;\n const end = start + timing.value.length;\n // Removal swallows the separating whitespace too, so dropping the\n // duration does not leave `fill ease-in` behind.\n const removeStart = name.offset + name.value.length;\n\n context.report({\n node,\n messageId: 'rawTransitionDuration',\n data: {\n duration: timing.value,\n available: durationTokens.length\n ? ` (${durationTokens.join(', ')})`\n : '',\n fallback: defaultTimingHint(name.value),\n },\n suggest: [\n // Config tokens first: naming the project's own duration is better\n // advice than deleting the author's intent, when one exists.\n ...durationTokens.map((token) => ({\n messageId: 'useDurationToken' as const,\n data: { duration: timing.value, token },\n fix: (fixer: Parameters<typeof replaceInStringValue>[0]) =>\n replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement: token }],\n context.sourceCode,\n ),\n })),\n {\n messageId: 'useDefaultDuration' as const,\n data: { duration: timing.value },\n fix: (fixer: Parameters<typeof replaceInStringValue>[0]) =>\n replaceInStringValue(\n fixer,\n node,\n [{ start: removeStart, end, replacement: '' }],\n context.sourceCode,\n ),\n },\n ],\n });\n }\n\n function checkTransitionValue(value: string, node: TSESTree.Node): void {\n let group: ScannedToken[] = [];\n for (const token of scanTokens(value)) {\n if (token.value) group.push(token);\n if (token.isComma) {\n checkGroup(group, node);\n group = [];\n }\n }\n checkGroup(group, node);\n }\n\n function isLocalTastyComponent(node: TSESTree.Node, name: string): boolean {\n let scope: ReturnType<typeof context.sourceCode.getScope> | null =\n context.sourceCode.getScope(node);\n while (scope) {\n const variable = scope.set.get(name);\n if (variable) {\n return variable.defs.some((definition) => {\n if (definition.type !== 'Variable') return false;\n const declaration = definition.node;\n return (\n declaration.id.type === 'Identifier' &&\n declaration.parent.type === 'VariableDeclaration' &&\n declaration.parent.kind === 'const' &&\n declaration.init?.type === 'CallExpression' &&\n ctx.isTastyCall(declaration.init)?.importedName === 'tasty'\n );\n });\n }\n scope = scope.upper;\n }\n return false;\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key !== 'transition') continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkTransitionValue(str, prop.value);\n continue;\n }\n\n if (\n prop.value.type === 'ObjectExpression' &&\n ctx.isStateMap(prop.value, prop)\n ) {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateStr = getStringValue(stateProp.value);\n if (stateStr) {\n checkTransitionValue(stateStr, stateProp.value);\n }\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n JSXAttribute(node) {\n if (\n node.name.type !== 'JSXIdentifier' ||\n node.name.name !== 'transition'\n ) {\n return;\n }\n const opening = node.parent;\n if (\n opening?.type !== 'JSXOpeningElement' ||\n opening.name.type !== 'JSXIdentifier' ||\n !isLocalTastyComponent(node, opening.name.name)\n ) {\n return;\n }\n\n const valueNode =\n node.value?.type === 'JSXExpressionContainer'\n ? node.value.expression\n : node.value;\n if (!valueNode) return;\n const value = getStringValue(valueNode);\n if (value) checkTransitionValue(value, valueNode);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { createRule } from "../create-rule.js";
2
- import { isStaticValue } from "../utils.js";
2
+ import { SPECIAL_STYLE_KEYS } from "../constants.js";
3
+ import { getKeyName, isStaticValue, unwrapExpression } from "../utils.js";
3
4
  import { TastyContext, styleObjectListeners } from "../context.js";
4
5
 
5
6
  //#region src/rules/no-runtime-styles-mutation.ts
@@ -7,39 +8,47 @@ var no_runtime_styles_mutation_default = createRule({
7
8
  name: "no-runtime-styles-mutation",
8
9
  meta: {
9
10
  type: "suggestion",
10
- docs: { description: "Warn when style objects contain runtime-computed values" },
11
- messages: { dynamicStyleValue: "Style value for '{{property}}' should be static. Use the 'mods' prop, tokens, or CSS custom properties for dynamic behavior." },
11
+ docs: { description: "Warn when style objects contain JavaScript-computed values" },
12
+ messages: {
13
+ dynamicStyleValue: "Style value for '{{property}}' should be static. Use states with mods, tokens, or styleProps for dynamic behavior.",
14
+ dynamicStyleKey: "Style key for '{{property}}' should be static. Define states as literal keys instead of computing them in JavaScript."
15
+ },
12
16
  schema: []
13
17
  },
14
18
  defaultOptions: [],
15
19
  create(context) {
16
20
  const ctx = new TastyContext(context);
17
- function checkProperties(node, parentProperty = null) {
21
+ function checkValue(node, property, isStateMap = false) {
22
+ const value = unwrapExpression(node);
23
+ if (value.type === "ObjectExpression") checkProperties(value, property, isStateMap);
24
+ else if (value.type === "ArrayExpression") {
25
+ for (const element of value.elements) if (element && element.type !== "SpreadElement") checkValue(element, property);
26
+ } else if (!isStaticValue(value)) context.report({
27
+ node: value,
28
+ messageId: "dynamicStyleValue",
29
+ data: { property }
30
+ });
31
+ }
32
+ function checkProperties(node, parentProperty = "(style)", isStateMap = false) {
18
33
  for (const prop of node.properties) {
19
- if (prop.type === "SpreadElement") {
20
- context.report({
21
- node: prop,
22
- messageId: "dynamicStyleValue",
23
- data: { property: parentProperty ?? "(spread)" }
24
- });
25
- continue;
26
- }
27
- if (prop.type !== "Property") continue;
28
- const key = !prop.computed && prop.key.type === "Identifier" ? prop.key.name : parentProperty;
29
- if (!prop.computed && prop.key.type === "Identifier" && /^[A-Z]/.test(prop.key.name)) {
30
- if (prop.value.type === "ObjectExpression") checkProperties(prop.value, key);
31
- continue;
32
- }
33
- if (!prop.computed && prop.key.type === "Identifier" && prop.key.name.startsWith("@")) continue;
34
- if (!isStaticValue(prop.value)) context.report({
35
- node: prop.value,
36
- messageId: "dynamicStyleValue",
37
- data: { property: key ?? "(unknown)" }
34
+ if (prop.type === "SpreadElement") continue;
35
+ const key = !prop.computed ? getKeyName(prop.key) : null;
36
+ if (key && SPECIAL_STYLE_KEYS.has(key)) continue;
37
+ const property = isStateMap || prop.computed ? parentProperty : key ?? parentProperty;
38
+ if (prop.computed) context.report({
39
+ node: prop.key,
40
+ messageId: "dynamicStyleKey",
41
+ data: { property }
38
42
  });
43
+ const value = unwrapExpression(prop.value);
44
+ const childIsStateMap = value.type === "ObjectExpression" && ctx.isStateMap(value, prop);
45
+ checkValue(prop.value, property, childIsStateMap);
39
46
  }
40
47
  }
41
48
  function handleStyleObject(node) {
42
- if (!ctx.isStyleObject(node)) return;
49
+ const styleCtx = ctx.getStyleContext(node);
50
+ if (!styleCtx || styleCtx.isStaticCall) return;
51
+ if (ctx.getRootStyleObject(node) !== node) return;
43
52
  checkProperties(node);
44
53
  }
45
54
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"no-runtime-styles-mutation.js","names":[],"sources":["../../src/rules/no-runtime-styles-mutation.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { isStaticValue } from '../utils.js';\n\ntype MessageIds = 'dynamicStyleValue';\n\nexport default createRule<[], MessageIds>({\n name: 'no-runtime-styles-mutation',\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Warn when style objects contain runtime-computed values',\n },\n messages: {\n dynamicStyleValue:\n \"Style value for '{{property}}' should be static. Use the 'mods' prop, tokens, or CSS custom properties for dynamic behavior.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkProperties(\n node: TSESTree.ObjectExpression,\n parentProperty: string | null = null,\n ): void {\n for (const prop of node.properties) {\n if (prop.type === 'SpreadElement') {\n context.report({\n node: prop,\n messageId: 'dynamicStyleValue',\n data: { property: parentProperty ?? '(spread)' },\n });\n continue;\n }\n\n if (prop.type !== 'Property') continue;\n\n const key =\n !prop.computed && prop.key.type === 'Identifier'\n ? prop.key.name\n : parentProperty;\n\n // Skip sub-elements (they contain nested style objects)\n if (\n !prop.computed &&\n prop.key.type === 'Identifier' &&\n /^[A-Z]/.test(prop.key.name)\n ) {\n if (prop.value.type === 'ObjectExpression') {\n checkProperties(prop.value, key);\n }\n continue;\n }\n\n // Skip at-rule blocks (@keyframes, @property, ...)\n if (\n !prop.computed &&\n prop.key.type === 'Identifier' &&\n prop.key.name.startsWith('@')\n ) {\n continue;\n }\n\n if (!isStaticValue(prop.value)) {\n context.report({\n node: prop.value,\n messageId: 'dynamicStyleValue',\n data: { property: key ?? '(unknown)' },\n });\n }\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n checkProperties(node);\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;AAOA,yCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,2DACd;EACD,UAAU,EACR,mBACE,gIACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,gBACP,MACA,iBAAgC,MAC1B;AACN,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,iBAAiB;AACjC,aAAQ,OAAO;MACb,MAAM;MACN,WAAW;MACX,MAAM,EAAE,UAAU,kBAAkB,YAAY;MACjD,CAAC;AACF;;AAGF,QAAI,KAAK,SAAS,WAAY;IAE9B,MAAM,MACJ,CAAC,KAAK,YAAY,KAAK,IAAI,SAAS,eAChC,KAAK,IAAI,OACT;AAGN,QACE,CAAC,KAAK,YACN,KAAK,IAAI,SAAS,gBAClB,SAAS,KAAK,KAAK,IAAI,KAAK,EAC5B;AACA,SAAI,KAAK,MAAM,SAAS,mBACtB,iBAAgB,KAAK,OAAO,IAAI;AAElC;;AAIF,QACE,CAAC,KAAK,YACN,KAAK,IAAI,SAAS,gBAClB,KAAK,IAAI,KAAK,WAAW,IAAI,CAE7B;AAGF,QAAI,CAAC,cAAc,KAAK,MAAM,CAC5B,SAAQ,OAAO;KACb,MAAM,KAAK;KACX,WAAW;KACX,MAAM,EAAE,UAAU,OAAO,aAAa;KACvC,CAAC;;;EAKR,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAC9B,mBAAgB,KAAK;;AAGvB,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
1
+ {"version":3,"file":"no-runtime-styles-mutation.js","names":[],"sources":["../../src/rules/no-runtime-styles-mutation.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { SPECIAL_STYLE_KEYS } from '../constants.js';\nimport { getKeyName, isStaticValue, unwrapExpression } from '../utils.js';\n\ntype MessageIds = 'dynamicStyleValue' | 'dynamicStyleKey';\n\nexport default createRule<[], MessageIds>({\n name: 'no-runtime-styles-mutation',\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Warn when style objects contain JavaScript-computed values',\n },\n messages: {\n dynamicStyleValue:\n \"Style value for '{{property}}' should be static. Use states with mods, tokens, or styleProps for dynamic behavior.\",\n dynamicStyleKey:\n \"Style key for '{{property}}' should be static. Define states as literal keys instead of computing them in JavaScript.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkValue(\n node: TSESTree.Node,\n property: string,\n isStateMap = false,\n ): void {\n const value = unwrapExpression(node);\n\n if (value.type === 'ObjectExpression') {\n checkProperties(value, property, isStateMap);\n } else if (value.type === 'ArrayExpression') {\n for (const element of value.elements) {\n // A separate rule reports spreads, including array spreads.\n if (element && element.type !== 'SpreadElement') {\n checkValue(element, property);\n }\n }\n } else if (!isStaticValue(value)) {\n context.report({\n node: value,\n messageId: 'dynamicStyleValue',\n data: { property },\n });\n }\n }\n\n function checkProperties(\n node: TSESTree.ObjectExpression,\n parentProperty = '(style)',\n isStateMap = false,\n ): void {\n for (const prop of node.properties) {\n // Spreads have their own warning and explicit per-line suppression.\n if (prop.type === 'SpreadElement') continue;\n\n // At-rule definitions have their own shape and may legitimately be\n // assembled as JavaScript objects (for example, shared keyframes).\n const key = !prop.computed ? getKeyName(prop.key) : null;\n if (key && SPECIAL_STYLE_KEYS.has(key)) continue;\n\n const property =\n isStateMap || prop.computed\n ? parentProperty\n : (key ?? parentProperty);\n\n if (prop.computed) {\n context.report({\n node: prop.key,\n messageId: 'dynamicStyleKey',\n data: { property },\n });\n }\n\n const value = unwrapExpression(prop.value);\n const childIsStateMap =\n value.type === 'ObjectExpression' && ctx.isStateMap(value, prop);\n checkValue(prop.value, property, childIsStateMap);\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression): void {\n const styleCtx = ctx.getStyleContext(node);\n // tastyStatic has a stricter build-time error for dynamic values.\n if (!styleCtx || styleCtx.isStaticCall) return;\n // Walk the full tree once; sub-element objects also match AST selectors.\n if (ctx.getRootStyleObject(node) !== node) return;\n\n checkProperties(node);\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;AAQA,yCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,8DACd;EACD,UAAU;GACR,mBACE;GACF,iBACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,WACP,MACA,UACA,aAAa,OACP;GACN,MAAM,QAAQ,iBAAiB,KAAK;AAEpC,OAAI,MAAM,SAAS,mBACjB,iBAAgB,OAAO,UAAU,WAAW;YACnC,MAAM,SAAS,mBACxB;SAAK,MAAM,WAAW,MAAM,SAE1B,KAAI,WAAW,QAAQ,SAAS,gBAC9B,YAAW,SAAS,SAAS;cAGxB,CAAC,cAAc,MAAM,CAC9B,SAAQ,OAAO;IACb,MAAM;IACN,WAAW;IACX,MAAM,EAAE,UAAU;IACnB,CAAC;;EAIN,SAAS,gBACP,MACA,iBAAiB,WACjB,aAAa,OACP;AACN,QAAK,MAAM,QAAQ,KAAK,YAAY;AAElC,QAAI,KAAK,SAAS,gBAAiB;IAInC,MAAM,MAAM,CAAC,KAAK,WAAW,WAAW,KAAK,IAAI,GAAG;AACpD,QAAI,OAAO,mBAAmB,IAAI,IAAI,CAAE;IAExC,MAAM,WACJ,cAAc,KAAK,WACf,iBACC,OAAO;AAEd,QAAI,KAAK,SACP,SAAQ,OAAO;KACb,MAAM,KAAK;KACX,WAAW;KACX,MAAM,EAAE,UAAU;KACnB,CAAC;IAGJ,MAAM,QAAQ,iBAAiB,KAAK,MAAM;IAC1C,MAAM,kBACJ,MAAM,SAAS,sBAAsB,IAAI,WAAW,OAAO,KAAK;AAClE,eAAW,KAAK,OAAO,UAAU,gBAAgB;;;EAIrD,SAAS,kBAAkB,MAAuC;GAChE,MAAM,WAAW,IAAI,gBAAgB,KAAK;AAE1C,OAAI,CAAC,YAAY,SAAS,aAAc;AAExC,OAAI,IAAI,mBAAmB,KAAK,KAAK,KAAM;AAE3C,mBAAgB,KAAK;;AAGvB,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
@@ -0,0 +1,50 @@
1
+ import { createRule } from "../create-rule.js";
2
+ import { unwrapExpression } from "../utils.js";
3
+ import { TastyContext, styleObjectListeners } from "../context.js";
4
+
5
+ //#region src/rules/no-style-spread.ts
6
+ var no_style_spread_default = createRule({
7
+ name: "no-style-spread",
8
+ meta: {
9
+ type: "suggestion",
10
+ docs: { description: "Warn about spreads inside Tasty style objects" },
11
+ messages: { styleSpread: "List Tasty style properties explicitly instead of spreading. If this spread is intentional, suppress this warning on this line with an ESLint comment and a reason." },
12
+ schema: []
13
+ },
14
+ defaultOptions: [],
15
+ create(context) {
16
+ const ctx = new TastyContext(context);
17
+ function checkValue(node) {
18
+ const value = unwrapExpression(node);
19
+ if (value.type === "ObjectExpression") for (const prop of value.properties) if (prop.type === "SpreadElement") context.report({
20
+ node: prop,
21
+ messageId: "styleSpread"
22
+ });
23
+ else checkValue(prop.value);
24
+ else if (value.type === "ArrayExpression") for (const element of value.elements) {
25
+ if (!element) continue;
26
+ if (element.type === "SpreadElement") context.report({
27
+ node: element,
28
+ messageId: "styleSpread"
29
+ });
30
+ else checkValue(element);
31
+ }
32
+ }
33
+ function handleStyleObject(node) {
34
+ const styleCtx = ctx.getStyleContext(node);
35
+ if (!styleCtx || styleCtx.isStaticCall) return;
36
+ if (ctx.getRootStyleObject(node) !== node) return;
37
+ checkValue(node);
38
+ }
39
+ return {
40
+ ImportDeclaration(node) {
41
+ ctx.trackImport(node);
42
+ },
43
+ ...styleObjectListeners(handleStyleObject)
44
+ };
45
+ }
46
+ });
47
+
48
+ //#endregion
49
+ export { no_style_spread_default as default };
50
+ //# sourceMappingURL=no-style-spread.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-style-spread.js","names":[],"sources":["../../src/rules/no-style-spread.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { unwrapExpression } from '../utils.js';\n\ntype MessageIds = 'styleSpread';\n\nexport default createRule<[], MessageIds>({\n name: 'no-style-spread',\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Warn about spreads inside Tasty style objects',\n },\n messages: {\n styleSpread:\n 'List Tasty style properties explicitly instead of spreading. If this spread is intentional, suppress this warning on this line with an ESLint comment and a reason.',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkValue(node: TSESTree.Node): void {\n const value = unwrapExpression(node);\n\n if (value.type === 'ObjectExpression') {\n for (const prop of value.properties) {\n if (prop.type === 'SpreadElement') {\n context.report({ node: prop, messageId: 'styleSpread' });\n } else {\n checkValue(prop.value);\n }\n }\n } else if (value.type === 'ArrayExpression') {\n for (const element of value.elements) {\n if (!element) continue;\n if (element.type === 'SpreadElement') {\n context.report({ node: element, messageId: 'styleSpread' });\n } else {\n checkValue(element);\n }\n }\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression): void {\n const styleCtx = ctx.getStyleContext(node);\n // tastyStatic rejects spreads during extraction, so its existing error\n // gives the useful diagnostic without a second warning.\n if (!styleCtx || styleCtx.isStaticCall) return;\n if (ctx.getRootStyleObject(node) !== node) return;\n\n checkValue(node);\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;AAOA,8BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,iDACd;EACD,UAAU,EACR,aACE,uKACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,WAAW,MAA2B;GAC7C,MAAM,QAAQ,iBAAiB,KAAK;AAEpC,OAAI,MAAM,SAAS,mBACjB,MAAK,MAAM,QAAQ,MAAM,WACvB,KAAI,KAAK,SAAS,gBAChB,SAAQ,OAAO;IAAE,MAAM;IAAM,WAAW;IAAe,CAAC;OAExD,YAAW,KAAK,MAAM;YAGjB,MAAM,SAAS,kBACxB,MAAK,MAAM,WAAW,MAAM,UAAU;AACpC,QAAI,CAAC,QAAS;AACd,QAAI,QAAQ,SAAS,gBACnB,SAAQ,OAAO;KAAE,MAAM;KAAS,WAAW;KAAe,CAAC;QAE3D,YAAW,QAAQ;;;EAM3B,SAAS,kBAAkB,MAAuC;GAChE,MAAM,WAAW,IAAI,gBAAgB,KAAK;AAG1C,OAAI,CAAC,YAAY,SAAS,aAAc;AACxC,OAAI,IAAI,mBAAmB,KAAK,KAAK,KAAM;AAE3C,cAAW,KAAK;;AAGlB,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
@@ -0,0 +1,65 @@
1
+ import { createRule } from "../create-rule.js";
2
+ import { getKeyName, getStringValue } from "../utils.js";
3
+ import { TastyContext, styleObjectListeners } from "../context.js";
4
+ import { getStateNegationEdits } from "../parsers/state-key-parser.js";
5
+ import { replaceInStringValue } from "../fix-utils.js";
6
+
7
+ //#region src/rules/prefer-state-negation.ts
8
+ function applyEdits(value, edits) {
9
+ return edits.reduceRight((result, edit) => result.slice(0, edit.start) + edit.replacement + result.slice(edit.end), value);
10
+ }
11
+ var prefer_state_negation_default = createRule({
12
+ name: "prefer-state-negation",
13
+ meta: {
14
+ type: "suggestion",
15
+ fixable: "code",
16
+ docs: { description: "Prefer the ! prefix over top-level :not() in state keys" },
17
+ messages: { preferStateNegation: "Use the '!' prefix instead of top-level :not() in state keys." },
18
+ schema: []
19
+ },
20
+ defaultOptions: [],
21
+ create(context) {
22
+ const ctx = new TastyContext(context);
23
+ function handleStyleObject(node) {
24
+ if (!ctx.isStyleObject(node)) return;
25
+ for (const prop of node.properties) {
26
+ if (prop.type !== "Property" || prop.computed) continue;
27
+ const key = getKeyName(prop.key);
28
+ if (key === null || /^[A-Z@&]/.test(key) || prop.value.type !== "ObjectExpression") continue;
29
+ const stateMap = prop.value;
30
+ const hasUnknownKeys = stateMap.properties.some((stateProp) => stateProp.type !== "Property" || stateProp.computed && getStringValue(stateProp.key) === null);
31
+ for (const stateProp of stateMap.properties) {
32
+ if (stateProp.type !== "Property") continue;
33
+ const stateKey = !stateProp.computed ? getKeyName(stateProp.key) : getStringValue(stateProp.key);
34
+ if (stateKey === null) continue;
35
+ const edits = getStateNegationEdits(stateKey);
36
+ if (edits.length === 0) continue;
37
+ context.report({
38
+ node: stateProp.key,
39
+ messageId: "preferStateNegation",
40
+ fix(fixer) {
41
+ if (hasUnknownKeys) return null;
42
+ const replacement = applyEdits(stateKey, edits);
43
+ if (stateMap.properties.some((other) => {
44
+ if (other === stateProp || other.type !== "Property") return false;
45
+ const otherKey = other.computed ? getStringValue(other.key) : getKeyName(other.key);
46
+ return otherKey !== null && applyEdits(otherKey, getStateNegationEdits(otherKey)) === replacement;
47
+ })) return null;
48
+ return replaceInStringValue(fixer, stateProp.key, edits, context.sourceCode);
49
+ }
50
+ });
51
+ }
52
+ }
53
+ }
54
+ return {
55
+ ImportDeclaration(node) {
56
+ ctx.trackImport(node);
57
+ },
58
+ ...styleObjectListeners(handleStyleObject)
59
+ };
60
+ }
61
+ });
62
+
63
+ //#endregion
64
+ export { prefer_state_negation_default as default };
65
+ //# sourceMappingURL=prefer-state-negation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer-state-negation.js","names":[],"sources":["../../src/rules/prefer-state-negation.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 { getStateNegationEdits } from '../parsers/state-key-parser.js';\nimport { replaceInStringValue } from '../fix-utils.js';\nimport type { StringEdit } from '../fix-utils.js';\n\nfunction applyEdits(value: string, edits: StringEdit[]): string {\n return edits.reduceRight(\n (result, edit) =>\n result.slice(0, edit.start) + edit.replacement + result.slice(edit.end),\n value,\n );\n}\n\nexport default createRule<[], 'preferStateNegation'>({\n name: 'prefer-state-negation',\n meta: {\n type: 'suggestion',\n fixable: 'code',\n docs: {\n description: 'Prefer the ! prefix over top-level :not() in state keys',\n },\n messages: {\n preferStateNegation:\n \"Use the '!' prefix instead of top-level :not() in state keys.\",\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 const key = getKeyName(prop.key);\n if (\n key === null ||\n /^[A-Z@&]/.test(key) ||\n prop.value.type !== 'ObjectExpression'\n )\n continue;\n\n const stateMap = prop.value;\n // Unknown keys can collide with the replacement. Overwriting one can\n // also move a state's authored priority to an earlier object slot.\n const hasUnknownKeys = stateMap.properties.some(\n (stateProp) =>\n stateProp.type !== 'Property' ||\n (stateProp.computed && getStringValue(stateProp.key) === null),\n );\n\n for (const stateProp of stateMap.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateKey = !stateProp.computed\n ? getKeyName(stateProp.key)\n : getStringValue(stateProp.key);\n if (stateKey === null) continue;\n\n const edits = getStateNegationEdits(stateKey);\n if (edits.length === 0) continue;\n\n // Fix a whole key together so repeated :not() atoms never compete.\n context.report({\n node: stateProp.key,\n messageId: 'preferStateNegation',\n fix(fixer) {\n if (hasUnknownKeys) return null;\n const replacement = applyEdits(stateKey, edits);\n // Never overwrite another entry when two spellings coexist.\n const collision = stateMap.properties.some((other) => {\n if (other === stateProp || other.type !== 'Property')\n return false;\n const otherKey = other.computed\n ? getStringValue(other.key)\n : getKeyName(other.key);\n return (\n otherKey !== null &&\n applyEdits(otherKey, getStateNegationEdits(otherKey)) ===\n replacement\n );\n });\n if (collision) return null;\n return replaceInStringValue(\n fixer,\n stateProp.key,\n edits,\n context.sourceCode,\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":";;;;;;;AAQA,SAAS,WAAW,OAAe,OAA6B;AAC9D,QAAO,MAAM,aACV,QAAQ,SACP,OAAO,MAAM,GAAG,KAAK,MAAM,GAAG,KAAK,cAAc,OAAO,MAAM,KAAK,IAAI,EACzE,MACD;;AAGH,oCAAe,WAAsC;CACnD,MAAM;CACN,MAAM;EACJ,MAAM;EACN,SAAS;EACT,MAAM,EACJ,aAAa,2DACd;EACD,UAAU,EACR,qBACE,iEACH;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;IAC/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QACE,QAAQ,QACR,WAAW,KAAK,IAAI,IACpB,KAAK,MAAM,SAAS,mBAEpB;IAEF,MAAM,WAAW,KAAK;IAGtB,MAAM,iBAAiB,SAAS,WAAW,MACxC,cACC,UAAU,SAAS,cAClB,UAAU,YAAY,eAAe,UAAU,IAAI,KAAK,KAC5D;AAED,SAAK,MAAM,aAAa,SAAS,YAAY;AAC3C,SAAI,UAAU,SAAS,WAAY;KACnC,MAAM,WAAW,CAAC,UAAU,WACxB,WAAW,UAAU,IAAI,GACzB,eAAe,UAAU,IAAI;AACjC,SAAI,aAAa,KAAM;KAEvB,MAAM,QAAQ,sBAAsB,SAAS;AAC7C,SAAI,MAAM,WAAW,EAAG;AAGxB,aAAQ,OAAO;MACb,MAAM,UAAU;MAChB,WAAW;MACX,IAAI,OAAO;AACT,WAAI,eAAgB,QAAO;OAC3B,MAAM,cAAc,WAAW,UAAU,MAAM;AAc/C,WAZkB,SAAS,WAAW,MAAM,UAAU;AACpD,YAAI,UAAU,aAAa,MAAM,SAAS,WACxC,QAAO;QACT,MAAM,WAAW,MAAM,WACnB,eAAe,MAAM,IAAI,GACzB,WAAW,MAAM,IAAI;AACzB,eACE,aAAa,QACb,WAAW,UAAU,sBAAsB,SAAS,CAAC,KACnD;SAEJ,CACa,QAAO;AACtB,cAAO,qBACL,OACA,UAAU,KACV,OACA,QAAQ,WACT;;MAEJ,CAAC;;;;AAKR,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": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "ESLint plugin for validating tasty() and tastyStatic() style objects",
5
5
  "type": "module",
6
6
  "exports": {