@plumeria/eslint-plugin 19.6.2 → 19.8.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 +2 -1
- package/dist/rules/no-invalid-selector.js +245 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -101,7 +101,8 @@ Disallow calling APIs inside functions.
|
|
|
101
101
|
### no-invalid-selector
|
|
102
102
|
|
|
103
103
|
Disallow invalid selector inside `css.create()` and `css.keyframes()` and `css.viewTransition()`.
|
|
104
|
-
`create()` example: (Pseudo -> Query, Query -> Query)
|
|
104
|
+
`create()` example: (Pseudo -> Query, Query -> Query)
|
|
105
|
+
Also reports a functional pseudo-class nested inside one of the same name, such as `:where(:where(.a), .b)`, and flattens it with a fix when the nested call is a whole argument. `:not()` may nest inside `:not()`. Reports a key nested deeper than 16 levels or holding a stray quote.
|
|
105
106
|
|
|
106
107
|
### no-mixed-styling-props
|
|
107
108
|
|
|
@@ -1,8 +1,203 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.noInvalidSelector = void 0;
|
|
4
|
+
const zss_engine_1 = require("zss-engine");
|
|
4
5
|
const styleObject_1 = require("../util/styleObject");
|
|
5
6
|
const staticValue_1 = require("../util/staticValue");
|
|
7
|
+
const flattenablePseudos = new Set([
|
|
8
|
+
'is',
|
|
9
|
+
'where',
|
|
10
|
+
'matches',
|
|
11
|
+
'-webkit-any',
|
|
12
|
+
'-moz-any',
|
|
13
|
+
'current',
|
|
14
|
+
'past',
|
|
15
|
+
'future',
|
|
16
|
+
'has',
|
|
17
|
+
'host',
|
|
18
|
+
'host-context',
|
|
19
|
+
'lang',
|
|
20
|
+
'dir',
|
|
21
|
+
'state',
|
|
22
|
+
'part',
|
|
23
|
+
'slotted',
|
|
24
|
+
'nth-child',
|
|
25
|
+
'nth-last-child',
|
|
26
|
+
'nth-of-type',
|
|
27
|
+
'nth-last-of-type',
|
|
28
|
+
'nth-col',
|
|
29
|
+
'nth-last-col',
|
|
30
|
+
'heading',
|
|
31
|
+
'active-view-transition-type',
|
|
32
|
+
'highlight',
|
|
33
|
+
'view-transition-group',
|
|
34
|
+
'view-transition-image-pair',
|
|
35
|
+
'view-transition-old',
|
|
36
|
+
'view-transition-new',
|
|
37
|
+
'cue',
|
|
38
|
+
'cue-region',
|
|
39
|
+
'picker',
|
|
40
|
+
'scroll-button',
|
|
41
|
+
]);
|
|
42
|
+
function skipString(text, i) {
|
|
43
|
+
const quote = text[i];
|
|
44
|
+
let j = i + 1;
|
|
45
|
+
while (j < text.length && text[j] !== quote) {
|
|
46
|
+
j += text[j] === '\\' ? 2 : 1;
|
|
47
|
+
}
|
|
48
|
+
return j + 1;
|
|
49
|
+
}
|
|
50
|
+
function matchClose(text, i) {
|
|
51
|
+
const close = text[i] === '(' ? ')' : ']';
|
|
52
|
+
let depth = 0;
|
|
53
|
+
let j = i;
|
|
54
|
+
while (j < text.length) {
|
|
55
|
+
const ch = text[j];
|
|
56
|
+
if (ch === '\\') {
|
|
57
|
+
j += 2;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (ch === '"' || ch === "'") {
|
|
61
|
+
j = skipString(text, j);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (ch === '(' || ch === '[')
|
|
65
|
+
depth++;
|
|
66
|
+
else if (ch === ')' || ch === ']') {
|
|
67
|
+
depth--;
|
|
68
|
+
if (depth === 0)
|
|
69
|
+
return ch === close ? j : -1;
|
|
70
|
+
}
|
|
71
|
+
j++;
|
|
72
|
+
}
|
|
73
|
+
return -1;
|
|
74
|
+
}
|
|
75
|
+
function findPseudoCalls(text) {
|
|
76
|
+
const calls = [];
|
|
77
|
+
let i = 0;
|
|
78
|
+
while (i < text.length) {
|
|
79
|
+
const ch = text[i];
|
|
80
|
+
if (ch === '\\') {
|
|
81
|
+
i += 2;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (ch === '"' || ch === "'") {
|
|
85
|
+
i = skipString(text, i);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (ch === '(' || ch === '[') {
|
|
89
|
+
const close = matchClose(text, i);
|
|
90
|
+
if (close < 0)
|
|
91
|
+
return null;
|
|
92
|
+
i = close + 1;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (ch === ':') {
|
|
96
|
+
const match = /^(::?)(-?[a-zA-Z][\w-]*)\(/.exec(text.slice(i));
|
|
97
|
+
if (match) {
|
|
98
|
+
const open = i + match[0].length - 1;
|
|
99
|
+
const close = matchClose(text, open);
|
|
100
|
+
if (close < 0)
|
|
101
|
+
return null;
|
|
102
|
+
calls.push({
|
|
103
|
+
prefix: match[1],
|
|
104
|
+
name: match[2].toLowerCase(),
|
|
105
|
+
start: i,
|
|
106
|
+
end: close + 1,
|
|
107
|
+
args: text.slice(open + 1, close),
|
|
108
|
+
});
|
|
109
|
+
i = close + 1;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
i++;
|
|
114
|
+
}
|
|
115
|
+
return calls;
|
|
116
|
+
}
|
|
117
|
+
function splitTopLevel(text) {
|
|
118
|
+
const parts = [];
|
|
119
|
+
let depth = 0;
|
|
120
|
+
let last = 0;
|
|
121
|
+
let i = 0;
|
|
122
|
+
while (i < text.length) {
|
|
123
|
+
const ch = text[i];
|
|
124
|
+
if (ch === '\\') {
|
|
125
|
+
i += 2;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (ch === '"' || ch === "'") {
|
|
129
|
+
i = skipString(text, i);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (ch === '(' || ch === '[')
|
|
133
|
+
depth++;
|
|
134
|
+
else if (ch === ')' || ch === ']')
|
|
135
|
+
depth--;
|
|
136
|
+
else if (ch === ',' && depth === 0) {
|
|
137
|
+
parts.push(text.slice(last, i));
|
|
138
|
+
last = i + 1;
|
|
139
|
+
}
|
|
140
|
+
i++;
|
|
141
|
+
}
|
|
142
|
+
parts.push(text.slice(last));
|
|
143
|
+
return parts;
|
|
144
|
+
}
|
|
145
|
+
function findSameNameNesting(text, ancestors) {
|
|
146
|
+
const calls = findPseudoCalls(text);
|
|
147
|
+
if (!calls)
|
|
148
|
+
return null;
|
|
149
|
+
for (const call of calls) {
|
|
150
|
+
const key = call.prefix + call.name;
|
|
151
|
+
if (key !== ':not' && ancestors.includes(key))
|
|
152
|
+
return call;
|
|
153
|
+
const nested = findSameNameNesting(call.args, [...ancestors, key]);
|
|
154
|
+
if (nested)
|
|
155
|
+
return nested;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
function flattenSelector(text) {
|
|
160
|
+
const calls = findPseudoCalls(text);
|
|
161
|
+
if (!calls)
|
|
162
|
+
return text;
|
|
163
|
+
let result = '';
|
|
164
|
+
let last = 0;
|
|
165
|
+
for (const call of calls) {
|
|
166
|
+
const args = flattenArgs(call);
|
|
167
|
+
result += text.slice(last, call.start);
|
|
168
|
+
result +=
|
|
169
|
+
args === call.args
|
|
170
|
+
? text.slice(call.start, call.end)
|
|
171
|
+
: `${call.prefix}${call.name}(${args})`;
|
|
172
|
+
last = call.end;
|
|
173
|
+
}
|
|
174
|
+
return result + text.slice(last);
|
|
175
|
+
}
|
|
176
|
+
function flattenArgs(call) {
|
|
177
|
+
let changed = false;
|
|
178
|
+
const items = [];
|
|
179
|
+
for (const part of splitTopLevel(call.args)) {
|
|
180
|
+
const flattened = flattenSelector(part);
|
|
181
|
+
if (flattened !== part)
|
|
182
|
+
changed = true;
|
|
183
|
+
const item = flattened.trim();
|
|
184
|
+
const inner = flattenablePseudos.has(call.name)
|
|
185
|
+
? findPseudoCalls(item)
|
|
186
|
+
: null;
|
|
187
|
+
if (inner?.length === 1 &&
|
|
188
|
+
inner[0].start === 0 &&
|
|
189
|
+
inner[0].end === item.length &&
|
|
190
|
+
inner[0].prefix === call.prefix &&
|
|
191
|
+
inner[0].name === call.name) {
|
|
192
|
+
items.push(...splitTopLevel(inner[0].args).map((s) => s.trim()));
|
|
193
|
+
changed = true;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
items.push(item);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return changed ? items.join(', ') : call.args;
|
|
200
|
+
}
|
|
6
201
|
exports.noInvalidSelector = {
|
|
7
202
|
meta: {
|
|
8
203
|
type: 'problem',
|
|
@@ -14,9 +209,14 @@ exports.noInvalidSelector = {
|
|
|
14
209
|
noQueryInsidePseudo: 'Media/Container queries cannot be nested inside pseudo-selectors.',
|
|
15
210
|
noQueryInsideQuery: 'Media/Container queries cannot be nested inside other queries.',
|
|
16
211
|
noPseudoInsidePseudo: 'Pseudo-selectors cannot be nested inside other pseudo-selectors.',
|
|
212
|
+
flattenSameNamePseudo: 'Flatten nested "{{name}}()" into a single "{{name}}()".',
|
|
213
|
+
noSameNamePseudoNesting: '"{{name}}()" cannot be nested inside another "{{name}}()"; rewrite it without nesting.',
|
|
214
|
+
tooDeepSelector: 'A selector key cannot nest parentheses deeper than {{max}} levels.',
|
|
215
|
+
strayQuote: 'A quote must open and close a string inside brackets or parentheses.',
|
|
17
216
|
invalidKeyframesKey: 'Keyframes keys must be "from", "to", or a percentage value (e.g. "0%", "50%", "100%").',
|
|
18
217
|
invalidViewTransitionKey: 'ViewTransition keys must be one of: "group", "imagePair", "new", "old".',
|
|
19
218
|
},
|
|
219
|
+
fixable: 'code',
|
|
20
220
|
schema: [],
|
|
21
221
|
},
|
|
22
222
|
create(context) {
|
|
@@ -46,6 +246,47 @@ exports.noInvalidSelector = {
|
|
|
46
246
|
return 'UNKNOWN';
|
|
47
247
|
return 'SKIP';
|
|
48
248
|
}
|
|
249
|
+
function checkPseudoNesting(node) {
|
|
250
|
+
const selector = node.type === 'Literal' && typeof node.value === 'string'
|
|
251
|
+
? node.value
|
|
252
|
+
: staticValue(node);
|
|
253
|
+
if (typeof selector !== 'string' || !/^[:[]/.test(selector))
|
|
254
|
+
return;
|
|
255
|
+
const invalid = (0, zss_engine_1.findInvalidSelector)((0, zss_engine_1.stripSelectorComments)(selector));
|
|
256
|
+
if (invalid?.kind === 'too-deep') {
|
|
257
|
+
context.report({
|
|
258
|
+
node,
|
|
259
|
+
messageId: 'tooDeepSelector',
|
|
260
|
+
data: { max: String(zss_engine_1.MAX_SELECTOR_NESTING) },
|
|
261
|
+
});
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (invalid?.kind === 'stray-quote') {
|
|
265
|
+
context.report({ node, messageId: 'strayQuote' });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const nested = findSameNameNesting(selector, []);
|
|
269
|
+
if (!nested)
|
|
270
|
+
return;
|
|
271
|
+
const flattened = flattenSelector(selector);
|
|
272
|
+
const raw = node.type === 'Literal' ? node.raw : '';
|
|
273
|
+
const quote = raw[0];
|
|
274
|
+
const fixable = flattened !== selector &&
|
|
275
|
+
(quote === '"' || quote === "'") &&
|
|
276
|
+
raw.slice(1, -1) === selector &&
|
|
277
|
+
!flattened.includes(quote) &&
|
|
278
|
+
!flattened.includes('\\');
|
|
279
|
+
context.report({
|
|
280
|
+
node,
|
|
281
|
+
messageId: fixable && !findSameNameNesting(flattened, [])
|
|
282
|
+
? 'flattenSameNamePseudo'
|
|
283
|
+
: 'noSameNamePseudoNesting',
|
|
284
|
+
data: { name: nested.prefix + nested.name },
|
|
285
|
+
fix: fixable
|
|
286
|
+
? (fixer) => fixer.replaceText(node, `${quote}${flattened}${quote}`)
|
|
287
|
+
: null,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
49
290
|
function checkNesting(node, parentType) {
|
|
50
291
|
for (const prop of node.properties) {
|
|
51
292
|
if (prop.type !== 'Property')
|
|
@@ -53,6 +294,10 @@ exports.noInvalidSelector = {
|
|
|
53
294
|
const currentType = getSelectorType(prop.key);
|
|
54
295
|
if (currentType === 'SKIP')
|
|
55
296
|
continue;
|
|
297
|
+
if (currentType === 'PSEUDO' ||
|
|
298
|
+
(currentType === 'PROPERTY' &&
|
|
299
|
+
(prop.computed || prop.key.type !== 'Identifier')))
|
|
300
|
+
checkPseudoNesting(prop.key);
|
|
56
301
|
if (currentType === 'UNKNOWN') {
|
|
57
302
|
context.report({
|
|
58
303
|
node: prop.key,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plumeria/eslint-plugin",
|
|
3
|
-
"version": "19.
|
|
3
|
+
"version": "19.8.0",
|
|
4
4
|
"description": "Plumeria ESLint plugin",
|
|
5
5
|
"author": "Refirst 11",
|
|
6
6
|
"license": "MIT",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"known-css-properties": "^0.37.0",
|
|
57
|
-
"zss-engine": "2.
|
|
58
|
-
"@plumeria/compiler": "19.
|
|
57
|
+
"zss-engine": "2.9.0",
|
|
58
|
+
"@plumeria/compiler": "19.8.0"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"build": "rimraf dist && pnpm cjs",
|