@plumeria/eslint-plugin 21.1.1 → 21.1.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.
@@ -1,6 +1,41 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.noStylePropRelay = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const compiler_1 = require("@plumeria/compiler");
4
39
  const style_prop_1 = require("../util/style-prop");
5
40
  const scan_1 = require("../util/scan");
6
41
  const isFunction = (node) => node?.type === 'FunctionDeclaration' ||
@@ -20,6 +55,14 @@ const componentFunctionOf = (node) => {
20
55
  }
21
56
  return undefined;
22
57
  };
58
+ const realPathOf = (filename) => {
59
+ try {
60
+ return fs.realpathSync.native(filename);
61
+ }
62
+ catch {
63
+ return filename;
64
+ }
65
+ };
23
66
  const unwrapDefault = (pattern) => pattern?.type === 'AssignmentPattern' ? pattern.left : pattern;
24
67
  const keyName = (key) => key.type === 'Identifier'
25
68
  ? key.name
@@ -35,6 +78,7 @@ exports.noStylePropRelay = {
35
78
  schema: style_prop_1.stylePropSchema,
36
79
  messages: {
37
80
  relay: "'{{prop}}' is never applied here. Apply it to {{styleProp}} or css.use() on an element this component renders.",
81
+ receiver: "'{{prop}}' is a style received through a prop and cannot be passed on to a component that declares {{styleProp}}. Apply it on an element this component renders.",
38
82
  },
39
83
  },
40
84
  create(context) {
@@ -120,13 +164,14 @@ exports.noStylePropRelay = {
120
164
  }
121
165
  return into;
122
166
  }
