@plumeria/eslint-plugin 18.2.10 → 18.2.12
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
|
@@ -107,6 +107,12 @@ physical name, and two shorthands that cross without either containing the
|
|
|
107
107
|
other. A shorthand and its longhand are ranked by specificity and never
|
|
108
108
|
reported.
|
|
109
109
|
|
|
110
|
+
It also reports two conditions in one style where one provably matches a subset
|
|
111
|
+
of the other — `(min-width: 900px)` inside `(min-width: 600px)`, or two queries
|
|
112
|
+
naming the same container — and the narrower one is written first. The optimizer
|
|
113
|
+
places the narrower one last whatever the source says, so the declarations read
|
|
114
|
+
the wrong way round. The swap is offered as a suggestion rather than a fix.
|
|
115
|
+
|
|
110
116
|
A pair of spellings is one property in a horizontal writing mode and two
|
|
111
117
|
properties in a vertical one, which is per element and cannot be read from the
|
|
112
118
|
source. The rule reads them as one property; disable the line where an element
|
|
@@ -97,9 +97,24 @@ exports.expandBorderShorthands = {
|
|
|
97
97
|
const kebab = (0, logicalPhysical_1.toKebabCase)(name);
|
|
98
98
|
if (!BUNDLES.has(kebab))
|
|
99
99
|
return;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
let literal = null;
|
|
101
|
+
let expressions = [];
|
|
102
|
+
if (prop.value.type === 'Literal' &&
|
|
103
|
+
typeof prop.value.value === 'string') {
|
|
104
|
+
literal = prop.value.value;
|
|
105
|
+
}
|
|
106
|
+
else if (prop.value.type === 'TemplateLiteral') {
|
|
107
|
+
const template = prop.value;
|
|
108
|
+
expressions = template.expressions.map((expression) => sourceCode.getText(expression));
|
|
109
|
+
literal = template.quasis
|
|
110
|
+
.map((quasi, index) => index < expressions.length
|
|
111
|
+
? quasi.value.raw +
|
|
112
|
+
borderShorthand_1.EXPRESSION_MARKER +
|
|
113
|
+
index +
|
|
114
|
+
borderShorthand_1.EXPRESSION_MARKER
|
|
115
|
+
: quasi.value.raw)
|
|
116
|
+
.join('');
|
|
117
|
+
}
|
|
103
118
|
const parts = literal === null ? null : (0, borderShorthand_1.splitBorderValue)(literal);
|
|
104
119
|
if (!parts) {
|
|
105
120
|
context.report({
|
|
@@ -112,9 +127,20 @@ exports.expandBorderShorthands = {
|
|
|
112
127
|
const quote = sourceCode.getText(prop.value).trim().startsWith('"')
|
|
113
128
|
? '"'
|
|
114
129
|
: "'";
|
|
130
|
+
const marker = new RegExp(`${borderShorthand_1.EXPRESSION_MARKER}(\\d+)${borderShorthand_1.EXPRESSION_MARKER}`, 'g');
|
|
131
|
+
const render = (value) => {
|
|
132
|
+
if (!value.includes(borderShorthand_1.EXPRESSION_MARKER)) {
|
|
133
|
+
return `${quote}${value}${quote}`;
|
|
134
|
+
}
|
|
135
|
+
const restored = value.replace(marker, (_, index) => `\${${expressions[Number(index)]}}`);
|
|
136
|
+
const alone = /^\$\{([\s\S]*)\}$/.exec(restored);
|
|
137
|
+
return alone && !alone[1].includes('${')
|
|
138
|
+
? alone[1]
|
|
139
|
+
: `\`${restored}\``;
|
|
140
|
+
};
|
|
115
141
|
const indent = ' '.repeat(prop.loc.start.column);
|
|
116
142
|
const declarations = ['width', 'style', 'color']
|
|
117
|
-
.map((part) => `${(0, logicalPhysical_1.toCamelCase)(`${kebab}-${part}`)}: ${
|
|
143
|
+
.map((part) => `${(0, logicalPhysical_1.toCamelCase)(`${kebab}-${part}`)}: ${render(parts[part])}`)
|
|
118
144
|
.join(`,\n${indent}`);
|
|
119
145
|
context.report({
|
|
120
146
|
node: prop.key,
|
|
@@ -58,7 +58,9 @@ exports.noOrderDependentOverlap = {
|
|
|
58
58
|
messages: {
|
|
59
59
|
alias: "'{{ first }}' and '{{ second }}' are the same property under two names, so neither outranks the other. The one written last wins.",
|
|
60
60
|
crossing: "'{{ first }}' and '{{ second }}' overlap, but neither property outranks the other. The result depends on the order they are written.",
|
|
61
|
+
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.",
|
|
61
62
|
keep: "Keep '{{ keep }}'",
|
|
63
|
+
swap: "Write '{{ narrow }}' after '{{ broad }}'",
|
|
62
64
|
},
|
|
63
65
|
schema: [],
|
|
64
66
|
},
|
|
@@ -120,6 +122,33 @@ exports.noOrderDependentOverlap = {
|
|
|
120
122
|
}
|
|
121
123
|
},
|
|
122
124
|
};
|
|
125
|
+
function keyNameOf(prop) {
|
|
126
|
+
if (!prop.computed) {
|
|
127
|
+
return prop.key.type === 'Identifier'
|
|
128
|
+
? prop.key.name
|
|
129
|
+
: String(prop.key.value);
|
|
130
|
+
}
|
|
131
|
+
return prop.key.type === 'Literal' && typeof prop.key.value === 'string'
|
|
132
|
+
? prop.key.value
|
|
133
|
+
: '';
|
|
134
|
+
}
|
|
135
|
+
function declaredNames(node) {
|
|
136
|
+
const names = new Set();
|
|
137
|
+
node.properties.forEach((prop) => {
|
|
138
|
+
if (prop.type !== 'Property' || prop.value.type === 'ObjectExpression')
|
|
139
|
+
return;
|
|
140
|
+
const name = keyNameOf(prop);
|
|
141
|
+
if (name && !name.startsWith('--'))
|
|
142
|
+
names.add(name);
|
|
143
|
+
});
|
|
144
|
+
return names;
|
|
145
|
+
}
|
|
146
|
+
function swapProperties(fixer, first, second) {
|
|
147
|
+
return [
|
|
148
|
+
fixer.replaceText(first, sourceCode.getText(second)),
|
|
149
|
+
fixer.replaceText(second, sourceCode.getText(first)),
|
|
150
|
+
];
|
|
151
|
+
}
|
|
123
152
|
function removeProperty(fixer, prop) {
|
|
124
153
|
const after = sourceCode.getTokenAfter(prop);
|
|
125
154
|
if (after && after.value === ',') {
|
|
@@ -132,24 +161,24 @@ exports.noOrderDependentOverlap = {
|
|
|
132
161
|
}
|
|
133
162
|
function checkStyleObject(node) {
|
|
134
163
|
const declarations = [];
|
|
164
|
+
const conditions = [];
|
|
135
165
|
node.properties.forEach((prop) => {
|
|
136
166
|
if (prop.type !== 'Property')
|
|
137
167
|
return;
|
|
138
168
|
if (prop.value.type === 'ObjectExpression') {
|
|
169
|
+
const condition = keyNameOf(prop);
|
|
170
|
+
if (condition.startsWith('@media') ||
|
|
171
|
+
condition.startsWith('@container')) {
|
|
172
|
+
conditions.push({
|
|
173
|
+
prop,
|
|
174
|
+
name: condition,
|
|
175
|
+
sets: declaredNames(prop.value),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
139
178
|
checkStyleObject(prop.value);
|
|
140
179
|
return;
|
|
141
180
|
}
|
|
142
|
-
|
|
143
|
-
if (!prop.computed) {
|
|
144
|
-
name =
|
|
145
|
-
prop.key.type === 'Identifier'
|
|
146
|
-
? prop.key.name
|
|
147
|
-
: String(prop.key.value);
|
|
148
|
-
}
|
|
149
|
-
else if (prop.key.type === 'Literal' &&
|
|
150
|
-
typeof prop.key.value === 'string') {
|
|
151
|
-
name = prop.key.value;
|
|
152
|
-
}
|
|
181
|
+
const name = keyNameOf(prop);
|
|
153
182
|
if (!name ||
|
|
154
183
|
name.startsWith(':') ||
|
|
155
184
|
name.startsWith('[') ||
|
|
@@ -159,6 +188,33 @@ exports.noOrderDependentOverlap = {
|
|
|
159
188
|
}
|
|
160
189
|
declarations.push({ prop, name, kebab: (0, logicalPhysical_1.toKebabCase)(name) });
|
|
161
190
|
});
|
|
191
|
+
for (let i = 0; i < conditions.length; i++) {
|
|
192
|
+
for (let j = i + 1; j < conditions.length; j++) {
|
|
193
|
+
const first = conditions[i];
|
|
194
|
+
const second = conditions[j];
|
|
195
|
+
if (!(0, zss_engine_1.impliesCondition)(first.name, second.name))
|
|
196
|
+
continue;
|
|
197
|
+
const shared = [...first.sets].filter((name) => second.sets.has(name));
|
|
198
|
+
if (shared.length === 0)
|
|
199
|
+
continue;
|
|
200
|
+
context.report({
|
|
201
|
+
node: first.prop.key,
|
|
202
|
+
messageId: 'condition',
|
|
203
|
+
data: {
|
|
204
|
+
narrow: first.name,
|
|
205
|
+
broad: second.name,
|
|
206
|
+
property: shared[0],
|
|
207
|
+
},
|
|
208
|
+
suggest: [
|
|
209
|
+
{
|
|
210
|
+
messageId: 'swap',
|
|
211
|
+
data: { narrow: first.name, broad: second.name },
|
|
212
|
+
fix: (fixer) => swapProperties(fixer, first.prop, second.prop),
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
162
218
|
for (let i = 0; i < declarations.length; i++) {
|
|
163
219
|
for (let j = i + 1; j < declarations.length; j++) {
|
|
164
220
|
const first = declarations[i];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.splitBorderValue = exports.BORDER_BUNDLES = void 0;
|
|
3
|
+
exports.splitBorderValue = exports.EXPRESSION_MARKER = exports.BORDER_BUNDLES = void 0;
|
|
4
4
|
exports.BORDER_BUNDLES = [
|
|
5
5
|
'border',
|
|
6
6
|
'border-block',
|
|
@@ -28,7 +28,8 @@ const STYLES = new Set([
|
|
|
28
28
|
]);
|
|
29
29
|
const WIDTH_KEYWORDS = new Set(['thin', 'medium', 'thick']);
|
|
30
30
|
const LENGTH = /^[+-]?(\d+\.?\d*|\.\d+)([a-z%]+)?$/i;
|
|
31
|
-
const INITIAL = { width: 'medium', style: 'none', color: '
|
|
31
|
+
const INITIAL = { width: 'medium', style: 'none', color: 'currentColor' };
|
|
32
|
+
exports.EXPRESSION_MARKER = '\u0000';
|
|
32
33
|
const tokenize = (value) => {
|
|
33
34
|
const tokens = [];
|
|
34
35
|
let depth = 0;
|
|
@@ -53,14 +54,22 @@ const tokenize = (value) => {
|
|
|
53
54
|
const isWidth = (token) => WIDTH_KEYWORDS.has(token) || LENGTH.test(token) || /^calc\(/i.test(token);
|
|
54
55
|
const isColor = (token) => /^(#|rgb|hsl|hwb|lab|lch|oklab|oklch|color\(|light-dark\()/i.test(token) ||
|
|
55
56
|
/^[a-z-]+$/i.test(token);
|
|
57
|
+
const isExpression = (token) => /\bvar\(/i.test(token) || token.includes(exports.EXPRESSION_MARKER);
|
|
56
58
|
const splitBorderValue = (value) => {
|
|
57
59
|
const tokens = tokenize(value);
|
|
58
60
|
if (!tokens)
|
|
59
61
|
return null;
|
|
60
|
-
if (tokens.some((token) =>
|
|
62
|
+
if (tokens.some((token) => /^inherit$|^initial$|^unset$|^revert/i.test(token)))
|
|
61
63
|
return null;
|
|
62
64
|
const parts = {};
|
|
65
|
+
let expression = null;
|
|
63
66
|
for (const token of tokens) {
|
|
67
|
+
if (isExpression(token)) {
|
|
68
|
+
if (expression !== null)
|
|
69
|
+
return null;
|
|
70
|
+
expression = token;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
64
73
|
const lower = token.toLowerCase();
|
|
65
74
|
if (!parts.style && STYLES.has(lower)) {
|
|
66
75
|
parts.style = token;
|
|
@@ -76,6 +85,12 @@ const splitBorderValue = (value) => {
|
|
|
76
85
|
}
|
|
77
86
|
return null;
|
|
78
87
|
}
|
|
88
|
+
if (expression !== null) {
|
|
89
|
+
const open = ['width', 'style', 'color'].filter((part) => !parts[part]);
|
|
90
|
+
if (open.length !== 1)
|
|
91
|
+
return null;
|
|
92
|
+
parts[open[0]] = expression;
|
|
93
|
+
}
|
|
79
94
|
return {
|
|
80
95
|
width: parts.width ?? INITIAL.width,
|
|
81
96
|
style: parts.style ?? INITIAL.style,
|
package/dist/util/validData.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plumeria/eslint-plugin",
|
|
3
|
-
"version": "18.2.
|
|
3
|
+
"version": "18.2.12",
|
|
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.5.0"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
60
|
"build": "rimraf dist && pnpm cjs",
|