@atlaskit/eslint-plugin-design-system 8.25.2 → 8.27.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.
Files changed (80) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +4 -0
  3. package/constellation/index/usage.mdx +402 -6
  4. package/dist/cjs/presets/all.codegen.js +5 -1
  5. package/dist/cjs/presets/recommended.codegen.js +5 -1
  6. package/dist/cjs/rules/consistent-css-prop-usage/index.js +254 -32
  7. package/dist/cjs/rules/index.codegen.js +9 -1
  8. package/dist/cjs/rules/no-empty-styled-expression/index.js +75 -0
  9. package/dist/cjs/rules/no-exported-css/index.js +37 -0
  10. package/dist/cjs/rules/no-exported-keyframes/index.js +37 -0
  11. package/dist/cjs/rules/no-invalid-css-map/index.js +102 -0
  12. package/dist/cjs/rules/no-invalid-css-map/utils.js +193 -0
  13. package/dist/cjs/rules/utils/create-no-exported-rule/check-if-supported-export.js +158 -0
  14. package/dist/cjs/rules/utils/create-no-exported-rule/is-styled-component.js +80 -0
  15. package/dist/cjs/rules/utils/create-no-exported-rule/main.js +66 -0
  16. package/dist/cjs/rules/utils/get-first-supported-import.js +28 -0
  17. package/dist/cjs/rules/utils/is-supported-import.js +53 -16
  18. package/dist/es2019/presets/all.codegen.js +5 -1
  19. package/dist/es2019/presets/recommended.codegen.js +5 -1
  20. package/dist/es2019/rules/consistent-css-prop-usage/index.js +251 -33
  21. package/dist/es2019/rules/index.codegen.js +9 -1
  22. package/dist/es2019/rules/no-empty-styled-expression/index.js +65 -0
  23. package/dist/es2019/rules/no-exported-css/index.js +31 -0
  24. package/dist/es2019/rules/no-exported-keyframes/index.js +31 -0
  25. package/dist/es2019/rules/no-invalid-css-map/index.js +95 -0
  26. package/dist/es2019/rules/no-invalid-css-map/utils.js +134 -0
  27. package/dist/es2019/rules/utils/create-no-exported-rule/check-if-supported-export.js +142 -0
  28. package/dist/es2019/rules/utils/create-no-exported-rule/is-styled-component.js +70 -0
  29. package/dist/es2019/rules/utils/create-no-exported-rule/main.js +59 -0
  30. package/dist/es2019/rules/utils/get-first-supported-import.js +22 -0
  31. package/dist/es2019/rules/utils/is-supported-import.js +50 -15
  32. package/dist/esm/presets/all.codegen.js +5 -1
  33. package/dist/esm/presets/recommended.codegen.js +5 -1
  34. package/dist/esm/rules/consistent-css-prop-usage/index.js +255 -33
  35. package/dist/esm/rules/index.codegen.js +9 -1
  36. package/dist/esm/rules/no-empty-styled-expression/index.js +68 -0
  37. package/dist/esm/rules/no-exported-css/index.js +31 -0
  38. package/dist/esm/rules/no-exported-keyframes/index.js +31 -0
  39. package/dist/esm/rules/no-invalid-css-map/index.js +96 -0
  40. package/dist/esm/rules/no-invalid-css-map/utils.js +186 -0
  41. package/dist/esm/rules/utils/create-no-exported-rule/check-if-supported-export.js +151 -0
  42. package/dist/esm/rules/utils/create-no-exported-rule/is-styled-component.js +74 -0
  43. package/dist/esm/rules/utils/create-no-exported-rule/main.js +60 -0
  44. package/dist/esm/rules/utils/get-first-supported-import.js +22 -0
  45. package/dist/esm/rules/utils/is-supported-import.js +51 -15
  46. package/dist/types/index.codegen.d.ts +8 -0
  47. package/dist/types/presets/all.codegen.d.ts +5 -1
  48. package/dist/types/presets/recommended.codegen.d.ts +5 -1
  49. package/dist/types/rules/consistent-css-prop-usage/types.d.ts +7 -2
  50. package/dist/types/rules/index.codegen.d.ts +4 -0
  51. package/dist/types/rules/no-empty-styled-expression/index.d.ts +3 -0
  52. package/dist/types/rules/no-exported-css/index.d.ts +3 -0
  53. package/dist/types/rules/no-exported-keyframes/index.d.ts +3 -0
  54. package/dist/types/rules/no-invalid-css-map/index.d.ts +3 -0
  55. package/dist/types/rules/no-invalid-css-map/utils.d.ts +14 -0
  56. package/dist/types/rules/use-primitives/utils/update-jsx-attribute-by-name.d.ts +1 -1
  57. package/dist/types/rules/utils/create-no-exported-rule/check-if-supported-export.d.ts +15 -0
  58. package/dist/types/rules/utils/create-no-exported-rule/is-styled-component.d.ts +14 -0
  59. package/dist/types/rules/utils/create-no-exported-rule/main.d.ts +19 -0
  60. package/dist/types/rules/utils/create-rule.d.ts +1 -1
  61. package/dist/types/rules/utils/get-first-supported-import.d.ts +17 -0
  62. package/dist/types/rules/utils/is-supported-import.d.ts +26 -8
  63. package/dist/types-ts4.5/index.codegen.d.ts +8 -0
  64. package/dist/types-ts4.5/presets/all.codegen.d.ts +5 -1
  65. package/dist/types-ts4.5/presets/recommended.codegen.d.ts +5 -1
  66. package/dist/types-ts4.5/rules/consistent-css-prop-usage/types.d.ts +7 -2
  67. package/dist/types-ts4.5/rules/index.codegen.d.ts +4 -0
  68. package/dist/types-ts4.5/rules/no-empty-styled-expression/index.d.ts +3 -0
  69. package/dist/types-ts4.5/rules/no-exported-css/index.d.ts +3 -0
  70. package/dist/types-ts4.5/rules/no-exported-keyframes/index.d.ts +3 -0
  71. package/dist/types-ts4.5/rules/no-invalid-css-map/index.d.ts +3 -0
  72. package/dist/types-ts4.5/rules/no-invalid-css-map/utils.d.ts +14 -0
  73. package/dist/types-ts4.5/rules/use-primitives/utils/update-jsx-attribute-by-name.d.ts +1 -1
  74. package/dist/types-ts4.5/rules/utils/create-no-exported-rule/check-if-supported-export.d.ts +15 -0
  75. package/dist/types-ts4.5/rules/utils/create-no-exported-rule/is-styled-component.d.ts +14 -0
  76. package/dist/types-ts4.5/rules/utils/create-no-exported-rule/main.d.ts +19 -0
  77. package/dist/types-ts4.5/rules/utils/create-rule.d.ts +1 -1
  78. package/dist/types-ts4.5/rules/utils/get-first-supported-import.d.ts +17 -0
  79. package/dist/types-ts4.5/rules/utils/is-supported-import.d.ts +26 -8
  80. package/package.json +3 -1
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
 
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
3
4
  Object.defineProperty(exports, "__esModule", {
4
5
  value: true
5
6
  });
