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