@plumeria/eslint-plugin 19.5.1 → 19.6.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 +44 -0
- package/dist/index.js +3 -0
- package/dist/rules/no-unresolved-composition.d.ts +2 -0
- package/dist/rules/no-unresolved-composition.js +205 -0
- package/oxlint.json +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ The `plugin:@plumeria/recommended` config enables the following:
|
|
|
17
17
|
- `@plumeria/no-order-dependent-overlap`: **warn**
|
|
18
18
|
- `@plumeria/no-unknown-css-properties`: **error**
|
|
19
19
|
- `@plumeria/no-unused-keys`: **warn**
|
|
20
|
+
- `@plumeria/no-unresolved-composition`: **warn**
|
|
20
21
|
- `@plumeria/sort-properties`: **warn**
|
|
21
22
|
- `@plumeria/format-properties`: **warn**
|
|
22
23
|
- `@plumeria/validate-values`: **warn**
|
|
@@ -157,6 +158,49 @@ Validates CSS pseudo-classes and pseudo-elements inside `css.create()`. It check
|
|
|
157
158
|
|
|
158
159
|
Validates at-rules inside `css.create()`. It accepts `@media`, `@container`, `@supports`, `@layer`, and `@scope`, each with a prelude, whether or not a space separates it from the keyword, and supports validation of computed keys when TypeScript is available.
|
|
159
160
|
|
|
161
|
+
### no-unresolved-composition
|
|
162
|
+
|
|
163
|
+
Warns when a `css.use()` result is combined with other class names using `+`,
|
|
164
|
+
a template literal, or an inline array's `.join()`, or passed to a function such
|
|
165
|
+
as `clsx` or `css.use()` itself. Enabled by `recommended` as `warn`:
|
|
166
|
+
|
|
167
|
+
```js
|
|
168
|
+
'@plumeria/no-unresolved-composition': 'warn'
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
// Warns: merge separate css.use() calls.
|
|
173
|
+
`${css.use(styles.base)} ${css.use(styles.active)}`
|
|
174
|
+
css.use(styles.base) + ' ' + (active ? css.use(styles.active) : '')
|
|
175
|
+
|
|
176
|
+
// Warns: pass styles, not css.use() results, to one call.
|
|
177
|
+
clsx(css.use(styles.base), className)
|
|
178
|
+
css.use(css.use(styles.base), styles.active)
|
|
179
|
+
|
|
180
|
+
// Warns: rewrite external classes with css.create().
|
|
181
|
+
css.use(styles.base) + ' external'
|
|
182
|
+
[css.use(styles.base), 'external'].join(' ')
|
|
183
|
+
|
|
184
|
+
// Compose everything in one call; nested arrays are allowed.
|
|
185
|
+
css.use(styles.base, [active && styles.active, styles.external])
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
When a `css.use()` result is a function argument, directly, inside an array or
|
|
189
|
+
spread, or in a conditional branch, the rule reports that the styles should be
|
|
190
|
+
passed to one `css.use()` call instead; this is reported before the other two.
|
|
191
|
+
When two or more `css.use()` results are combined, the rule reports that they
|
|
192
|
+
should be merged into one call; this is reported first, even if external classes
|
|
193
|
+
are also present. When a `css.use()` result is combined with external class
|
|
194
|
+
names, it lists them as written in the source and reports that their precedence is left to the CSS cascade and that they
|
|
195
|
+
should be rewritten as styles with `css.create()` and passed into the same
|
|
196
|
+
`css.use()` call. It offers no automatic fix. Whitespace-only
|
|
197
|
+
additions are allowed. Imports and their aliases are resolved with scope, so
|
|
198
|
+
unrelated or shadowed `use` functions are ignored. Detection covers calls within
|
|
199
|
+
these expressions, including conditional branches, and follows results through
|
|
200
|
+
local variables, `+=` assignments, arrays joined through a variable, and object
|
|
201
|
+
properties. It does not follow values returned from functions or imported from
|
|
202
|
+
other modules.
|
|
203
|
+
|
|
160
204
|
## Optional rules
|
|
161
205
|
|
|
162
206
|
These rules are not enabled by `plumeria.configs.recommended`. Enable only the
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ const no_mixed_styling_props_1 = require("./rules/no-mixed-styling-props");
|
|
|
11
11
|
const no_physical_properties_1 = require("./rules/no-physical-properties");
|
|
12
12
|
const no_order_dependent_overlap_1 = require("./rules/no-order-dependent-overlap");
|
|
13
13
|
const no_unknown_css_properties_1 = require("./rules/no-unknown-css-properties");
|
|
14
|
+
const no_unresolved_composition_1 = require("./rules/no-unresolved-composition");
|
|
14
15
|
const no_unused_keys_1 = require("./rules/no-unused-keys");
|
|
15
16
|
const sort_properties_1 = require("./rules/sort-properties");
|
|
16
17
|
const format_properties_1 = require("./rules/format-properties");
|
|
@@ -31,6 +32,7 @@ const rules = {
|
|
|
31
32
|
'no-order-dependent-overlap': no_order_dependent_overlap_1.noOrderDependentOverlap,
|
|
32
33
|
'no-unknown-css-properties': no_unknown_css_properties_1.noUnknownCssProperties,
|
|
33
34
|
'no-unused-keys': no_unused_keys_1.noUnusedKeys,
|
|
35
|
+
'no-unresolved-composition': no_unresolved_composition_1.noUnresolvedComposition,
|
|
34
36
|
'sort-properties': sort_properties_1.sortProperties,
|
|
35
37
|
'format-properties': format_properties_1.formatProperties,
|
|
36
38
|
'validate-values': validate_values_1.validateValues,
|
|
@@ -55,6 +57,7 @@ const configs = {
|
|
|
55
57
|
'@plumeria/no-order-dependent-overlap': 'warn',
|
|
56
58
|
'@plumeria/no-unknown-css-properties': 'error',
|
|
57
59
|
'@plumeria/no-unused-keys': 'warn',
|
|
60
|
+
'@plumeria/no-unresolved-composition': 'warn',
|
|
58
61
|
'@plumeria/sort-properties': 'warn',
|
|
59
62
|
'@plumeria/format-properties': 'warn',
|
|
60
63
|
'@plumeria/validate-values': 'warn',
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.noUnresolvedComposition = void 0;
|
|
4
|
+
const memberName = (node) => !node.computed && node.property.type === 'Identifier'
|
|
5
|
+
? node.property.name
|
|
6
|
+
: node.computed && node.property.type === 'Literal'
|
|
7
|
+
? node.property.value
|
|
8
|
+
: null;
|
|
9
|
+
exports.noUnresolvedComposition = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: 'problem',
|
|
12
|
+
docs: {
|
|
13
|
+
description: 'Warn when class names are composed outside css.use()',
|
|
14
|
+
},
|
|
15
|
+
schema: [],
|
|
16
|
+
messages: {
|
|
17
|
+
merge: 'Separate css.use() results cannot resolve conflicts with each other. Merge them into one css.use(a, b) call, passing conditional styles as css.use(a, active && b).',
|
|
18
|
+
wrapped: 'css.use() results cannot be passed to functions, including css.use() itself. Pass the styles directly to one css.use() call, e.g. css.use(a, active && b, [c, d]).',
|
|
19
|
+
external: 'css.use() cannot order external class names ({{values}}); their precedence is left to the CSS cascade. Rewrite them as styles with css.create() and pass them into the same css.use() call.',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
create(context) {
|
|
23
|
+
const reported = new Set();
|
|
24
|
+
function variableOf(node) {
|
|
25
|
+
let scope = context.sourceCode.getScope(node);
|
|
26
|
+
while (scope) {
|
|
27
|
+
const variable = scope.set.get(node.name);
|
|
28
|
+
if (variable)
|
|
29
|
+
return variable;
|
|
30
|
+
if (!scope.upper)
|
|
31
|
+
break;
|
|
32
|
+
scope = scope.upper;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
function imported(node, name) {
|
|
37
|
+
if (node.type !== 'Identifier')
|
|
38
|
+
return false;
|
|
39
|
+
{
|
|
40
|
+
const variable = variableOf(node);
|
|
41
|
+
if (variable) {
|
|
42
|
+
return variable.defs.some((def) => {
|
|
43
|
+
if (def.type !== 'ImportBinding' ||
|
|
44
|
+
def.parent.source.value !== '@plumeria/core')
|
|
45
|
+
return false;
|
|
46
|
+
const specifier = def.node;
|
|
47
|
+
return name === '*'
|
|
48
|
+
? specifier.type === 'ImportNamespaceSpecifier' ||
|
|
49
|
+
specifier.type === 'ImportDefaultSpecifier'
|
|
50
|
+
: specifier.type === 'ImportSpecifier' &&
|
|
51
|
+
(specifier.imported.type === 'Identifier'
|
|
52
|
+
? specifier.imported.name
|
|
53
|
+
: specifier.imported.value) === name;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
function resolve(node, seen) {
|
|
60
|
+
if (seen.has(node))
|
|
61
|
+
return [];
|
|
62
|
+
seen.add(node);
|
|
63
|
+
if (node.type === 'Identifier') {
|
|
64
|
+
const variable = variableOf(node);
|
|
65
|
+
if (!variable || variable.defs.some((def) => def.type !== 'Variable'))
|
|
66
|
+
return [];
|
|
67
|
+
const start = node.range?.[0] ?? 0;
|
|
68
|
+
return variable.references.flatMap((reference) => reference.writeExpr && (reference.writeExpr.range?.[1] ?? 0) <= start
|
|
69
|
+
? [reference.writeExpr, ...resolve(reference.writeExpr, seen)]
|
|
70
|
+
: []);
|
|
71
|
+
}
|
|
72
|
+
if (node.type === 'MemberExpression' && !isUse(node)) {
|
|
73
|
+
const name = memberName(node);
|
|
74
|
+
if (name === null)
|
|
75
|
+
return [];
|
|
76
|
+
return resolve(node.object, seen).flatMap((object) => {
|
|
77
|
+
if (object.type !== 'ObjectExpression')
|
|
78
|
+
return [];
|
|
79
|
+
return object.properties.flatMap((property) => property.type === 'Property' &&
|
|
80
|
+
!property.computed &&
|
|
81
|
+
(property.key.type === 'Identifier'
|
|
82
|
+
? property.key.name
|
|
83
|
+
: property.key.type === 'Literal'
|
|
84
|
+
? property.key.value
|
|
85
|
+
: null) === name
|
|
86
|
+
? [property.value, ...resolve(property.value, seen)]
|
|
87
|
+
: []);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
function stored(node, seen) {
|
|
93
|
+
return resolve(node, new Set(seen)).some((value) => value !== node && uses(value, new Set([...seen, node])).length > 0);
|
|
94
|
+
}
|
|
95
|
+
function isUse(node) {
|
|
96
|
+
if (node.type !== 'CallExpression')
|
|
97
|
+
return false;
|
|
98
|
+
const callee = node.callee;
|
|
99
|
+
return callee.type === 'MemberExpression'
|
|
100
|
+
? memberName(callee) === 'use' && imported(callee.object, '*')
|
|
101
|
+
: callee.type === 'Identifier' && imported(callee, 'use');
|
|
102
|
+
}
|
|
103
|
+
function parts(node) {
|
|
104
|
+
if (node.type === 'BinaryExpression' && node.operator === '+') {
|
|
105
|
+
return [node.left, node.right];
|
|
106
|
+
}
|
|
107
|
+
if (node.type === 'TemplateLiteral') {
|
|
108
|
+
return [...node.expressions, ...node.quasis];
|
|
109
|
+
}
|
|
110
|
+
if (node.type === 'AssignmentExpression' && node.operator === '+=') {
|
|
111
|
+
return [node.left, node.right];
|
|
112
|
+
}
|
|
113
|
+
if (node.type === 'CallExpression' &&
|
|
114
|
+
node.callee.type === 'MemberExpression' &&
|
|
115
|
+
memberName(node.callee) === 'join') {
|
|
116
|
+
const object = node.callee.object;
|
|
117
|
+
const arrays = [object, ...resolve(object, new Set())].filter((value) => value.type === 'ArrayExpression');
|
|
118
|
+
if (arrays.length === 0)
|
|
119
|
+
return null;
|
|
120
|
+
return arrays.flatMap((array) => array.elements.filter((item) => item !== null));
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
function meaningful(node) {
|
|
125
|
+
if (node.type === 'Literal' && typeof node.value === 'string') {
|
|
126
|
+
return node.value.trim().length > 0;
|
|
127
|
+
}
|
|
128
|
+
if (node.type === 'TemplateElement') {
|
|
129
|
+
return (node.value.cooked ?? node.value.raw).trim().length > 0;
|
|
130
|
+
}
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
function uses(node, seen = new Set()) {
|
|
134
|
+
if (isUse(node))
|
|
135
|
+
return [node];
|
|
136
|
+
if ((node.type === 'Identifier' || node.type === 'MemberExpression') &&
|
|
137
|
+
stored(node, seen))
|
|
138
|
+
return [node];
|
|
139
|
+
const children = parts(node);
|
|
140
|
+
if (children)
|
|
141
|
+
return children.flatMap((child) => uses(child, seen));
|
|
142
|
+
if (node.type === 'ConditionalExpression') {
|
|
143
|
+
return [...uses(node.consequent, seen), ...uses(node.alternate, seen)];
|
|
144
|
+
}
|
|
145
|
+
if (node.type === 'LogicalExpression') {
|
|
146
|
+
return [...uses(node.left, seen), ...uses(node.right, seen)];
|
|
147
|
+
}
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
function passed(node) {
|
|
151
|
+
if (node.type === 'SpreadElement')
|
|
152
|
+
return passed(node.argument);
|
|
153
|
+
const arrays = [node, ...resolve(node, new Set())].filter((value) => value.type === 'ArrayExpression');
|
|
154
|
+
if (arrays.length > 0) {
|
|
155
|
+
return arrays.flatMap((array) => array.elements.flatMap((element) => element && passed(element).length > 0 ? [node] : []));
|
|
156
|
+
}
|
|
157
|
+
return uses(node);
|
|
158
|
+
}
|
|
159
|
+
function check(node) {
|
|
160
|
+
if (node.type === 'CallExpression') {
|
|
161
|
+
const calls = node.arguments.flatMap(passed);
|
|
162
|
+
if (calls.some((call) => !reported.has(call))) {
|
|
163
|
+
calls.forEach((call) => reported.add(call));
|
|
164
|
+
context.report({ node, messageId: 'wrapped' });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const children = parts(node);
|
|
169
|
+
if (!children)
|
|
170
|
+
return;
|
|
171
|
+
const flatten = (child) => parts(child)?.flatMap(flatten) ?? [child];
|
|
172
|
+
const values = children.flatMap(flatten).filter(meaningful);
|
|
173
|
+
const calls = values.flatMap((value) => uses(value));
|
|
174
|
+
if (values.length < 2 || !calls.some((call) => !reported.has(call)))
|
|
175
|
+
return;
|
|
176
|
+
calls.forEach((call) => reported.add(call));
|
|
177
|
+
if (values.filter((value) => uses(value).length > 0).length > 1) {
|
|
178
|
+
context.report({ node, messageId: 'merge' });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const label = (value) => {
|
|
182
|
+
const text = value.type === 'TemplateElement'
|
|
183
|
+
? JSON.stringify((value.value.cooked ?? value.value.raw).trim())
|
|
184
|
+
: context.sourceCode.getText(value);
|
|
185
|
+
return text.length > 40 ? `${text.slice(0, 39)}…` : text;
|
|
186
|
+
};
|
|
187
|
+
context.report({
|
|
188
|
+
node,
|
|
189
|
+
messageId: 'external',
|
|
190
|
+
data: {
|
|
191
|
+
values: values
|
|
192
|
+
.filter((value) => uses(value).length === 0)
|
|
193
|
+
.map(label)
|
|
194
|
+
.join(', '),
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
BinaryExpression: check,
|
|
200
|
+
TemplateLiteral: check,
|
|
201
|
+
CallExpression: check,
|
|
202
|
+
AssignmentExpression: check,
|
|
203
|
+
};
|
|
204
|
+
},
|
|
205
|
+
};
|
package/oxlint.json
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"@plumeria/no-order-dependent-overlap": "warn",
|
|
20
20
|
"@plumeria/no-unknown-css-properties": "error",
|
|
21
21
|
"@plumeria/no-unused-keys": "warn",
|
|
22
|
+
"@plumeria/no-unresolved-composition": "warn",
|
|
22
23
|
"@plumeria/sort-properties": "warn",
|
|
23
24
|
"@plumeria/format-properties": "warn",
|
|
24
25
|
"@plumeria/validate-values": "warn",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plumeria/eslint-plugin",
|
|
3
|
-
"version": "19.
|
|
3
|
+
"version": "19.6.0",
|
|
4
4
|
"description": "Plumeria ESLint plugin",
|
|
5
5
|
"author": "Refirst 11",
|
|
6
6
|
"license": "MIT",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"known-css-properties": "^0.37.0",
|
|
57
57
|
"zss-engine": "2.8.0",
|
|
58
|
-
"@plumeria/compiler": "19.
|
|
58
|
+
"@plumeria/compiler": "19.6.0"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"build": "rimraf dist && pnpm cjs",
|