@atlaskit/eslint-plugin-design-system 8.32.0 → 8.32.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.
Files changed (22) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/constellation/no-styled-tagged-template-expression/usage.mdx +42 -0
  3. package/dist/cjs/rules/consistent-css-prop-usage/index.js +375 -334
  4. package/dist/cjs/rules/no-styled-tagged-template-expression/index.js +2 -2
  5. package/dist/cjs/rules/utils/create-no-tagged-template-expression-rule/index.js +10 -3
  6. package/dist/cjs/rules/utils/is-supported-import.js +64 -10
  7. package/dist/es2019/rules/consistent-css-prop-usage/index.js +283 -267
  8. package/dist/es2019/rules/no-styled-tagged-template-expression/index.js +1 -1
  9. package/dist/es2019/rules/utils/create-no-tagged-template-expression-rule/index.js +26 -1
  10. package/dist/es2019/rules/utils/is-supported-import.js +60 -10
  11. package/dist/esm/rules/consistent-css-prop-usage/index.js +375 -334
  12. package/dist/esm/rules/no-styled-tagged-template-expression/index.js +1 -1
  13. package/dist/esm/rules/utils/create-no-tagged-template-expression-rule/index.js +11 -4
  14. package/dist/esm/rules/utils/is-supported-import.js +63 -9
  15. package/dist/types/rules/utils/is-supported-import.d.ts +11 -0
  16. package/dist/types-ts4.5/rules/utils/is-supported-import.d.ts +11 -0
  17. package/package.json +1 -1
  18. package/dist/cjs/rules/no-styled-tagged-template-expression/is-styled.js +0 -53
  19. package/dist/es2019/rules/no-styled-tagged-template-expression/is-styled.js +0 -45
  20. package/dist/esm/rules/no-styled-tagged-template-expression/is-styled.js +0 -47
  21. package/dist/types/rules/no-styled-tagged-template-expression/is-styled.d.ts +0 -7
  22. package/dist/types-ts4.5/rules/no-styled-tagged-template-expression/is-styled.d.ts +0 -7
@@ -8,8 +8,6 @@ import { createLintRule } from '../utils/create-rule';
8
8
  import { getFirstSupportedImport } from '../utils/get-first-supported-import';
9
9
  import { getModuleOfIdentifier } from '../utils/get-import-node-by-source';
10
10
  import { CSS_IN_JS_IMPORTS } from '../utils/is-supported-import';
11
- // File-level tracking of styles hoisted from the cssAtTopOfModule/cssAtBottomOfModule fixers
12
- let hoistedCss = [];
13
11
  const isDOMElementName = elementName => elementName.charAt(0) !== elementName.charAt(0).toUpperCase() && elementName.charAt(0) === elementName.charAt(0).toLowerCase();
14
12
  function isCssCallExpression(node, cssFunctions) {
15
13
  cssFunctions = [...cssFunctions, 'cssMap'];
@@ -26,334 +24,352 @@ const getProgramNode = expression => {
26
24
  }
27
25
  return expression.parent;
28
26
  };
