@plumeria/eslint-plugin 18.2.14 → 18.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -64,7 +64,10 @@ A single rule can override that if you need it to:
64
64
  The name must match whatever the loader or unplugin was given. If they disagree,
65
65
  the lint rules report against a prop the compiler never transforms.
66
66
 
67
- ## Rules
67
+ ## Recommended rules
68
+
69
+ The following rules are enabled by `plumeria.configs.recommended` with the
70
+ severity shown in [Recommended configuration](#recommended-configuration).
68
71
 
69
72
  ### props-require-import
70
73
 
@@ -118,6 +121,50 @@ properties in a vertical one, which is per element and cannot be read from the
118
121
  source. The rule reads them as one property; disable the line where an element
119
122
  is known to be vertical.
120
123
 
124
+ ### no-unknown-css-properties
125
+
126
+ Disallow unknown CSS properties in camelCase within `css.create`, `css.keyframes`, and `css.viewTransition`.
127
+
128
+ ### no-unused-keys
129
+
130
+ Warns when object keys are defined but not used, mainly in component files.
131
+
132
+ ### sort-properties
133
+
134
+ Automatically sorts CSS properties in the recommended order for consistency and maintainability.
135
+
136
+ ### format-properties
137
+
138
+ Automatically format for consistency and maintainability.
139
+
140
+ - Formats a line into a multi-line.
141
+ - Formats by filling in blank lines.
142
+
143
+ ### validate-values
144
+
145
+ Validates CSS property values for correctness. Only standard CSS properties are checked; properties with string literal keys (e.g., computed or dynamic property names) are not validated.
146
+
147
+ ### validate-pseudos
148
+
149
+ Validates CSS pseudo-classes and pseudo-elements inside `css.create()`. It checks for typos and structural correctness and supports validation of computed keys when TypeScript is available.
150
+
151
+ ## Optional rules
152
+
153
+ These rules are not enabled by `plumeria.configs.recommended`. Enable only the
154
+ policy or transformation that fits the project:
155
+
156
+ ```js
157
+ export default [
158
+ plumeria.configs.recommended,
159
+ {
160
+ rules: {
161
+ '@plumeria/no-physical-properties': 'warn',
162
+ '@plumeria/expand-border-shorthands': 'warn',
163
+ },
164
+ },
165
+ ];
166
+ ```
167
+
121
168
  ### expand-border-shorthands
122
169
 
123
170
  Expands a border shorthand that bundles a width, a style and a color —
@@ -128,7 +175,7 @@ them turns the last unrankable pairs into ordinary shorthand-to-longhand ones.
128
175
 
129
176
  Fixable. A value it cannot split, such as `var(--edge)` or `inherit`, is
130
177
  reported without a fix: leaving it silent would let the expanded declarations
131
- elsewhere outrank it. Not part of `recommended`.
178
+ elsewhere outrank it.
132
179
 
133
180
  ```js
134
181
  borderTop: '1px solid red'
@@ -149,38 +196,11 @@ Accepts `{ sizes }`, which adds `width`, `height`, their `min`/`max` forms,
149
196
  default: a size is one property under either spelling, while an edge is what a
150
197
  direction reverses.
151
198
 
152
- Neither is part of `recommended`, and turning both on is contradictory. Reach
153
- for one when you want the pairs `no-order-dependent-overlap` reports to be
154
- impossible to write: a property that appears under one spelling only can never
155
- meet its other spelling on an element. A shorthand with no single counterpart,
156
- such as `borderBlockWidth`, is outside either rule and stays reported.
157
-
158
- ### no-unknown-css-properties
159
-
160
- Disallow unknown CSS properties in camelCase within `css.create`, `css.keyframes`, and `css.viewTransition`.
161
-
162
- ### no-unused-keys
163
-
164
- Warns when object keys are defined but not used, mainly in component files.
165
-
166
- ### sort-properties
167
-
168
- Automatically sorts CSS properties in the recommended order for consistency and maintainability.
169
-
170
- ### format-properties
171
-
172
- Automatically format for consistency and maintainability.
173
-
174
- - Formats a line into a multi-line.
175
- - Formats by filling in blank lines.
176
-
177
- ### validate-values
178
-
179
- Validates CSS property values for correctness. Only standard CSS properties are checked; properties with string literal keys (e.g., computed or dynamic property names) are not validated.
180
-
181
- ### validate-pseudos
182
-
183
- Validates CSS pseudo-classes and pseudo-elements inside `css.create()`. It checks for typos and structural correctness and supports validation of computed keys when TypeScript is available.
199
+ Turning both rules on is contradictory. Reach for one when you want the pairs
200
+ `no-order-dependent-overlap` reports to be impossible to write: a property that
201
+ appears under one spelling only can never meet its other spelling on an element.
202
+ A shorthand with no single counterpart, such as `borderBlockWidth`, is outside
203
+ either rule and stays reported.
184
204
 
185
205
  ## CLI (plumerialint)
186
206
 
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.noOrderDependentOverlap = void 0;
4
4
  const zss_engine_1 = require("zss-engine");
5
5
  const logicalPhysical_1 = require("../util/logicalPhysical");
6
+ const selector_1 = require("../util/selector");
6
7
  const styleObject_1 = require("../util/styleObject");
7
8
  const DIRECT_SHORTHANDS = {};
8
9
  for (const [shorthand, longhands] of Object.entries(zss_engine_1.DIRECT_LONGHANDS)) {
@@ -60,6 +61,7 @@ exports.noOrderDependentOverlap = {
60
61
  alias: "'{{ first }}' and '{{ second }}' are the same property under two names, so neither outranks the other. The one written last wins.",
61
62
  crossing: "'{{ first }}' and '{{ second }}' overlap, but neither property outranks the other. The result depends on the order they are written.",
62
63
  condition: "'{{ narrow }}' matches only where '{{ broad }}' also matches, so it is the more specific of the two, and both set '{{ property }}'. Written first, it reads as though the broader one wins, which is the opposite of what the stylesheet does.",
64
+ state: "'{{ first }}' and '{{ second }}' can match at the same time and both set '{{ property }}', but neither outranks the other, so the result depends on the order they are written. Declare '{{ compound }}' if the intersection has a value of its own, or split them into separate styles and compose them in the order that should win.",
63
65
  keep: "Keep '{{ keep }}'",
64
66
  swap: "Write '{{ narrow }}' after '{{ broad }}'",
65
67
  },
@@ -134,17 +136,31 @@ exports.noOrderDependentOverlap = {
134
136
  ? prop.key.value
135
137
  : '';
136
138
  }
137
- function declaredNames(node) {
139
+ function declaredNames(node, withCustomProperties = false) {
138
140
  const names = new Set();
139
141
  node.properties.forEach((prop) => {
140
142
  if (prop.type !== 'Property' || prop.value.type === 'ObjectExpression')
141
143
  return;
142
144
  const name = keyNameOf(prop);
143
- if (name && !name.startsWith('--'))
144
- names.add(name);
145
+ if (!name)
146
+ return;
147
+ if (name.startsWith('--')) {
148
+ if (withCustomProperties)
149
+ names.add(name);
150
+ return;
151
+ }
152
+ names.add(name);
145
153
  });
146
154
  return names;
147
155
  }
156
+ function overlapsProperty(first, second) {
157
+ if (first.startsWith('--') || second.startsWith('--')) {
158
+ return first === second;
159
+ }
160
+ return ((0, logicalPhysical_1.canonicalProperty)((0, logicalPhysical_1.toKebabCase)(first)) ===
161
+ (0, logicalPhysical_1.canonicalProperty)((0, logicalPhysical_1.toKebabCase)(second)) ||
162
+ overlapOf((0, logicalPhysical_1.toKebabCase)(first), (0, logicalPhysical_1.toKebabCase)(second)) === 'crossing');
163
+ }
148
164
  function swapProperties(fixer, first, second) {
149
165
  return [
150
166
  fixer.replaceText(first, sourceCode.getText(second)),
@@ -164,6 +180,7 @@ exports.noOrderDependentOverlap = {
164
180
  function checkStyleObject(node) {
165
181
  const declarations = [];
166
182
  const conditions = [];
183
+ const states = [];
167
184
  node.properties.forEach((prop) => {
168
185
  if (prop.type !== 'Property')
169
186
  return;
@@ -177,6 +194,13 @@ exports.noOrderDependentOverlap = {
177
194
  sets: declaredNames(prop.value),
178
195
  });
179
196
  }
197
+ else if ((0, selector_1.isStateSelector)(condition)) {
198
+ states.push({
199
+ prop,
200
+ name: condition,
201
+ sets: declaredNames(prop.value, true),
202
+ });
203
+ }
180
204
  checkStyleObject(prop.value);
181
205
  return;
182
206
  }
@@ -217,6 +241,40 @@ exports.noOrderDependentOverlap = {
217
241
  });
218
242
  }
219
243
  }
244
+ for (let i = 0; i < states.length; i++) {
245
+ for (let j = i + 1; j < states.length; j++) {
246
+ const first = states[i];
247
+ const second = states[j];
248
+ if ((0, zss_engine_1.getPseudoElement)(first.name) !== (0, zss_engine_1.getPseudoElement)(second.name)) {
249
+ continue;
250
+ }
251
+ if ((0, zss_engine_1.getSpecificity)(first.name).join() !==
252
+ (0, zss_engine_1.getSpecificity)(second.name).join()) {
253
+ continue;
254
+ }
255
+ if ((0, selector_1.areExclusive)(first.name, second.name))
256
+ continue;
257
+ const shared = [...first.sets].find((name) => [...second.sets].some((other) => overlapsProperty(name, other)));
258
+ if (!shared)
259
+ continue;
260
+ if (states.some((state) => state !== first &&
261
+ state !== second &&
262
+ (0, selector_1.covers)(state.name, first.name, second.name) &&
263
+ [...state.sets].some((name) => overlapsProperty(name, shared)))) {
264
+ continue;
265
+ }
266
+ context.report({
267
+ node: second.prop.key,
268
+ messageId: 'state',
269
+ data: {
270
+ first: first.name,
271
+ second: second.name,
272
+ property: shared,
273
+ compound: (0, selector_1.compoundOf)(first.name, second.name),
274
+ },
275
+ });
276
+ }
277
+ }
220
278
  for (let i = 0; i < declarations.length; i++) {
221
279
  for (let j = i + 1; j < declarations.length; j++) {
222
280
  const first = declarations[i];
@@ -0,0 +1,11 @@
1
+ interface Token {
2
+ text: string;
3
+ isPseudoElement: boolean;
4
+ }
5
+ export declare function tokenize(selector: string): Token[];
6
+ export declare function isStateSelector(selector: string): boolean;
7
+ export declare function areExclusive(first: string, second: string): boolean;
8
+ export declare function statesOf(selector: string): string[];
9
+ export declare function compoundOf(first: string, second: string): string;
10
+ export declare function covers(compound: string, first: string, second: string): boolean;
11
+ export {};
@@ -0,0 +1,185 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tokenize = tokenize;
4
+ exports.isStateSelector = isStateSelector;
5
+ exports.areExclusive = areExclusive;
6
+ exports.statesOf = statesOf;
7
+ exports.compoundOf = compoundOf;
8
+ exports.covers = covers;
9
+ const zss_engine_1 = require("zss-engine");
10
+ const LEGACY_PSEUDO_ELEMENTS = new Set([
11
+ 'before',
12
+ 'after',
13
+ 'first-line',
14
+ 'first-letter',
15
+ ]);
16
+ const EXCLUSIVE_PAIRS = [
17
+ [':link', ':visited'],
18
+ [':enabled', ':disabled'],
19
+ [':read-only', ':read-write'],
20
+ [':valid', ':invalid'],
21
+ [':in-range', ':out-of-range'],
22
+ [':required', ':optional'],
23
+ ].map((pair) => pair.slice().sort().join('\0'));
24
+ const isNameChar = (char) => /[\w-]/.test(char);
25
+ const skipName = (selector, from) => {
26
+ let index = from;
27
+ while (index < selector.length) {
28
+ if (selector[index] === '\\') {
29
+ index += 2;
30
+ continue;
31
+ }
32
+ if (!isNameChar(selector[index]))
33
+ break;
34
+ index += 1;
35
+ }
36
+ return index;
37
+ };
38
+ const findClose = (selector, open) => {
39
+ let depth = 0;
40
+ for (let index = open; index < selector.length; index++) {
41
+ const char = selector[index];
42
+ if (char === '\\') {
43
+ index += 1;
44
+ continue;
45
+ }
46
+ if (char === '"' || char === "'") {
47
+ index += 1;
48
+ while (index < selector.length && selector[index] !== char) {
49
+ if (selector[index] === '\\')
50
+ index += 1;
51
+ index += 1;
52
+ }
53
+ continue;
54
+ }
55
+ if (char === '(')
56
+ depth += 1;
57
+ else if (char === ')') {
58
+ depth -= 1;
59
+ if (depth === 0)
60
+ return index;
61
+ }
62
+ }
63
+ return selector.length;
64
+ };
65
+ const skipBracket = (selector, open) => {
66
+ for (let index = open + 1; index < selector.length; index++) {
67
+ const char = selector[index];
68
+ if (char === '\\') {
69
+ index += 1;
70
+ continue;
71
+ }
72
+ if (char === '"' || char === "'") {
73
+ index += 1;
74
+ while (index < selector.length && selector[index] !== char) {
75
+ if (selector[index] === '\\')
76
+ index += 1;
77
+ index += 1;
78
+ }
79
+ continue;
80
+ }
81
+ if (char === ']')
82
+ return index + 1;
83
+ }
84
+ return selector.length;
85
+ };
86
+ function tokenize(selector) {
87
+ const tokens = [];
88
+ let index = 0;
89
+ while (index < selector.length) {
90
+ const char = selector[index];
91
+ const start = index;
92
+ if (char === '#' || char === '.') {
93
+ index = skipName(selector, index + 1);
94
+ tokens.push({
95
+ text: selector.slice(start, index),
96
+ isPseudoElement: false,
97
+ });
98
+ continue;
99
+ }
100
+ if (char === '[') {
101
+ index = skipBracket(selector, index);
102
+ tokens.push({
103
+ text: selector.slice(start, index),
104
+ isPseudoElement: false,
105
+ });
106
+ continue;
107
+ }
108
+ if (char === ':') {
109
+ const doubleColon = selector[index + 1] === ':';
110
+ const nameStart = index + (doubleColon ? 2 : 1);
111
+ const nameEnd = skipName(selector, nameStart);
112
+ const name = selector.slice(nameStart, nameEnd).toLowerCase();
113
+ index = nameEnd;
114
+ let inner = '';
115
+ if (selector[index] === '(') {
116
+ const close = findClose(selector, index);
117
+ inner = selector.slice(index + 1, close);
118
+ index = close + 1;
119
+ }
120
+ const text = selector.slice(start, index);
121
+ const argument = inner ? `(${inner})` : '';
122
+ if (doubleColon || LEGACY_PSEUDO_ELEMENTS.has(name)) {
123
+ tokens.push({
124
+ text: `::${name}${argument}`,
125
+ isPseudoElement: true,
126
+ });
127
+ continue;
128
+ }
129
+ tokens.push({ text, isPseudoElement: false });
130
+ continue;
131
+ }
132
+ if (isNameChar(char) || char === '\\') {
133
+ index = skipName(selector, index);
134
+ tokens.push({
135
+ text: selector.slice(start, index),
136
+ isPseudoElement: false,
137
+ });
138
+ continue;
139
+ }
140
+ index += 1;
141
+ }
142
+ return tokens;
143
+ }
144
+ function isStateSelector(selector) {
145
+ const tokens = tokenize(selector);
146
+ return (tokens.length > 0 &&
147
+ tokens.every((token) => token.isPseudoElement ||
148
+ token.text.startsWith(':') ||
149
+ token.text.startsWith('[')));
150
+ }
151
+ const negationOf = (state) => {
152
+ if (!state.startsWith(':not(') || !state.endsWith(')'))
153
+ return null;
154
+ return state.slice(5, -1).trim();
155
+ };
156
+ function areExclusive(first, second) {
157
+ const left = new Set(statesOf(first));
158
+ const right = new Set(statesOf(second));
159
+ for (const a of left) {
160
+ for (const b of right) {
161
+ if (a === b)
162
+ continue;
163
+ if (negationOf(a) === b || negationOf(b) === a)
164
+ return true;
165
+ if (EXCLUSIVE_PAIRS.includes([a, b].sort().join('\0')))
166
+ return true;
167
+ }
168
+ }
169
+ return false;
170
+ }
171
+ function statesOf(selector) {
172
+ return tokenize(selector)
173
+ .filter((token) => !token.isPseudoElement)
174
+ .map((token) => token.text);
175
+ }
176
+ function compoundOf(first, second) {
177
+ const states = [...new Set([...statesOf(first), ...statesOf(second)])];
178
+ return states.join('') + (0, zss_engine_1.getPseudoElement)(first);
179
+ }
180
+ function covers(compound, first, second) {
181
+ const declared = new Set(statesOf(compound));
182
+ if ((0, zss_engine_1.getPseudoElement)(compound) !== (0, zss_engine_1.getPseudoElement)(first))
183
+ return false;
184
+ return [...statesOf(first), ...statesOf(second)].every((state) => declared.has(state));
185
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plumeria/eslint-plugin",
3
- "version": "18.2.14",
3
+ "version": "18.2.16",
4
4
  "description": "Plumeria ESLint plugin",
5
5
  "author": "Refirst 11",
6
6
  "license": "MIT",
@@ -54,7 +54,7 @@
54
54
  "dependencies": {
55
55
  "@typescript-eslint/utils": "^8.60.0",
56
56
  "known-css-properties": "^0.37.0",
57
- "zss-engine": "2.5.0"
57
+ "zss-engine": "2.6.0"
58
58
  },
59
59
  "scripts": {
60
60
  "build": "rimraf dist && pnpm cjs",