@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 +54 -34
- package/dist/rules/no-order-dependent-overlap.js +61 -3
- package/dist/util/selector.d.ts +11 -0
- package/dist/util/selector.js +185 -0
- package/package.json +2 -2
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
|
-
##
|
|
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.
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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 (
|
|
144
|
-
|
|
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.
|
|
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.
|
|
57
|
+
"zss-engine": "2.6.0"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
60
|
"build": "rimraf dist && pnpm cjs",
|