@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
- const literal = prop.value.type === 'Literal' && typeof prop.value.value === 'string'
101
- ? prop.value.value
102
- : null;
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}`)}: ${quote}${parts[part]}${quote}`)
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
- let name = '';
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,4 +1,5 @@
1
1
  export declare const BORDER_BUNDLES: string[];
2
+ export declare const EXPRESSION_MARKER = "\0";
2
3
  export declare const splitBorderValue: (value: string) => {
3
4
  width: string;
4
5
  style: string;
@@ -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: 'currentcolor' };
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) => /\bvar\(|^inherit$|^initial$|^unset$|^revert/i.test(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,
@@ -286,7 +286,7 @@ const validData = {
286
286
  textDecorationColor: [],
287
287
  caretColor: ['auto'],
288
288
  columnRuleColor: [],
289
- borderColor: [],
289
+ borderColor: ['currentColor'],
290
290
  captionSide: ['top', 'bottom'],
291
291
  clear: [
292
292
  'inline-start',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plumeria/eslint-plugin",
3
- "version": "18.2.10",
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.4.3"
57
+ "zss-engine": "2.5.0"
58
58
  },
59
59
  "scripts": {
60
60
  "build": "rimraf dist && pnpm cjs",