@atlaskit/eslint-plugin-design-system 8.26.0 → 8.28.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 (112) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +31 -29
  3. package/constellation/consistent-css-prop-usage/usage.mdx +193 -0
  4. package/constellation/ensure-design-token-usage/usage.mdx +72 -0
  5. package/constellation/ensure-design-token-usage-preview/usage.mdx +5 -0
  6. package/constellation/icon-label/usage.mdx +39 -0
  7. package/constellation/index/usage.mdx +31 -1474
  8. package/constellation/local-cx-xcss/usage.mdx +37 -0
  9. package/constellation/no-banned-imports/usage.mdx +17 -0
  10. package/constellation/no-css-tagged-template-expression/usage.mdx +66 -0
  11. package/constellation/no-deprecated-apis/usage.mdx +76 -0
  12. package/constellation/no-deprecated-design-token-usage/usage.mdx +27 -0
  13. package/constellation/no-deprecated-imports/usage.mdx +62 -0
  14. package/constellation/no-empty-styled-expression/usage.mdx +77 -0
  15. package/constellation/no-exported-css/usage.mdx +50 -0
  16. package/constellation/no-exported-keyframes/usage.mdx +50 -0
  17. package/constellation/no-invalid-css-map/usage.mdx +199 -0
  18. package/constellation/no-keyframes-tagged-template-expression/usage.mdx +76 -0
  19. package/constellation/no-margin/usage.mdx +20 -0
  20. package/constellation/no-nested-styles/usage.mdx +47 -0
  21. package/constellation/no-physical-properties/usage.mdx +53 -0
  22. package/constellation/no-styled-tagged-template-expression/usage.mdx +90 -0
  23. package/constellation/no-unsafe-design-token-usage/usage.mdx +49 -0
  24. package/constellation/no-unsafe-style-overrides/usage.mdx +49 -0
  25. package/constellation/no-unsupported-drag-and-drop-libraries/usage.mdx +17 -0
  26. package/constellation/prefer-primitives/usage.mdx +31 -0
  27. package/constellation/use-button-group-label/usage.mdx +58 -0
  28. package/constellation/use-drawer-label/usage.mdx +53 -0
  29. package/constellation/use-heading-level-in-spotlight-card/usage.mdx +20 -0
  30. package/constellation/use-href-in-link-item/usage.mdx +18 -0
  31. package/constellation/use-primitives/usage.mdx +77 -0
  32. package/constellation/use-visually-hidden/usage.mdx +34 -0
  33. package/dist/cjs/presets/all.codegen.js +3 -1
  34. package/dist/cjs/rules/consistent-css-prop-usage/index.js +254 -32
  35. package/dist/cjs/rules/index.codegen.js +5 -1
  36. package/dist/cjs/rules/no-css-tagged-template-expression/index.js +3 -2
  37. package/dist/cjs/rules/no-keyframes-tagged-template-expression/index.js +27 -0
  38. package/dist/cjs/rules/no-styled-tagged-template-expression/index.js +27 -0
  39. package/dist/cjs/rules/no-styled-tagged-template-expression/is-styled.js +53 -0
  40. package/dist/cjs/rules/utils/create-no-exported-rule/is-styled-component.js +6 -10
  41. package/dist/cjs/rules/{no-css-tagged-template-expression → utils}/create-no-tagged-template-expression-rule/index.js +26 -6
  42. package/dist/cjs/rules/{no-css-tagged-template-expression → utils}/create-no-tagged-template-expression-rule/to-arguments.js +28 -2
  43. package/dist/cjs/rules/utils/create-rule.js +7 -5
  44. package/dist/cjs/rules/utils/get-first-supported-import.js +28 -0
  45. package/dist/cjs/rules/utils/is-supported-import.js +9 -11
  46. package/dist/es2019/presets/all.codegen.js +3 -1
  47. package/dist/es2019/rules/consistent-css-prop-usage/index.js +251 -33
  48. package/dist/es2019/rules/index.codegen.js +5 -1
  49. package/dist/es2019/rules/no-css-tagged-template-expression/index.js +3 -2
  50. package/dist/es2019/rules/no-keyframes-tagged-template-expression/index.js +21 -0
  51. package/dist/es2019/rules/no-styled-tagged-template-expression/index.js +21 -0
  52. package/dist/es2019/rules/no-styled-tagged-template-expression/is-styled.js +45 -0
  53. package/dist/es2019/rules/utils/create-no-exported-rule/is-styled-component.js +4 -8
  54. package/dist/es2019/rules/utils/create-no-tagged-template-expression-rule/index.js +84 -0
  55. package/dist/es2019/rules/{no-css-tagged-template-expression → utils}/create-no-tagged-template-expression-rule/to-arguments.js +28 -2
  56. package/dist/es2019/rules/utils/create-rule.js +7 -5
  57. package/dist/es2019/rules/utils/get-first-supported-import.js +22 -0
  58. package/dist/es2019/rules/utils/is-supported-import.js +9 -9
  59. package/dist/esm/presets/all.codegen.js +3 -1
  60. package/dist/esm/rules/consistent-css-prop-usage/index.js +255 -33
  61. package/dist/esm/rules/index.codegen.js +5 -1
  62. package/dist/esm/rules/no-css-tagged-template-expression/index.js +3 -2
  63. package/dist/esm/rules/no-keyframes-tagged-template-expression/index.js +21 -0
  64. package/dist/esm/rules/no-styled-tagged-template-expression/index.js +21 -0
  65. package/dist/esm/rules/no-styled-tagged-template-expression/is-styled.js +47 -0
  66. package/dist/esm/rules/utils/create-no-exported-rule/is-styled-component.js +6 -10
  67. package/dist/esm/rules/{no-css-tagged-template-expression → utils}/create-no-tagged-template-expression-rule/index.js +25 -5
  68. package/dist/esm/rules/{no-css-tagged-template-expression → utils}/create-no-tagged-template-expression-rule/to-arguments.js +28 -2
  69. package/dist/esm/rules/utils/create-rule.js +7 -5
  70. package/dist/esm/rules/utils/get-first-supported-import.js +22 -0
  71. package/dist/esm/rules/utils/is-supported-import.js +9 -10
  72. package/dist/types/index.codegen.d.ts +2 -0
  73. package/dist/types/presets/all.codegen.d.ts +3 -1
  74. package/dist/types/rules/consistent-css-prop-usage/types.d.ts +7 -2
  75. package/dist/types/rules/index.codegen.d.ts +2 -0
  76. package/dist/types/rules/no-keyframes-tagged-template-expression/index.d.ts +2 -0
  77. package/dist/types/rules/no-styled-tagged-template-expression/index.d.ts +2 -0
  78. package/dist/types/rules/no-styled-tagged-template-expression/is-styled.d.ts +7 -0
  79. package/dist/types/rules/use-primitives/utils/update-jsx-attribute-by-name.d.ts +1 -1
  80. package/dist/types/rules/{no-css-tagged-template-expression → utils}/create-no-tagged-template-expression-rule/index.d.ts +3 -1
  81. package/dist/types/rules/{no-css-tagged-template-expression → utils}/create-no-tagged-template-expression-rule/types.d.ts +3 -0
  82. package/dist/types/rules/utils/get-first-supported-import.d.ts +17 -0
  83. package/dist/types/rules/utils/is-supported-import.d.ts +6 -5
  84. package/dist/types-ts4.5/index.codegen.d.ts +2 -0
  85. package/dist/types-ts4.5/presets/all.codegen.d.ts +3 -1
  86. package/dist/types-ts4.5/rules/consistent-css-prop-usage/types.d.ts +7 -2
  87. package/dist/types-ts4.5/rules/index.codegen.d.ts +2 -0
  88. package/dist/types-ts4.5/rules/no-keyframes-tagged-template-expression/index.d.ts +2 -0
  89. package/dist/types-ts4.5/rules/no-styled-tagged-template-expression/index.d.ts +2 -0
  90. package/dist/types-ts4.5/rules/no-styled-tagged-template-expression/is-styled.d.ts +7 -0
  91. package/dist/types-ts4.5/rules/use-primitives/utils/update-jsx-attribute-by-name.d.ts +1 -1
  92. package/dist/types-ts4.5/rules/{no-css-tagged-template-expression → utils}/create-no-tagged-template-expression-rule/index.d.ts +3 -1
  93. package/dist/types-ts4.5/rules/{no-css-tagged-template-expression → utils}/create-no-tagged-template-expression-rule/types.d.ts +3 -0
  94. package/dist/types-ts4.5/rules/utils/get-first-supported-import.d.ts +17 -0
  95. package/dist/types-ts4.5/rules/utils/is-supported-import.d.ts +6 -5
  96. package/package.json +3 -1
  97. package/dist/es2019/rules/no-css-tagged-template-expression/create-no-tagged-template-expression-rule/index.js +0 -62
  98. /package/dist/cjs/rules/{no-css-tagged-template-expression → utils}/create-no-tagged-template-expression-rule/generate.js +0 -0
  99. /package/dist/cjs/rules/{no-css-tagged-template-expression → utils}/create-no-tagged-template-expression-rule/get-tagged-template-expression-offset.js +0 -0
  100. /package/dist/cjs/rules/{no-css-tagged-template-expression → utils}/create-no-tagged-template-expression-rule/types.js +0 -0
  101. /package/dist/es2019/rules/{no-css-tagged-template-expression → utils}/create-no-tagged-template-expression-rule/generate.js +0 -0
  102. /package/dist/es2019/rules/{no-css-tagged-template-expression → utils}/create-no-tagged-template-expression-rule/get-tagged-template-expression-offset.js +0 -0
  103. /package/dist/es2019/rules/{no-css-tagged-template-expression → utils}/create-no-tagged-template-expression-rule/types.js +0 -0
  104. /package/dist/esm/rules/{no-css-tagged-template-expression → utils}/create-no-tagged-template-expression-rule/generate.js +0 -0
  105. /package/dist/esm/rules/{no-css-tagged-template-expression → utils}/create-no-tagged-template-expression-rule/get-tagged-template-expression-offset.js +0 -0
  106. /package/dist/esm/rules/{no-css-tagged-template-expression → utils}/create-no-tagged-template-expression-rule/types.js +0 -0
  107. /package/dist/types/rules/{no-css-tagged-template-expression → utils}/create-no-tagged-template-expression-rule/generate.d.ts +0 -0
  108. /package/dist/types/rules/{no-css-tagged-template-expression → utils}/create-no-tagged-template-expression-rule/get-tagged-template-expression-offset.d.ts +0 -0
  109. /package/dist/types/rules/{no-css-tagged-template-expression → utils}/create-no-tagged-template-expression-rule/to-arguments.d.ts +0 -0
  110. /package/dist/types-ts4.5/rules/{no-css-tagged-template-expression → utils}/create-no-tagged-template-expression-rule/generate.d.ts +0 -0
  111. /package/dist/types-ts4.5/rules/{no-css-tagged-template-expression → utils}/create-no-tagged-template-expression-rule/get-tagged-template-expression-offset.d.ts +0 -0
  112. /package/dist/types-ts4.5/rules/{no-css-tagged-template-expression → utils}/create-no-tagged-template-expression-rule/to-arguments.d.ts +0 -0
