@tenphi/eslint-plugin-tasty 1.2.0 → 1.4.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 +47 -1
- package/dist/configs.js +4 -1
- package/dist/configs.js.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/parsers/state-key-parser.js +25 -1
- package/dist/parsers/state-key-parser.js.map +1 -1
- package/dist/rules/no-runtime-styles-mutation.js +33 -24
- package/dist/rules/no-runtime-styles-mutation.js.map +1 -1
- package/dist/rules/no-state-in-selector.js +85 -0
- package/dist/rules/no-state-in-selector.js.map +1 -0
- package/dist/rules/no-style-spread.js +50 -0
- package/dist/rules/no-style-spread.js.map +1 -0
- package/dist/rules/prefer-state-negation.js +65 -0
- package/dist/rules/prefer-state-negation.js.map +1 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -185,6 +185,7 @@ for typing shared signatures.
|
|
|
185
185
|
| `tasty/valid-recipe` | error | Unknown recipe names |
|
|
186
186
|
| `tasty/valid-transition` | warn | Unknown transition property names |
|
|
187
187
|
| `tasty/no-nested-selector` | warn | `&`-prefixed nested selectors (use sub-elements) |
|
|
188
|
+
| `tasty/no-state-in-selector` | warn | Pseudo-class and attribute conditions in sub-element `$` selectors; put conditions in property state maps ([details](docs/rules/no-state-in-selector.md)) |
|
|
188
189
|
| `tasty/static-no-dynamic-values` | error | Dynamic values in `tastyStatic()` |
|
|
189
190
|
| `tasty/static-valid-selector` | error | Invalid selector in `tastyStatic(selector, ...)` |
|
|
190
191
|
| `tasty/require-default-state` | error | Missing default (`''`) or fallback floor (`_`) key in state mappings (skipped for extending calls) |
|
|
@@ -193,8 +194,11 @@ for typing shared signatures.
|
|
|
193
194
|
| `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
195
|
| `tasty/no-raw-color-values` | warn | Raw hex/rgb/`okhsl`/`okhst`/`oklch`/named colors instead of `#color` tokens |
|
|
195
196
|
| `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 |
|
|
197
|
+
| `tasty/no-runtime-styles-mutation` | warn | JavaScript variables, calls, conditionals, computed keys, or interpolated templates in Tasty style values; use states and tokens instead |
|
|
198
|
+
| `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
199
|
| `tasty/consistent-token-usage` | warn | Raw px values when custom units or tokens exist |
|
|
197
200
|
| `tasty/prefer-auto-calc` | warn | `calc(...)` instead of Tasty auto-calc `(...)` |
|
|
201
|
+
| `tasty/prefer-state-negation` | warn | Top-level `:not(...)` in state keys instead of the `!` prefix (autofixable; nested CSS selectors stay intact) |
|
|
198
202
|
| `tasty/prefer-custom-property-syntax` | warn | `var(--prop)` / `$x-color` / `transparent` / `currentColor` instead of `$prop` / `#color` / `#clear` / `#current` |
|
|
199
203
|
| `tasty/prefer-hide` | warn | `display: 'none'` instead of `hide: true` |
|
|
200
204
|
| `tasty/prefer-directional-shorthand` | warn | 4-value `margin`/`padding`/`inset`/`radius`/`fade`/`border` with placeholder positions instead of directional form |
|
|
@@ -208,7 +212,49 @@ for typing shared signatures.
|
|
|
208
212
|
| `tasty/valid-state-definition` | warn | Invalid state definition values in `configure()` or `tasty.config` |
|
|
209
213
|
| `tasty/no-unknown-state-alias` | warn | Unknown `@name` state aliases |
|
|
210
214
|
| `tasty/no-styles-prop` | warn | Direct `styles` prop usage |
|
|
211
|
-
|
|
215
|
+
|
|
216
|
+
### Structural selectors and state maps
|
|
217
|
+
|
|
218
|
+
Keep a sub-element's `$` selector focused on the element's identity and position.
|
|
219
|
+
`tasty/no-state-in-selector` warns on conditions such as `img:not([width])`,
|
|
220
|
+
`button[disabled]`, and `>@:hover`. Express those conditions in property state
|
|
221
|
+
maps, using `@own(...)` for the sub-element's own state. Root states stay outside
|
|
222
|
+
`@own(...)`.
|
|
223
|
+
|
|
224
|
+
Tags, combinators, classes, IDs, exact `[data-element="Name"]` identities, and
|
|
225
|
+
pseudo-elements remain valid structural selectors. `:is()` and `:where()` may
|
|
226
|
+
group structural selectors, but conditions inside them still produce a warning:
|
|
227
|
+
`:where(picture)` is accepted, while `:where(img:not([width]))` is reported.
|
|
228
|
+
The rule does not autofix because moving a condition can change its scope and
|
|
229
|
+
the behavior of other properties. See the [rule guide](docs/rules/no-state-in-selector.md)
|
|
230
|
+
for examples and migration advice.
|
|
231
|
+
|
|
232
|
+
### Static values and intentional spreads
|
|
233
|
+
|
|
234
|
+
The recommended rules flag JavaScript-computed style values, including
|
|
235
|
+
identifiers, conditionals, function calls, computed keys, and template
|
|
236
|
+
interpolation. Use a Tasty state map with `mods` for discrete states, or a token
|
|
237
|
+
for a value that changes per instance. Literal CSS strings, token references,
|
|
238
|
+
and state maps are accepted.
|
|
239
|
+
|
|
240
|
+
Spreads in runtime style objects get a separate warning because they hide which
|
|
241
|
+
properties are defined and can make overrides hard to review. If a spread is
|
|
242
|
+
necessary, suppress just that line and explain why:
|
|
243
|
+
|
|
244
|
+
```js
|
|
245
|
+
const Card = tasty({
|
|
246
|
+
styles: {
|
|
247
|
+
// eslint-disable-next-line tasty/no-style-spread -- generated defaults are shared by all cards
|
|
248
|
+
...sharedStyles,
|
|
249
|
+
fill: { '': '#surface', active: '#primary' },
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
`tastyStatic()` still rejects spreads and dynamic expressions as errors because
|
|
255
|
+
its build-time extractor cannot evaluate them. An ESLint suppression does not
|
|
256
|
+
make a spread valid there. To make warnings fail CI until addressed or explicitly
|
|
257
|
+
suppressed, run ESLint with `--max-warnings=0`.
|
|
212
258
|
|
|
213
259
|
### Optional motion duration rule
|
|
214
260
|
|
package/dist/configs.js
CHANGED
|
@@ -13,6 +13,7 @@ const recommended = {
|
|
|
13
13
|
"tasty/valid-directional-modifier": "error",
|
|
14
14
|
"tasty/valid-radius-shape": "error",
|
|
15
15
|
"tasty/no-nested-selector": "error",
|
|
16
|
+
"tasty/no-state-in-selector": "warn",
|
|
16
17
|
"tasty/static-no-dynamic-values": "error",
|
|
17
18
|
"tasty/static-valid-selector": "error",
|
|
18
19
|
"tasty/valid-preset": "error",
|
|
@@ -24,8 +25,11 @@ const recommended = {
|
|
|
24
25
|
"tasty/prefer-shorthand-property": "warn",
|
|
25
26
|
"tasty/no-raw-color-values": "warn",
|
|
26
27
|
"tasty/no-raw-transition-duration": "warn",
|
|
28
|
+
"tasty/no-runtime-styles-mutation": "warn",
|
|
29
|
+
"tasty/no-style-spread": "warn",
|
|
27
30
|
"tasty/consistent-token-usage": "warn",
|
|
28
31
|
"tasty/prefer-auto-calc": "warn",
|
|
32
|
+
"tasty/prefer-state-negation": "warn",
|
|
29
33
|
"tasty/prefer-custom-property-syntax": "warn",
|
|
30
34
|
"tasty/prefer-hide": "warn",
|
|
31
35
|
"tasty/prefer-directional-shorthand": "warn",
|
|
@@ -36,7 +40,6 @@ const strict = {
|
|
|
36
40
|
"tasty/valid-custom-property": "warn",
|
|
37
41
|
"tasty/no-unknown-state-alias": "warn",
|
|
38
42
|
"tasty/no-styles-prop": "warn",
|
|
39
|
-
"tasty/no-runtime-styles-mutation": "warn",
|
|
40
43
|
"tasty/valid-state-definition": "warn"
|
|
41
44
|
};
|
|
42
45
|
|
package/dist/configs.js.map
CHANGED
|
@@ -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/
|
|
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/no-state-in-selector': 'warn',\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,8BAA8B;CAC9B,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
|
@@ -5,6 +5,7 @@ import valid_custom_unit_default from "./rules/valid-custom-unit.js";
|
|
|
5
5
|
import valid_state_key_default from "./rules/valid-state-key.js";
|
|
6
6
|
import valid_styles_structure_default from "./rules/valid-styles-structure.js";
|
|
7
7
|
import no_nested_selector_default from "./rules/no-nested-selector.js";
|
|
8
|
+
import no_state_in_selector_default from "./rules/no-state-in-selector.js";
|
|
8
9
|
import valid_custom_property_default from "./rules/valid-custom-property.js";
|
|
9
10
|
import valid_preset_default from "./rules/valid-preset.js";
|
|
10
11
|
import valid_recipe_default from "./rules/valid-recipe.js";
|
|
@@ -26,10 +27,12 @@ import no_raw_motion_duration_default from "./rules/no-raw-motion-duration.js";
|
|
|
26
27
|
import no_styles_prop_default from "./rules/no-styles-prop.js";
|
|
27
28
|
import consistent_token_usage_default from "./rules/consistent-token-usage.js";
|
|
28
29
|
import no_runtime_styles_mutation_default from "./rules/no-runtime-styles-mutation.js";
|
|
30
|
+
import no_style_spread_default from "./rules/no-style-spread.js";
|
|
29
31
|
import valid_state_definition_default from "./rules/valid-state-definition.js";
|
|
30
32
|
import no_own_at_root_default from "./rules/no-own-at-root.js";
|
|
31
33
|
import valid_default_state_order_default from "./rules/valid-default-state-order.js";
|
|
32
34
|
import prefer_auto_calc_default from "./rules/prefer-auto-calc.js";
|
|
35
|
+
import prefer_state_negation_default from "./rules/prefer-state-negation.js";
|
|
33
36
|
import prefer_custom_property_syntax_default from "./rules/prefer-custom-property-syntax.js";
|
|
34
37
|
import prefer_hide_default from "./rules/prefer-hide.js";
|
|
35
38
|
import prefer_directional_shorthand_default from "./rules/prefer-directional-shorthand.js";
|
|
@@ -46,6 +49,7 @@ const ruleMap = {
|
|
|
46
49
|
"valid-state-key": valid_state_key_default,
|
|
47
50
|
"valid-styles-structure": valid_styles_structure_default,
|
|
48
51
|
"no-nested-selector": no_nested_selector_default,
|
|
52
|
+
"no-state-in-selector": no_state_in_selector_default,
|
|
49
53
|
"valid-custom-property": valid_custom_property_default,
|
|
50
54
|
"valid-preset": valid_preset_default,
|
|
51
55
|
"valid-recipe": valid_recipe_default,
|
|
@@ -67,10 +71,12 @@ const ruleMap = {
|
|
|
67
71
|
"no-styles-prop": no_styles_prop_default,
|
|
68
72
|
"consistent-token-usage": consistent_token_usage_default,
|
|
69
73
|
"no-runtime-styles-mutation": no_runtime_styles_mutation_default,
|
|
74
|
+
"no-style-spread": no_style_spread_default,
|
|
70
75
|
"valid-state-definition": valid_state_definition_default,
|
|
71
76
|
"no-own-at-root": no_own_at_root_default,
|
|
72
77
|
"valid-default-state-order": valid_default_state_order_default,
|
|
73
78
|
"prefer-auto-calc": prefer_auto_calc_default,
|
|
79
|
+
"prefer-state-negation": prefer_state_negation_default,
|
|
74
80
|
"prefer-custom-property-syntax": prefer_custom_property_syntax_default,
|
|
75
81
|
"prefer-hide": prefer_hide_default,
|
|
76
82
|
"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.noRawMotionDuration","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-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 '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":"
|
|
1
|
+
{"version":3,"file":"index.js","names":["rules.knownProperty","rules.validValue","rules.validColorToken","rules.validCustomUnit","rules.validStateKey","rules.validStylesStructure","rules.noNestedSelector","rules.noStateInSelector","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 'no-state-in-selector': rules.noStateInSelector,\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,wBAAwBC;CACxB,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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"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createRule } from "../create-rule.js";
|
|
2
|
-
import {
|
|
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
|
|
11
|
-
messages: {
|
|
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
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
|
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,85 @@
|
|
|
1
|
+
import { createRule } from "../create-rule.js";
|
|
2
|
+
import { getKeyName, getStringValue, unwrapExpression } from "../utils.js";
|
|
3
|
+
import { TastyContext, styleObjectListeners } from "../context.js";
|
|
4
|
+
import selectorParser from "postcss-selector-parser";
|
|
5
|
+
|
|
6
|
+
//#region src/rules/no-state-in-selector.ts
|
|
7
|
+
const STRUCTURAL_PSEUDOS = new Set([":is", ":where"]);
|
|
8
|
+
const LEGACY_PSEUDO_ELEMENTS = new Set([
|
|
9
|
+
":before",
|
|
10
|
+
":after",
|
|
11
|
+
":first-line",
|
|
12
|
+
":first-letter"
|
|
13
|
+
]);
|
|
14
|
+
function firstStateCondition(selector) {
|
|
15
|
+
let root;
|
|
16
|
+
try {
|
|
17
|
+
root = selectorParser().astSync(selector);
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
let condition = null;
|
|
22
|
+
root.walk((node) => {
|
|
23
|
+
if (node.type === "attribute") {
|
|
24
|
+
if (node.attribute === "data-element" && !node.namespace && node.operator === "=" && node.value) return;
|
|
25
|
+
} else if (node.type === "pseudo") {
|
|
26
|
+
let name = node.value.toLowerCase();
|
|
27
|
+
if (name.includes("\\")) {
|
|
28
|
+
const colons = name.startsWith("::") ? "::" : ":";
|
|
29
|
+
const identifier = selectorParser().astSync(name.slice(colons.length)).first?.first;
|
|
30
|
+
if (identifier?.type === "tag") name = colons + identifier.value.toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
if (name.startsWith("::") || LEGACY_PSEUDO_ELEMENTS.has(name) || STRUCTURAL_PSEUDOS.has(name)) return;
|
|
33
|
+
} else return;
|
|
34
|
+
condition = node.toString().trim();
|
|
35
|
+
return false;
|
|
36
|
+
});
|
|
37
|
+
return condition;
|
|
38
|
+
}
|
|
39
|
+
var no_state_in_selector_default = createRule({
|
|
40
|
+
name: "no-state-in-selector",
|
|
41
|
+
meta: {
|
|
42
|
+
type: "suggestion",
|
|
43
|
+
docs: { description: "Keep sub-element selectors structural and express conditions in property state maps" },
|
|
44
|
+
messages: { stateInSelector: "Move state condition '{{condition}}' out of the sub-element '$' selector and into property state maps. Use @own(...) for the sub-element's own state." },
|
|
45
|
+
schema: []
|
|
46
|
+
},
|
|
47
|
+
defaultOptions: [],
|
|
48
|
+
create(context) {
|
|
49
|
+
const ctx = new TastyContext(context);
|
|
50
|
+
const visited = /* @__PURE__ */ new WeakSet();
|
|
51
|
+
function handleStyleObject(node) {
|
|
52
|
+
if (visited.has(node) || !ctx.isStyleObject(node)) return;
|
|
53
|
+
visited.add(node);
|
|
54
|
+
const isSubElement = ctx.isInsideSubElement(node);
|
|
55
|
+
for (const prop of node.properties) {
|
|
56
|
+
if (prop.type !== "Property") continue;
|
|
57
|
+
const key = prop.computed ? getStringValue(unwrapExpression(prop.key)) : getKeyName(prop.key);
|
|
58
|
+
if (!prop.computed && key && ctx.isSubElementKey(key)) {
|
|
59
|
+
const child = unwrapExpression(prop.value);
|
|
60
|
+
if (child.type === "ObjectExpression") handleStyleObject(child);
|
|
61
|
+
}
|
|
62
|
+
if (!isSubElement || key !== "$") continue;
|
|
63
|
+
const selector = getStringValue(unwrapExpression(prop.value));
|
|
64
|
+
if (selector === null) continue;
|
|
65
|
+
const condition = firstStateCondition(selector);
|
|
66
|
+
if (condition === null) continue;
|
|
67
|
+
context.report({
|
|
68
|
+
node: prop.value,
|
|
69
|
+
messageId: "stateInSelector",
|
|
70
|
+
data: { condition }
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
ImportDeclaration(node) {
|
|
76
|
+
ctx.trackImport(node);
|
|
77
|
+
},
|
|
78
|
+
...styleObjectListeners(handleStyleObject)
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
//#endregion
|
|
84
|
+
export { no_state_in_selector_default as default };
|
|
85
|
+
//# sourceMappingURL=no-state-in-selector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-state-in-selector.js","names":[],"sources":["../../src/rules/no-state-in-selector.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport selectorParser from 'postcss-selector-parser';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue, unwrapExpression } from '../utils.js';\n\nconst STRUCTURAL_PSEUDOS = new Set([':is', ':where']);\nconst LEGACY_PSEUDO_ELEMENTS = new Set([\n ':before',\n ':after',\n ':first-line',\n ':first-letter',\n]);\n\nfunction firstStateCondition(selector: string): string | null {\n let root: selectorParser.Root;\n try {\n root = selectorParser().astSync(selector);\n } catch {\n // This recommendation is not a CSS syntax validator. Incomplete selectors\n // must not crash lint or produce a misleading state-map recommendation.\n return null;\n }\n\n let condition: string | null = null;\n root.walk((node) => {\n if (node.type === 'attribute') {\n // An exact data-element identity is structural, like Tasty's uppercase\n // element names. Other attributes express conditions on that element.\n if (\n node.attribute === 'data-element' &&\n !node.namespace &&\n node.operator === '=' &&\n node.value\n ) {\n return;\n }\n } else if (node.type === 'pseudo') {\n let name = node.value.toLowerCase();\n if (name.includes('\\\\')) {\n // The parser decodes tag identifiers but preserves pseudo spelling.\n const colons = name.startsWith('::') ? '::' : ':';\n const identifier = selectorParser().astSync(name.slice(colons.length))\n .first?.first;\n if (identifier?.type === 'tag') {\n name = colons + identifier.value.toLowerCase();\n }\n }\n if (\n name.startsWith('::') ||\n LEGACY_PSEUDO_ELEMENTS.has(name) ||\n STRUCTURAL_PSEUDOS.has(name)\n ) {\n // Continue walking: :where(img:hover) and ::slotted(img[width])\n // still contain conditions, although the outer wrapper is structural.\n return;\n }\n } else {\n return;\n }\n\n condition = node.toString().trim();\n return false;\n });\n return condition;\n}\n\nexport default createRule<[], 'stateInSelector'>({\n name: 'no-state-in-selector',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Keep sub-element selectors structural and express conditions in property state maps',\n },\n messages: {\n stateInSelector:\n \"Move state condition '{{condition}}' out of the sub-element '$' selector and into property state maps. Use @own(...) for the sub-element's own state.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n const visited = new WeakSet<TSESTree.ObjectExpression>();\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (visited.has(node) || !ctx.isStyleObject(node)) return;\n visited.add(node);\n const isSubElement = ctx.isInsideSubElement(node);\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property') continue;\n const key = prop.computed\n ? getStringValue(unwrapExpression(prop.key))\n : getKeyName(prop.key);\n // Some listeners match only the shared/JSX root. Follow its owned\n // sub-elements explicitly, without entering state maps or metadata.\n if (!prop.computed && key && ctx.isSubElementKey(key)) {\n const child = unwrapExpression(prop.value);\n if (child.type === 'ObjectExpression') handleStyleObject(child);\n }\n if (!isSubElement || key !== '$') continue;\n\n const selector = getStringValue(unwrapExpression(prop.value));\n if (selector === null) continue;\n const condition = firstStateCondition(selector);\n if (condition === null) continue;\n\n context.report({\n node: prop.value,\n messageId: 'stateInSelector',\n data: { condition },\n });\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;AAMA,MAAM,qBAAqB,IAAI,IAAI,CAAC,OAAO,SAAS,CAAC;AACrD,MAAM,yBAAyB,IAAI,IAAI;CACrC;CACA;CACA;CACA;CACD,CAAC;AAEF,SAAS,oBAAoB,UAAiC;CAC5D,IAAI;AACJ,KAAI;AACF,SAAO,gBAAgB,CAAC,QAAQ,SAAS;SACnC;AAGN,SAAO;;CAGT,IAAI,YAA2B;AAC/B,MAAK,MAAM,SAAS;AAClB,MAAI,KAAK,SAAS,aAGhB;OACE,KAAK,cAAc,kBACnB,CAAC,KAAK,aACN,KAAK,aAAa,OAClB,KAAK,MAEL;aAEO,KAAK,SAAS,UAAU;GACjC,IAAI,OAAO,KAAK,MAAM,aAAa;AACnC,OAAI,KAAK,SAAS,KAAK,EAAE;IAEvB,MAAM,SAAS,KAAK,WAAW,KAAK,GAAG,OAAO;IAC9C,MAAM,aAAa,gBAAgB,CAAC,QAAQ,KAAK,MAAM,OAAO,OAAO,CAAC,CACnE,OAAO;AACV,QAAI,YAAY,SAAS,MACvB,QAAO,SAAS,WAAW,MAAM,aAAa;;AAGlD,OACE,KAAK,WAAW,KAAK,IACrB,uBAAuB,IAAI,KAAK,IAChC,mBAAmB,IAAI,KAAK,CAI5B;QAGF;AAGF,cAAY,KAAK,UAAU,CAAC,MAAM;AAClC,SAAO;GACP;AACF,QAAO;;AAGT,mCAAe,WAAkC;CAC/C,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,uFACH;EACD,UAAU,EACR,iBACE,yJACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EACrC,MAAM,0BAAU,IAAI,SAAoC;EAExD,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,QAAQ,IAAI,KAAK,IAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AACnD,WAAQ,IAAI,KAAK;GACjB,MAAM,eAAe,IAAI,mBAAmB,KAAK;AAEjD,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,WAAY;IAC9B,MAAM,MAAM,KAAK,WACb,eAAe,iBAAiB,KAAK,IAAI,CAAC,GAC1C,WAAW,KAAK,IAAI;AAGxB,QAAI,CAAC,KAAK,YAAY,OAAO,IAAI,gBAAgB,IAAI,EAAE;KACrD,MAAM,QAAQ,iBAAiB,KAAK,MAAM;AAC1C,SAAI,MAAM,SAAS,mBAAoB,mBAAkB,MAAM;;AAEjE,QAAI,CAAC,gBAAgB,QAAQ,IAAK;IAElC,MAAM,WAAW,eAAe,iBAAiB,KAAK,MAAM,CAAC;AAC7D,QAAI,aAAa,KAAM;IACvB,MAAM,YAAY,oBAAoB,SAAS;AAC/C,QAAI,cAAc,KAAM;AAExB,YAAQ,OAAO;KACb,MAAM,KAAK;KACX,WAAW;KACX,MAAM,EAAE,WAAW;KACpB,CAAC;;;AAIN,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|
|
@@ -0,0 +1,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.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "ESLint plugin for validating tasty() and tastyStatic() style objects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -44,7 +44,8 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@typescript-eslint/utils": "^8.56.0",
|
|
47
|
-
"jiti": "^2.6.1"
|
|
47
|
+
"jiti": "^2.6.1",
|
|
48
|
+
"postcss-selector-parser": "^7.1.6"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"@changesets/changelog-github": "^0.5.2",
|