27
+ class JSXExpressionLinter {
28
+ // File-level tracking of styles hoisted from the cssAtTopOfModule/cssAtBottomOfModule fixers.
29
29
 
30
- /**
31
- * Generates the declarator string when fixing the cssAtTopOfModule/cssAtBottomOfModule cases.
32
- * When `styles` already exists, `styles_1, styles_2, ..., styles_X` are incrementally created for each unhoisted style.
33
- * The generated `styles` varibale declaration names must be manually modified to be more informative at the discretion of owning teams.
34
- */
35
- const getDeclaratorString = context => {
36
- let scope = context.getScope();
37
-
38
- // Get to ModuleScope
39
- while (scope && scope.upper && scope.upper.type !== 'global') {
40
- var _scope;
41
- scope = (_scope = scope) === null || _scope === void 0 ? void 0 : _scope.upper;
30
+ /**
31
+ * Traverses and lints a expression found in a JSX css or xcss prop, e.g.
32
+ * <div css={expressionToLint} />
33
+ *
34
+ * @param context The context of the rule. Used to find the current scope and the source code of the file.
35
+ * @param cssAttributeName Used to encapsulate ObjectExpressions when cssAtTopOfModule/cssAtBottomOfModule violations are triggered.
36
+ * @param configuration What css-related functions to account for (eg. css, xcss, cssMap), and whether to detect bottom vs top expressions.
37
+ * @param expression The expression to traverse and lint.
38
+ */
39
+ constructor(context, cssAttributeName, configuration, expression) {
40
+ this.context = context;
41
+ this.cssAttributeName = cssAttributeName;
42
+ this.configuration = configuration;
43
+ this.expression = expression;
44
+ this.hoistedCss = [];
42
45
  }
43
- const variables = scope.variables.map(variable => variable.name).concat(hoistedCss);
44
- let count = 2;
45
- let declaratorName = 'styles';
46
46
 
47
- // Base case
48
- if (!variables.includes(declaratorName)) {
49
- return declaratorName;
50
- } else {
51
- // If styles already exists, increment the number
52
- while (variables.includes(`${declaratorName}${count}`)) {
53
- count++;
47
+ /**
48
+ * Generates the declarator string when fixing the cssAtTopOfModule/cssAtBottomOfModule cases.
49
+ * When `styles` already exists, `styles_1, styles_2, ..., styles_X` are incrementally created for each unhoisted style.
50
+ *
51
+ * The generated `styles` varibale declaration names must be manually modified to be more informative at the discretion of owning teams.
52
+ */
53
+ getDeclaratorString() {
54
+ let scope = this.context.getScope();
55
+
56
+ // Get to ModuleScope
57
+ while (scope && scope.upper && scope.upper.type !== 'global') {
58
+ var _scope;
59
+ scope = (_scope = scope) === null || _scope === void 0 ? void 0 : _scope.upper;
54
60
  }
55
- }
61
+ const variables = scope.variables.map(variable => variable.name).concat(this.hoistedCss);
62
+ let count = 2;
63
+ let declaratorName = 'styles';
56
64
 
57
- // Keep track of it by adding it to the hoistedCss global array
58
- hoistedCss = [...hoistedCss, `${declaratorName}${count}`];
59
- return `${declaratorName}${count}`;
60
- };
61
- function analyzeIdentifier(context, sourceIdentifier, configuration, cssAttributeName) {
62
- var _getIdentifierInParen, _getIdentifierInParen2;
63
- const scope = context.getScope();
64
- const [identifier] = (_getIdentifierInParen = (_getIdentifierInParen2 = getIdentifierInParentScope(scope, sourceIdentifier.name)) === null || _getIdentifierInParen2 === void 0 ? void 0 : _getIdentifierInParen2.identifiers) !== null && _getIdentifierInParen !== void 0 ? _getIdentifierInParen : [];
65
- if (!identifier || !identifier.parent) {
66
- // Identifier isn't in the module, skip!
67
- return;
68
- }
69
- if (identifier.parent.type !== 'VariableDeclarator') {
70
- // When variable is not in the file or coming from import
71
- context.report({
72
- node: sourceIdentifier,
73
- messageId: 'cssInModule'
74
- });
75
- return;
76
- }
77
- if (identifier.parent.parent.parent.type !== 'Program') {
78
- // When variable is declared inside the component
79
- context.report({
80
- node: sourceIdentifier,
81
- messageId: configuration.stylesPlacement === 'bottom' ? 'cssAtBottomOfModule' : 'cssAtTopOfModule',
82
- fix: fixer => {
83
- if (configuration.fixNamesOnly) {
84
- return [];
85
- }
86
- return fixCssNotInModuleScope(fixer, context, configuration, identifier);
65
+ // Base case
66
+ if (!variables.includes(declaratorName)) {
67
+ return declaratorName;
68
+ } else {
69
+ // If styles already exists, increment the number
70
+ while (variables.includes(`${declaratorName}${count}`)) {
71
+ count++;
87
72
  }
88
- });
89
- return;
73
+ }
74
+
75
+ // Keep track of it by adding it to the hoistedCss global array
76
+ this.hoistedCss = [...this.hoistedCss, `${declaratorName}${count}`];
77
+ return `${declaratorName}${count}`;
90
78
  }
91
- if (identifier.parent && identifier.parent.init && !isCssCallExpression(identifier.parent.init, configuration.cssFunctions)) {
92
- // When variable value is not of type css({})
93
- const value = identifier.parent.init;
94
- if (!value) {
79
+ analyzeIdentifier(sourceIdentifier) {
80
+ var _getIdentifierInParen, _getIdentifierInParen2;
81
+ const scope = this.context.getScope();
82
+ const [identifier] = (_getIdentifierInParen = (_getIdentifierInParen2 = getIdentifierInParentScope(scope, sourceIdentifier.name)) === null || _getIdentifierInParen2 === void 0 ? void 0 : _getIdentifierInParen2.identifiers) !== null && _getIdentifierInParen !== void 0 ? _getIdentifierInParen : [];
83
+ if (!identifier || !identifier.parent) {
84
+ // Identifier isn't in the module, skip!
85
+ return;
86
+ }
87
+ if (identifier.parent.type !== 'VariableDeclarator') {
88
+ // When variable is not in the file or coming from import
89
+ this.context.report({
90
+ node: sourceIdentifier,
91
+ messageId: 'cssInModule'
92
+ });
95
93
  return;
96
94
  }
97
- const valueExpression =
98
- // @ts-expect-error remove once eslint types are switched to @typescript-eslint
99
- value.type === 'TSAsExpression' ? value.expression : value;
100
- if (['ObjectExpression', 'TemplateLiteral'].includes(valueExpression.type)) {
101
- context.report({
102
- node: identifier,
103
- messageId: 'cssObjectTypeOnly',
95
+ if (identifier.parent.parent.parent.type !== 'Program') {
96
+ // When variable is declared inside the component
97
+ this.context.report({
98
+ node: sourceIdentifier,
99
+ messageId: this.configuration.stylesPlacement === 'bottom' ? 'cssAtBottomOfModule' : 'cssAtTopOfModule',
104
100
  fix: fixer => {
105
- if (configuration.fixNamesOnly) {
101
+ if (this.configuration.fixNamesOnly) {
106
102
  return [];
107
103
  }
108
- return addCssFunctionCall(fixer, context, identifier.parent, configuration, cssAttributeName);
104
+ return this.fixCssNotInModuleScope(fixer, identifier, false);
109
105
  }
110
106
  });
111
- } else {
112
- context.report({
113
- node: identifier,
114
- messageId: 'cssObjectTypeOnly'
115
- });
107
+ return;
116
108
  }
117
- return;
118
- }
119
- const spreadProperties = isNodeOfType(identifier.parent.init, 'CallExpression') && findSpreadProperties(identifier.parent.init.arguments[0]);
120
- if (spreadProperties) {
121
- // TODO: Recursively handle spread items in children properties.
122
- spreadProperties.forEach(prop => {
123
- context.report({
124
- node: prop,
125
- messageId: 'cssArrayStylesOnly'
109
+ if (identifier.parent && identifier.parent.init && !isCssCallExpression(identifier.parent.init, this.configuration.cssFunctions)) {
110
+ // When variable value is not of type css({})
111
+ const value = identifier.parent.init;
112
+ if (!value) {
113
+ return;
114
+ }
115
+ const valueExpression =
116
+ // @ts-expect-error remove once eslint types are switched to @typescript-eslint
117
+ value.type === 'TSAsExpression' ? value.expression : value;
118
+ if (['ObjectExpression', 'TemplateLiteral'].includes(valueExpression.type)) {
119
+ this.context.report({
120
+ node: identifier,
121
+ messageId: 'cssObjectTypeOnly',
122
+ fix: fixer => {
123
+ if (this.configuration.fixNamesOnly) {
124
+ return [];
125
+ }
126
+ return this.addCssFunctionCall(fixer, identifier.parent);
127
+ }
128
+ });
129
+ } else {
130
+ this.context.report({
131
+ node: identifier,
132
+ messageId: 'cssObjectTypeOnly'
133
+ });
134
+ }
135
+ return;
136
+ }
137
+ const spreadProperties = isNodeOfType(identifier.parent.init, 'CallExpression') && findSpreadProperties(identifier.parent.init.arguments[0]);
138
+ if (spreadProperties) {
139
+ // TODO: Recursively handle spread items in children properties.
140
+ spreadProperties.forEach(prop => {
141
+ this.context.report({
142
+ node: prop,
143
+ messageId: 'cssArrayStylesOnly'
144
+ });
126
145
  });
127
- });
146
+ }
128
147
  }
129
- }
130
148
 
131
- /**
132
- * Returns a fixer that adds `import { css } from 'import-source'` or
133
- * `import { xcss } from 'import-source'` to the start of the file, depending
134
- * on the value of cssAttributeName and importSource.
135
- */
136
- const addImportSource = (context, fixer, configuration, cssAttributeName) => {
137
- const importSource = cssAttributeName === 'xcss' ? configuration.xcssImportSource : configuration.cssImportSource;
149
+ /**
150
+ * Returns a fixer that adds `import { css } from 'import-source'` or
151
+ * `import { xcss } from 'import-source'` to the start of the file, depending
152
+ * on the value of cssAttributeName and importSource.
153
+ */
154
+ addImportSource(fixer) {
155
+ const importSource = this.cssAttributeName === 'xcss' ? this.configuration.xcssImportSource : this.configuration.cssImportSource;
138
156
 
139
- // Add the `import { css } from 'my-css-in-js-library';` statement
140
- const packageImport = getFirstSupportedImport(context, [importSource]);
141
- if (packageImport) {
142
- const addCssImport = Import.insertNamedSpecifiers(packageImport, [cssAttributeName], fixer);
143
- if (addCssImport) {
144
- return addCssImport;
157
+ // Add the `import { css } from 'my-css-in-js-library';` statement
158
+ const packageImport = getFirstSupportedImport(this.context, [importSource]);
159
+ if (packageImport) {
160
+ const addCssImport = Import.insertNamedSpecifiers(packageImport, [this.cssAttributeName], fixer);
161
+ if (addCssImport) {
162
+ return addCssImport;
163
+ }
164
+ } else {
165
+ return insertAtStartOfFile(fixer, `${insertImportDeclaration(importSource, [this.cssAttributeName])};\n`);
145
166
  }
146
- } else {
147
- return insertAtStartOfFile(fixer, `${insertImportDeclaration(importSource, [cssAttributeName])};\n`);
148
167
  }
149
- };
150
168
 
151
- /**
152
- * Returns a list of fixes that:
153
- * - add the `css` or `xcss` function call around the current node.
154
- * - add an import statement for the package from which `css` is imported
155
- */
156
- const addCssFunctionCall = (fixer, context, node, configuration, cssAttributeName) => {
157
- const fixes = [];
158
- const sourceCode = context.getSourceCode();
159
- if (node.type !== 'VariableDeclarator' || !node.init || !cssAttributeName) {
160
- return [];
161
- }
162
- const compiledImportFix = addImportSource(context, fixer, configuration, cssAttributeName);
163
- if (compiledImportFix) {
164
- fixes.push(compiledImportFix);
165
- }
166
- const init = node.init;
167
- const initString = sourceCode.getText(init);
168
- if (node.init.type === 'TemplateLiteral') {
169
- fixes.push(fixer.replaceText(init, `${cssAttributeName}${initString}`));
170
- } else {
171
- fixes.push(fixer.replaceText(init, `${cssAttributeName}(${initString})`));
169
+ /**
170
+ * Returns a list of fixes that:
171
+ * - add the `css` or `xcss` function call around the current node.
172
+ * - add an import statement for the package from which `css` is imported
173
+ */
174
+ addCssFunctionCall(fixer, node) {
175
+ const fixes = [];
176
+ const sourceCode = this.context.getSourceCode();
177
+ if (node.type !== 'VariableDeclarator' || !node.init || !this.cssAttributeName) {
178
+ return [];
179
+ }
180
+ const compiledImportFix = this.addImportSource(fixer);
181
+ if (compiledImportFix) {
182
+ fixes.push(compiledImportFix);
183
+ }
184
+ const init = node.init;
185
+ const initString = sourceCode.getText(init);
186
+ if (node.init.type === 'TemplateLiteral') {
187
+ fixes.push(fixer.replaceText(init, `${this.cssAttributeName}${initString}`));
188
+ } else {
189
+ fixes.push(fixer.replaceText(init, `${this.cssAttributeName}(${initString})`));
190
+ }
191
+ return fixes;
172
192
  }
173
- return fixes;
174
- };
175
193
 
176
- /**
177
- * Check if the expression has or potentially has a local variable
178
- * (as opposed to an imported one), erring on the side ot "yes"
179
- * when an expression is too complicated to analyse.
180
- *
181
- * This is useful because expressions containing local variables
182
- * cannot be easily hoisted, whereas this is not a problem with imported
183
- * variables.
184
- *
185
- * @param context Context of the rule.
186
- * @param node Any node that is potentially hoistable.
187
- * @returns Whether the node potentially has a local variable (and thus is not safe to hoist).
188
- */
189
- const potentiallyHasLocalVariable = (context, node) => {
190
- let hasPotentiallyLocalVariable = false;
191
- const isImportedVariable = identifier => !!getModuleOfIdentifier(context.getSourceCode(), identifier);
192
- estraverse.traverse(node, {
193
- enter: function (node, _parent) {
194
- if (isNodeOfType(node, 'SpreadElement') ||
195
- // @ts-expect-error remove once we can be sure that no parser interprets
196
- // the spread operator as ExperimentalSpreadProperty anymore
197
- isNodeOfType(node, 'ExperimentalSpreadProperty')) {
198
- // Spread elements could contain anything... so we don't bother.
199
- //
200
- // e.g. <div css={css({ ...(!height && { visibility: 'hidden' })} />
201
- hasPotentiallyLocalVariable = true;
202
- this.break();
203
- }
204
- if (!isNodeOfType(node, 'Property')) {
205
- return;
206
- }
207
- switch (node.value.type) {
208
- case 'Literal':
209
- break;
210
- case 'Identifier':
211
- // e.g. css({ margin: myVariable })
212
- if (!isImportedVariable(node.value.name)) {
213
- hasPotentiallyLocalVariable = true;
214
- }
215
- this.break();
216
- break;
217
- case 'MemberExpression':
218
- // e.g. css({ margin: props.color })
219
- // css({ margin: props.media.color })
220
- if (node.value.object.type === 'Identifier' && isImportedVariable(node.value.object.name)) {
221
- // We found an imported variable, don't do anything.
222
- } else {
223
- // e.g. css({ margin: [some complicated expression].media.color })
224
- // This can potentially get too complex, so we assume there's a local
225
- // variable in there somewhere.
226
- hasPotentiallyLocalVariable = true;
227
- }
194
+ /**
195
+ * Check if the expression has or potentially has a local variable
196
+ * (as opposed to an imported one), erring on the side ot "yes"
197
+ * when an expression is too complicated to analyse.
198
+ *
199
+ * This is useful because expressions containing local variables
200
+ * cannot be easily hoisted, whereas this is not a problem with imported
201
+ * variables.
202
+ *
203
+ * @param context Context of the rule.
204
+ * @param node Any node that is potentially hoistable.
205
+ * @returns Whether the node potentially has a local variable (and thus is not safe to hoist).
206
+ */
207
+ potentiallyHasLocalVariable(node) {
208
+ let hasPotentiallyLocalVariable = false;
209
+ const isImportedVariable = identifier => !!getModuleOfIdentifier(this.context.getSourceCode(), identifier);
210
+ estraverse.traverse(node, {
211
+ enter: function (node, _parent) {
212
+ if (isNodeOfType(node, 'SpreadElement') ||
213
+ // @ts-expect-error remove once we can be sure that no parser interprets
214
+ // the spread operator as ExperimentalSpreadProperty anymore
215
+ isNodeOfType(node, 'ExperimentalSpreadProperty')) {
216
+ // Spread elements could contain anything... so we don't bother.
217
+ //
218
+ // e.g. <div css={css({ ...(!height && { visibility: 'hidden' })} />
219
+ hasPotentiallyLocalVariable = true;
228
220
  this.break();
229
- break;
230
- case 'TemplateLiteral':
231
- if (!!node.value.expressions.length) {
232
- // Too many edge cases here, don't bother...
233
- // e.g. css({ animation: `${expandStyles(right, rightExpanded, isExpanded)} 0.2s ease-in-out` });
221
+ }
222
+ if (!isNodeOfType(node, 'Property')) {
223
+ return;
224
+ }
225
+ switch (node.value.type) {
226
+ case 'Literal':
227
+ break;
228
+ case 'Identifier':
229
+ // e.g. css({ margin: myVariable })
230
+ if (!isImportedVariable(node.value.name)) {
231
+ hasPotentiallyLocalVariable = true;
232
+ }
233
+ this.break();
234
+ break;
235
+ case 'MemberExpression':
236
+ // e.g. css({ margin: props.color })
237
+ // css({ margin: props.media.color })
238
+ if (node.value.object.type === 'Identifier' && isImportedVariable(node.value.object.name)) {
239
+ // We found an imported variable, don't do anything.
240
+ } else {
241
+ // e.g. css({ margin: [some complicated expression].media.color })
242
+ // This can potentially get too complex, so we assume there's a local
243
+ // variable in there somewhere.
244
+ hasPotentiallyLocalVariable = true;
245
+ }
246
+ this.break();
247
+ break;
248
+ case 'TemplateLiteral':
249
+ if (!!node.value.expressions.length) {
250
+ // Too many edge cases here, don't bother...
251
+ // e.g. css({ animation: `${expandStyles(right, rightExpanded, isExpanded)} 0.2s ease-in-out` });
252
+ hasPotentiallyLocalVariable = true;
253
+ this.break();
254
+ }
255
+ break;
256
+ default:
257
+ // Catch-all for values such as "A && B", "A ? B : C"
234
258
  hasPotentiallyLocalVariable = true;
235
259
  this.break();
236
- }
237
- break;
238
- default:
239
- // Catch-all for values such as "A && B", "A ? B : C"
240
- hasPotentiallyLocalVariable = true;
241
- this.break();
242
- break;
260
+ break;
261
+ }
243
262
  }
244
- }
245
- });
246
- return hasPotentiallyLocalVariable;
247
- };
263
+ });
264
+ return hasPotentiallyLocalVariable;
265
+ }
248
266
 
249
- /**
250
- * Fixer for the cssAtTopOfModule/cssAtBottomOfModule violation cases.
251
- * This deals with Identifiers and Expressions passed from the traverseExpressionWithConfig() function.
252
- *
253
- * @param fixer The ESLint RuleFixer object
254
- * @param context The context of the rule
255
- * @param configuration The configuration of the rule, determining whether the fix is implmeneted at the top or bottom of the module
256
- * @param node Any potentially hoistable node, or an identifier.
257
- * @param cssAttributeName An optional parameter only added when we fix an ObjectExpression
258
- */
259
- const fixCssNotInModuleScope = (fixer, context, configuration, node, cssAttributeName) => {
260
- const sourceCode = context.getSourceCode();
267
+ /**
268
+ * Fixer for the cssAtTopOfModule/cssAtBottomOfModule violation cases.
269
+ *
270
+ * This deals with Identifiers and Expressions passed from the traverseExpressionWithConfig() function.
271
+ *
272
+ * @param fixer The ESLint RuleFixer object
273
+ * @param context The context of the rule
274
+ * @param configuration The configuration of the rule, determining whether the fix is implmeneted at the top or bottom of the module
275
+ * @param node Any potentially hoistable node, or an identifier.
276
+ * @param cssAttributeName An optional parameter only added when we fix an ObjectExpression
277
+ */
278
+ fixCssNotInModuleScope(fixer, node, isObjectExpression) {
279
+ const sourceCode = this.context.getSourceCode();
261
280
 
262
- // Get the program node in order to properly position the hoisted styles
263
- const programNode = getProgramNode(node);
264
- let fixerNodePlacement = programNode;
265
- if (configuration.stylesPlacement === 'bottom') {
266
- // The last value is the bottom of the file
267
- fixerNodePlacement = programNode.body[programNode.body.length - 1];
268
- } else {
269
- // Place after the last ImportDeclaration
270
- fixerNodePlacement = programNode.body.length === 1 ? programNode.body[0] : programNode.body.find(node => node.type !== 'ImportDeclaration');
271
- }
272
- let moduleString;
273
- let fixes = [];
274
- if (node.type === 'Identifier') {
275
- const identifier = node;
276
- const declarator = identifier.parent.parent;
277
- moduleString = sourceCode.getText(declarator);
278
- fixes.push(fixer.remove(declarator));
279
- } else {
280
- if (potentiallyHasLocalVariable(context, node)) {
281
- return [];
281
+ // Get the program node in order to properly position the hoisted styles
282
+ const programNode = getProgramNode(node);
283
+ let fixerNodePlacement = programNode;
284
+ if (this.configuration.stylesPlacement === 'bottom') {
285
+ // The last value is the bottom of the file
286
+ fixerNodePlacement = programNode.body[programNode.body.length - 1];
287
+ } else {
288
+ // Place after the last ImportDeclaration
289
+ fixerNodePlacement = programNode.body.length === 1 ? programNode.body[0] : programNode.body.find(node => node.type !== 'ImportDeclaration');
282
290
  }
283
- const declarator = getDeclaratorString(context);
284
- const text = sourceCode.getText(node);
291
+ let moduleString;
292
+ let fixes = [];
293
+ if (node.type === 'Identifier') {
294
+ const identifier = node;
295
+ const declarator = identifier.parent.parent;
296
+ moduleString = sourceCode.getText(declarator);
297
+ fixes.push(fixer.remove(declarator));
298
+ } else {
299
+ if (this.potentiallyHasLocalVariable(node)) {
300
+ return [];
301
+ }
302
+ const declarator = this.getDeclaratorString();
303
+ const text = sourceCode.getText(node);
285
304
 
286
- // If this has been passed, then we know it's an ObjectExpression
287
- if (cssAttributeName) {
288
- moduleString = `const ${declarator} = ${cssAttributeName}(${text});`;
289
- const compiledImportFix = addImportSource(context, fixer, configuration, cssAttributeName);
290
- if (compiledImportFix) {
291
- fixes.push(compiledImportFix);
305
+ // If this has been passed, then we know it's an ObjectExpression
306
+ if (isObjectExpression) {
307
+ moduleString = `const ${declarator} = ${this.cssAttributeName}(${text});`;
308
+ const compiledImportFix = this.addImportSource(fixer);
309
+ if (compiledImportFix) {
310
+ fixes.push(compiledImportFix);
311
+ }
312
+ } else {
313
+ moduleString = `const ${declarator} = ${text};`;
292
314
  }
293
- } else {
294
- moduleString = `const ${declarator} = ${text};`;
315
+ fixes.push(fixer.replaceText(node, declarator));
295
316
  }
296
- fixes.push(fixer.replaceText(node, declarator));
317
+ return [...fixes,
318
+ // Insert the node either before or after, depending on the rule configuration
319
+ this.configuration.stylesPlacement === 'bottom' ? fixer.insertTextAfter(fixerNodePlacement, '\n' + moduleString) : fixer.insertTextBefore(fixerNodePlacement, moduleString + '\n')];
297
320
  }
298
- return [...fixes,
299
- // Insert the node either before or after, depending on the rule configuration
300
- configuration.stylesPlacement === 'bottom' ? fixer.insertTextAfter(fixerNodePlacement, '\n' + moduleString) : fixer.insertTextBefore(fixerNodePlacement, moduleString + '\n')];
301
- };
302
321
 
303
- /**
304
- * Handle different cases based on what's been passed in the css-related JSXAttribute
305
- * @param context the context of the rule
306
- * @param expression the expression of the JSXAttribute value
307
- * @param configuration what css-related functions to account for (eg. css, xcss, cssMap), and whether to detect bottom vs top expressions
308
- * @param cssAttributeName used to encapsulate ObjectExpressions when cssAtTopOfModule/cssAtBottomOfModule violations are triggered
309
- */
310
- const traverseExpressionWithConfig = (context, expression, configuration, cssAttributeName) => {
311
- function traverseExpression(expression) {
322
+ /**
323
+ * Handle different cases based on what's been passed in the css-related JSXAttribute.
324
+ *
325
+ * @param expression the expression of the JSXAttribute value.
326
+ */
327
+ traverseExpression(expression) {
312
328
  switch (expression.type) {
313
329
  case 'Identifier':
314
330
  // {styles}
315
331
  // We've found an identifier - time to analyze it!
316
- analyzeIdentifier(context, expression, configuration, cssAttributeName);
332
+ this.analyzeIdentifier(expression);
317
333
  break;
318
334
  case 'ArrayExpression':
319
335
  // {[styles, moreStyles]}
320
336
  // We've found an array expression - let's traverse again over each element individually.
321
- expression.elements.forEach(element => traverseExpression(element));
337
+ expression.elements.forEach(element => this.traverseExpression(element));
322
338
  break;
323
339
  case 'LogicalExpression':
324
340
  // {isEnabled && styles}
325
341
  // We've found a logical expression - we're only interested in the right expression so
326
342
  // let's traverse that and see what it is!
327
- traverseExpression(expression.right);
343
+ this.traverseExpression(expression.right);
328
344
  break;
329
345
  case 'ConditionalExpression':
330
346
  // {isEnabled ? styles : null}
331
347
  // We've found a conditional expression - we're only interested in the consequent and
332
348
  // alternate (styles : null)
333
- traverseExpression(expression.consequent);
334
- traverseExpression(expression.alternate);
349
+ this.traverseExpression(expression.consequent);
350
+ this.traverseExpression(expression.alternate);
335
351
  break;
336
352
  case 'ObjectExpression':
337
353
  case 'CallExpression':
338
354
  case 'TaggedTemplateExpression':
339
355
  case 'TemplateLiteral':
340
356
  // We've found elements that shouldn't be here! Report an error.
341
- context.report({
357
+ this.context.report({
342
358
  node: expression,
343
- messageId: configuration.stylesPlacement === 'bottom' ? 'cssAtBottomOfModule' : 'cssAtTopOfModule',
359
+ messageId: this.configuration.stylesPlacement === 'bottom' ? 'cssAtBottomOfModule' : 'cssAtTopOfModule',
344
360
  fix: fixer => {
345
- if (configuration.fixNamesOnly) {
361
+ if (this.configuration.fixNamesOnly) {
346
362
  return [];
347
363
  }
348
364
 
349
365
  // Don't fix CallExpressions unless they're from cssFunctions or cssMap
350
- if (expression.type === 'CallExpression' && !isCssCallExpression(expression, configuration.cssFunctions)) {
366
+ if (expression.type === 'CallExpression' && !isCssCallExpression(expression, this.configuration.cssFunctions)) {
351
367
  return [];
352
368
  }
353
369
  if (expression.type === 'ObjectExpression') {
354
- return fixCssNotInModuleScope(fixer, context, configuration, expression, cssAttributeName);
370
+ return this.fixCssNotInModuleScope(fixer, expression, true);
355
371
  }
356
- return fixCssNotInModuleScope(fixer, context, configuration, expression);
372
+ return this.fixCssNotInModuleScope(fixer, expression, false);
357
373
  }
358
374
  });
359
375
  break;
@@ -366,15 +382,17 @@ const traverseExpressionWithConfig = (context, expression, configuration, cssAtt
366
382
  // so we just leave this as-is.
367
383
  case 'TSAsExpression':
368
384
  // @ts-expect-error
369
- traverseExpression(expression.expression);
385
+ this.traverseExpression(expression.expression);
370
386
  break;
371
387
  default:
372
388
  // Do nothing!
373
389
  break;
374
390
  }
375
391
  }
376
- traverseExpression(expression);
377
- };
392
+ run() {
393
+ return this.traverseExpression(this.expression);
394
+ }
395
+ }
378
396
  const defaultConfig = {
379
397
  cssFunctions: ['css', 'xcss'],
380
398
  stylesPlacement: 'top',
@@ -483,9 +501,6 @@ const rule = createLintRule({
483
501
  return;
484
502
  }
485
503
  }
486
-
487
- // Always reset to empty array
488
- hoistedCss = [];
489
504
  if (name.type === 'JSXIdentifier' && mergedConfig.cssFunctions.includes(name.name)) {
490
505
  // When not a jsx expression. For eg. css=""
491
506
  if ((value === null || value === void 0 ? void 0 : value.type) !== 'JSXExpressionContainer') {
@@ -500,7 +515,8 @@ const rule = createLintRule({
500
515
  // <div css={/* Hello there */} />
501
516
  return;
502
517
  }
503
- traverseExpressionWithConfig(context, value.expression, mergedConfig, name.name);
518
+ const linter = new JSXExpressionLinter(context, name.name, mergedConfig, value.expression);
519
+ linter.run();
504
520
  }
505
521
  }
506
522
  };