@plumeria/eslint-plugin 19.6.1 → 19.7.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 CHANGED
@@ -15,6 +15,7 @@ The `plugin:@plumeria/recommended` config enables the following:
15
15
  - `@plumeria/no-invalid-selector`: **error**
16
16
  - `@plumeria/no-mixed-styling-props`: **error**
17
17
  - `@plumeria/no-order-dependent-overlap`: **warn**
18
+ - `@plumeria/no-style-prop-relay`: **error**
18
19
  - `@plumeria/no-unknown-css-properties`: **error**
19
20
  - `@plumeria/no-unresolved-composition`: **warn**
20
21
  - `@plumeria/no-unused-keys`: **warn**
@@ -100,7 +101,8 @@ Disallow calling APIs inside functions.
100
101
  ### no-invalid-selector
101
102
 
102
103
  Disallow invalid selector inside `css.create()` and `css.keyframes()` and `css.viewTransition()`.
103
- `create()` example: (Pseudo -> Query, Query -> Query)
104
+ `create()` example: (Pseudo -> Query, Query -> Query)
105
+ Also reports a functional pseudo-class nested inside one of the same name, such as `:where(:where(.a), .b)`, and flattens it with a fix when the nested call is a whole argument. `:not()` is reported without a fix.
104
106
 
105
107
  ### no-mixed-styling-props
106
108
 
@@ -127,13 +129,21 @@ properties in a vertical one, which is per element and cannot be read from the
127
129
  source. The rule reads them as one property; disable the line where an element
128
130
  is known to be vertical.
129
131
 
