@plumeria/eslint-plugin 19.6.1 → 19.6.2

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**
@@ -127,13 +128,21 @@ properties in a vertical one, which is per element and cannot be read from the
127
128
  source. The rule reads them as one property; disable the line where an element
128
129
  is known to be vertical.
129
130
 
131
+ ### no-style-prop-relay
132
+
133
+ 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.
134
+
135
+ Accepts `{ styleProp }`; see [Configuring the styling prop](#configuring-the-styling-prop).
136
+
130
137
  ### no-unknown-css-properties
131
138
 
132
139
  Disallow unknown CSS properties in camelCase within `css.create`, `css.keyframes`, and `css.viewTransition`.
133
140
 
134
141
  ### no-unresolved-composition
135
142
 
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.
143
+ 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.
144
+
145
+ Accepts `{ styleProp }`; see [Configuring the styling prop](#configuring-the-styling-prop).
137
146
 
138
147
  ### no-unused-keys
139
148
 
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;
@@ -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.6.2",
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.6.1"
58
+ "@plumeria/compiler": "19.6.2"
59
59
  },
60
60
  "scripts": {
61
61
  "build": "rimraf dist && pnpm cjs",