@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 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.6.2",
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.8.0",
58
- "@plumeria/compiler": "19.6.2"
57
+ "zss-engine": "2.9.0",
58
+ "@plumeria/compiler": "19.8.0"
59
59
  },
60
60
  "scripts": {
61
61
  "build": "rimraf dist && pnpm cjs",