@plumeria/eslint-plugin 19.13.0 → 19.14.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
@@ -15,6 +15,7 @@ The `plugin:@plumeria/recommended` config enables the following:
15
15
  - `@plumeria/no-inner-call`: **error**
16
16
  - `@plumeria/no-invalid-selector`: **error**
17
17
  - `@plumeria/no-mixed-styling-props`: **error**
18
+ - `@plumeria/no-mutable-binding`: **warn**
18
19
  - `@plumeria/no-order-dependent-overlap`: **warn**
19
20
  - `@plumeria/no-style-prop-relay`: **error**
20
21
  - `@plumeria/no-unknown-css-properties`: **error**
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ const no_inner_call_1 = require("./rules/no-inner-call");
8
8
  const no_invalid_selector_1 = require("./rules/no-invalid-selector");
9
9
  const no_logical_properties_1 = require("./rules/no-logical-properties");
10
10
  const no_mixed_styling_props_1 = require("./rules/no-mixed-styling-props");
11
+ const no_mutable_binding_1 = require("./rules/no-mutable-binding");
11
12
  const no_physical_properties_1 = require("./rules/no-physical-properties");
12
13
  const no_style_prop_relay_1 = require("./rules/no-style-prop-relay");
13
14
  const no_order_dependent_overlap_1 = require("./rules/no-order-dependent-overlap");
