@atlaskit/eslint-plugin-design-system 8.21.0 → 8.22.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @atlaskit/eslint-plugin-design-system
2
2
 
3
+ ## 8.22.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#63589](https://stash.atlassian.com/projects/CONFCLOUD/repos/confluence-frontend/pull-requests/63589) [`f59d997d1913`](https://stash.atlassian.com/projects/CONFCLOUD/repos/confluence-frontend/commits/f59d997d1913) - Implemented new fixers for cssOnTopOfModule and cssAtBottomOfModule violation cases
8
+
3
9
  ## 8.21.0
4
10
 
5
11
  ### Minor Changes
@@ -66,6 +66,8 @@ This rule has options - see below.
66
66
 
67
67
  👎 Example of **incorrect** code for this rule:
68
68
 
69
+ **Calling a css/xcss function or direct objects inside the JSX attribute.**
70
+
69
71
  ```js
70
72
  function Button({ children }) {
71
73
  return <div css={css({...})}>{children}</div>;
@@ -73,6 +75,8 @@ function Button({ children }) {
73
75
  }
74
76
  ```
75
77
 
78
+ **Inserting a non css-function based object identifier into a css JSX attribute.**
79
+
76
80
  ```js
77
81
  const container = {
78
82
  ^^^^^^^^^ should be a css function call
@@ -84,6 +88,8 @@ function Button({ children }) {
84
88
  }
85
89
  ```
86
90
 
91
+ **Importing styles from another file.**
92
+
87
93
  ```js
88
94
  import { container } from './styles';
89
95
  ^^^^^^^^^ styles should be local, not shared
@@ -93,6 +99,8 @@ function Button({ children }) {
93
99
  }
94
100
  ```
95
101
 
102
+ **Nesting styles with objects instead of arrays.**
103
+
96
104
  ```js
97
105
  const baseContainerStyles = css({
98
106
  zIndex: 5,
@@ -111,6 +119,13 @@ function Button({ children }) {
111
119
 
112
120
  👍 Example of **correct** code for this rule:
113
121
 
122
+ **Using the css() function to create a style object that follows the naming convention (ends in Styles) and passing it as a variable into the css={...} JSX attribute.**
123
+
124
+ With the following options turned on:
125
+
126
+ - cssFunctions = ['css']
127
+ - stylesPlacement = 'top'
128
+
114
129
  ```js
