@tenphi/eslint-plugin-tasty 1.3.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 +17 -0
- package/dist/configs.js +1 -0
- package/dist/configs.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.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/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) |
|
|
@@ -212,6 +213,22 @@ for typing shared signatures.
|
|
|
212
213
|
| `tasty/no-unknown-state-alias` | warn | Unknown `@name` state aliases |
|
|
213
214
|
| `tasty/no-styles-prop` | warn | Direct `styles` prop usage |
|
|
214
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
|
+
|
|
215
232
|
### Static values and intentional spreads
|
|
216
233
|
|
|
217
234
|
The recommended rules flag JavaScript-computed style values, including
|
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",
|
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/no-runtime-styles-mutation': 'warn',\n 'tasty/no-style-spread': 'warn',\n 'tasty/consistent-token-usage': 'warn',\n 'tasty/prefer-auto-calc': 'warn',\n 'tasty/prefer-state-negation': 'warn',\n 'tasty/prefer-custom-property-syntax': 'warn',\n 'tasty/prefer-hide': 'warn',\n 'tasty/prefer-directional-shorthand': 'warn',\n 'tasty/prefer-longhand-property': 'error',\n};\n\nexport const strict: TSESLint.SharedConfig.RulesRecord = {\n ...recommended,\n 'tasty/valid-custom-property': 'warn',\n 'tasty/no-unknown-state-alias': 'warn',\n 'tasty/no-styles-prop': 'warn',\n 'tasty/valid-state-definition': 'warn',\n};\n"],"mappings":";AAEA,MAAa,cAAiD;CAC5D,wBAAwB;CACxB,qBAAqB;CACrB,2BAA2B;CAC3B,2BAA2B;CAC3B,gCAAgC;CAChC,yBAAyB;CACzB,gCAAgC;CAChC,6BAA6B;CAC7B,sBAAsB;CACtB,2BAA2B;CAC3B,oCAAoC;CACpC,4BAA4B;CAC5B,4BAA4B;CAC5B,kCAAkC;CAClC,+BAA+B;CAC/B,sBAAsB;CACtB,sBAAsB;CACtB,0BAA0B;CAC1B,+BAA+B;CAC/B,wBAAwB;CACxB,mCAAmC;CACnC,mCAAmC;CACnC,6BAA6B;CAC7B,oCAAoC;CACpC,oCAAoC;CACpC,yBAAyB;CACzB,gCAAgC;CAChC,0BAA0B;CAC1B,+BAA+B;CAC/B,uCAAuC;CACvC,qBAAqB;CACrB,sCAAsC;CACtC,kCAAkC;CACnC;AAED,MAAa,SAA4C;CACvD,GAAG;CACH,+BAA+B;CAC/B,gCAAgC;CAChC,wBAAwB;CACxB,gCAAgC;CACjC"}
|
|
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";
|
|
@@ -48,6 +49,7 @@ const ruleMap = {
|
|
|
48
49
|
"valid-state-key": valid_state_key_default,
|
|
49
50
|
"valid-styles-structure": valid_styles_structure_default,
|
|
50
51
|
"no-nested-selector": no_nested_selector_default,
|
|
52
|
+
"no-state-in-selector": no_state_in_selector_default,
|
|
51
53
|
"valid-custom-property": valid_custom_property_default,
|
|
52
54
|
"valid-preset": valid_preset_default,
|
|
53
55
|
"valid-recipe": valid_recipe_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.noStyleSpread","rules.validStateDefinition","rules.noOwnAtRoot","rules.validDefaultStateOrder","rules.preferAutoCalc","rules.preferStateNegation","rules.preferCustomPropertySyntax","rules.preferHide","rules.preferDirectionalShorthand","rules.preferLonghandProperty"],"sources":["../src/index.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport type { TSESLint } from '@typescript-eslint/utils';\nimport * as rules from './rules/index.js';\nimport { recommended, strict } from './configs.js';\n\nconst ruleMap: Record<string, TSESLint.RuleModule<string, unknown[]>> = {\n 'known-property': rules.knownProperty,\n 'valid-value': rules.validValue,\n 'valid-color-token': rules.validColorToken,\n 'valid-custom-unit': rules.validCustomUnit,\n 'valid-state-key': rules.validStateKey,\n 'valid-styles-structure': rules.validStylesStructure,\n 'no-nested-selector': rules.noNestedSelector,\n 'valid-custom-property': rules.validCustomProperty,\n 'valid-preset': rules.validPreset,\n 'valid-recipe': rules.validRecipe,\n 'valid-boolean-property': rules.validBooleanProperty,\n 'valid-directional-modifier': rules.validDirectionalModifier,\n 'valid-radius-shape': rules.validRadiusShape,\n 'no-important': rules.noImportant,\n 'valid-sub-element': rules.validSubElement,\n 'no-nested-state-map': rules.noNestedStateMap,\n 'static-no-dynamic-values': rules.staticNoDynamicValues,\n 'static-valid-selector': rules.staticValidSelector,\n 'prefer-shorthand-property': rules.preferShorthandProperty,\n 'valid-transition': rules.validTransition,\n 'require-default-state': rules.requireDefaultState,\n 'no-unknown-state-alias': rules.noUnknownStateAlias,\n 'no-raw-color-values': rules.noRawColorValues,\n 'no-raw-transition-duration': rules.noRawTransitionDuration,\n 'no-raw-motion-duration': rules.noRawMotionDuration,\n 'no-styles-prop': rules.noStylesProp,\n 'consistent-token-usage': rules.consistentTokenUsage,\n 'no-runtime-styles-mutation': rules.noRuntimeStylesMutation,\n 'no-style-spread': rules.noStyleSpread,\n 'valid-state-definition': rules.validStateDefinition,\n 'no-own-at-root': rules.noOwnAtRoot,\n 'valid-default-state-order': rules.validDefaultStateOrder,\n 'prefer-auto-calc': rules.preferAutoCalc,\n 'prefer-state-negation': rules.preferStateNegation,\n 'prefer-custom-property-syntax': rules.preferCustomPropertySyntax,\n 'prefer-hide': rules.preferHide,\n 'prefer-directional-shorthand': rules.preferDirectionalShorthand,\n 'prefer-longhand-property': rules.preferLonghandProperty,\n};\n\n// Read from package.json so `meta.version` cannot drift from the released\n// version, as the previous hardcoded literal had. `../package.json` resolves to\n// the package root from both `src/` and the unbundled `dist/`.\nconst { version } = createRequire(import.meta.url)('../package.json') as {\n version: string;\n};\n\nconst plugin = {\n meta: {\n name: '@tenphi/eslint-plugin-tasty',\n version,\n },\n rules: ruleMap,\n configs: {\n recommended: {\n plugins: {\n get tasty() {\n return plugin;\n },\n },\n rules: recommended,\n },\n strict: {\n plugins: {\n get tasty() {\n return plugin;\n },\n },\n rules: strict,\n },\n },\n} satisfies TSESLint.FlatConfig.Plugin & {\n configs: Record<string, TSESLint.FlatConfig.Config>;\n};\n\nexport default plugin;\n\nexport { recommended, strict } from './configs.js';\nexport type {\n TastyValidationConfig,\n ResolvedConfig,\n StyleFunctionConfig,\n} from './types.js';\n"],"mappings":"
|
|
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"}
|
|
@@ -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"}
|
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",
|