@@ -29,6 +30,7 @@ const rules = {
29
30
  'no-invalid-selector': no_invalid_selector_1.noInvalidSelector,
30
31
  'no-logical-properties': no_logical_properties_1.noLogicalProperties,
31
32
  'no-mixed-styling-props': no_mixed_styling_props_1.noMixedStylingProps,
33
+ 'no-mutable-binding': no_mutable_binding_1.noMutableBinding,
32
34
  'no-physical-properties': no_physical_properties_1.noPhysicalProperties,
33
35
  'no-order-dependent-overlap': no_order_dependent_overlap_1.noOrderDependentOverlap,
34
36
  'no-style-prop-relay': no_style_prop_relay_1.noStylePropRelay,
@@ -57,6 +59,7 @@ const configs = {
57
59
  '@plumeria/no-inner-call': 'error',
58
60
  '@plumeria/no-invalid-selector': 'error',
59
61
  '@plumeria/no-mixed-styling-props': 'error',
62
+ '@plumeria/no-mutable-binding': 'warn',
60
63
  '@plumeria/no-order-dependent-overlap': 'warn',
61
64
  '@plumeria/no-style-prop-relay': 'error',
62
65
  '@plumeria/no-unknown-css-properties': 'error',
@@ -0,0 +1,2 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const noMutableBinding: Rule.RuleModule;
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.noMutableBinding = void 0;
4
+ const STYLE_APIS = new Set([
5
+ 'create',
6
+ 'createStatic',
7
+ 'createTheme',
8
+ 'keyframes',
9
+ 'viewTransition',
10
+ ]);
11
+ const isReference = (node, parent) => {
12
+ if (!parent)
13
+ return true;
14
+ if (parent.type === 'Property' && parent.key === node && !parent.computed)
15
+ return false;
16
+ if (parent.type === 'MemberExpression' &&
17
+ parent.property === node &&
18
+ !parent.computed)
19
+ return false;
20
+ return true;
21
+ };
22
+ const findVariable = (scope, name) => {
23
+ for (let current = scope; current; current = current.upper) {
24
+ const variable = current.set.get(name);
25
+ if (variable)
26
+ return variable;
27
+ }
28
+ return undefined;
29
+ };
30
+ exports.noMutableBinding = {
31
+ meta: {
32
+ type: 'problem',
33
+ docs: {
34
+ description: 'Disallow let and var bindings in Plumeria style definitions',
35
+ },
36
+ messages: {
37
+ mutable: "'{{ name }}' is declared with '{{ kind }}', but the build reads only the value it starts with. Declare it with 'const'.",
38
+ },
39
+ schema: [],
40
+ },
41
+ create(context) {
42
+ const plumeriaAliases = {};
43
+ const sourceCode = context.sourceCode;
44
+ const isStyleCall = (node) => {
45
+ const callee = node.callee;
46
+ if (callee.type === 'Identifier') {
47
+ return STYLE_APIS.has(plumeriaAliases[callee.name] ?? '');
48
+ }
49
+ return (callee.type === 'MemberExpression' &&
50
+ !callee.computed &&
51
+ callee.object.type === 'Identifier' &&
52
+ plumeriaAliases[callee.object.name] === 'NAMESPACE' &&
53
+ callee.property.type === 'Identifier' &&
54
+ STYLE_APIS.has(callee.property.name));
55
+ };
56
+ const check = (node, parent) => {
57
+ if (node.type === 'Identifier' && isReference(node, parent)) {
58
+ const variable = findVariable(sourceCode.getScope(node), node.name);
59
+ const definition = variable?.defs[0];
60
+ if (definition?.type === 'Variable' &&
61
+ definition.parent.kind !== 'const') {
62
+ context.report({
63
+ node,
64
+ messageId: 'mutable',
65
+ data: { name: node.name, kind: definition.parent.kind },
66
+ });
67
+ }
68
+ }
69
+ for (const [key, value] of Object.entries(node)) {
70
+ if (key === 'parent')
71
+ continue;
72
+ if (Array.isArray(value)) {
73
+ for (const child of value) {
74
+ if (child && typeof child.type === 'string')
75
+ check(child, node);
76
+ }
77
+ }
78
+ else if (value && typeof value.type === 'string') {
79
+ check(value, node);
80
+ }
81
+ }
82
+ };
83
+ return {
84
+ ImportDeclaration(node) {
85
+ if (node.source.value !== '@plumeria/core')
86
+ return;
87
+ node.specifiers.forEach((specifier) => {
88
+ if (specifier.type === 'ImportNamespaceSpecifier' ||
89
+ specifier.type === 'ImportDefaultSpecifier') {
90
+ plumeriaAliases[specifier.local.name] = 'NAMESPACE';
91
+ }
92
+ else {
93
+ const spec = specifier;
94
+ plumeriaAliases[specifier.local.name] =
95
+ spec.imported.type === 'Identifier'
96
+ ? spec.imported.name
97
+ : String(spec.imported.value);
98
+ }
99
+ });
100
+ },
101
+ CallExpression(node) {
102
+ if (!isStyleCall(node))
103
+ return;
104
+ for (const argument of node.arguments)
105
+ check(argument, node);
106
+ },
107
+ };
108
+ },
109
+ };
@@ -4,6 +4,30 @@ exports.noOrderDependentOverlap = void 0;
4
4
  const zss_engine_1 = require("zss-engine");
5
5
  const selector_1 = require("../util/selector");
6
6
  const styleObject_1 = require("../util/styleObject");
7
+ const staticValue_1 = require("../util/staticValue");
8
+ function overlapRange(first, second) {
9
+ if (first.kind !== second.kind || first.name !== second.name)
10
+ return false;
11
+ const axes = new Set([...first.axes.keys(), ...second.axes.keys()]);
12
+ for (const axis of axes) {
13
+ const a = first.axes.get(axis);
14
+ const b = second.axes.get(axis);
15
+ if (!a || !b)
16
+ continue;
17
+ if (a.unit !== b.unit)
18
+ return false;
19
+ if (Math.max(a.lower, b.lower) >= Math.min(a.upper, b.upper))
20
+ return false;
21
+ }
22
+ return true;
23
+ }
24
+ function crossesCondition(first, second) {
25
+ const a = (0, zss_engine_1.parseConditionalRange)(first);
26
+ const b = (0, zss_engine_1.parseConditionalRange)(second);
27
+ if (!a || !b || !overlapRange(a, b))
28
+ return false;
29
+ return !(0, zss_engine_1.impliesCondition)(first, second) && !(0, zss_engine_1.impliesCondition)(second, first);
30
+ }
7
31
  const DIRECT_SHORTHANDS = {};
8
32
  for (const [shorthand, longhands] of Object.entries(zss_engine_1.DIRECT_LONGHANDS)) {
9
33
  for (const longhand of longhands) {
@@ -59,6 +83,7 @@ exports.noOrderDependentOverlap = {
59
83
  messages: {
60
84
  alias: "'{{ first }}' and '{{ second }}' are the same property under two names, so neither outranks the other. The one written last wins.",
61
85
  crossing: "'{{ first }}' and '{{ second }}' overlap, but neither property outranks the other. The result depends on the order they are written.",
86
+ crossingCondition: "'{{ first }}' and '{{ second }}' can match at the same time, but neither contains the other, so the order they are written decides which one sets '{{ property }}' where both match. Split them into ranges that do not overlap, or give the overlap a condition of its own.",
62
87
  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.",
63
88
  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.",
64
89
  keep: "Keep '{{ keep }}'",
@@ -69,6 +94,7 @@ exports.noOrderDependentOverlap = {
69
94
  create(context) {
70
95
  const plumeriaAliases = {};
71
96
  const sourceCode = context.sourceCode;
97
+ const staticValue = (0, staticValue_1.staticValueResolver)(context);
72
98
  return {
73
99
  ImportDeclaration(node) {
74
100
  if (node.source.value === '@plumeria/core') {
@@ -131,9 +157,10 @@ exports.noOrderDependentOverlap = {
131
157
  ? prop.key.name
132
158
  : String(prop.key.value);
133
159
  }
134
- return prop.key.type === 'Literal' && typeof prop.key.value === 'string'
135
- ? prop.key.value
136
- : '';
160
+ if (prop.key.type === 'Literal' && typeof prop.key.value === 'string')
161
+ return prop.key.value;
162
+ const value = staticValue(prop.key);
163
+ return typeof value === 'string' ? value : '';
137
164
  }
138
165
  function declaredNames(node, withCustomProperties = false) {
139
166
  const names = new Set();
@@ -218,11 +245,23 @@ exports.noOrderDependentOverlap = {
218
245
  for (let j = i + 1; j < conditions.length; j++) {
219
246
  const first = conditions[i];
220
247
  const second = conditions[j];
221
- if (!(0, zss_engine_1.impliesCondition)(first.name, second.name))
222
- continue;
223
248
  const shared = [...first.sets].filter((name) => second.sets.has(name));
224
249
  if (shared.length === 0)
225
250
  continue;
251
+ if (crossesCondition(first.name, second.name)) {
252
+ context.report({
253
+ node: second.prop.key,
254
+ messageId: 'crossingCondition',
255
+ data: {
256
+ first: first.name,
257
+ second: second.name,
258
+ property: shared[0],
259
+ },
260
+ });
261
+ continue;
262
+ }
263
+ if (!(0, zss_engine_1.impliesCondition)(first.name, second.name))
264
+ continue;
226
265
  context.report({
227
266
  node: first.prop.key,
228
267
  messageId: 'condition',
@@ -175,6 +175,7 @@ function statesOf(selector) {
175
175
  }
176
176
  function compoundOf(first, second) {
177
177
  const states = [...new Set([...statesOf(first), ...statesOf(second)])];
178
+ states.sort((a, b) => Number(b.startsWith('[')) - Number(a.startsWith('[')));
178
179
  return states.join('') + (0, zss_engine_1.getPseudoElement)(first);
179
180
  }
180
181
  function covers(compound, first, second) {
package/oxlint.json CHANGED
@@ -18,6 +18,7 @@
18
18
  "@plumeria/no-inner-call": "error",
19
19
  "@plumeria/no-invalid-selector": "error",
20
20
  "@plumeria/no-mixed-styling-props": "error",
21
+ "@plumeria/no-mutable-binding": "warn",
21
22
  "@plumeria/no-order-dependent-overlap": "warn",
22
23
  "@plumeria/no-style-prop-relay": "error",
23
24
  "@plumeria/no-unknown-css-properties": "error",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plumeria/eslint-plugin",
3
- "version": "19.13.0",
3
+ "version": "19.14.1",
4
4
  "description": "Plumeria ESLint plugin",
5
5
  "author": "Refirst 11",
6
6
  "license": "MIT",
@@ -58,8 +58,8 @@
58
58
  },
59
59
  "dependencies": {
60
60
  "oxlint": "1.87.0",
61
- "zss-engine": "2.9.2",
62
- "@plumeria/compiler": "19.13.0"
61
+ "zss-engine": "2.10.0",
62
+ "@plumeria/compiler": "19.14.1"
63
63
  },
64
64
  "scripts": {
65
65
  "build": "rimraf dist && pnpm cjs",