132
+ ### no-style-prop-relay
133
+
134
+ Reports a style prop that a component receives but only passes on. Apply it to the styling prop or `css.use()` on an element the component renders.
135
+
136
+ Accepts `{ styleProp }`; see [Configuring the styling prop](#configuring-the-styling-prop).
137
+
130
138
  ### no-unknown-css-properties
131
139
 
132
140
  Disallow unknown CSS properties in camelCase within `css.create`, `css.keyframes`, and `css.viewTransition`.
133
141
 
134
142
  ### no-unresolved-composition
135
143
 
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.
144
+ A safety net for `css.use()`. Warns when its result is combined with other class names or passed to another function, or when a function key is passed to it. Pass every style to one `css.use()` call, and apply function keys to the styling prop.
145
+
146
+ Accepts `{ styleProp }`; see [Configuring the styling prop](#configuring-the-styling-prop).
137
147
 
138
148
  ### no-unused-keys
139
149
 
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ 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
11
  const no_physical_properties_1 = require("./rules/no-physical-properties");
12
+ const no_style_prop_relay_1 = require("./rules/no-style-prop-relay");
12
13
  const no_order_dependent_overlap_1 = require("./rules/no-order-dependent-overlap");
13
14
  const no_unknown_css_properties_1 = require("./rules/no-unknown-css-properties");
14
15
  const no_unresolved_composition_1 = require("./rules/no-unresolved-composition");
@@ -30,6 +31,7 @@ const rules = {
30
31
  'no-mixed-styling-props': no_mixed_styling_props_1.noMixedStylingProps,
31
32
  'no-physical-properties': no_physical_properties_1.noPhysicalProperties,
32
33
  'no-order-dependent-overlap': no_order_dependent_overlap_1.noOrderDependentOverlap,
34
+ 'no-style-prop-relay': no_style_prop_relay_1.noStylePropRelay,
33
35
  'no-unknown-css-properties': no_unknown_css_properties_1.noUnknownCssProperties,
34
36
  'no-unused-keys': no_unused_keys_1.noUnusedKeys,
35
37
  'no-unresolved-composition': no_unresolved_composition_1.noUnresolvedComposition,
@@ -55,6 +57,7 @@ const configs = {
55
57
  '@plumeria/no-invalid-selector': 'error',
56
58
  '@plumeria/no-mixed-styling-props': 'error',
57
59
  '@plumeria/no-order-dependent-overlap': 'warn',
60
+ '@plumeria/no-style-prop-relay': 'error',
58
61
  '@plumeria/no-unknown-css-properties': 'error',
59
62
  '@plumeria/no-unused-keys': 'warn',
60
63
  '@plumeria/no-unresolved-composition': 'warn',
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.customPropsRequireImport = void 0;
4
4
  const compiler_1 = require("@plumeria/compiler");
5
5
  const style_prop_1 = require("../util/style-prop");
6
+ const scan_1 = require("../util/scan");
6
7
  const CORE = '@plumeria/core';
7
8
  const styleRoots = (node, roots) => {
8
9
  const expression = node;
@@ -46,19 +47,7 @@ const styleRoots = (node, roots) => {
46
47
  }
47
48
  return roots;
48
49
  };
49
- const SCAN_REUSE_MS = 1000;
50
50
  const MAX_ALIAS_DEPTH = 4;
51
- let recentScan;
52
- const scanTables = (cwd, styleProp) => {
53
- const key = `${cwd}\0${styleProp}`;
54
- const now = Date.now();
55
- if (!recentScan ||
56
- recentScan.key !== key ||
57
- now - recentScan.at > SCAN_REUSE_MS) {
58
- recentScan = { key, at: now, tables: (0, compiler_1.scanAll)(cwd, styleProp) };
59
- }
60
- return recentScan.tables;
61
- };
62
51
  exports.customPropsRequireImport = {
63
52
  meta: {
64
53
  type: 'problem',
@@ -108,7 +97,7 @@ exports.customPropsRequireImport = {
108
97
  const actualPath = (0, compiler_1.resolveImportPath)(binding.source, filename);
109
98
  if (!actualPath)
110
99
  return null;
111
- const tables = scanTables(context.cwd, styleProp);
100
+ const tables = (0, scan_1.scanTables)(context.cwd, styleProp);
112
101
  const resolved = (0, compiler_1.resolveExport)(actualPath, exportName);
113
102
  if (!resolved)
114
103
  return null;
@@ -3,6 +3,200 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.noInvalidSelector = void 0;
4
4
  const styleObject_1 = require("../util/styleObject");
5
5
  const staticValue_1 = require("../util/staticValue");
6
+ const flattenablePseudos = new Set([
7
+ 'is',
8
+ 'where',
9
+ 'matches',
10
+ '-webkit-any',
11
+ '-moz-any',
12
+ 'current',
13
+ 'past',
14
+ 'future',
15
+ 'has',
16
+ 'host',
17
+ 'host-context',
18
+ 'lang',
19
+ 'dir',
20
+ 'state',
21
+ 'part',
22
+ 'slotted',
23
+ 'nth-child',
24
+ 'nth-last-child',
25
+ 'nth-of-type',
26
+ 'nth-last-of-type',
27
+ 'nth-col',
28
+ 'nth-last-col',
29
+ 'heading',
30
+ 'active-view-transition-type',
31
+ 'highlight',
32
+ 'view-transition-group',
33
+ 'view-transition-image-pair',
34
+ 'view-transition-old',
35
+ 'view-transition-new',
36
+ 'cue',
37
+ 'cue-region',
38
+ 'picker',
39
+ 'scroll-button',
40
+ ]);
41
+ function skipString(text, i) {
42
+ const quote = text[i];
43
+ let j = i + 1;
44
+ while (j < text.length && text[j] !== quote) {
45
+ j += text[j] === '\\' ? 2 : 1;
46
+ }
47
+ return j + 1;
48
+ }
49
+ function matchClose(text, i) {
50
+ const close = text[i] === '(' ? ')' : ']';
51
+ let depth = 0;
52
+ let j = i;
53
+ while (j < text.length) {
54
+ const ch = text[j];
55
+ if (ch === '\\') {
56
+ j += 2;
57
+ continue;
58
+ }
59
+ if (ch === '"' || ch === "'") {
60
+ j = skipString(text, j);
61
+ continue;
62
+ }
63
+ if (ch === '(' || ch === '[')
64
+ depth++;
65
+ else if (ch === ')' || ch === ']') {
66
+ depth--;
67
+ if (depth === 0)
68
+ return ch === close ? j : -1;
69
+ }
70
+ j++;
71
+ }
72
+ return -1;
73
+ }
74
+ function findPseudoCalls(text) {
75
+ const calls = [];
76
+ let i = 0;
77
+ while (i < text.length) {
78
+ const ch = text[i];
79
+ if (ch === '\\') {
80
+ i += 2;
81
+ continue;
82
+ }
83
+ if (ch === '"' || ch === "'") {
84
+ i = skipString(text, i);
85
+ continue;
86
+ }
87
+ if (ch === '(' || ch === '[') {
88
+ const close = matchClose(text, i);
89
+ if (close < 0)
90
+ return null;
91
+ i = close + 1;
92
+ continue;
93
+ }
94
+ if (ch === ':') {
95
+ const match = /^(::?)(-?[a-zA-Z][\w-]*)\(/.exec(text.slice(i));
96
+ if (match) {
97
+ const open = i + match[0].length - 1;
98
+ const close = matchClose(text, open);
99
+ if (close < 0)
100
+ return null;
101
+ calls.push({
102
+ prefix: match[1],
103
+ name: match[2].toLowerCase(),
104
+ start: i,
105
+ end: close + 1,
106
+ args: text.slice(open + 1, close),
107
+ });
108
+ i = close + 1;
109
+ continue;
110
+ }
111
+ }
112
+ i++;
113
+ }
114
+ return calls;
115
+ }
116
+ function splitTopLevel(text) {
117
+ const parts = [];
118
+ let depth = 0;
119
+ let last = 0;
120
+ let i = 0;
121
+ while (i < text.length) {
122
+ const ch = text[i];
123
+ if (ch === '\\') {
124
+ i += 2;
125
+ continue;
126
+ }
127
+ if (ch === '"' || ch === "'") {
128
+ i = skipString(text, i);
129
+ continue;
130
+ }
131
+ if (ch === '(' || ch === '[')
132
+ depth++;
133
+ else if (ch === ')' || ch === ']')
134
+ depth--;
135
+ else if (ch === ',' && depth === 0) {
136
+ parts.push(text.slice(last, i));
137
+ last = i + 1;
138
+ }
139
+ i++;
140
+ }
141
+ parts.push(text.slice(last));
142
+ return parts;
143
+ }
144
+ function findSameNameNesting(text, ancestors) {
145
+ const calls = findPseudoCalls(text);
146
+ if (!calls)
147
+ return null;
148
+ for (const call of calls) {
149
+ const key = call.prefix + call.name;
150
+ if (ancestors.includes(key))
151
+ return call;
152
+ const nested = findSameNameNesting(call.args, [...ancestors, key]);
153
+ if (nested)
154
+ return nested;
155
+ }
156
+ return null;
157
+ }
158
+ function flattenSelector(text) {
159
+ const calls = findPseudoCalls(text);
160
+ if (!calls)
161
+ return text;
162
+ let result = '';
163
+ let last = 0;
164
+ for (const call of calls) {
165
+ const args = flattenArgs(call);
166
+ result += text.slice(last, call.start);
167
+ result +=
168
+ args === call.args
169
+ ? text.slice(call.start, call.end)
170
+ : `${call.prefix}${call.name}(${args})`;
171
+ last = call.end;
172
+ }
173
+ return result + text.slice(last);
174
+ }
175
+ function flattenArgs(call) {
176
+ let changed = false;
177
+ const items = [];
178
+ for (const part of splitTopLevel(call.args)) {
179
+ const flattened = flattenSelector(part);
180
+ if (flattened !== part)
181
+ changed = true;
182
+ const item = flattened.trim();
183
+ const inner = flattenablePseudos.has(call.name)
184
+ ? findPseudoCalls(item)
185
+ : null;
186
+ if (inner?.length === 1 &&
187
+ inner[0].start === 0 &&
188
+ inner[0].end === item.length &&
189
+ inner[0].prefix === call.prefix &&
190
+ inner[0].name === call.name) {
191
+ items.push(...splitTopLevel(inner[0].args).map((s) => s.trim()));
192
+ changed = true;
193
+ }
194
+ else {
195
+ items.push(item);
196
+ }
197
+ }
198
+ return changed ? items.join(', ') : call.args;
199
+ }
6
200
  exports.noInvalidSelector = {
7
201
  meta: {
8
202
  type: 'problem',
@@ -14,9 +208,12 @@ exports.noInvalidSelector = {
14
208
  noQueryInsidePseudo: 'Media/Container queries cannot be nested inside pseudo-selectors.',
15
209
  noQueryInsideQuery: 'Media/Container queries cannot be nested inside other queries.',
16
210
  noPseudoInsidePseudo: 'Pseudo-selectors cannot be nested inside other pseudo-selectors.',
211
+ flattenSameNamePseudo: 'Flatten nested "{{name}}()" into a single "{{name}}()".',
212
+ noSameNamePseudoNesting: '"{{name}}()" cannot be nested inside another "{{name}}()"; rewrite it without nesting.',
17
213
  invalidKeyframesKey: 'Keyframes keys must be "from", "to", or a percentage value (e.g. "0%", "50%", "100%").',
18
214
  invalidViewTransitionKey: 'ViewTransition keys must be one of: "group", "imagePair", "new", "old".',
19
215
  },
216
+ fixable: 'code',
20
217
  schema: [],
21
218
  },
22
219
  create(context) {
@@ -46,6 +243,34 @@ exports.noInvalidSelector = {
46
243
  return 'UNKNOWN';
47
244
  return 'SKIP';
48
245
  }
246
+ function checkPseudoNesting(node) {
247
+ const selector = node.type === 'Literal' && typeof node.value === 'string'
248
+ ? node.value
249
+ : staticValue(node);
250
+ if (typeof selector !== 'string' || !/^[:[]/.test(selector))
251
+ return;
252
+ const nested = findSameNameNesting(selector, []);
253
+ if (!nested)
254
+ return;
255
+ const flattened = flattenSelector(selector);
256
+ const raw = node.type === 'Literal' ? node.raw : '';
257
+ const quote = raw[0];
258
+ const fixable = flattened !== selector &&
259
+ (quote === '"' || quote === "'") &&
260
+ raw.slice(1, -1) === selector &&
261
+ !flattened.includes(quote) &&
262
+ !flattened.includes('\\');
263
+ context.report({
264
+ node,
265
+ messageId: fixable && !findSameNameNesting(flattened, [])
266
+ ? 'flattenSameNamePseudo'
267
+ : 'noSameNamePseudoNesting',
268
+ data: { name: nested.prefix + nested.name },
269
+ fix: fixable
270
+ ? (fixer) => fixer.replaceText(node, `${quote}${flattened}${quote}`)
271
+ : null,
272
+ });
273
+ }
49
274
  function checkNesting(node, parentType) {
50
275
  for (const prop of node.properties) {
51
276
  if (prop.type !== 'Property')
@@ -53,6 +278,10 @@ exports.noInvalidSelector = {
53
278
  const currentType = getSelectorType(prop.key);
54
279
  if (currentType === 'SKIP')
55
280
  continue;
281
+ if (currentType === 'PSEUDO' ||
282
+ (currentType === 'PROPERTY' &&
283
+ (prop.computed || prop.key.type !== 'Identifier')))
284
+ checkPseudoNesting(prop.key);
56
285
  if (currentType === 'UNKNOWN') {
57
286
  context.report({
58
287
  node: prop.key,
@@ -0,0 +1,2 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const noStylePropRelay: Rule.RuleModule;
@@ -0,0 +1,203 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.noStylePropRelay = void 0;
4
+ const style_prop_1 = require("../util/style-prop");
5
+ const scan_1 = require("../util/scan");
6
+ const isFunction = (node) => node?.type === 'FunctionDeclaration' ||
7
+ node?.type === 'FunctionExpression' ||
8
+ node?.type === 'ArrowFunctionExpression';
9
+ const componentFunctionOf = (node) => {
10
+ if (!node)
11
+ return undefined;
12
+ if (isFunction(node))
13
+ return node;
14
+ if (node.type === 'CallExpression') {
15
+ for (const argument of node.arguments) {
16
+ const fn = componentFunctionOf(argument);
17
+ if (fn)
18
+ return fn;
19
+ }
20
+ }
21
+ return undefined;
22
+ };
23
+ const unwrapDefault = (pattern) => pattern?.type === 'AssignmentPattern' ? pattern.left : pattern;
24
+ const keyName = (key) => key.type === 'Identifier'
25
+ ? key.name
26
+ : key.type === 'Literal' && typeof key.value === 'string'
27
+ ? key.value
28
+ : null;
29
+ exports.noStylePropRelay = {
30
+ meta: {
31
+ type: 'problem',
32
+ docs: {
33
+ description: 'Disallow passing a received style prop on without applying it',
34
+ },
35
+ schema: style_prop_1.stylePropSchema,
36
+ messages: {
37
+ relay: "'{{prop}}' is never applied here. Apply it to {{styleProp}} or css.use() on an element this component renders.",
38
+ },
39
+ },
40
+ create(context) {
41
+ const styleProp = (0, style_prop_1.resolveStyleProp)(context);
42
+ function components(program) {
43
+ if (program.type !== 'Program')
44
+ return [];
45
+ const found = [];
46
+ for (const node of program.body) {
47
+ const statement = node.type === 'ExportNamedDeclaration' ||
48
+ node.type === 'ExportDefaultDeclaration'
49
+ ? node.declaration
50
+ : node;
51
+ if (!statement)
52
+ continue;
53
+ if (isFunction(statement)) {
54
+ const id = statement.id;
55
+ found.push({
56
+ name: id?.name ?? 'default',
57
+ fn: statement,
58
+ });
59
+ }
60
+ else if (statement.type === 'CallExpression') {
61
+ const fn = componentFunctionOf(statement);
62
+ if (fn)
63
+ found.push({ name: 'default', fn });
64
+ }
65
+ else if (statement.type === 'VariableDeclaration') {
66
+ for (const declarator of statement.declarations) {
67
+ if (declarator.id.type !== 'Identifier')
68
+ continue;
69
+ const fn = componentFunctionOf(declarator.init);
70
+ if (fn)
71
+ found.push({ name: declarator.id.name, fn });
72
+ }
73
+ }
74
+ }
75
+ return found;
76
+ }
77
+ function isUse(node) {
78
+ if (node.type !== 'CallExpression')
79
+ return false;
80
+ const callee = node.callee;
81
+ return ((callee.type === 'MemberExpression' &&
82
+ !callee.computed &&
83
+ callee.property.type === 'Identifier' &&
84
+ callee.property.name === 'use') ||
85
+ (callee.type === 'Identifier' && callee.name === 'use'));
86
+ }
87
+ function roots(node, into) {
88
+ if (!node)
89
+ return into;
90
+ switch (node.type) {
91
+ case 'Identifier':
92
+ case 'MemberExpression':
93
+ into.push(node);
94
+ break;
95
+ case 'ArrayExpression':
96
+ node.elements.forEach((element) => roots(element, into));
97
+ break;
98
+ case 'SpreadElement':
99
+ roots(node.argument, into);
100
+ break;
101
+ case 'ConditionalExpression':
102
+ roots(node.consequent, into);
103
+ roots(node.alternate, into);
104
+ break;
105
+ case 'LogicalExpression':
106
+ roots(node.left, into);
107
+ roots(node.right, into);
108
+ break;
109
+ default: {
110
+ const inner = node.expression;
111
+ if (inner &&
112
+ [
113
+ 'ChainExpression',
114
+ 'TSAsExpression',
115
+ 'TSNonNullExpression',
116
+ 'TSSatisfiesExpression',
117
+ ].includes(node.type))
118
+ roots(inner, into);
119
+ }
120
+ }
121
+ return into;
122
+ }
123
+ function applied(fn) {
124
+ const found = [];
125
+ const visit = (node) => {
126
+ if (!node || typeof node !== 'object')
127
+ return;
128
+ if (Array.isArray(node)) {
129
+ node.forEach(visit);
130
+ return;
131
+ }
132
+ const current = node;
133
+ if (typeof current.type !== 'string')
134
+ return;
135
+ if (current.type === 'JSXAttribute' &&
136
+ current.name?.name === styleProp &&
137
+ current.value?.type === 'JSXExpressionContainer') {
138
+ roots(current.value.expression, found);
139
+ }
140
+ if (isUse(current) && current.type === 'CallExpression') {
141
+ current.arguments.forEach((argument) => roots(argument, found));
142
+ }
143
+ for (const key of Object.keys(current)) {
144
+ if (key === 'parent')
145
+ continue;
146
+ visit(current[key]);
147
+ }
148
+ };
149
+ visit(fn.body);
150
+ return found;
151
+ }
152
+ return {
153
+ Program(program) {
154
+ const tables = (0, scan_1.scanTables)(context.cwd, styleProp);
155
+ const table = tables.componentPropsTable ?? {};
156
+ for (const { name, fn } of components(program)) {
157
+ const props = table[`${context.filename}-${name}`];
158
+ if (!props)
159
+ continue;
160
+ const param = unwrapDefault(fn.params[0]);
161
+ const locals = new Map();
162
+ const patterns = new Map();
163
+ const paramName = param?.type === 'Identifier' ? param.name : null;
164
+ if (param?.type === 'ObjectPattern') {
165
+ for (const property of param.properties) {
166
+ if (property.type !== 'Property')
167
+ continue;
168
+ const prop = keyName(property.key);
169
+ const local = unwrapDefault(property.value);
170
+ if (prop === null)
171
+ continue;
172
+ patterns.set(prop, property);
173
+ if (local?.type === 'Identifier')
174
+ locals.set(local.name, prop);
175
+ }
176
+ }
177
+ const done = new Set();
178
+ for (const root of applied(fn)) {
179
+ if (root.type === 'Identifier') {
180
+ done.add(locals.get(root.name) ?? root.name);
181
+ }
182
+ else if (root.type === 'MemberExpression' &&
183
+ root.object.type === 'Identifier' &&
184
+ root.object.name === paramName &&
185
+ !root.computed &&
186
+ root.property.type === 'Identifier') {
187
+ done.add(root.property.name);
188
+ }
189
+ }
190
+ for (const prop of Object.keys(props)) {
191
+ if (done.has(prop))
192
+ continue;
193
+ context.report({
194
+ node: patterns.get(prop) ?? fn,
195
+ messageId: 'relay',
196
+ data: { prop, styleProp },
197
+ });
198
+ }
199
+ }
200
+ },
201
+ };
202
+ },
203
+ };
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.noUnresolvedComposition = void 0;
4
+ const style_prop_1 = require("../util/style-prop");
4
5
  const memberName = (node) => !node.computed && node.property.type === 'Identifier'
5
6
  ? node.property.name
6
7
  : node.computed && node.property.type === 'Literal'
@@ -12,14 +13,16 @@ exports.noUnresolvedComposition = {
12
13
  docs: {
13
14
  description: 'Warn when class names are composed outside css.use()',
14
15
  },
15
- schema: [],
16
+ schema: style_prop_1.stylePropSchema,
16
17
  messages: {
17
18
  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
19
  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
20
  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.',
21
+ dynamic: 'Function keys cannot go through css.use(). Apply {{source}} to {{styleProp}} instead.',
20
22
  },
21
23
  },
22
24
  create(context) {
25
+ const styleProp = (0, style_prop_1.resolveStyleProp)(context);
23
26
  const reported = new Set();
24
27
  function variableOf(node) {
25
28
  let scope = context.sourceCode.getScope(node);
@@ -100,6 +103,40 @@ exports.noUnresolvedComposition = {
100
103
  ? memberName(callee) === 'use' && imported(callee.object, '*')
101
104
  : callee.type === 'Identifier' && imported(callee, 'use');
102
105
  }
106
+ function isCreate(node) {
107
+ if (node.type !== 'CallExpression')
108
+ return false;
109
+ const callee = node.callee;
110
+ return callee.type === 'MemberExpression'
111
+ ? memberName(callee) === 'create' && imported(callee.object, '*')
112
+ : callee.type === 'Identifier' && imported(callee, 'create');
113
+ }
114
+ function dynamic(node, seen = new Set()) {
115
+ if (seen.has(node))
116
+ return false;
117
+ seen.add(node);
118
+ if (node.type === 'SpreadElement')
119
+ return dynamic(node.argument, seen);
120
+ if (node.type === 'ArrayExpression') {
121
+ return node.elements.some((element) => element !== null && dynamic(element, seen));
122
+ }
123
+ if (node.type === 'ConditionalExpression') {
124
+ return dynamic(node.consequent, seen) || dynamic(node.alternate, seen);
125
+ }
126
+ if (node.type === 'LogicalExpression') {
127
+ return dynamic(node.left, seen) || dynamic(node.right, seen);
128
+ }
129
+ if (node.type === 'CallExpression' &&
130
+ node.callee.type === 'MemberExpression' &&
131
+ memberName(node.callee) !== null) {
132
+ const object = node.callee.object;
133
+ return [object, ...resolve(object, new Set())].some(isCreate);
134
+ }
135
+ if (node.type === 'Identifier' || node.type === 'MemberExpression') {
136
+ return resolve(node, new Set()).some((value) => dynamic(value, seen));
137
+ }
138
+ return false;
139
+ }
103
140
  function parts(node) {
104
141
  if (node.type === 'BinaryExpression' && node.operator === '+') {
105
142
  return [node.left, node.right];
@@ -157,6 +194,18 @@ exports.noUnresolvedComposition = {
157
194
  return uses(node);
158
195
  }
159
196
  function check(node) {
197
+ if (isUse(node) && node.type === 'CallExpression') {
198
+ node.arguments
199
+ .filter((argument) => dynamic(argument))
200
+ .forEach((argument) => context.report({
201
+ node: argument,
202
+ messageId: 'dynamic',
203
+ data: {
204
+ source: context.sourceCode.getText(argument),
205
+ styleProp,
206
+ },
207
+ }));
208
+ }
160
209
  if (node.type === 'CallExpression') {
161
210
  const calls = node.arguments.flatMap(passed);
162
211
  if (calls.some((call) => !reported.has(call))) {
@@ -0,0 +1 @@
1
+ export declare const scanTables: (cwd: string, styleProp: string) => import("@plumeria/compiler").ScanSummary;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.scanTables = void 0;
4
+ const compiler_1 = require("@plumeria/compiler");
5
+ const SCAN_REUSE_MS = 1000;
6
+ let recentScan;
7
+ const scanTables = (cwd, styleProp) => {
8
+ const key = `${cwd}\0${styleProp}`;
9
+ const now = Date.now();
10
+ if (!recentScan ||
11
+ recentScan.key !== key ||
12
+ now - recentScan.at > SCAN_REUSE_MS) {
13
+ recentScan = { key, at: now, tables: (0, compiler_1.scanAll)(cwd, styleProp) };
14
+ }
15
+ return recentScan.tables;
16
+ };
17
+ exports.scanTables = scanTables;
package/oxlint.json CHANGED
@@ -17,6 +17,7 @@
17
17
  "@plumeria/no-invalid-selector": "error",
18
18
  "@plumeria/no-mixed-styling-props": "error",
19
19
  "@plumeria/no-order-dependent-overlap": "warn",
20
+ "@plumeria/no-style-prop-relay": "error",
20
21
  "@plumeria/no-unknown-css-properties": "error",
21
22
  "@plumeria/no-unused-keys": "warn",
22
23
  "@plumeria/no-unresolved-composition": "warn",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plumeria/eslint-plugin",
3
- "version": "19.6.1",
3
+ "version": "19.7.0",
4
4
  "description": "Plumeria ESLint plugin",
5
5
  "author": "Refirst 11",
6
6
  "license": "MIT",
@@ -54,8 +54,8 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "known-css-properties": "^0.37.0",
57
- "zss-engine": "2.8.0",
58
- "@plumeria/compiler": "19.6.1"
57
+ "zss-engine": "2.8.2",
58
+ "@plumeria/compiler": "19.7.0"
59
59
  },
60
60
  "scripts": {
61
61
  "build": "rimraf dist && pnpm cjs",