@plumeria/eslint-plugin 18.2.14 → 18.2.15

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.
@@ -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.15",
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",