6
- exports.isCxFunction = exports.isCss = exports.CSS_IN_JS_IMPORTS = void 0;
7
+ exports.isStyled = exports.isKeyframes = exports.isCxFunction = exports.isCssMap = exports.isCss = exports.getImportSources = exports.DEFAULT_IMPORT_SOURCES = exports.CSS_IN_JS_IMPORTS = void 0;
8
+ var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
7
9
  // eslint-disable-next-line import/no-extraneous-dependencies
8
10
 
9
11
  var CSS_IN_JS_IMPORTS = exports.CSS_IN_JS_IMPORTS = {
@@ -11,35 +13,69 @@ var CSS_IN_JS_IMPORTS = exports.CSS_IN_JS_IMPORTS = {
11
13
  emotionReact: '@emotion/react',
12
14
  emotionCore: '@emotion/core',
13
15
  styledComponents: 'styled-components',
14
- atlaskitCss: '@atlaskit/css'
16
+ atlaskitCss: '@atlaskit/css',
17
+ atlaskitPrimitives: '@atlaskit/primitives'
15
18
  };
16
19
 
17
- // List of CSS-in-JS libraries an import of a valid css, cx, cssMap, etc.
20
+ // A CSS-in-JS library an import of a valid css, cx, cssMap, etc.
18
21
  // function might originate from, e.g. @compiled/react, @emotion/core.
19
22
 
23
+ // All ESLint rules originating from `@compiled/eslint-plugin` should apply to these libraries.
24
+ var DEFAULT_IMPORT_SOURCES = exports.DEFAULT_IMPORT_SOURCES = [CSS_IN_JS_IMPORTS.compiled, CSS_IN_JS_IMPORTS.atlaskitCss];
25
+
26
+ /**
27
+ * Given the ESLint rule context, extract and parse the value of the importSources rule option.
28
+ * The importSources option is used to define additional libraries for which an ESLint rule
29
+ * should apply to.
30
+ *
31
+ * Note that `@compiled/react` and `@atlaskit/css` are always included in importSources, regardless
32
+ * of what importSources is configured to by the user.
33
+ *
34
+ * @param context The rule context.
35
+ * @returns An array of strings representing what CSS-in-JS packages that should be checked, based
36
+ * on the rule options configuration.
37
+ */
38
+ var getImportSources = exports.getImportSources = function getImportSources(context) {
39
+ var options = context.options;
40
+ if (!options.length) {
41
+ return DEFAULT_IMPORT_SOURCES;
42
+ }
43
+ if (options[0].importSources && Array.isArray(options[0].importSources)) {
44
+ return [].concat(DEFAULT_IMPORT_SOURCES, (0, _toConsumableArray2.default)(options[0].importSources));
45
+ }
46
+ return DEFAULT_IMPORT_SOURCES;
47
+ };
20
48
  var isSupportedImportWrapper = function isSupportedImportWrapper(functionName) {
21
- // This will need to be extended to support default imports once we start
22
- // checking cases like `import css from '@emotion/css'`
23
49
  var checkDefinitionHasImport = function checkDefinitionHasImport(def, importSources) {
24
- var _def$parent, _def$parent2;
25
- return def.node.type === 'ImportSpecifier' && def.node.imported.type === 'Identifier' && def.node.imported.name === functionName && ((_def$parent = def.parent) === null || _def$parent === void 0 ? void 0 : _def$parent.type) === 'ImportDeclaration' && importSources.includes((_def$parent2 = def.parent) === null || _def$parent2 === void 0 ? void 0 : _def$parent2.source.value);
50
+ if (def.type !== 'ImportBinding') {
51
+ return false;
52
+ }
53
+ if (!def.parent || !importSources.includes(def.parent.source.value)) {
54
+ return false;
55
+ }
56
+ return (
57
+ // import { functionName } from 'import-source';
58
+ def.node.type === 'ImportSpecifier' && def.node.imported.name === functionName ||
59
+ // import functionName from 'import-source';
60
+ def.node.type === 'ImportDefaultSpecifier' && def.node.local.name === functionName
61
+ );
26
62
  };
27
63
 
28
64
  /**
29
65
  * Checks whether:
30
66
  *
31
- * 1. a function name `nodeToCheck` matches the name of the function we
67
+ * 1. A function name `nodeToCheck` matches the name of the function we
32
68
  * want to check for (e.g. `cx`, `css`, `cssMap`, or `keyframes`), and
33
- * 2. whether `nodeToCheck` originates from one of the libraries listed
69
+ * 2. Whether `nodeToCheck` originates from one of the libraries listed
34
70
  * in `importSources`.
35
71
  *
36
- * @param nodeToCheck the function callee we are checking (e.g. the `css` in `css()`)
37
- * @param referencesInScope list of references that are in scope. We'll use this
72
+ * @param nodeToCheck The function callee we are checking (e.g. The `css` in `css()`).
73
+ * @param referencesInScope List of references that are in scope. We'll use this
38
74
  * to check where the function callee is imported from.
39
- * @param importSources list of libraries that we want to ensure `nodeToCheck`
40
- * comes from
75
+ * @param importSources List of libraries that we want to ensure `nodeToCheck`
76
+ * comes from.
41
77
  *
42
- * @returns whether the above conditions are true
78
+ * @returns Whether the above conditions are true.
43
79
  */
44
80
  var isSupportedImport = function isSupportedImport(nodeToCheck, referencesInScope, importSources) {
45
81
  return nodeToCheck.type === 'Identifier' && referencesInScope.some(function (reference) {
@@ -57,5 +93,6 @@ var isSupportedImportWrapper = function isSupportedImportWrapper(functionName) {
57
93
  //
58
94
  var isCss = exports.isCss = isSupportedImportWrapper('css');
59
95
  var isCxFunction = exports.isCxFunction = isSupportedImportWrapper('cx');
60
- // export const isCssMap = isLibraryImportWrapper('cssMap');
61
- // export const isKeyframes = isLibraryImportWrapper('keyframes');
96
+ var isCssMap = exports.isCssMap = isSupportedImportWrapper('cssMap');
97
+ var isKeyframes = exports.isKeyframes = isSupportedImportWrapper('keyframes');
98
+ var isStyled = exports.isStyled = isSupportedImportWrapper('styled');
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * THIS FILE WAS CREATED VIA CODEGEN DO NOT MODIFY {@see http://go/af-codegen}
3
- * @codegen <<SignedSource::5026ba2cb55b3c1bcacbfe7fb7728a6c>>
3
+ * @codegen <<SignedSource::914085544778f4543f43e3e30d0982e0>>
4
4
  * @codegenCommand yarn workspace @atlaskit/eslint-plugin-design-system codegen
5
5
  */
6
6
  export default {
@@ -16,6 +16,10 @@ export default {
16
16
  '@atlaskit/design-system/no-deprecated-apis': 'error',
17
17
  '@atlaskit/design-system/no-deprecated-design-token-usage': 'warn',
18
18
  '@atlaskit/design-system/no-deprecated-imports': 'error',
19
+ '@atlaskit/design-system/no-empty-styled-expression': 'warn',
20
+ '@atlaskit/design-system/no-exported-css': 'warn',
21
+ '@atlaskit/design-system/no-exported-keyframes': 'warn',
22
+ '@atlaskit/design-system/no-invalid-css-map': 'error',
19
23
  '@atlaskit/design-system/no-margin': 'warn',
20
24
  '@atlaskit/design-system/no-nested-styles': 'error',
21
25
  '@atlaskit/design-system/no-physical-properties': 'error',
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * THIS FILE WAS CREATED VIA CODEGEN DO NOT MODIFY {@see http://go/af-codegen}
3
- * @codegen <<SignedSource::839224bfab98c1ddf6018dec5320968e>>
3
+ * @codegen <<SignedSource::577269c832952ce359cde6a50f26f4e0>>
4
4
  * @codegenCommand yarn workspace @atlaskit/eslint-plugin-design-system codegen
5
5
  */
6
6
  export default {
@@ -14,6 +14,10 @@ export default {
14
14
  '@atlaskit/design-system/no-deprecated-apis': 'error',
15
15
  '@atlaskit/design-system/no-deprecated-design-token-usage': 'warn',
16
16
  '@atlaskit/design-system/no-deprecated-imports': 'error',
17
+ '@atlaskit/design-system/no-empty-styled-expression': 'warn',
18
+ '@atlaskit/design-system/no-exported-css': 'warn',
19
+ '@atlaskit/design-system/no-exported-keyframes': 'warn',
20
+ '@atlaskit/design-system/no-invalid-css-map': 'error',
17
21
  '@atlaskit/design-system/no-nested-styles': 'error',
18
22
  '@atlaskit/design-system/no-unsafe-design-token-usage': 'error',
19
23
  '@atlaskit/design-system/no-unsafe-style-overrides': 'warn',
@@ -1,18 +1,23 @@
1
1
  // eslint-disable-next-line import/no-extraneous-dependencies
2
2
 
3
- import { getIdentifierInParentScope, isNodeOfType } from 'eslint-codemod-utils';
3
+ import { getIdentifierInParentScope, insertAtStartOfFile, insertImportDeclaration, isNodeOfType } from 'eslint-codemod-utils';
4
+ import estraverse from 'estraverse';
4
5
  import assign from 'lodash/assign';
6
+ import { Import } from '../../ast-nodes';
5
7
  import { createLintRule } from '../utils/create-rule';
6
- // File-level tracking of styles hoisted from the cssOnTopOfModule/cssAtBottomOfModule fixers
8
+ import { getFirstSupportedImport } from '../utils/get-first-supported-import';
9
+ import { getModuleOfIdentifier } from '../utils/get-import-node-by-source';
10
+ import { CSS_IN_JS_IMPORTS } from '../utils/is-supported-import';
11
+ // File-level tracking of styles hoisted from the cssAtTopOfModule/cssAtBottomOfModule fixers
7
12
  let hoistedCss = [];
13
+ const isDOMElementName = elementName => elementName.charAt(0) !== elementName.charAt(0).toUpperCase() && elementName.charAt(0) === elementName.charAt(0).toLowerCase();
8
14
  function isCssCallExpression(node, cssFunctions) {
9
15
  cssFunctions = [...cssFunctions, 'cssMap'];
10
16
  return !!(isNodeOfType(node, 'CallExpression') && node.callee && node.callee.type === 'Identifier' && cssFunctions.includes(node.callee.name) && node.arguments.length && node.arguments[0].type === 'ObjectExpression');
11
17
  }
12
18
  function findSpreadProperties(node) {
13
- // @ts-ignore
14
19
  return node.properties.filter(property => property.type === 'SpreadElement' ||
15
- // @ts-ignore
20
+ // @ts-expect-error
16
21
  property.type === 'ExperimentalSpreadProperty');
17
22
  }
18
23
  const getProgramNode = expression => {
@@ -22,9 +27,8 @@ const getProgramNode = expression => {
22
27
  return expression.parent;
23
28
  };
24
29
 
25
- // TODO: This can be optimised by implementing a fixer at the very end (Program:exit) and handling all validations at once
26
30
  /**
27
- * Generates the declarator string when fixing the cssOnTopOfModule/cssAtBottomOfModule cases.
31
+ * Generates the declarator string when fixing the cssAtTopOfModule/cssAtBottomOfModule cases.
28
32
  * When `styles` already exists, `styles_1, styles_2, ..., styles_X` are incrementally created for each unhoisted style.
29
33
  * The generated `styles` varibale declaration names must be manually modified to be more informative at the discretion of owning teams.
30
34
  */
@@ -54,7 +58,7 @@ const getDeclaratorString = context => {
54
58
  hoistedCss = [...hoistedCss, `${declaratorName}${count}`];
55
59
  return `${declaratorName}${count}`;
56
60
  };
57
- function analyzeIdentifier(context, sourceIdentifier, configuration) {
61
+ function analyzeIdentifier(context, sourceIdentifier, configuration, cssAttributeName) {
58
62
  var _getIdentifierInParen, _getIdentifierInParen2;
59
63
  const scope = context.getScope();
60
64
  const [identifier] = (_getIdentifierInParen = (_getIdentifierInParen2 = getIdentifierInParentScope(scope, sourceIdentifier.name)) === null || _getIdentifierInParen2 === void 0 ? void 0 : _getIdentifierInParen2.identifiers) !== null && _getIdentifierInParen !== void 0 ? _getIdentifierInParen : [];
@@ -74,8 +78,11 @@ function analyzeIdentifier(context, sourceIdentifier, configuration) {
74
78
  // When variable is declared inside the component
75
79
  context.report({
76
80
  node: sourceIdentifier,
77
- messageId: configuration.stylesPlacement === 'bottom' ? 'cssAtBottomOfModule' : 'cssOnTopOfModule',
81
+ messageId: configuration.stylesPlacement === 'bottom' ? 'cssAtBottomOfModule' : 'cssAtTopOfModule',
78
82
  fix: fixer => {
83
+ if (configuration.fixNamesOnly) {
84
+ return [];
85
+ }
79
86
  return fixCssNotInModuleScope(fixer, context, configuration, identifier);
80
87
  }
81
88
  });
@@ -83,10 +90,30 @@ function analyzeIdentifier(context, sourceIdentifier, configuration) {
83
90
  }
84
91
  if (identifier.parent && identifier.parent.init && !isCssCallExpression(identifier.parent.init, configuration.cssFunctions)) {
85
92
  // When variable value is not of type css({})
86
- context.report({
87
- node: identifier,
88
- messageId: 'cssObjectTypeOnly'
89
- });
93
+ const value = identifier.parent.init;
94
+ if (!value) {
95
+ return;
96
+ }
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',
104
+ fix: fixer => {
105
+ if (configuration.fixNamesOnly) {
106
+ return [];
107
+ }
108
+ return addCssFunctionCall(fixer, context, identifier.parent, configuration, cssAttributeName);
109
+ }
110
+ });
111
+ } else {
112
+ context.report({
113
+ node: identifier,
114
+ messageId: 'cssObjectTypeOnly'
115
+ });
116
+ }
90
117
  return;
91
118
  }
92
119
  const spreadProperties = isNodeOfType(identifier.parent.init, 'CallExpression') && findSpreadProperties(identifier.parent.init.arguments[0]);
@@ -102,12 +129,131 @@ function analyzeIdentifier(context, sourceIdentifier, configuration) {
102
129
  }
103
130
 
104
131
  /**
105
- * Fixer for the cssOnTopOfModule/cssAtBottomOfModule violation cases.
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;
138
+
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;
145
+ }
146
+ } else {
147
+ return insertAtStartOfFile(fixer, `${insertImportDeclaration(importSource, [cssAttributeName])};\n`);
148
+ }
149
+ };
150
+
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})`));
172
+ }
173
+ return fixes;
174
+ };
175
+
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
+ }
228
+ 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` });
234
+ hasPotentiallyLocalVariable = true;
235
+ 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;
243
+ }
244
+ }
245
+ });
246
+ return hasPotentiallyLocalVariable;
247
+ };
248
+
249
+ /**
250
+ * Fixer for the cssAtTopOfModule/cssAtBottomOfModule violation cases.
106
251
  * This deals with Identifiers and Expressions passed from the traverseExpressionWithConfig() function.
252
+ *
107
253
  * @param fixer The ESLint RuleFixer object
108
- * @param context The context of the node
254
+ * @param context The context of the rule
109
255
  * @param configuration The configuration of the rule, determining whether the fix is implmeneted at the top or bottom of the module
110
- * @param node Either an IdentifierWithParent node. Expression, or SpreadElement that we handle
256
+ * @param node Any potentially hoistable node, or an identifier.
111
257
  * @param cssAttributeName An optional parameter only added when we fix an ObjectExpression
112
258
  */
113
259
  const fixCssNotInModuleScope = (fixer, context, configuration, node, cssAttributeName) => {
@@ -124,35 +270,42 @@ const fixCssNotInModuleScope = (fixer, context, configuration, node, cssAttribut
124
270
  fixerNodePlacement = programNode.body.length === 1 ? programNode.body[0] : programNode.body.find(node => node.type !== 'ImportDeclaration');
125
271
  }
126
272
  let moduleString;
127
- let implementFixer = [];
273
+ let fixes = [];
128
274
  if (node.type === 'Identifier') {
129
275
  const identifier = node;
130
276
  const declarator = identifier.parent.parent;
131
277
  moduleString = sourceCode.getText(declarator);
132
- implementFixer.push(fixer.remove(declarator));
278
+ fixes.push(fixer.remove(declarator));
133
279
  } else {
280
+ if (potentiallyHasLocalVariable(context, node)) {
281
+ return [];
282
+ }
134
283
  const declarator = getDeclaratorString(context);
135
284
  const text = sourceCode.getText(node);
136
285
 
137
286
  // If this has been passed, then we know it's an ObjectExpression
138
287
  if (cssAttributeName) {
139
288
  moduleString = `const ${declarator} = ${cssAttributeName}(${text});`;
289
+ const compiledImportFix = addImportSource(context, fixer, configuration, cssAttributeName);
290
+ if (compiledImportFix) {
291
+ fixes.push(compiledImportFix);
292
+ }
140
293
  } else {
141
- moduleString = moduleString = `const ${declarator} = ${text};`;
294
+ moduleString = `const ${declarator} = ${text};`;
142
295
  }
143
- implementFixer.push(fixer.replaceText(node, declarator));
296
+ fixes.push(fixer.replaceText(node, declarator));
144
297
  }
145
- return [...implementFixer,
146
- // Insert the node either before or after
298
+ return [...fixes,
299
+ // Insert the node either before or after, depending on the rule configuration
147
300
  configuration.stylesPlacement === 'bottom' ? fixer.insertTextAfter(fixerNodePlacement, '\n' + moduleString) : fixer.insertTextBefore(fixerNodePlacement, moduleString + '\n')];
148
301
  };
149
302
 
150
303
  /**
151
304
  * Handle different cases based on what's been passed in the css-related JSXAttribute
152
- * @param context the context of the node
305
+ * @param context the context of the rule
153
306
  * @param expression the expression of the JSXAttribute value
154
307
  * @param configuration what css-related functions to account for (eg. css, xcss, cssMap), and whether to detect bottom vs top expressions
155
- * @param cssAttributeName used to encapsulate ObjectExpressions when cssOnTopOfModule/cssAtBottomOfModule violations are triggered
308
+ * @param cssAttributeName used to encapsulate ObjectExpressions when cssAtTopOfModule/cssAtBottomOfModule violations are triggered
156
309
  */
157
310
  const traverseExpressionWithConfig = (context, expression, configuration, cssAttributeName) => {
158
311
  function traverseExpression(expression) {
@@ -160,7 +313,7 @@ const traverseExpressionWithConfig = (context, expression, configuration, cssAtt
160
313
  case 'Identifier':
161
314
  // {styles}
162
315
  // We've found an identifier - time to analyze it!
163
- analyzeIdentifier(context, expression, configuration);
316
+ analyzeIdentifier(context, expression, configuration, cssAttributeName);
164
317
  break;
165
318
  case 'ArrayExpression':
166
319
  // {[styles, moreStyles]}
@@ -187,8 +340,12 @@ const traverseExpressionWithConfig = (context, expression, configuration, cssAtt
187
340
  // We've found elements that shouldn't be here! Report an error.
188
341
  context.report({
189
342
  node: expression,
190
- messageId: configuration.stylesPlacement === 'bottom' ? 'cssAtBottomOfModule' : 'cssOnTopOfModule',
343
+ messageId: configuration.stylesPlacement === 'bottom' ? 'cssAtBottomOfModule' : 'cssAtTopOfModule',
191
344
  fix: fixer => {
345
+ if (configuration.fixNamesOnly) {
346
+ return [];
347
+ }
348
+
192
349
  // Don't fix CallExpressions unless they're from cssFunctions or cssMap
193
350
  if (expression.type === 'CallExpression' && !isCssCallExpression(expression, configuration.cssFunctions)) {
194
351
  return [];
@@ -200,6 +357,17 @@ const traverseExpressionWithConfig = (context, expression, configuration, cssAtt
200
357
  }
201
358
  });
202
359
  break;
360
+
361
+ // @ts-expect-error - our ESLint-related types assume vanilla JS, when in fact
362
+ // it is running @typescript-eslint
363
+ //
364
+ // Switching to the more accurate @typescript-eslint types would break
365
+ // eslint-codemod-utils and all ESLint rules in packages/design-system,
366
+ // so we just leave this as-is.
367
+ case 'TSAsExpression':
368
+ // @ts-expect-error
369
+ traverseExpression(expression.expression);
370
+ break;
203
371
  default:
204
372
  // Do nothing!
205
373
  break;
@@ -209,10 +377,15 @@ const traverseExpressionWithConfig = (context, expression, configuration, cssAtt
209
377
  };
210
378
  const defaultConfig = {
211
379
  cssFunctions: ['css', 'xcss'],
212
- stylesPlacement: 'top'
380
+ stylesPlacement: 'top',
381
+ cssImportSource: CSS_IN_JS_IMPORTS.compiled,
382
+ xcssImportSource: CSS_IN_JS_IMPORTS.atlaskitPrimitives,
383
+ excludeReactComponents: false,
384
+ fixNamesOnly: false
213
385
  };
214
386
  const rule = createLintRule({
215
387
  meta: {
388
+ type: 'problem',
216
389
  name: 'consistent-css-prop-usage',
217
390
  docs: {
218
391
  description: 'Ensures consistency with `css` and `xcss` prop usages',
@@ -222,13 +395,42 @@ const rule = createLintRule({
222
395
  },
223
396
  fixable: 'code',
224
397
  messages: {
225
- cssOnTopOfModule: `Create styles at the top of the module scope using the respective css function.`,
398
+ cssAtTopOfModule: `Create styles at the top of the module scope using the respective css function.`,
226
399
  cssAtBottomOfModule: `Create styles at the bottom of the module scope using the respective css function.`,
227
- cssObjectTypeOnly: `Create styles using objects passed to the css function.`,
228
- cssInModule: `Imported styles should not be used, instead define in the module, import a component, or use a design token.`,
400
+ cssObjectTypeOnly: `Create styles using objects passed to a css function call, e.g. \`css({ textAlign: 'center'; })\`.`,
401
+ cssInModule: `Imported styles should not be used; instead define in the module, import a component, or use a design token.`,
229
402
  cssArrayStylesOnly: `Compose styles with an array on the css prop instead of using object spread.`,
403
+ noMemberExpressions: `Styles should be a regular variable (e.g. 'buttonStyles'), not a member of an object (e.g. 'myObject.styles').`,
230
404
  shouldEndInStyles: 'Declared styles should end in "styles".'
231
- }
405
+ },
406
+ schema: [{
407
+ type: 'object',
408
+ properties: {
409
+ cssFunctions: {
410
+ type: 'array',
411
+ items: [{
412
+ type: 'string'
413
+ }]
414
+ },
415
+ stylesPlacement: {
416
+ type: 'string',
417
+ enum: ['top', 'bottom']
418
+ },
419
+ cssImportSource: {
420
+ type: 'string'
421
+ },
422
+ xcssImportSource: {
423
+ type: 'string'
424
+ },
425
+ excludeReactComponents: {
426
+ type: 'boolean'
427
+ },
428
+ fixNamesOnly: {
429
+ type: 'boolean'
430
+ }
431
+ },
432
+ additionalProperties: false
433
+ }]
232
434
  },
233
435
  create(context) {
234
436
  const mergedConfig = assign({}, defaultConfig, context.options[0]);
@@ -265,11 +467,22 @@ const rule = createLintRule({
265
467
  }
266
468
  });
267
469
  },
268
- JSXAttribute(node) {
470
+ JSXAttribute(nodeOriginal) {
471
+ const node = nodeOriginal;
269
472
  const {
270
473
  name,
271
474
  value
272
475
  } = node;
476
+ if (mergedConfig.excludeReactComponents && node.parent.type === 'JSXOpeningElement') {
477
+ // e.g. <item.before />
478
+ if (node.parent.name.type === 'JSXMemberExpression') {
479
+ return;
480
+ }
481
+ // e.g. <div />, <MenuItem />
482
+ if (node.parent.name.type === 'JSXIdentifier' && !isDOMElementName(node.parent.name.name)) {
483
+ return;
484
+ }
485
+ }
273
486
 
274
487
  // Always reset to empty array
275
488
  hoistedCss = [];
@@ -277,11 +490,16 @@ const rule = createLintRule({
277
490
  // When not a jsx expression. For eg. css=""
278
491
  if ((value === null || value === void 0 ? void 0 : value.type) !== 'JSXExpressionContainer') {
279
492
  context.report({
280
- node: value,
281
- messageId: mergedConfig.stylesPlacement === 'bottom' ? 'cssAtBottomOfModule' : 'cssOnTopOfModule'
493
+ node,
494
+ messageId: mergedConfig.stylesPlacement === 'bottom' ? 'cssAtBottomOfModule' : 'cssAtTopOfModule'
282
495
  });
283
496
  return;
284
497
  }
498
+ if (value.expression.type === 'JSXEmptyExpression') {
499
+ // e.g. the comment in
500
+ // <div css={/* Hello there */} />
501
+ return;
502
+ }
285
503
  traverseExpressionWithConfig(context, value.expression, mergedConfig, name.name);
286
504
  }
287
505
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * THIS FILE WAS CREATED VIA CODEGEN DO NOT MODIFY {@see http://go/af-codegen}
3
- * @codegen <<SignedSource::a67afc33adf50db651edb1cf12e16ef3>>
3
+ * @codegen <<SignedSource::0a2d88c9772eb438048415f13550f592>>
4
4
  * @codegenCommand yarn workspace @atlaskit/eslint-plugin-design-system codegen
5
5
  */
6
6
  import consistentCssPropUsage from './consistent-css-prop-usage';
@@ -13,6 +13,10 @@ import noCssTaggedTemplateExpression from './no-css-tagged-template-expression';
13
13
  import noDeprecatedApis from './no-deprecated-apis';
14
14
  import noDeprecatedDesignTokenUsage from './no-deprecated-design-token-usage';
15
15
  import noDeprecatedImports from './no-deprecated-imports';
16
+ import noEmptyStyledExpression from './no-empty-styled-expression';
17
+ import noExportedCss from './no-exported-css';
18
+ import noExportedKeyframes from './no-exported-keyframes';
19
+ import noInvalidCssMap from './no-invalid-css-map';
16
20
  import noMargin from './no-margin';
17
21
  import noNestedStyles from './no-nested-styles';
18
22
  import noPhysicalProperties from './no-physical-properties';
@@ -37,6 +41,10 @@ export default {
37
41
  'no-deprecated-apis': noDeprecatedApis,
38
42
  'no-deprecated-design-token-usage': noDeprecatedDesignTokenUsage,
39
43
  'no-deprecated-imports': noDeprecatedImports,
44
+ 'no-empty-styled-expression': noEmptyStyledExpression,
45
+ 'no-exported-css': noExportedCss,
46
+ 'no-exported-keyframes': noExportedKeyframes,
47
+ 'no-invalid-css-map': noInvalidCssMap,
40
48
  'no-margin': noMargin,
41
49
  'no-nested-styles': noNestedStyles,
42
50
  'no-physical-properties': noPhysicalProperties,
@@ -0,0 +1,65 @@
1
+ import { createLintRule } from '../utils/create-rule';
2
+ import { getImportSources, isStyled } from '../utils/is-supported-import';
3
+ const isEmptyStyledExpression = node => {
4
+ const [firstArg] = node.arguments;
5
+ if (node.arguments.length === 0) {
6
+ return true;
7
+ } else if (node.arguments.length === 1 && (firstArg === null || firstArg === void 0 ? void 0 : firstArg.type) === 'ArrayExpression') {
8
+ return firstArg.elements.length === 0;
9
+ } else if (node.arguments.length === 1 && (firstArg === null || firstArg === void 0 ? void 0 : firstArg.type) === 'ObjectExpression') {
10
+ return firstArg.properties.length === 0;
11
+ }
12
+ return false;
13
+ };
14
+ const createNoEmptyStyledExpressionRule = (isEmptyStyledExpression, messageId) => context => {
15
+ const importSources = getImportSources(context);
16
+ return {
17
+ 'CallExpression[callee.type="MemberExpression"]': node => {
18
+ const {
19
+ references
20
+ } = context.getScope();
21
+
22
+ // If we have styled.div(...), make sure `callee` only refers to the
23
+ // `styled` part instead of the whole `styled.div` expression.
24
+ const callee = node.callee.type === 'MemberExpression' ? node.callee.object : node.callee;
25
+ if (!isStyled(callee, references, importSources)) {
26
+ return;
27
+ }
28
+ if (!isEmptyStyledExpression(node)) {
29
+ return;
30
+ }
31
+ context.report({
32
+ messageId,
33
+ node
34
+ });
35
+ }
36
+ };
37
+ };
38
+ const noEmptyStyledExpressionRule = createLintRule({
39
+ meta: {
40
+ name: 'no-empty-styled-expression',
41
+ docs: {
42
+ description: 'Forbids any styled expression to be used when passing empty arguments to styled.div() (or other JSX elements).',
43
+ recommended: true,
44
+ severity: 'warn'
45
+ },
46
+ messages: {
47
+ unexpected: 'Found an empty expression, or empty object argument passed to `styled` function call. This unnecessarily causes a major performance penalty - please use a plain JSX element or a React fragment instead (e.g. `<div>Hello</div>` or `<>Hello</>`).'
48
+ },
49
+ type: 'problem',
50
+ schema: [{
51
+ type: 'object',
52
+ properties: {
53
+ importSources: {
54
+ type: 'array',
55
+ items: [{
56
+ type: 'string'
57
+ }]
58
+ }
59
+ },
60
+ additionalProperties: false
61
+ }]
62
+ },
63
+ create: createNoEmptyStyledExpressionRule(isEmptyStyledExpression, 'unexpected')
64
+ });
65
+ export default noEmptyStyledExpressionRule;