@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.
- package/CHANGELOG.md +12 -0
- package/constellation/no-styled-tagged-template-expression/usage.mdx +42 -0
- package/dist/cjs/rules/consistent-css-prop-usage/index.js +375 -334
- package/dist/cjs/rules/no-styled-tagged-template-expression/index.js +2 -2
- package/dist/cjs/rules/utils/create-no-tagged-template-expression-rule/index.js +10 -3
- package/dist/cjs/rules/utils/is-supported-import.js +64 -10
- package/dist/es2019/rules/consistent-css-prop-usage/index.js +283 -267
- package/dist/es2019/rules/no-styled-tagged-template-expression/index.js +1 -1
- package/dist/es2019/rules/utils/create-no-tagged-template-expression-rule/index.js +26 -1
- package/dist/es2019/rules/utils/is-supported-import.js +60 -10
- package/dist/esm/rules/consistent-css-prop-usage/index.js +375 -334
- package/dist/esm/rules/no-styled-tagged-template-expression/index.js +1 -1
- package/dist/esm/rules/utils/create-no-tagged-template-expression-rule/index.js +11 -4
- package/dist/esm/rules/utils/is-supported-import.js +63 -9
- package/dist/types/rules/utils/is-supported-import.d.ts +11 -0
- package/dist/types-ts4.5/rules/utils/is-supported-import.d.ts +11 -0
- package/package.json +1 -1
- package/dist/cjs/rules/no-styled-tagged-template-expression/is-styled.js +0 -53
- package/dist/es2019/rules/no-styled-tagged-template-expression/is-styled.js +0 -45
- package/dist/esm/rules/no-styled-tagged-template-expression/is-styled.js +0 -47
- package/dist/types/rules/no-styled-tagged-template-expression/is-styled.d.ts +0 -7
- 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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
104
|
+
return this.fixCssNotInModuleScope(fixer, identifier, false);
|
|
109
105
|
}
|
|
110
106
|
});
|
|
111
|
-
|
|
112
|
-
context.report({
|
|
113
|
-
node: identifier,
|
|
114
|
-
messageId: 'cssObjectTypeOnly'
|
|
115
|
-
});
|
|
107
|
+
return;
|
|
116
108
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
-
|
|
247
|
-
};
|
|
263
|
+
});
|
|
264
|
+
return hasPotentiallyLocalVariable;
|
|
265
|
+
}
|
|
248
266
|
|
|
249
|
-
/**
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
284
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
294
|
-
moduleString = `const ${declarator} = ${text};`;
|
|
315
|
+
fixes.push(fixer.replaceText(node, declarator));
|
|
295
316
|
}
|
|
296
|
-
fixes
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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(
|
|
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,
|
|
370
|
+
return this.fixCssNotInModuleScope(fixer, expression, true);
|
|
355
371
|
}
|
|
356
|
-
return fixCssNotInModuleScope(fixer,
|
|
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
|
-
|
|
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
|
-
|
|
518
|
+
const linter = new JSXExpressionLinter(context, name.name, mergedConfig, value.expression);
|
|
519
|
+
linter.run();
|
|
504
520
|
}
|
|
505
521
|
}
|
|
506
522
|
};
|