@@ -4,23 +4,43 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
4
4
  Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
- exports.createNoTaggedTemplateExpressionRule = void 0;
7
+ exports.noTaggedTemplateExpressionRuleSchema = exports.createNoTaggedTemplateExpressionRule = void 0;
8
8
  var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
9
- var _isSupportedImport = require("../../utils/is-supported-import");
9
+ var _isSupportedImport = require("../is-supported-import");
10
10
  var _generate = require("./generate");
11
11
  var _getTaggedTemplateExpressionOffset = require("./get-tagged-template-expression-offset");
12
12
  var _toArguments = require("./to-arguments");
13
13
  // Original source from Compiled https://github.com/atlassian-labs/compiled/blob/master/packages/eslint-plugin/src/utils/create-no-tagged-template-expression-rule/index.ts
14
14
  // eslint-disable-next-line import/no-extraneous-dependencies
15
15
 
16
- var IMPORT_SOURCES = [_isSupportedImport.CSS_IN_JS_IMPORTS.compiled, _isSupportedImport.CSS_IN_JS_IMPORTS.emotionReact, _isSupportedImport.CSS_IN_JS_IMPORTS.emotionCore, _isSupportedImport.CSS_IN_JS_IMPORTS.styledComponents];
16
+ var noTaggedTemplateExpressionRuleSchema = exports.noTaggedTemplateExpressionRuleSchema = [{
17
+ type: 'object',
18
+ properties: {
19
+ importSources: {
20
+ type: 'array',
21
+ items: {
22
+ type: 'string'
23
+ },
24
+ uniqueItems: true
25
+ }
26
+ }
27
+ }];
28
+
29
+ /**
30
+ * When true, template strings containing multiline comments are completely skipped over.
31
+ *
32
+ * When false, multiline comments are stripped out. Ideally we would preserve them,
33
+ * but it would add a lot of complexity.
34
+ */
35
+ var shouldSkipMultilineComments = false;
17
36
  var createNoTaggedTemplateExpressionRule = exports.createNoTaggedTemplateExpressionRule = function createNoTaggedTemplateExpressionRule(isUsage, messageId) {
18
37
  return function (context) {
38
+ var importSources = (0, _isSupportedImport.getImportSources)(context);
19
39
  return {
20
40
  TaggedTemplateExpression: function TaggedTemplateExpression(node) {
21
41
  var _context$getScope = context.getScope(),
22
42
  references = _context$getScope.references;
23
- if (!isUsage(node.tag, references, IMPORT_SOURCES)) {
43
+ if (!isUsage(node.tag, references, importSources)) {
24
44
  return;
25
45
  }
26
46
  context.report({
@@ -34,9 +54,9 @@ var createNoTaggedTemplateExpressionRule = exports.createNoTaggedTemplateExpress
34
54
  quasi = node.quasi;
35
55
  source = context.getSourceCode(); // TODO Eventually handle comments instead of skipping them
36
56
  // Skip auto-fixing comments
37
- if (!quasi.quasis.map(function (q) {
57
+ if (!(shouldSkipMultilineComments && quasi.quasis.map(function (q) {
38
58
  return q.value.raw;
39
- }).join('').match(/\/\*[\s\S]*\*\//g)) {
59
+ }).join('').match(/\/\*[\s\S]*\*\//g))) {
40
60
  _context.next = 4;
41
61
  break;
42
62
  }
@@ -56,7 +56,30 @@ var getArguments = function getArguments(chars) {
56
56
  }
57
57
  }
58
58
  var getValue = function getValue() {
59
- if (!value.trim().length && expressions.length) {
59
+ /**
60
+ * This branch is required for handling interpolated functions:
61
+ *
62
+ * css`
63
+ * color: ${(props) => props.textColor}
64
+ * `
65
+ *
66
+ * But it also breaks interpolations of multiple tokens:
67
+ *
68
+ * css`
69
+ * padding: ${token('space.100')} ${token('space.200')}
70
+ * `
71
+ *
72
+ * which becomes invalid syntax:
73
+ *
74
+ * css({
75
+ * padding: token('space.100')token('space.200')
76
+ * })
77
+ *
78
+ * Limiting this branch to when `expressions.length === 1` seems
79
+ * to allow both cases to work. There may be other edge cases,
80
+ * but none were caught by the existing test suite.
81
+ */
82
+ if (!value.trim().length && expressions.length === 1) {
60
83
  return {
61
84
  type: 'expression',
62
85
  expression: expressions.map(function (e) {
@@ -184,7 +207,10 @@ var toArguments = exports.toArguments = function toArguments(source, template) {
184
207
  i = _step4$value[0],
185
208
  quasi = _step4$value[1];
186
209
  // Deal with selectors across multiple lines
187
- var styleTemplateElement = quasi.value.raw.replace(/(\r\n|\n|\r)/gm, ' ').replace(/\s+/g, ' ');
210
+ var styleTemplateElement = quasi.value.raw.replace(/\/\*(.|\n|\r)*?\*\//g, '') // Removes multi-line comments
211
+ // Remove single line comments
212
+ // Negative lookbehind to handle URL-like double slashes
213
+ .replace(/(?<!https?:)\/\/.*$/gm, '').replace(/(\r\n|\n|\r)/gm, ' ').replace(/\s+/g, ' ');
188
214
  var _iterator5 = _createForOfIteratorHelper(styleTemplateElement),
189
215
  _step5;
190
216
  try {
@@ -17,7 +17,7 @@ var _utils = require("@typescript-eslint/utils");
17
17
  * @deprecated
18
18
  */
19
19
  var createRule = exports.createRule = _utils.ESLintUtils.RuleCreator(function (name) {
20
- return "https://atlassian.design/components/eslint-plugin-design-system/usage#".concat(name);
20
+ return getRuleUrl(name);
21
21
  });
22
22
  /**
23
23
  * Tiny wrapped over the ESLint rule module type that ensures
@@ -25,8 +25,10 @@ var createRule = exports.createRule = _utils.ESLintUtils.RuleCreator(function (n
25
25
  * as well as improving type support.
26
26
  */
27
27
  var createLintRule = exports.createLintRule = function createLintRule(rule) {
28
- var ruleName = rule.meta.name.replace('/', ''); // If it's a nested rule, ensure the url is clean
29
- var url = "https://atlassian.design/components/eslint-plugin-design-system/usage#".concat(ruleName);
30
- rule.meta.docs.url = url;
28
+ rule.meta.docs.url = getRuleUrl(rule.meta.name);
31
29
  return rule;
32
- };
30
+ };
31
+ function getRuleUrl(ruleName) {
32
+ var name = ruleName.replace('/', '-'); // If it's a nested rule, ensure the url is clean and matches codegen/gatsby
33
+ return "https://atlassian.design/components/eslint-plugin-design-system/".concat(name, "/usage");
34
+ }
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.getFirstSupportedImport = void 0;
7
+ var _eslintCodemodUtils = require("eslint-codemod-utils");
8
+ /**
9
+ * Get the first import declaration in the file that matches any of the packages
10
+ * in `importSources`.
11
+ *
12
+ * @param context Rule context.
13
+ * @param importSources The packages to check import statements for. If importSources
14
+ * contains more than one package, the first import statement
15
+ * detected in the file that matches any of the packages will be
16
+ * returned.
17
+ * @returns The first import declaration found in the file.
18
+ */
19
+ var getFirstSupportedImport = exports.getFirstSupportedImport = function getFirstSupportedImport(context, importSources) {
20
+ var isSupportedImport = function isSupportedImport(node) {
21
+ return (0, _eslintCodemodUtils.isNodeOfType)(node, 'ImportDeclaration') && typeof node.source.value === 'string' && importSources.includes(node.source.value);
22
+ };
23
+ var source = context.getSourceCode();
24
+ var supportedImports = source.ast.body.filter(isSupportedImport);
25
+ if (supportedImports.length) {
26
+ return supportedImports[0];
27
+ }
28
+ };
@@ -1,34 +1,32 @@
1
1
  "use strict";
2
2
 
3
- var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
3
  Object.defineProperty(exports, "__esModule", {
5
4
  value: true
6
5
  });
7
6
  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"));
9
7
  // eslint-disable-next-line import/no-extraneous-dependencies
10
8
 
11
9
  var CSS_IN_JS_IMPORTS = exports.CSS_IN_JS_IMPORTS = {
12
10
  compiled: '@compiled/react',
13
11
  emotionReact: '@emotion/react',
14
12
  emotionCore: '@emotion/core',
13
+ emotionStyled: '@emotion/styled',
15
14
  styledComponents: 'styled-components',
16
- atlaskitCss: '@atlaskit/css'
15
+ atlaskitCss: '@atlaskit/css',
16
+ atlaskitPrimitives: '@atlaskit/primitives'
17
17
  };
18
18
 
19
19
  // A CSS-in-JS library an import of a valid css, cx, cssMap, etc.
20
20
  // function might originate from, e.g. @compiled/react, @emotion/core.
21
21
 
22
- // All ESLint rules originating from `@compiled/eslint-plugin` should apply to these libraries.
23
- var DEFAULT_IMPORT_SOURCES = exports.DEFAULT_IMPORT_SOURCES = [CSS_IN_JS_IMPORTS.compiled, CSS_IN_JS_IMPORTS.atlaskitCss];
22
+ /**
23
+ * By default all known import sources are checked against.
24
+ */
25
+ var DEFAULT_IMPORT_SOURCES = exports.DEFAULT_IMPORT_SOURCES = Object.values(CSS_IN_JS_IMPORTS);
24
26
 
25
27
  /**
26
28
  * Given the ESLint rule context, extract and parse the value of the importSources rule option.
27
- * The importSources option is used to define additional libraries for which an ESLint rule
28
- * should apply to.
29
- *
30
- * Note that `@compiled/react` and `@atlaskit/css` are always included in importSources, regardless
31
- * of what importSources is configured to by the user.
29
+ * The importSources option is used to override which libraries an ESLint rule applies to.
32
30
  *
33
31
  * @param context The rule context.
34
32
  * @returns An array of strings representing what CSS-in-JS packages that should be checked, based
@@ -40,7 +38,7 @@ var getImportSources = exports.getImportSources = function getImportSources(cont
40
38
  return DEFAULT_IMPORT_SOURCES;
41
39
  }
42
40
  if (options[0].importSources && Array.isArray(options[0].importSources)) {
43
- return [].concat(DEFAULT_IMPORT_SOURCES, (0, _toConsumableArray2.default)(options[0].importSources));
41
+ return options[0].importSources;
44
42
  }
45
43
  return DEFAULT_IMPORT_SOURCES;
46
44
  };
@@ -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::914085544778f4543f43e3e30d0982e0>>
3
+ * @codegen <<SignedSource::5647ce9c10ba880cffece66b5924fd6e>>
4
4
  * @codegenCommand yarn workspace @atlaskit/eslint-plugin-design-system codegen
5
5
  */
6
6
  export default {
@@ -20,9 +20,11 @@ export default {
20
20
  '@atlaskit/design-system/no-exported-css': 'warn',
21
21
  '@atlaskit/design-system/no-exported-keyframes': 'warn',
22
22
  '@atlaskit/design-system/no-invalid-css-map': 'error',
23
+ '@atlaskit/design-system/no-keyframes-tagged-template-expression': 'error',
23
24
  '@atlaskit/design-system/no-margin': 'warn',
24
25
  '@atlaskit/design-system/no-nested-styles': 'error',
25
26
  '@atlaskit/design-system/no-physical-properties': 'error',
27
+ '@atlaskit/design-system/no-styled-tagged-template-expression': 'error',
26
28
  '@atlaskit/design-system/no-unsafe-design-token-usage': 'error',
27
29
  '@atlaskit/design-system/no-unsafe-style-overrides': 'warn',
28
30
  '@atlaskit/design-system/no-unsupported-drag-and-drop-libraries': 'error',
@@ -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
  }