123
- function applied(fn) {
167
+ function applied(fn, receives) {
124
168
  const found = [];
125
- const visit = (node) => {
169
+ const passed = [];
170
+ const visit = (node, owner) => {
126
171
  if (!node || typeof node !== 'object')
127
172
  return;
128
173
  if (Array.isArray(node)) {
129
- node.forEach(visit);
174
+ node.forEach((item) => visit(item, owner));
130
175
  return;
131
176
  }
132
177
  const current = node;
@@ -135,26 +180,74 @@ exports.noStylePropRelay = {
135
180
  if (current.type === 'JSXAttribute' &&
136
181
  current.name?.name === styleProp &&
137
182
  current.value?.type === 'JSXExpressionContainer') {
138
- roots(current.value.expression, found);
183
+ const expression = current.value.expression;
184
+ if (owner !== undefined && receives(owner)) {
185
+ passed.push({ attribute: current, roots: roots(expression, []) });
186
+ }
187
+ else {
188
+ roots(expression, found);
189
+ }
139
190
  }
140
191
  if (isUse(current) && current.type === 'CallExpression') {
141
192
  current.arguments.forEach((argument) => roots(argument, found));
142
193
  }
194
+ const next = current.type === 'JSXOpeningElement'
195
+ ? current.name?.type === 'JSXIdentifier'
196
+ ? current.name.name
197
+ : undefined
198
+ : owner;
143
199
  for (const key of Object.keys(current)) {
144
200
  if (key === 'parent')
145
201
  continue;
146
- visit(current[key]);
202
+ visit(current[key], next);
147
203
  }
148
204
  };
149
205
  visit(fn.body);
150
- return found;
206
+ return { found, passed };
151
207
  }
152
208
  return {
153
209
  Program(program) {
154
210
  const tables = (0, scan_1.scanTables)(context.cwd, styleProp);
155
211
  const table = tables.componentPropsTable ?? {};
212
+ const filename = realPathOf(context.filename);
213
+ const imports = new Map();
214
+ for (const node of program.body ?? []) {
215
+ if (node.type !== 'ImportDeclaration')
216
+ continue;
217
+ for (const specifier of node.specifiers) {
218
+ const name = specifier.type === 'ImportDefaultSpecifier'
219
+ ? 'default'
220
+ : specifier.type === 'ImportSpecifier'
221
+ ? keyName(specifier.imported)
222
+ : null;
223
+ if (name === null)
224
+ continue;
225
+ imports.set(specifier.local.name, {
226
+ source: node.source.value,
227
+ name,
228
+ });
229
+ }
230
+ }
231
+ const receivers = tables.styleReceiverTable ?? {};
232
+ const known = new Map();
233
+ const receives = (name) => {
234
+ if (known.has(name))
235
+ return known.get(name);
236
+ const binding = imports.get(name);
237
+ let key = `${filename}-${name}`;
238
+ if (binding) {
239
+ const actualPath = (0, compiler_1.resolveImportPath)(binding.source, filename);
240
+ const site = actualPath
241
+ ? (0, compiler_1.resolveExport)(actualPath, binding.name)
242
+ : null;
243
+ key = site ? `${site.filePath}-${site.localName}` : null;
244
+ }
245
+ const result = key !== null && (receivers[key]?.includes(styleProp) ?? false);
246
+ known.set(name, result);
247
+ return result;
248
+ };
156
249
  for (const { name, fn } of components(program)) {
157
- const props = table[`${context.filename}-${name}`];
250
+ const props = table[`${filename}-${name}`];
158
251
  if (!props)
159
252
  continue;
160
253
  const param = unwrapDefault(fn.params[0]);
@@ -174,18 +267,34 @@ exports.noStylePropRelay = {
174
267
  locals.set(local.name, prop);
175
268
  }
176
269
  }
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' &&
270
+ const propOf = (root) => root.type === 'Identifier'
271
+ ? (locals.get(root.name) ?? root.name)
272
+ : root.type === 'MemberExpression' &&
183
273
  root.object.type === 'Identifier' &&
184
274
  root.object.name === paramName &&
185
275
  !root.computed &&
186
- root.property.type === 'Identifier') {
187
- done.add(root.property.name);
188
- }
276
+ root.property.type === 'Identifier'
277
+ ? root.property.name
278
+ : null;
279
+ const done = new Set();
280
+ const { found, passed } = applied(fn, receives);
281
+ for (const root of found) {
282
+ const prop = propOf(root);
283
+ if (prop !== null)
284
+ done.add(prop);
285
+ }
286
+ for (const { attribute, roots: passedRoots } of passed) {
287
+ const prop = passedRoots
288
+ .map(propOf)
289
+ .find((name) => name !== null && name in props);
290
+ if (!prop)
291
+ continue;
292
+ done.add(prop);
293
+ context.report({
294
+ node: attribute,
295
+ messageId: 'receiver',
296
+ data: { prop, styleProp },
297
+ });
189
298
  }
190
299
  for (const prop of Object.keys(props)) {
191
300
  if (done.has(prop))
@@ -1 +1,4 @@
1
- export declare const scanTables: (cwd: string, styleProp: string) => import("@plumeria/compiler").ScanSummary;
1
+ import { styleReceivers } from '@plumeria/compiler';
2
+ export declare const scanTables: (cwd: string, styleProp: string) => import("@plumeria/compiler").ScanSummary & {
3
+ styleReceiverTable: ReturnType<typeof styleReceivers>;
4
+ };
package/dist/util/scan.js CHANGED
@@ -10,7 +10,12 @@ const scanTables = (cwd, styleProp) => {
10
10
  if (!recentScan ||
11
11
  recentScan.key !== key ||
12
12
  now - recentScan.at > SCAN_REUSE_MS) {
13
- recentScan = { key, at: now, tables: (0, compiler_1.scanAll)(cwd, styleProp) };
13
+ const summary = (0, compiler_1.scanAll)(cwd, styleProp);
14
+ recentScan = {
15
+ key,
16
+ at: now,
17
+ tables: { ...summary, styleReceiverTable: (0, compiler_1.styleReceivers)() },
18
+ };
14
19
  }
15
20
  return recentScan.tables;
16
21
  };
@@ -20,6 +20,13 @@ const EXCLUSIVE_PAIRS = [
20
20
  [':valid', ':invalid'],
21
21
  [':in-range', ':out-of-range'],
22
22
  [':required', ':optional'],
23
+ [':user-valid', ':user-invalid'],
24
+ [':playing', ':paused'],
25
+ [':dir(ltr)', ':dir(rtl)'],
26
+ [':nth-child(odd)', ':nth-child(even)'],
27
+ [':nth-last-child(odd)', ':nth-last-child(even)'],
28
+ [':nth-of-type(odd)', ':nth-of-type(even)'],
29
+ [':nth-last-of-type(odd)', ':nth-last-of-type(even)'],
23
30
  ].map((pair) => pair.slice().sort().join('\0'));
24
31
  const isNameChar = (char) => /[\w-]/.test(char);
25
32
  const skipName = (selector, from) => {
@@ -153,6 +160,24 @@ const negationOf = (state) => {
153
160
  return null;
154
161
  return state.slice(5, -1).trim();
155
162
  };
163
+ const exactAttributeOf = (state) => {
164
+ const match = /^\[\s*([\w-]+)\s*=\s*(?:"([^"\\]*)"|'([^'\\]*)'|([\w-]+))\s*([is])?\s*\]$/i.exec(state);
165
+ if (!match)
166
+ return null;
167
+ return {
168
+ name: match[1],
169
+ value: (match[2] ?? match[3] ?? match[4]).toLowerCase(),
170
+ };
171
+ };
172
+ const attributeNameOf = (state) => /^\[\s*([\w-]+)/.exec(state)?.[1] ?? null;
173
+ const absentAttribute = (negated, other) => negated !== null &&
174
+ /^\[\s*[\w-]+\s*\]$/.test(negated) &&
175
+ attributeNameOf(negated) === attributeNameOf(other);
176
+ const differentAttributeValues = (first, second) => {
177
+ const a = exactAttributeOf(first);
178
+ const b = exactAttributeOf(second);
179
+ return a !== null && b !== null && a.name === b.name && a.value !== b.value;
180
+ };
156
181
  function areExclusive(first, second) {
157
182
  const left = new Set(statesOf(first));
158
183
  const right = new Set(statesOf(second));
@@ -164,6 +189,11 @@ function areExclusive(first, second) {
164
189
  return true;
165
190
  if (EXCLUSIVE_PAIRS.includes([a, b].sort().join('\0')))
166
191
  return true;
192
+ if (differentAttributeValues(a, b))
193
+ return true;
194
+ if (absentAttribute(negationOf(a), b) ||
195
+ absentAttribute(negationOf(b), a))
196
+ return true;
167
197
  }
168
198
  }
169
199
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plumeria/eslint-plugin",
3
- "version": "21.1.1",
3
+ "version": "21.1.2",
4
4
  "description": "Plumeria ESLint plugin",
5
5
  "author": "Refirst 11",
6
6
  "license": "MIT",
@@ -59,7 +59,7 @@
59
59
  "dependencies": {
60
60
  "oxlint": "1.87.0",
61
61
  "zss-engine": "3.0.1",
62
- "@plumeria/compiler": "21.1.1"
62
+ "@plumeria/compiler": "21.1.2"
63
63
  },
64
64
  "scripts": {
65
65
  "build": "rimraf dist && pnpm cjs",