115
130
  const containerStyles = css({
116
131
  zIndex: 1,
@@ -121,7 +136,38 @@ function Button({ children }) {
121
136
  }
122
137
  ```
123
138
 
139
+ **Technically correct usage of the cssMap function.**
140
+
141
+ With the following options turned on:
142
+
143
+ - cssFunctions = ['css']
144
+ - stylesPlacement = 'top'
145
+
124
146
  ```js
147
+ const borderStyles = cssMap({
148
+ 'solid': '1px solid';
149
+ 'none': '0px';
150
+ })
151
+
152
+ function Button({ children }) {
153
+ return <button css={borderStyles[solid]}>{children}</button>;
154
+ }
155
+ ```
156
+
157
+ **Create composite styles with arrays, not objects.**
158
+
159
+ With the following options turned on:
160
+
161
+ - cssFunctions = ['css']
162
+ - stylesPlacement = 'bottom'
163
+
164
+ ```js
165
+ function Button({ children }) {
166
+ return (
167
+ <button css={[baseContainerStyles, containerStyles]}>{children}</button>
168
+ );
169
+ }
170
+
125
171
  const baseContainerStyles = css({
126
172
  zIndex: 5,
127
173
  });
@@ -129,12 +175,15 @@ const baseContainerStyles = css({
129
175
  const containerStyles = css({
130
176
  zIndex: 7,
131
177
  });
178
+ ```
132
179
 
133
- function Button({ children }) {
134
- return (
135
- <button css={[baseContainerStyles, containerStyles]}>{children}</button>
136
- );
137
- }
180
+ **Ternaries can be used inline**
181
+
182
+ ```js
183
+ const baseStyles = css({ color: token('color.text.primary') });
184
+ const disabledStyles = css({ color: token('color.text.disabled') });
185
+
186
+ <div css={props.disabled ? disabledStyles : baseStyles}></div>;
138
187
  ```
139
188
 
140
189
  <h3>Options</h3>
@@ -6,15 +6,15 @@ Object.defineProperty(exports, "__esModule", {
6
6
  });
7
7
  exports.default = void 0;
8
8
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
- var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
10
9
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
10
+ var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
11
11
  var _eslintCodemodUtils = require("eslint-codemod-utils");
12
12
  var _assign = _interopRequireDefault(require("lodash/assign"));
13
13
  var _createRule = require("../utils/create-rule");
14
14
  // eslint-disable-next-line import/no-extraneous-dependencies
15
15
 
16
- var declarationSuffix = 'Styles';
17
16
  function isCssCallExpression(node, cssFunctions) {
17
+ cssFunctions = [].concat((0, _toConsumableArray2.default)(cssFunctions), ['cssMap']);
18
18
  return !!((0, _eslintCodemodUtils.isNodeOfType)(node, 'CallExpression') && node.callee && node.callee.type === 'Identifier' && cssFunctions.includes(node.callee.name) && node.arguments.length && node.arguments[0].type === 'ObjectExpression');
19
19
  }
20
20
  function findSpreadProperties(node) {
@@ -25,6 +25,44 @@ function findSpreadProperties(node) {
25
25
  property.type === 'ExperimentalSpreadProperty';
26
26
  });
27
27
  }
28
+ var getTopLevelNode = function getTopLevelNode(expression) {
29
+ while (expression.parent.type !== 'Program') {
30
+ expression = expression.parent;
31
+ }
32
+ return expression;
33
+ };
34
+
35
+ // TODO: This can be optimised by implementing a fixer at the very end (Program:exit) and handling all validations at once
36
+ /**
37
+ * Generates the declarator string when fixing the cssOnTopOfModule/cssAtBottomOfModule cases.
38
+ * When `styles` already exists, `styles_1, styles_2, ..., styles_X` are incrementally created for each unhoisted style.
39
+ * The generated `styles` varibale declaration names must be manually modified to be more informative at the discretion of owning teams.
40
+ */
41
+ var getDeclaratorString = function getDeclaratorString(context) {
42
+ var scope = context.getScope();
43
+
44
+ // Get to ModuleScope
45
+ while (scope && scope.upper && scope.upper.type !== 'global') {
46
+ var _scope;
47
+ scope = (_scope = scope) === null || _scope === void 0 ? void 0 : _scope.upper;
48
+ }
49
+ var variables = scope.variables.map(function (variable) {
50
+ return variable.name;
51
+ });
52
+ var count = 2;
53
+ var declaratorName = 'styles';
54
+
55
+ // Base case
56
+ if (!variables.includes(declaratorName)) {
57
+ return declaratorName;
58
+ } else {
59
+ // If styles already exists, increment the number
60
+ while (variables.includes("".concat(declaratorName, "_").concat(count))) {
61
+ count++;
62
+ }
63
+ }
64
+ return "".concat(declaratorName, "_").concat(count);
65
+ };
28
66
  function analyzeIdentifier(context, sourceIdentifier, configuration) {
29
67
  var _getIdentifierInParen, _getIdentifierInParen2;
30
68
  var scope = context.getScope();
@@ -47,11 +85,14 @@ function analyzeIdentifier(context, sourceIdentifier, configuration) {
47
85
  // When variable is declared inside the component
48
86
  context.report({
49
87
  node: sourceIdentifier,
50
- messageId: configuration.stylesPlacement === 'bottom' ? 'cssAtBottomOfModule' : 'cssOnTopOfModule'
88
+ messageId: configuration.stylesPlacement === 'bottom' ? 'cssAtBottomOfModule' : 'cssOnTopOfModule',
89
+ fix: function fix(fixer) {
90
+ return fixCssNotInModuleScope(fixer, context, configuration, identifier);
91
+ }
51
92
  });
52
93
  return;
53
94
  }
54
- if (identifier.parent && identifier.parent.init && !isCssCallExpression(identifier.parent.init, [].concat((0, _toConsumableArray2.default)(configuration.cssFunctions), ['cssMap']))) {
95
+ if (identifier.parent && identifier.parent.init && !isCssCallExpression(identifier.parent.init, configuration.cssFunctions)) {
55
96
  // When variable value is not of type css({})
56
97
  context.report({
57
98
  node: identifier,
@@ -71,13 +112,50 @@ function analyzeIdentifier(context, sourceIdentifier, configuration) {
71
112
  }
72
113
  }
73
114
 
115
+ /**
116
+ * Fixer for the cssOnTopOfModule/cssAtBottomOfModule violation cases.
117
+ * This deals with Identifiers and Expressions passed from the traverseExpressionWithConfig() function.
118
+ * @param fixer The ESLint RuleFixer object
119
+ * @param context The context of the node
120
+ * @param configuration The configuration of the rule, determining whether the fix is implmeneted at the top or bottom of the module
121
+ * @param node Either an IdentifierWithParent node. Expression, or SpreadElement that we handle
122
+ * @param cssAttributeName An optional parameter only added when we fix an ObjectExpression
123
+ */
124
+ var fixCssNotInModuleScope = function fixCssNotInModuleScope(fixer, context, configuration, node, cssAttributeName) {
125
+ var sourceCode = context.getSourceCode();
126
+ var topLevelNode = getTopLevelNode(node);
127
+ var moduleString;
128
+ var implementFixer = [];
129
+ if (node.type === 'Identifier') {
130
+ var identifier = node;
131
+ var declarator = identifier.parent.parent;
132
+ moduleString = sourceCode.getText(declarator);
133
+ implementFixer.push(fixer.remove(declarator));
134
+ } else {
135
+ var _declarator = getDeclaratorString(context);
136
+ var text = sourceCode.getText(node);
137
+
138
+ // If this has been passed, then we know it's an ObjectExpression
139
+ if (cssAttributeName) {
140
+ moduleString = "const ".concat(_declarator, " = ").concat(cssAttributeName, "(").concat(text, ");");
141
+ } else {
142
+ moduleString = moduleString = "const ".concat(_declarator, " = ").concat(text, ";");
143
+ }
144
+ implementFixer.push(fixer.replaceText(node, _declarator));
145
+ }
146
+ return [].concat(implementFixer, [
147
+ // Insert the node either before or after
148
+ configuration.stylesPlacement === 'bottom' ? fixer.insertTextAfter(topLevelNode, '\n' + moduleString) : fixer.insertTextBefore(topLevelNode, moduleString + '\n')]);
149
+ };
150
+
74
151
  /**
75
152
  * Handle different cases based on what's been passed in the css-related JSXAttribute
76
153
  * @param context the context of the node
77
154
  * @param expression the expression of the JSXAttribute value
78
155
  * @param configuration what css-related functions to account for (eg. css, xcss, cssMap), and whether to detect bottom vs top expressions
156
+ * @param cssAttributeName used to encapsulate ObjectExpressions when cssOnTopOfModule/cssAtBottomOfModule violations are triggered
79
157
  */
80
- var traverseExpressionWithConfig = function traverseExpressionWithConfig(context, expression, configuration) {
158
+ var traverseExpressionWithConfig = function traverseExpressionWithConfig(context, expression, configuration, cssAttributeName) {
81
159
  function traverseExpression(expression) {
82
160
  switch (expression.type) {
83
161
  case 'Identifier':
@@ -105,14 +183,24 @@ var traverseExpressionWithConfig = function traverseExpressionWithConfig(context
105
183
  traverseExpression(expression.consequent);
106
184
  traverseExpression(expression.alternate);
107
185
  break;
108
- case 'CallExpression':
109
186
  case 'ObjectExpression':
187
+ case 'CallExpression':
110
188
  case 'TaggedTemplateExpression':
111
189
  case 'TemplateLiteral':
112
190
  // We've found elements that shouldn't be here! Report an error.
113
191
  context.report({
114
192
  node: expression,
115
- messageId: configuration.stylesPlacement === 'bottom' ? 'cssAtBottomOfModule' : 'cssOnTopOfModule'
193
+ messageId: configuration.stylesPlacement === 'bottom' ? 'cssAtBottomOfModule' : 'cssOnTopOfModule',
194
+ fix: function fix(fixer) {
195
+ // Don't fix CallExpressions unless they're from cssFunctions or cssMap
196
+ if (expression.type === 'CallExpression' && !isCssCallExpression(expression, configuration.cssFunctions)) {
197
+ return [];
198
+ }
199
+ if (expression.type === 'ObjectExpression') {
200
+ return fixCssNotInModuleScope(fixer, context, configuration, expression, cssAttributeName);
201
+ }
202
+ return fixCssNotInModuleScope(fixer, context, configuration, expression);
203
+ }
116
204
  });
117
205
  break;
118
206
  default:
@@ -148,6 +236,7 @@ var rule = (0, _createRule.createLintRule)({
148
236
  create: function create(context) {
149
237
  var _ref3;
150
238
  var mergedConfig = (0, _assign.default)({}, defaultConfig, context.options[0]);
239
+ var declarationSuffix = 'Styles';
151
240
  var callSelectorFunctions = [].concat((0, _toConsumableArray2.default)(mergedConfig.cssFunctions), ['cssMap']);
152
241
  var callSelector = callSelectorFunctions.map(function (fn) {
153
242
  return "CallExpression[callee.name=".concat(fn, "]");
@@ -158,7 +247,7 @@ var rule = (0, _createRule.createLintRule)({
158
247
  return;
159
248
  }
160
249
  var identifier = node.parent.id;
161
- if (identifier.type === 'Identifier' && identifier.name.endsWith(declarationSuffix)) {
250
+ if (identifier.type === 'Identifier' && (identifier.name.endsWith(declarationSuffix) || identifier.name.startsWith(declarationSuffix.toLowerCase() + '_') || identifier.name === declarationSuffix.toLowerCase())) {
162
251
  // Already prefixed! Nothing to do.
163
252
  return;
164
253
  }
@@ -196,7 +285,7 @@ var rule = (0, _createRule.createLintRule)({
196
285
  });
197
286
  return;
198
287
  }
199
- traverseExpressionWithConfig(context, value.expression, mergedConfig);
288
+ traverseExpressionWithConfig(context, value.expression, mergedConfig, name.name);
200
289
  }
201
290
  }), _ref3;
202
291
  }
@@ -3,8 +3,8 @@
3
3
  import { getIdentifierInParentScope, isNodeOfType } from 'eslint-codemod-utils';
4
4
  import assign from 'lodash/assign';
5
5
  import { createLintRule } from '../utils/create-rule';
6
- const declarationSuffix = 'Styles';
7
6
  function isCssCallExpression(node, cssFunctions) {
7
+ cssFunctions = [...cssFunctions, 'cssMap'];
8
8
  return !!(isNodeOfType(node, 'CallExpression') && node.callee && node.callee.type === 'Identifier' && cssFunctions.includes(node.callee.name) && node.arguments.length && node.arguments[0].type === 'ObjectExpression');
9
9
  }
10
10
  function findSpreadProperties(node) {
@@ -13,6 +13,42 @@ function findSpreadProperties(node) {
13
13
  // @ts-ignore
14
14
  property.type === 'ExperimentalSpreadProperty');
15
15
  }
16
+ const getTopLevelNode = expression => {
17
+ while (expression.parent.type !== 'Program') {
18
+ expression = expression.parent;
19
+ }
20
+ return expression;
21
+ };
22
+
23
+ // TODO: This can be optimised by implementing a fixer at the very end (Program:exit) and handling all validations at once
24
+ /**
25
+ * Generates the declarator string when fixing the cssOnTopOfModule/cssAtBottomOfModule cases.
26
+ * When `styles` already exists, `styles_1, styles_2, ..., styles_X` are incrementally created for each unhoisted style.
27
+ * The generated `styles` varibale declaration names must be manually modified to be more informative at the discretion of owning teams.
28
+ */
29
+ const getDeclaratorString = context => {
30
+ let scope = context.getScope();
31
+
32
+ // Get to ModuleScope
33
+ while (scope && scope.upper && scope.upper.type !== 'global') {
34
+ var _scope;
35
+ scope = (_scope = scope) === null || _scope === void 0 ? void 0 : _scope.upper;
36
+ }
37
+ const variables = scope.variables.map(variable => variable.name);
38
+ let count = 2;
39
+ let declaratorName = 'styles';
40
+
41
+ // Base case
42
+ if (!variables.includes(declaratorName)) {
43
+ return declaratorName;
44
+ } else {
45
+ // If styles already exists, increment the number
46
+ while (variables.includes(`${declaratorName}_${count}`)) {
47
+ count++;
48
+ }
49
+ }
50
+ return `${declaratorName}_${count}`;
51
+ };
16
52
  function analyzeIdentifier(context, sourceIdentifier, configuration) {
17
53
  var _getIdentifierInParen, _getIdentifierInParen2;
18
54
  const scope = context.getScope();
@@ -33,11 +69,14 @@ function analyzeIdentifier(context, sourceIdentifier, configuration) {
33
69
  // When variable is declared inside the component
34
70
  context.report({
35
71
  node: sourceIdentifier,
36
- messageId: configuration.stylesPlacement === 'bottom' ? 'cssAtBottomOfModule' : 'cssOnTopOfModule'
72
+ messageId: configuration.stylesPlacement === 'bottom' ? 'cssAtBottomOfModule' : 'cssOnTopOfModule',
73
+ fix: fixer => {
74
+ return fixCssNotInModuleScope(fixer, context, configuration, identifier);
75
+ }
37
76
  });
38
77
  return;
39
78
  }
40
- if (identifier.parent && identifier.parent.init && !isCssCallExpression(identifier.parent.init, [...configuration.cssFunctions, 'cssMap'])) {
79
+ if (identifier.parent && identifier.parent.init && !isCssCallExpression(identifier.parent.init, configuration.cssFunctions)) {
41
80
  // When variable value is not of type css({})
42
81
  context.report({
43
82
  node: identifier,
@@ -57,13 +96,50 @@ function analyzeIdentifier(context, sourceIdentifier, configuration) {
57
96
  }
58
97
  }
59
98
 
99
+ /**
100
+ * Fixer for the cssOnTopOfModule/cssAtBottomOfModule violation cases.
101
+ * This deals with Identifiers and Expressions passed from the traverseExpressionWithConfig() function.
102
+ * @param fixer The ESLint RuleFixer object
103
+ * @param context The context of the node
104
+ * @param configuration The configuration of the rule, determining whether the fix is implmeneted at the top or bottom of the module
105
+ * @param node Either an IdentifierWithParent node. Expression, or SpreadElement that we handle
106
+ * @param cssAttributeName An optional parameter only added when we fix an ObjectExpression
107
+ */
108
+ const fixCssNotInModuleScope = (fixer, context, configuration, node, cssAttributeName) => {
109
+ const sourceCode = context.getSourceCode();
110
+ const topLevelNode = getTopLevelNode(node);
111
+ let moduleString;
112
+ let implementFixer = [];
113
+ if (node.type === 'Identifier') {
114
+ const identifier = node;
115
+ const declarator = identifier.parent.parent;
116
+ moduleString = sourceCode.getText(declarator);
117
+ implementFixer.push(fixer.remove(declarator));
118
+ } else {
119
+ const declarator = getDeclaratorString(context);
120
+ const text = sourceCode.getText(node);
121
+
122
+ // If this has been passed, then we know it's an ObjectExpression
123
+ if (cssAttributeName) {
124
+ moduleString = `const ${declarator} = ${cssAttributeName}(${text});`;
125
+ } else {
126
+ moduleString = moduleString = `const ${declarator} = ${text};`;
127
+ }
128
+ implementFixer.push(fixer.replaceText(node, declarator));
129
+ }
130
+ return [...implementFixer,
131
+ // Insert the node either before or after
132
+ configuration.stylesPlacement === 'bottom' ? fixer.insertTextAfter(topLevelNode, '\n' + moduleString) : fixer.insertTextBefore(topLevelNode, moduleString + '\n')];
133
+ };
134
+
60
135
  /**
61
136
  * Handle different cases based on what's been passed in the css-related JSXAttribute
62
137
  * @param context the context of the node
63
138
  * @param expression the expression of the JSXAttribute value
64
139
  * @param configuration what css-related functions to account for (eg. css, xcss, cssMap), and whether to detect bottom vs top expressions
140
+ * @param cssAttributeName used to encapsulate ObjectExpressions when cssOnTopOfModule/cssAtBottomOfModule violations are triggered
65
141
  */
66
- const traverseExpressionWithConfig = (context, expression, configuration) => {
142
+ const traverseExpressionWithConfig = (context, expression, configuration, cssAttributeName) => {
67
143
  function traverseExpression(expression) {
68
144
  switch (expression.type) {
69
145
  case 'Identifier':
@@ -89,14 +165,24 @@ const traverseExpressionWithConfig = (context, expression, configuration) => {
89
165
  traverseExpression(expression.consequent);
90
166
  traverseExpression(expression.alternate);
91
167
  break;
92
- case 'CallExpression':
93
168
  case 'ObjectExpression':
169
+ case 'CallExpression':
94
170
  case 'TaggedTemplateExpression':
95
171
  case 'TemplateLiteral':
96
172
  // We've found elements that shouldn't be here! Report an error.
97
173
  context.report({
98
174
  node: expression,
99
- messageId: configuration.stylesPlacement === 'bottom' ? 'cssAtBottomOfModule' : 'cssOnTopOfModule'
175
+ messageId: configuration.stylesPlacement === 'bottom' ? 'cssAtBottomOfModule' : 'cssOnTopOfModule',
176
+ fix: fixer => {
177
+ // Don't fix CallExpressions unless they're from cssFunctions or cssMap
178
+ if (expression.type === 'CallExpression' && !isCssCallExpression(expression, configuration.cssFunctions)) {
179
+ return [];
180
+ }
181
+ if (expression.type === 'ObjectExpression') {
182
+ return fixCssNotInModuleScope(fixer, context, configuration, expression, cssAttributeName);
183
+ }
184
+ return fixCssNotInModuleScope(fixer, context, configuration, expression);
185
+ }
100
186
  });
101
187
  break;
102
188
  default:
@@ -131,6 +217,7 @@ const rule = createLintRule({
131
217
  },
132
218
  create(context) {
133
219
  const mergedConfig = assign({}, defaultConfig, context.options[0]);
220
+ const declarationSuffix = 'Styles';
134
221
  const callSelectorFunctions = [...mergedConfig.cssFunctions, 'cssMap'];
135
222
  const callSelector = callSelectorFunctions.map(fn => `CallExpression[callee.name=${fn}]`).join(',');
136
223
  return {
@@ -140,7 +227,7 @@ const rule = createLintRule({
140
227
  return;
141
228
  }
142
229
  const identifier = node.parent.id;
143
- if (identifier.type === 'Identifier' && identifier.name.endsWith(declarationSuffix)) {
230
+ if (identifier.type === 'Identifier' && (identifier.name.endsWith(declarationSuffix) || identifier.name.startsWith(declarationSuffix.toLowerCase() + '_') || identifier.name === declarationSuffix.toLowerCase())) {
144
231
  // Already prefixed! Nothing to do.
145
232
  return;
146
233
  }
@@ -177,7 +264,7 @@ const rule = createLintRule({
177
264
  });
178
265
  return;
179
266
  }
180
- traverseExpressionWithConfig(context, value.expression, mergedConfig);
267
+ traverseExpressionWithConfig(context, value.expression, mergedConfig, name.name);
181
268
  }
182
269
  }
183
270
  };
@@ -1,13 +1,13 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
- import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
3
2
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
3
+ import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
4
4
  // eslint-disable-next-line import/no-extraneous-dependencies
5
5
 
6
6
  import { getIdentifierInParentScope, isNodeOfType } from 'eslint-codemod-utils';
7
7
  import assign from 'lodash/assign';
8
8
  import { createLintRule } from '../utils/create-rule';
9
- var declarationSuffix = 'Styles';
10
9
  function isCssCallExpression(node, cssFunctions) {
10
+ cssFunctions = [].concat(_toConsumableArray(cssFunctions), ['cssMap']);
11
11
  return !!(isNodeOfType(node, 'CallExpression') && node.callee && node.callee.type === 'Identifier' && cssFunctions.includes(node.callee.name) && node.arguments.length && node.arguments[0].type === 'ObjectExpression');
12
12
  }
13
13
  function findSpreadProperties(node) {
@@ -18,6 +18,44 @@ function findSpreadProperties(node) {
18
18
  property.type === 'ExperimentalSpreadProperty';
19
19
  });
20
20
  }
21
+ var getTopLevelNode = function getTopLevelNode(expression) {
22
+ while (expression.parent.type !== 'Program') {
23
+ expression = expression.parent;
24
+ }
25
+ return expression;
26
+ };
27
+
28
+ // TODO: This can be optimised by implementing a fixer at the very end (Program:exit) and handling all validations at once
29
+ /**
30
+ * Generates the declarator string when fixing the cssOnTopOfModule/cssAtBottomOfModule cases.
31
+ * When `styles` already exists, `styles_1, styles_2, ..., styles_X` are incrementally created for each unhoisted style.
32
+ * The generated `styles` varibale declaration names must be manually modified to be more informative at the discretion of owning teams.
33
+ */
34
+ var getDeclaratorString = function getDeclaratorString(context) {
35
+ var scope = context.getScope();
36
+
37
+ // Get to ModuleScope
38
+ while (scope && scope.upper && scope.upper.type !== 'global') {
39
+ var _scope;
40
+ scope = (_scope = scope) === null || _scope === void 0 ? void 0 : _scope.upper;
41
+ }
42
+ var variables = scope.variables.map(function (variable) {
43
+ return variable.name;
44
+ });
45
+ var count = 2;
46
+ var declaratorName = 'styles';
47
+
48
+ // Base case
49
+ if (!variables.includes(declaratorName)) {
50
+ return declaratorName;
51
+ } else {
52
+ // If styles already exists, increment the number
53
+ while (variables.includes("".concat(declaratorName, "_").concat(count))) {
54
+ count++;
55
+ }
56
+ }
57
+ return "".concat(declaratorName, "_").concat(count);
58
+ };
21
59
  function analyzeIdentifier(context, sourceIdentifier, configuration) {
22
60
  var _getIdentifierInParen, _getIdentifierInParen2;
23
61
  var scope = context.getScope();
@@ -40,11 +78,14 @@ function analyzeIdentifier(context, sourceIdentifier, configuration) {
40
78
  // When variable is declared inside the component
41
79
  context.report({
42
80
  node: sourceIdentifier,
43
- messageId: configuration.stylesPlacement === 'bottom' ? 'cssAtBottomOfModule' : 'cssOnTopOfModule'
81
+ messageId: configuration.stylesPlacement === 'bottom' ? 'cssAtBottomOfModule' : 'cssOnTopOfModule',
82
+ fix: function fix(fixer) {
83
+ return fixCssNotInModuleScope(fixer, context, configuration, identifier);
84
+ }
44
85
  });
45
86
  return;
46
87
  }
47
- if (identifier.parent && identifier.parent.init && !isCssCallExpression(identifier.parent.init, [].concat(_toConsumableArray(configuration.cssFunctions), ['cssMap']))) {
88
+ if (identifier.parent && identifier.parent.init && !isCssCallExpression(identifier.parent.init, configuration.cssFunctions)) {
48
89
  // When variable value is not of type css({})
49
90
  context.report({
50
91
  node: identifier,
@@ -64,13 +105,50 @@ function analyzeIdentifier(context, sourceIdentifier, configuration) {
64
105
  }
65
106
  }
66
107
 
108
+ /**
109
+ * Fixer for the cssOnTopOfModule/cssAtBottomOfModule violation cases.
110
+ * This deals with Identifiers and Expressions passed from the traverseExpressionWithConfig() function.
111
+ * @param fixer The ESLint RuleFixer object
112
+ * @param context The context of the node
113
+ * @param configuration The configuration of the rule, determining whether the fix is implmeneted at the top or bottom of the module
114
+ * @param node Either an IdentifierWithParent node. Expression, or SpreadElement that we handle
115
+ * @param cssAttributeName An optional parameter only added when we fix an ObjectExpression
116
+ */
117
+ var fixCssNotInModuleScope = function fixCssNotInModuleScope(fixer, context, configuration, node, cssAttributeName) {
118
+ var sourceCode = context.getSourceCode();
119
+ var topLevelNode = getTopLevelNode(node);
120
+ var moduleString;
121
+ var implementFixer = [];
122
+ if (node.type === 'Identifier') {
123
+ var identifier = node;
124
+ var declarator = identifier.parent.parent;
125
+ moduleString = sourceCode.getText(declarator);
126
+ implementFixer.push(fixer.remove(declarator));
127
+ } else {
128
+ var _declarator = getDeclaratorString(context);
129
+ var text = sourceCode.getText(node);
130
+
131
+ // If this has been passed, then we know it's an ObjectExpression
132
+ if (cssAttributeName) {
133
+ moduleString = "const ".concat(_declarator, " = ").concat(cssAttributeName, "(").concat(text, ");");
134
+ } else {
135
+ moduleString = moduleString = "const ".concat(_declarator, " = ").concat(text, ";");
136
+ }
137
+ implementFixer.push(fixer.replaceText(node, _declarator));
138
+ }
139
+ return [].concat(implementFixer, [
140
+ // Insert the node either before or after
141
+ configuration.stylesPlacement === 'bottom' ? fixer.insertTextAfter(topLevelNode, '\n' + moduleString) : fixer.insertTextBefore(topLevelNode, moduleString + '\n')]);
142
+ };
143
+
67
144
  /**
68
145
  * Handle different cases based on what's been passed in the css-related JSXAttribute
69
146
  * @param context the context of the node
70
147
  * @param expression the expression of the JSXAttribute value
71
148
  * @param configuration what css-related functions to account for (eg. css, xcss, cssMap), and whether to detect bottom vs top expressions
149
+ * @param cssAttributeName used to encapsulate ObjectExpressions when cssOnTopOfModule/cssAtBottomOfModule violations are triggered
72
150
  */
73
- var traverseExpressionWithConfig = function traverseExpressionWithConfig(context, expression, configuration) {
151
+ var traverseExpressionWithConfig = function traverseExpressionWithConfig(context, expression, configuration, cssAttributeName) {
74
152
  function traverseExpression(expression) {
75
153
  switch (expression.type) {
76
154
  case 'Identifier':
@@ -98,14 +176,24 @@ var traverseExpressionWithConfig = function traverseExpressionWithConfig(context
98
176
  traverseExpression(expression.consequent);
99
177
  traverseExpression(expression.alternate);
100
178
  break;
101
- case 'CallExpression':
102
179
  case 'ObjectExpression':
180
+ case 'CallExpression':
103
181
  case 'TaggedTemplateExpression':
104
182
  case 'TemplateLiteral':
105
183
  // We've found elements that shouldn't be here! Report an error.
106
184
  context.report({
107
185
  node: expression,
108
- messageId: configuration.stylesPlacement === 'bottom' ? 'cssAtBottomOfModule' : 'cssOnTopOfModule'
186
+ messageId: configuration.stylesPlacement === 'bottom' ? 'cssAtBottomOfModule' : 'cssOnTopOfModule',
187
+ fix: function fix(fixer) {
188
+ // Don't fix CallExpressions unless they're from cssFunctions or cssMap
189
+ if (expression.type === 'CallExpression' && !isCssCallExpression(expression, configuration.cssFunctions)) {
190
+ return [];
191
+ }
192
+ if (expression.type === 'ObjectExpression') {
193
+ return fixCssNotInModuleScope(fixer, context, configuration, expression, cssAttributeName);
194
+ }
195
+ return fixCssNotInModuleScope(fixer, context, configuration, expression);
196
+ }
109
197
  });
110
198
  break;
111
199
  default:
@@ -141,6 +229,7 @@ var rule = createLintRule({
141
229
  create: function create(context) {
142
230
  var _ref3;
143
231
  var mergedConfig = assign({}, defaultConfig, context.options[0]);
232
+ var declarationSuffix = 'Styles';
144
233
  var callSelectorFunctions = [].concat(_toConsumableArray(mergedConfig.cssFunctions), ['cssMap']);
145
234
  var callSelector = callSelectorFunctions.map(function (fn) {
146
235
  return "CallExpression[callee.name=".concat(fn, "]");
@@ -151,7 +240,7 @@ var rule = createLintRule({
151
240
  return;
152
241
  }
153
242
  var identifier = node.parent.id;
154
- if (identifier.type === 'Identifier' && identifier.name.endsWith(declarationSuffix)) {
243
+ if (identifier.type === 'Identifier' && (identifier.name.endsWith(declarationSuffix) || identifier.name.startsWith(declarationSuffix.toLowerCase() + '_') || identifier.name === declarationSuffix.toLowerCase())) {
155
244
  // Already prefixed! Nothing to do.
156
245
  return;
157
246
  }
@@ -189,7 +278,7 @@ var rule = createLintRule({
189
278
  });
190
279
  return;
191
280
  }
192
- traverseExpressionWithConfig(context, value.expression, mergedConfig);
281
+ traverseExpressionWithConfig(context, value.expression, mergedConfig, name.name);
193
282
  }
194
283
  }), _ref3;
195
284
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@atlaskit/eslint-plugin-design-system",
3
3
  "description": "The essential plugin for use with the Atlassian Design System.",
4
- "version": "8.21.0",
4
+ "version": "8.22.0",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "publishConfig": {
7
7
  "registry": "https://registry.npmjs.org/"
@@ -55,6 +55,7 @@
55
55
  "@types/eslint": "^8.4.5",
56
56
  "eslint": "^7.7.0",
57
57
  "jscodeshift": "^0.13.0",
58
+ "outdent": "^0.5.0",
58
59
  "prettier": "^2.8.0",
59
60
  "react": "^16.8.0",
60
61
  "ts-jest": "26.5.6",