@plumeria/eslint-plugin 19.2.16 → 19.4.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
@@ -7,7 +7,7 @@ Below are the available rules and the recommended configuration.
7
7
 
8
8
  The `plugin:@plumeria/recommended` config enables the following:
9
9
 
10
- - `@plumeria/props-require-import`: **error**
10
+ - `@plumeria/custom-props-require-import`: **error**
11
11
  - `@plumeria/no-combinator`: **error**
12
12
  - `@plumeria/no-destructure`: **error**
13
13
  - `@plumeria/no-inline-object`: **error**
@@ -20,8 +20,8 @@ The `plugin:@plumeria/recommended` config enables the following:
20
20
  - `@plumeria/sort-properties`: **warn**
21
21
  - `@plumeria/format-properties`: **warn**
22
22
  - `@plumeria/validate-values`: **warn**
23
- - `@plumeria/validate-pseudos`: **error**
24
- - `@plumeria/validate-at-rules`: **error**
23
+ - `@plumeria/validate-pseudos`: **warn**
24
+ - `@plumeria/validate-at-rules`: **warn**
25
25
 
26
26
  ```js
27
27
  import plumeria from '@plumeria/eslint-plugin';
@@ -31,7 +31,7 @@ export default [plumeria.configs.recommended];
31
31
 
32
32
  ## Configuring the styling prop
33
33
 
34
- Both `props-require-import` and `no-mixed-styling-props` need to know which JSX
34
+ Both `custom-props-require-import` and `no-mixed-styling-props` need to know which JSX
35
35
  prop carries styles. It is `classStyle` unless you changed it, so most projects
36
36
  configure nothing.
37
37
 
@@ -70,9 +70,13 @@ the lint rules report against a prop the compiler never transforms.
70
70
  The following rules are enabled by `plumeria.configs.recommended` with the
71
71
  severity shown in [Recommended configuration](#recommended-configuration).
72
72
 
73
- ### props-require-import
73
+ ### custom-props-require-import
74
74
 
75
- Disallow the styling prop in files without a `@plumeria/core` import.
75
+ Require the `@plumeria/core` import in a file that passes a `css.create` style to a
76
+ component through a prop other than the styling prop. The compiler cannot tell that
77
+ such a prop carries a style, so it compiles the file only when the file imports
78
+ `@plumeria/core`. A file that writes the styling prop itself is compiled without
79
+ the import.
76
80
 
77
81
  Accepts `{ styleProp }`; see [Configuring the styling prop](#configuring-the-styling-prop).
78
82
 
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
- const props_require_import_1 = require("./rules/props-require-import");
2
+ const custom_props_require_import_1 = require("./rules/custom-props-require-import");
3
3
  const expand_border_shorthands_1 = require("./rules/expand-border-shorthands");
4
4
  const no_combinator_1 = require("./rules/no-combinator");
5
5
  const no_destructure_1 = require("./rules/no-destructure");
@@ -18,7 +18,7 @@ const validate_values_1 = require("./rules/validate-values");
18
18
  const validate_pseudos_1 = require("./rules/validate-pseudos");
19
19
  const validate_at_rules_1 = require("./rules/validate-at-rules");
20
20
  const rules = {
21
- 'props-require-import': props_require_import_1.propsRequireImport,
21
+ 'custom-props-require-import': custom_props_require_import_1.customPropsRequireImport,
22
22
  'expand-border-shorthands': expand_border_shorthands_1.expandBorderShorthands,
23
23
  'no-combinator': no_combinator_1.noCombinator,
24
24
  'no-destructure': no_destructure_1.noDestructure,
@@ -45,7 +45,7 @@ const configs = {
45
45
  },
46
46
  },
47
47
  rules: {
48
- '@plumeria/props-require-import': 'error',
48
+ '@plumeria/custom-props-require-import': 'error',
49
49
  '@plumeria/no-combinator': 'error',
50
50
  '@plumeria/no-destructure': 'error',
51
51
  '@plumeria/no-inline-object': 'error',
@@ -58,8 +58,8 @@ const configs = {
58
58
  '@plumeria/sort-properties': 'warn',
59
59
  '@plumeria/format-properties': 'warn',
60
60
  '@plumeria/validate-values': 'warn',
61
- '@plumeria/validate-pseudos': 'error',
62
- '@plumeria/validate-at-rules': 'error',
61
+ '@plumeria/validate-pseudos': 'warn',
62
+ '@plumeria/validate-at-rules': 'warn',
63
63
  },
64
64
  },
65
65
  };
@@ -0,0 +1,2 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const customPropsRequireImport: Rule.RuleModule;
@@ -0,0 +1,185 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.customPropsRequireImport = void 0;
4
+ const utils_1 = require("@plumeria/utils");
5
+ const style_prop_1 = require("../util/style-prop");
6
+ const CORE = '@plumeria/core';
7
+ const styleRoots = (node, roots) => {
8
+ const expression = node;
9
+ if (!expression)
10
+ return roots;
11
+ switch (expression.type) {
12
+ case 'Identifier':
13
+ roots.push({ name: expression.name, member: null });
14
+ break;
15
+ case 'MemberExpression': {
16
+ let object = expression;
17
+ let member = null;
18
+ while (object.type === 'MemberExpression') {
19
+ member =
20
+ !object.computed && object.property.type === 'Identifier'
21
+ ? object.property.name
22
+ : null;
23
+ object = object.object;
24
+ }
25
+ if (object.type === 'Identifier')
26
+ roots.push({ name: object.name, member });
27
+ break;
28
+ }
29
+ case 'ArrayExpression':
30
+ expression.elements.forEach((element) => styleRoots(element, roots));
31
+ break;
32
+ case 'ConditionalExpression':
33
+ styleRoots(expression.consequent, roots);
34
+ styleRoots(expression.alternate, roots);
35
+ break;
36
+ case 'LogicalExpression':
37
+ styleRoots(expression.left, roots);
38
+ styleRoots(expression.right, roots);
39
+ break;
40
+ case 'ChainExpression':
41
+ case 'TSAsExpression':
42
+ case 'TSNonNullExpression':
43
+ case 'TSSatisfiesExpression':
44
+ styleRoots(expression.expression, roots);
45
+ break;
46
+ }
47
+ return roots;
48
+ };
49
+ const SCAN_REUSE_MS = 1000;
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, utils_1.scanAll)(cwd, styleProp) };
59
+ }
60
+ return recentScan.tables;
61
+ };
62
+ exports.customPropsRequireImport = {
63
+ meta: {
64
+ type: 'problem',
65
+ docs: {
66
+ description: 'Require the @plumeria/core import in files that pass a style through a prop other than the styling prop',
67
+ },
68
+ fixable: 'code',
69
+ messages: {
70
+ requiresImport: '{{prop}} passes a style from "{{source}}", so this file must import "@plumeria/core".',
71
+ },
72
+ schema: style_prop_1.stylePropSchema,
73
+ },
74
+ create(context) {
75
+ const styleProp = (0, style_prop_1.resolveStyleProp)(context);
76
+ const filename = context.filename;
77
+ const imports = new Map();
78
+ const candidates = [];
79
+ let hasPlumeriaImport = false;
80
+ const findVariable = (scope, name) => {
81
+ for (let current = scope; current; current = current.upper) {
82
+ const variable = current.set.get(name);
83
+ if (variable)
84
+ return variable;
85
+ }
86
+ return undefined;
87
+ };
88
+ const importedRoots = (roots, scope, depth) => roots.flatMap((root) => {
89
+ const definition = findVariable(scope, root.name)?.defs[0];
90
+ if (!definition)
91
+ return [];
92
+ if (definition.type === 'ImportBinding')
93
+ return [root];
94
+ const declarator = definition.node;
95
+ if (definition.type === 'Variable' &&
96
+ definition.parent?.kind === 'const' &&
97
+ declarator.init &&
98
+ depth < MAX_ALIAS_DEPTH) {
99
+ return importedRoots(styleRoots(declarator.init, []), context.sourceCode.getScope(definition.node), depth + 1);
100
+ }
101
+ return [];
102
+ });
103
+ const isStyle = (root) => {
104
+ const binding = imports.get(root.name);
105
+ const exportName = binding.importedName ?? root.member;
106
+ if (!exportName)
107
+ return null;
108
+ const actualPath = (0, utils_1.resolveImportPath)(binding.source, filename);
109
+ if (!actualPath)
110
+ return null;
111
+ const tables = scanTables(context.cwd, styleProp);
112
+ const resolved = (0, utils_1.resolveExport)(actualPath, exportName);
113
+ if (!resolved)
114
+ return null;
115
+ const key = `${resolved.filePath}-${resolved.localName}`;
116
+ return tables.createHashTable[key] !== undefined ? binding.source : null;
117
+ };
118
+ return {
119
+ ImportDeclaration(node) {
120
+ const source = node.source.value;
121
+ if (source.startsWith(CORE)) {
122
+ hasPlumeriaImport = true;
123
+ return;
124
+ }
125
+ for (const specifier of node.specifiers) {
126
+ if (specifier.type === 'ImportNamespaceSpecifier') {
127
+ imports.set(specifier.local.name, { source, importedName: null });
128
+ }
129
+ else if (specifier.type === 'ImportDefaultSpecifier') {
130
+ imports.set(specifier.local.name, {
131
+ source,
132
+ importedName: 'default',
133
+ });
134
+ }
135
+ else {
136
+ const imported = specifier.imported;
137
+ imports.set(specifier.local.name, {
138
+ source,
139
+ importedName: imported.name ?? String(imported.value),
140
+ });
141
+ }
142
+ }
143
+ },
144
+ JSXAttribute(node) {
145
+ if (node.name.type !== 'JSXIdentifier')
146
+ return;
147
+ const prop = node.name.name;
148
+ if (prop === styleProp)
149
+ return;
150
+ if (node.value?.type !== 'JSXExpressionContainer')
151
+ return;
152
+ const roots = styleRoots(node.value.expression, []);
153
+ if (roots.length > 0)
154
+ candidates.push({
155
+ node,
156
+ prop,
157
+ roots,
158
+ scope: context.sourceCode.getScope(node),
159
+ });
160
+ },
161
+ 'Program:exit'() {
162
+ if (hasPlumeriaImport || candidates.length === 0)
163
+ return;
164
+ let fixed = false;
165
+ for (const { node, prop, roots, scope } of candidates) {
166
+ const source = importedRoots(roots, scope, 0)
167
+ .map(isStyle)
168
+ .find((found) => found !== null);
169
+ if (!source)
170
+ continue;
171
+ const first = !fixed;
172
+ fixed = true;
173
+ context.report({
174
+ node,
175
+ messageId: 'requiresImport',
176
+ data: { prop, source },
177
+ fix: first
178
+ ? (fixer) => fixer.insertTextBefore(context.sourceCode.ast, 'import "@plumeria/core";\n')
179
+ : null,
180
+ });
181
+ }
182
+ },
183
+ };
184
+ },
185
+ };
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.noInvalidSelector = void 0;
4
- const utils_1 = require("@typescript-eslint/utils");
5
4
  const styleObject_1 = require("../util/styleObject");
5
+ const staticValue_1 = require("../util/staticValue");
6
6
  exports.noInvalidSelector = {
7
7
  meta: {
8
8
  type: 'problem',
@@ -21,43 +21,34 @@ exports.noInvalidSelector = {
21
21
  },
22
22
  create(context) {
23
23
  const plumeriaAliases = {};
24
- const parserServices = context.sourceCode.parserServices;
25
- const checker = parserServices?.program?.getTypeChecker();
24
+ const staticValue = (0, staticValue_1.staticValueResolver)(context);
26
25
  function getSelectorType(node) {
27
- if (node.type === utils_1.TSESTree.AST_NODE_TYPES.Literal &&
28
- typeof node.value === 'string') {
26
+ if (node.type === 'Literal' && typeof node.value === 'string') {
29
27
  if (node.value.startsWith('@'))
30
28
  return 'QUERY';
31
29
  if (node.value.startsWith(':'))
32
30
  return 'PSEUDO';
33
31
  return 'PROPERTY';
34
32
  }
35
- if (node.type === utils_1.TSESTree.AST_NODE_TYPES.Identifier &&
36
- !(node.parent?.type === utils_1.TSESTree.AST_NODE_TYPES.Property &&
37
- node.parent.computed)) {
33
+ if (node.type === 'Identifier' &&
34
+ !(node.parent?.type === 'Property' && node.parent.computed)) {
38
35
  return 'PROPERTY';
39
36
  }
40
- if (checker && parserServices?.esTreeNodeToTSNodeMap) {
41
- try {
42
- const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
43
- const type = checker.getTypeAtLocation(tsNode);
44
- if (type.isStringLiteral()) {
45
- if (type.value.startsWith('@'))
46
- return 'QUERY';
47
- if (type.value.startsWith(':'))
48
- return 'PSEUDO';
49
- return 'PROPERTY';
50
- }
51
- }
52
- catch (error) {
53
- }
54
- return 'UNKNOWN';
37
+ const value = staticValue(node);
38
+ if (typeof value === 'string') {
39
+ if (value.startsWith('@'))
40
+ return 'QUERY';
41
+ if (value.startsWith(':'))
42
+ return 'PSEUDO';
43
+ return 'PROPERTY';
55
44
  }
45
+ if (value !== undefined)
46
+ return 'UNKNOWN';
56
47
  return 'SKIP';
57
48
  }
58
49
  function checkNesting(node, parentType) {
59
50
  for (const prop of node.properties) {
60
- if (prop.type !== utils_1.TSESTree.AST_NODE_TYPES.Property)
51
+ if (prop.type !== 'Property')
61
52
  continue;
62
53
  const currentType = getSelectorType(prop.key);
63
54
  if (currentType === 'SKIP')
@@ -83,14 +74,14 @@ exports.noInvalidSelector = {
83
74
  messageId: 'noPseudoInsidePseudo',
84
75
  });
85
76
  }
86
- if (prop.value.type === utils_1.TSESTree.AST_NODE_TYPES.ObjectExpression) {
77
+ if (prop.value.type === 'ObjectExpression') {
87
78
  checkNesting(prop.value, currentType === 'PROPERTY' ? parentType : currentType);
88
79
  }
89
80
  }
90
81
  }
91
82
  function checkOnlyProperties(node) {
92
83
  for (const prop of node.properties) {
93
- if (prop.type !== utils_1.TSESTree.AST_NODE_TYPES.Property)
84
+ if (prop.type !== 'Property')
94
85
  continue;
95
86
  const currentType = getSelectorType(prop.key);
96
87
  if (currentType !== 'PROPERTY') {
@@ -99,24 +90,23 @@ exports.noInvalidSelector = {
99
90
  messageId: 'invalidKeySelector',
100
91
  });
101
92
  }
102
- if (prop.value.type === utils_1.TSESTree.AST_NODE_TYPES.ObjectExpression) {
93
+ if (prop.value.type === 'ObjectExpression') {
103
94
  checkOnlyProperties(prop.value);
104
95
  }
105
96
  }
106
97
  }
107
98
  function getKeyString(node) {
108
- if (node.type === utils_1.TSESTree.AST_NODE_TYPES.Literal &&
109
- typeof node.value === 'string') {
99
+ if (node.type === 'Literal' && typeof node.value === 'string') {
110
100
  return node.value;
111
101
  }
112
- if (node.type === utils_1.TSESTree.AST_NODE_TYPES.Identifier) {
102
+ if (node.type === 'Identifier') {
113
103
  return node.name;
114
104
  }
115
105
  return null;
116
106
  }
117
107
  function checkKeyframesKeys(node) {
118
108
  for (const prop of node.properties) {
119
- if (prop.type !== utils_1.TSESTree.AST_NODE_TYPES.Property)
109
+ if (prop.type !== 'Property')
120
110
  continue;
121
111
  const key = getKeyString(prop.key);
122
112
  if (key == null) {
@@ -125,7 +115,7 @@ exports.noInvalidSelector = {
125
115
  messageId: 'invalidKeyframesKey',
126
116
  });
127
117
  }
128
- if (prop.value.type === utils_1.TSESTree.AST_NODE_TYPES.ObjectExpression) {
118
+ if (prop.value.type === 'ObjectExpression') {
129
119
  checkOnlyProperties(prop.value);
130
120
  }
131
121
  if (key !== null &&
@@ -139,7 +129,7 @@ exports.noInvalidSelector = {
139
129
  function checkViewTransitionKeys(node) {
140
130
  const allowed = new Set(['group', 'imagePair', 'new', 'old']);
141
131
  for (const prop of node.properties) {
142
- if (prop.type !== utils_1.TSESTree.AST_NODE_TYPES.Property)
132
+ if (prop.type !== 'Property')
143
133
  continue;
144
134
  const key = getKeyString(prop.key);
145
135
  if (key == null) {
@@ -148,7 +138,7 @@ exports.noInvalidSelector = {
148
138
  messageId: 'invalidViewTransitionKey',
149
139
  });
150
140
  }
151
- if (prop.value.type === utils_1.TSESTree.AST_NODE_TYPES.ObjectExpression) {
141
+ if (prop.value.type === 'ObjectExpression') {
152
142
  checkOnlyProperties(prop.value);
153
143
  }
154
144
  if (key !== null && !allowed.has(key)) {
@@ -163,13 +153,12 @@ exports.noInvalidSelector = {
163
153
  ImportDeclaration(node) {
164
154
  if (node.source.value === '@plumeria/core') {
165
155
  node.specifiers.forEach((specifier) => {
166
- if (specifier.type ===
167
- utils_1.TSESTree.AST_NODE_TYPES.ImportNamespaceSpecifier ||
168
- specifier.type === utils_1.TSESTree.AST_NODE_TYPES.ImportDefaultSpecifier) {
156
+ if (specifier.type === 'ImportNamespaceSpecifier' ||
157
+ specifier.type === 'ImportDefaultSpecifier') {
169
158
  plumeriaAliases[specifier.local.name] = 'NAMESPACE';
170
159
  }
171
160
  else {
172
- const importedName = specifier.imported.type === utils_1.TSESTree.AST_NODE_TYPES.Identifier
161
+ const importedName = specifier.imported.type === 'Identifier'
173
162
  ? specifier.imported.name
174
163
  : String(specifier.imported.value);
175
164
  plumeriaAliases[specifier.local.name] = importedName;
@@ -181,10 +170,10 @@ exports.noInvalidSelector = {
181
170
  let isCssCreate = false;
182
171
  let isCssKeyframes = false;
183
172
  let isCssViewTransition = false;
184
- if (node.callee.type === utils_1.TSESTree.AST_NODE_TYPES.MemberExpression) {
185
- if (node.callee.object.type === utils_1.TSESTree.AST_NODE_TYPES.Identifier &&
173
+ if (node.callee.type === 'MemberExpression') {
174
+ if (node.callee.object.type === 'Identifier' &&
186
175
  plumeriaAliases[node.callee.object.name] === 'NAMESPACE') {
187
- const propertyName = node.callee.property.type === utils_1.TSESTree.AST_NODE_TYPES.Identifier
176
+ const propertyName = node.callee.property.type === 'Identifier'
188
177
  ? node.callee.property.name
189
178
  : null;
190
179
  if (propertyName === 'create')
@@ -195,7 +184,7 @@ exports.noInvalidSelector = {
195
184
  isCssViewTransition = true;
196
185
  }
197
186
  }
198
- else if (node.callee.type === utils_1.TSESTree.AST_NODE_TYPES.Identifier) {
187
+ else if (node.callee.type === 'Identifier') {
199
188
  const alias = plumeriaAliases[node.callee.name];
200
189
  if (alias === 'create')
201
190
  isCssCreate = true;
@@ -204,11 +193,10 @@ exports.noInvalidSelector = {
204
193
  if (alias === 'viewTransition')
205
194
  isCssViewTransition = true;
206
195
  }
207
- if (isCssCreate &&
208
- node.arguments[0]?.type === utils_1.TSESTree.AST_NODE_TYPES.ObjectExpression) {
196
+ if (isCssCreate && node.arguments[0]?.type === 'ObjectExpression') {
209
197
  const styleObj = node.arguments[0];
210
198
  styleObj.properties.forEach((prop) => {
211
- if (prop.type === utils_1.TSESTree.AST_NODE_TYPES.Property) {
199
+ if (prop.type === 'Property') {
212
200
  const currentType = getSelectorType(prop.key);
213
201
  if (currentType !== 'SKIP' && currentType === 'UNKNOWN') {
214
202
  context.report({
@@ -217,19 +205,18 @@ exports.noInvalidSelector = {
217
205
  });
218
206
  }
219
207
  }
220
- if (prop.type !== utils_1.TSESTree.AST_NODE_TYPES.Property)
208
+ if (prop.type !== 'Property')
221
209
  return;
222
210
  const style = (0, styleObject_1.styleObjectFromValue)(prop.value);
223
211
  if (style)
224
212
  checkNesting(style, 'CLASS');
225
213
  });
226
214
  }
227
- if (isCssKeyframes &&
228
- node.arguments[0]?.type === utils_1.TSESTree.AST_NODE_TYPES.ObjectExpression) {
215
+ if (isCssKeyframes && node.arguments[0]?.type === 'ObjectExpression') {
229
216
  checkKeyframesKeys(node.arguments[0]);
230
217
  }
231
218
  if (isCssViewTransition &&
232
- node.arguments[0]?.type === utils_1.TSESTree.AST_NODE_TYPES.ObjectExpression) {
219
+ node.arguments[0]?.type === 'ObjectExpression') {
233
220
  checkViewTransitionKeys(node.arguments[0]);
234
221
  }
235
222
  },
@@ -2,8 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.validateAtRules = void 0;
4
4
  exports.isValidAtRule = isValidAtRule;
5
- const utils_1 = require("@typescript-eslint/utils");
6
5
  const styleObject_1 = require("../util/styleObject");
6
+ const staticValue_1 = require("../util/staticValue");
7
7
  const VALID_AT_RULES = [
8
8
  '@media',
9
9
  '@container',
@@ -34,28 +34,17 @@ exports.validateAtRules = {
34
34
  },
35
35
  create(context) {
36
36
  const plumeriaAliases = {};
37
- const parserServices = context.sourceCode.parserServices;
38
- const checker = parserServices?.program?.getTypeChecker();
37
+ const staticValue = (0, staticValue_1.staticValueResolver)(context);
39
38
  function getKeyString(node) {
40
- if (node.type === utils_1.TSESTree.AST_NODE_TYPES.Literal &&
41
- typeof node.value === 'string') {
39
+ if (node.type === 'Literal' && typeof node.value === 'string') {
42
40
  return node.value;
43
41
  }
44
- if (checker && parserServices?.esTreeNodeToTSNodeMap) {
45
- try {
46
- const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
47
- const type = checker.getTypeAtLocation(tsNode);
48
- if (type.isStringLiteral())
49
- return type.value;
50
- }
51
- catch (error) {
52
- }
53
- }
54
- return null;
42
+ const value = staticValue(node);
43
+ return typeof value === 'string' ? value : null;
55
44
  }
56
45
  function checkProperties(node) {
57
46
  for (const prop of node.properties) {
58
- if (prop.type !== utils_1.TSESTree.AST_NODE_TYPES.Property)
47
+ if (prop.type !== 'Property')
59
48
  continue;
60
49
  const key = getKeyString(prop.key);
61
50
  if (key?.startsWith('@') && !isValidAtRule(key)) {
@@ -65,7 +54,7 @@ exports.validateAtRules = {
65
54
  data: { rule: key },
66
55
  });
67
56
  }
68
- if (prop.value.type === utils_1.TSESTree.AST_NODE_TYPES.ObjectExpression) {
57
+ if (prop.value.type === 'ObjectExpression') {
69
58
  checkProperties(prop.value);
70
59
  }
71
60
  }
@@ -75,13 +64,12 @@ exports.validateAtRules = {
75
64
  if (node.source.value !== '@plumeria/core')
76
65
  return;
77
66
  node.specifiers.forEach((specifier) => {
78
- if (specifier.type ===
79
- utils_1.TSESTree.AST_NODE_TYPES.ImportNamespaceSpecifier ||
80
- specifier.type === utils_1.TSESTree.AST_NODE_TYPES.ImportDefaultSpecifier) {
67
+ if (specifier.type === 'ImportNamespaceSpecifier' ||
68
+ specifier.type === 'ImportDefaultSpecifier') {
81
69
  plumeriaAliases[specifier.local.name] = 'NAMESPACE';
82
70
  }
83
71
  else {
84
- const importedName = specifier.imported.type === utils_1.TSESTree.AST_NODE_TYPES.Identifier
72
+ const importedName = specifier.imported.type === 'Identifier'
85
73
  ? specifier.imported.name
86
74
  : String(specifier.imported.value);
87
75
  plumeriaAliases[specifier.local.name] = importedName;
@@ -90,23 +78,22 @@ exports.validateAtRules = {
90
78
  },
91
79
  CallExpression(node) {
92
80
  let isCssCreate = false;
93
- if (node.callee.type === utils_1.TSESTree.AST_NODE_TYPES.MemberExpression &&
94
- node.callee.object.type === utils_1.TSESTree.AST_NODE_TYPES.Identifier &&
81
+ if (node.callee.type === 'MemberExpression' &&
82
+ node.callee.object.type === 'Identifier' &&
95
83
  plumeriaAliases[node.callee.object.name] === 'NAMESPACE' &&
96
- node.callee.property.type === utils_1.TSESTree.AST_NODE_TYPES.Identifier &&
84
+ node.callee.property.type === 'Identifier' &&
97
85
  node.callee.property.name === 'create') {
98
86
  isCssCreate = true;
99
87
  }
100
- else if (node.callee.type === utils_1.TSESTree.AST_NODE_TYPES.Identifier &&
88
+ else if (node.callee.type === 'Identifier' &&
101
89
  plumeriaAliases[node.callee.name] === 'create') {
102
90
  isCssCreate = true;
103
91
  }
104
- if (!isCssCreate ||
105
- node.arguments[0]?.type !== utils_1.TSESTree.AST_NODE_TYPES.ObjectExpression) {
92
+ if (!isCssCreate || node.arguments[0]?.type !== 'ObjectExpression') {
106
93
  return;
107
94
  }
108
95
  for (const prop of node.arguments[0].properties) {
109
- if (prop.type !== utils_1.TSESTree.AST_NODE_TYPES.Property)
96
+ if (prop.type !== 'Property')
110
97
  continue;
111
98
  const style = (0, styleObject_1.styleObjectFromValue)(prop.value);
112
99
  if (style)
@@ -3,8 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.validatePseudos = void 0;
4
4
  exports.splitChainedPseudos = splitChainedPseudos;
5
5
  exports.isValidPseudo = isValidPseudo;
6
- const utils_1 = require("@typescript-eslint/utils");
7
6
  const styleObject_1 = require("../util/styleObject");
7
+ const staticValue_1 = require("../util/staticValue");
8
8
  const VALID_STATIC_PSEUDOS = new Set([
9
9
  ':hover',
10
10
  ':active',
@@ -147,34 +147,21 @@ exports.validatePseudos = {
147
147
  },
148
148
  create(context) {
149
149
  const plumeriaAliases = {};
150
- const parserServices = context.sourceCode.parserServices;
151
- const checker = parserServices?.program?.getTypeChecker();
150
+ const staticValue = (0, staticValue_1.staticValueResolver)(context);
152
151
  function getSelectorString(node) {
153
- if (node.type === utils_1.TSESTree.AST_NODE_TYPES.Literal &&
154
- typeof node.value === 'string') {
152
+ if (node.type === 'Literal' && typeof node.value === 'string') {
155
153
  return node.value;
156
154
  }
157
- if (node.type === utils_1.TSESTree.AST_NODE_TYPES.Identifier &&
158
- !(node.parent?.type === utils_1.TSESTree.AST_NODE_TYPES.Property &&
159
- node.parent.computed)) {
155
+ if (node.type === 'Identifier' &&
156
+ !(node.parent?.type === 'Property' && node.parent.computed)) {
160
157
  return node.name;
161
158
  }
162
- if (checker && parserServices?.esTreeNodeToTSNodeMap) {
163
- try {
164
- const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
165
- const type = checker.getTypeAtLocation(tsNode);
166
- if (type.isStringLiteral()) {
167
- return type.value;
168
- }
169
- }
170
- catch (error) {
171
- }
172
- }
173
- return null;
159
+ const value = staticValue(node);
160
+ return typeof value === 'string' ? value : null;
174
161
  }
175
162
  function checkProperties(node) {
176
163
  for (const prop of node.properties) {
177
- if (prop.type !== utils_1.TSESTree.AST_NODE_TYPES.Property)
164
+ if (prop.type !== 'Property')
178
165
  continue;
179
166
  const selectorString = getSelectorString(prop.key);
180
167
  if (selectorString !== null && selectorString.startsWith(':')) {
@@ -192,7 +179,7 @@ exports.validatePseudos = {
192
179
  }
193
180
  }
194
181
  }
195
- if (prop.value.type === utils_1.TSESTree.AST_NODE_TYPES.ObjectExpression) {
182
+ if (prop.value.type === 'ObjectExpression') {
196
183
  checkProperties(prop.value);
197
184
  }
198
185
  }
@@ -201,13 +188,12 @@ exports.validatePseudos = {
201
188
  ImportDeclaration(node) {
202
189
  if (node.source.value === '@plumeria/core') {
203
190
  node.specifiers.forEach((specifier) => {
204
- if (specifier.type ===
205
- utils_1.TSESTree.AST_NODE_TYPES.ImportNamespaceSpecifier ||
206
- specifier.type === utils_1.TSESTree.AST_NODE_TYPES.ImportDefaultSpecifier) {
191
+ if (specifier.type === 'ImportNamespaceSpecifier' ||
192
+ specifier.type === 'ImportDefaultSpecifier') {
207
193
  plumeriaAliases[specifier.local.name] = 'NAMESPACE';
208
194
  }
209
195
  else {
210
- const importedName = specifier.imported.type === utils_1.TSESTree.AST_NODE_TYPES.Identifier
196
+ const importedName = specifier.imported.type === 'Identifier'
211
197
  ? specifier.imported.name
212
198
  : String(specifier.imported.value);
213
199
  plumeriaAliases[specifier.local.name] = importedName;
@@ -217,26 +203,25 @@ exports.validatePseudos = {
217
203
  },
218
204
  CallExpression(node) {
219
205
  let isCssCreate = false;
220
- if (node.callee.type === utils_1.TSESTree.AST_NODE_TYPES.MemberExpression) {
221
- if (node.callee.object.type === utils_1.TSESTree.AST_NODE_TYPES.Identifier &&
206
+ if (node.callee.type === 'MemberExpression') {
207
+ if (node.callee.object.type === 'Identifier' &&
222
208
  plumeriaAliases[node.callee.object.name] === 'NAMESPACE') {
223
- const propertyName = node.callee.property.type === utils_1.TSESTree.AST_NODE_TYPES.Identifier
209
+ const propertyName = node.callee.property.type === 'Identifier'
224
210
  ? node.callee.property.name
225
211
  : null;
226
212
  if (propertyName === 'create')
227
213
  isCssCreate = true;
228
214
  }
229
215
  }
230
- else if (node.callee.type === utils_1.TSESTree.AST_NODE_TYPES.Identifier) {
216
+ else if (node.callee.type === 'Identifier') {
231
217
  const alias = plumeriaAliases[node.callee.name];
232
218
  if (alias === 'create')
233
219
  isCssCreate = true;
234
220
  }
235
- if (isCssCreate &&
236
- node.arguments[0]?.type === utils_1.TSESTree.AST_NODE_TYPES.ObjectExpression) {
221
+ if (isCssCreate && node.arguments[0]?.type === 'ObjectExpression') {
237
222
  const styleObj = node.arguments[0];
238
223
  styleObj.properties.forEach((prop) => {
239
- if (prop.type !== utils_1.TSESTree.AST_NODE_TYPES.Property)
224
+ if (prop.type !== 'Property')
240
225
  return;
241
226
  const style = (0, styleObject_1.styleObjectFromValue)(prop.value);
242
227
  if (style)
@@ -0,0 +1,2 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare function staticValueResolver(context: Rule.RuleContext): (node: Rule.Node | any) => unknown;
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.staticValueResolver = staticValueResolver;
4
+ const utils_1 = require("@plumeria/utils");
5
+ const CORE = '@plumeria/core';
6
+ const unwrap = (node) => {
7
+ while (node &&
8
+ (node.type === 'TSAsExpression' ||
9
+ node.type === 'TSSatisfiesExpression' ||
10
+ node.type === 'TSNonNullExpression' ||
11
+ node.type === 'TSTypeAssertion'))
12
+ node = node.expression;
13
+ return node;
14
+ };
15
+ const findVariable = (scope, name) => {
16
+ for (let current = scope; current; current = current.upper) {
17
+ const variable = current.set.get(name);
18
+ if (variable)
19
+ return variable;
20
+ }
21
+ return undefined;
22
+ };
23
+ const importedNameOf = (specifier) => {
24
+ if (specifier.type === 'ImportNamespaceSpecifier')
25
+ return null;
26
+ if (specifier.type === 'ImportDefaultSpecifier')
27
+ return 'default';
28
+ return specifier.imported.type === 'Identifier'
29
+ ? specifier.imported.name
30
+ : String(specifier.imported.value);
31
+ };
32
+ function staticValueResolver(context) {
33
+ const { sourceCode, filename } = context;
34
+ const visiting = new Set();
35
+ const bindingOf = (node) => {
36
+ const definition = findVariable(sourceCode.getScope(node), node.name)
37
+ ?.defs[0];
38
+ if (!definition)
39
+ return undefined;
40
+ if (definition.type === 'Variable' &&
41
+ definition.parent.kind === 'const' &&
42
+ definition.node.init) {
43
+ return { kind: 'value', node: definition.node.init };
44
+ }
45
+ if (definition.type === 'ImportBinding') {
46
+ return {
47
+ kind: 'import',
48
+ source: String(definition.parent.source.value),
49
+ importedName: importedNameOf(definition.node),
50
+ };
51
+ }
52
+ return undefined;
53
+ };
54
+ const importedValue = (source, exportName) => {
55
+ const actualPath = (0, utils_1.resolveImportPath)(source, filename);
56
+ if (!actualPath)
57
+ return undefined;
58
+ return (0, utils_1.resolveExportValue)(actualPath, exportName);
59
+ };
60
+ const isCreateStatic = (callee) => {
61
+ callee = unwrap(callee);
62
+ if (callee.type === 'Identifier') {
63
+ const binding = bindingOf(callee);
64
+ return (binding?.kind === 'import' &&
65
+ binding.source === CORE &&
66
+ binding.importedName === 'createStatic');
67
+ }
68
+ if (callee.type === 'MemberExpression' &&
69
+ !callee.computed &&
70
+ callee.property.type === 'Identifier' &&
71
+ callee.property.name === 'createStatic' &&
72
+ callee.object.type === 'Identifier') {
73
+ const binding = bindingOf(callee.object);
74
+ return (binding?.kind === 'import' &&
75
+ binding.source === CORE &&
76
+ (binding.importedName === null || binding.importedName === 'default'));
77
+ }
78
+ return false;
79
+ };
80
+ const memberKey = (node) => {
81
+ if (!node.computed && node.property.type === 'Identifier') {
82
+ return node.property.name;
83
+ }
84
+ const key = resolve(node.property);
85
+ return typeof key === 'string' ? key : undefined;
86
+ };
87
+ const resolve = (node) => {
88
+ node = unwrap(node);
89
+ if (!node || visiting.has(node))
90
+ return undefined;
91
+ visiting.add(node);
92
+ try {
93
+ switch (node.type) {
94
+ case 'Literal':
95
+ return typeof node.value === 'string' ||
96
+ typeof node.value === 'number' ||
97
+ typeof node.value === 'boolean'
98
+ ? node.value
99
+ : undefined;
100
+ case 'BinaryExpression': {
101
+ const left = resolve(node.left);
102
+ const right = resolve(node.right);
103
+ if (left === undefined || right === undefined)
104
+ return undefined;
105
+ if (node.operator === '+') {
106
+ return typeof left === 'object' || typeof right === 'object'
107
+ ? undefined
108
+ : left + right;
109
+ }
110
+ if (typeof left !== 'number' || typeof right !== 'number')
111
+ return undefined;
112
+ switch (node.operator) {
113
+ case '-':
114
+ return left - right;
115
+ case '*':
116
+ return left * right;
117
+ case '/':
118
+ return left / right;
119
+ case '%':
120
+ return left % right;
121
+ case '**':
122
+ return left ** right;
123
+ default:
124
+ return undefined;
125
+ }
126
+ }
127
+ case 'TemplateLiteral':
128
+ return node.expressions.length === 0
129
+ ? node.quasis[0].value.cooked
130
+ : undefined;
131
+ case 'Identifier': {
132
+ const binding = bindingOf(node);
133
+ if (binding?.kind === 'value')
134
+ return resolve(binding.node);
135
+ if (binding?.kind === 'import' && binding.importedName) {
136
+ return importedValue(binding.source, binding.importedName);
137
+ }
138
+ return undefined;
139
+ }
140
+ case 'MemberExpression': {
141
+ const key = memberKey(node);
142
+ if (key === undefined)
143
+ return undefined;
144
+ const object = unwrap(node.object);
145
+ if (object.type === 'Identifier') {
146
+ const binding = bindingOf(object);
147
+ if (binding?.kind === 'import' && !binding.importedName) {
148
+ return importedValue(binding.source, key);
149
+ }
150
+ }
151
+ const value = resolve(object);
152
+ return value && typeof value === 'object' && Object.hasOwn(value, key)
153
+ ? value[key]
154
+ : undefined;
155
+ }
156
+ case 'ObjectExpression': {
157
+ const value = Object.create(null);
158
+ for (const property of node.properties) {
159
+ if (property.type !== 'Property' || property.kind !== 'init')
160
+ continue;
161
+ const key = !property.computed && property.key.type === 'Identifier'
162
+ ? property.key.name
163
+ : resolve(property.key);
164
+ if (typeof key !== 'string')
165
+ continue;
166
+ value[key] = resolve(property.value);
167
+ }
168
+ return value;
169
+ }
170
+ case 'CallExpression':
171
+ return isCreateStatic(node.callee)
172
+ ? resolve(node.arguments[0])
173
+ : undefined;
174
+ default:
175
+ return undefined;
176
+ }
177
+ }
178
+ finally {
179
+ visiting.delete(node);
180
+ }
181
+ };
182
+ return (node) => resolve(node);
183
+ }
package/oxlint.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "ignorePatterns": ["**/*.svelte", "**/*.vue"],
10
10
 
11
11
  "rules": {
12
- "@plumeria/props-require-import": "error",
12
+ "@plumeria/custom-props-require-import": "error",
13
13
  "@plumeria/no-combinator": "error",
14
14
  "@plumeria/no-destructure": "error",
15
15
  "@plumeria/no-inline-object": "error",
@@ -22,7 +22,7 @@
22
22
  "@plumeria/sort-properties": "warn",
23
23
  "@plumeria/format-properties": "warn",
24
24
  "@plumeria/validate-values": "warn",
25
- "@plumeria/validate-pseudos": "error",
26
- "@plumeria/validate-at-rules": "error"
25
+ "@plumeria/validate-pseudos": "warn",
26
+ "@plumeria/validate-at-rules": "warn"
27
27
  }
28
28
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plumeria/eslint-plugin",
3
- "version": "19.2.16",
3
+ "version": "19.4.0",
4
4
  "description": "Plumeria ESLint plugin",
5
5
  "author": "Refirst 11",
6
6
  "license": "MIT",
@@ -45,6 +45,7 @@
45
45
  "@types/estree": "^1.0.9",
46
46
  "@types/estree-jsx": "^1.0.5",
47
47
  "@typescript-eslint/parser": "^8.60.0",
48
+ "@typescript-eslint/utils": "^8.60.0",
48
49
  "eslint": "^10.4.0"
49
50
  },
50
51
  "publishConfig": {
@@ -52,9 +53,9 @@
52
53
  "provenance": true
53
54
  },
54
55
  "dependencies": {
55
- "@typescript-eslint/utils": "^8.60.0",
56
56
  "known-css-properties": "^0.37.0",
57
- "zss-engine": "2.8.0"
57
+ "zss-engine": "2.8.0",
58
+ "@plumeria/utils": "19.4.0"
58
59
  },
59
60
  "scripts": {
60
61
  "build": "rimraf dist && pnpm cjs",
@@ -1,2 +0,0 @@
1
- import type { Rule } from 'eslint';
2
- export declare const propsRequireImport: Rule.RuleModule;
@@ -1,52 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.propsRequireImport = void 0;
4
- const style_prop_1 = require("../util/style-prop");
5
- exports.propsRequireImport = {
6
- meta: {
7
- type: 'problem',
8
- docs: {
9
- description: 'Disallow the styling prop in files without a @plumeria/core import',
10
- },
11
- fixable: 'code',
12
- messages: {
13
- requiresImport: '{{prop}} requires importing "@plumeria/core".',
14
- },
15
- schema: style_prop_1.stylePropSchema,
16
- },
17
- create(context) {
18
- const styleProp = (0, style_prop_1.resolveStyleProp)(context);
19
- let hasPlumeriaImport = false;
20
- const stylePropNodes = [];
21
- return {
22
- ImportDeclaration(node) {
23
- const source = node.source.value;
24
- if (typeof source === 'string' && source.startsWith('@plumeria/core')) {
25
- hasPlumeriaImport = true;
26
- }
27
- },
28
- JSXAttribute(node) {
29
- if (node.name && node.name.name === styleProp) {
30
- stylePropNodes.push(node);
31
- }
32
- },
33
- 'Program:exit'() {
34
- if (!hasPlumeriaImport) {
35
- stylePropNodes.forEach((node, index) => {
36
- context.report({
37
- node,
38
- messageId: 'requiresImport',
39
- data: { prop: styleProp },
40
- fix(fixer) {
41
- if (index === 0) {
42
- return fixer.insertTextBefore(context.sourceCode.ast, 'import "@plumeria/core";\n');
43
- }
44
- return null;
45
- },
46
- });
47
- });
48
- }
49
- },
50
- };
51
- },
52
- };