@plumeria/eslint-plugin 19.5.2 → 19.6.1

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
@@ -16,6 +16,7 @@ The `plugin:@plumeria/recommended` config enables the following:
16
16
  - `@plumeria/no-mixed-styling-props`: **error**
17
17
  - `@plumeria/no-order-dependent-overlap`: **warn**
18
18
  - `@plumeria/no-unknown-css-properties`: **error**
19
+ - `@plumeria/no-unresolved-composition`: **warn**
19
20
  - `@plumeria/no-unused-keys`: **warn**
20
21
  - `@plumeria/sort-properties`: **warn**
21
22
  - `@plumeria/format-properties`: **warn**
@@ -130,6 +131,10 @@ is known to be vertical.
130
131
 
131
132
  Disallow unknown CSS properties in camelCase within `css.create`, `css.keyframes`, and `css.viewTransition`.
132
133
 
134
+ ### no-unresolved-composition
135
+
136
+ A safety net for `css.use()`. Warns when its result is combined with other class names or passed to another function; pass every style to one `css.use()` call instead.
137
+
133
138
  ### no-unused-keys
134
139
 
135
140
  Warns when object keys are defined but not used, mainly in component files.
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,2 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const noUnresolvedComposition: Rule.RuleModule;
@@ -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.5.2",
3
+ "version": "19.6.1",
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.5.2"
58
+ "@plumeria/compiler": "19.6.1"
59
59
  },
60
60
  "scripts": {
61
61
  "build": "rimraf dist && pnpm cjs",