@atlaskit/eslint-plugin-design-system 8.20.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.
Files changed (29) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -0
  3. package/constellation/index/usage.mdx +114 -5
  4. package/dist/cjs/presets/all.codegen.js +2 -1
  5. package/dist/cjs/presets/recommended.codegen.js +2 -1
  6. package/dist/cjs/rules/consistent-css-prop-usage/index.js +98 -9
  7. package/dist/cjs/rules/index.codegen.js +3 -1
  8. package/dist/cjs/rules/use-button-group-label/index.js +83 -0
  9. package/dist/es2019/presets/all.codegen.js +2 -1
  10. package/dist/es2019/presets/recommended.codegen.js +2 -1
  11. package/dist/es2019/rules/consistent-css-prop-usage/index.js +95 -8
  12. package/dist/es2019/rules/index.codegen.js +3 -1
  13. package/dist/es2019/rules/use-button-group-label/index.js +75 -0
  14. package/dist/esm/presets/all.codegen.js +2 -1
  15. package/dist/esm/presets/recommended.codegen.js +2 -1
  16. package/dist/esm/rules/consistent-css-prop-usage/index.js +98 -9
  17. package/dist/esm/rules/index.codegen.js +3 -1
  18. package/dist/esm/rules/use-button-group-label/index.js +77 -0
  19. package/dist/types/index.codegen.d.ts +2 -0
  20. package/dist/types/presets/all.codegen.d.ts +2 -1
  21. package/dist/types/presets/recommended.codegen.d.ts +2 -1
  22. package/dist/types/rules/index.codegen.d.ts +1 -0
  23. package/dist/types/rules/use-button-group-label/index.d.ts +3 -0
  24. package/dist/types-ts4.5/index.codegen.d.ts +2 -0
  25. package/dist/types-ts4.5/presets/all.codegen.d.ts +2 -1
  26. package/dist/types-ts4.5/presets/recommended.codegen.d.ts +2 -1
  27. package/dist/types-ts4.5/rules/index.codegen.d.ts +1 -0
  28. package/dist/types-ts4.5/rules/use-button-group-label/index.d.ts +3 -0
  29. package/package.json +2 -1
@@ -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,6 +1,6 @@
1
1
  /**
2
2
  * THIS FILE WAS CREATED VIA CODEGEN DO NOT MODIFY {@see http://go/af-codegen}
3
- * @codegen <<SignedSource::ab1f5b129d07027c228dbd79da5f3572>>
3
+ * @codegen <<SignedSource::14cdfdcbd8b999ee097a1a5b245d7117>>
4
4
  * @codegenCommand yarn workspace @atlaskit/eslint-plugin-design-system codegen
5
5
  */
6
6
  import consistentCssPropUsage from './consistent-css-prop-usage';
@@ -19,6 +19,7 @@ import noUnsafeDesignTokenUsage from './no-unsafe-design-token-usage';
19
19
  import noUnsafeStyleOverrides from './no-unsafe-style-overrides';
20
20
  import noUnsupportedDragAndDropLibraries from './no-unsupported-drag-and-drop-libraries';
21
21
  import preferPrimitives from './prefer-primitives';
22
+ import useButtonGroupLabel from './use-button-group-label';
22
23
  import useDrawerLabel from './use-drawer-label';
23
24
  import useHeadingLevelInSpotlightCard from './use-heading-level-in-spotlight-card';
24
25
  import useHrefInLinkItem from './use-href-in-link-item';
@@ -41,6 +42,7 @@ export default {
41
42
  'no-unsafe-style-overrides': noUnsafeStyleOverrides,
42
43
  'no-unsupported-drag-and-drop-libraries': noUnsupportedDragAndDropLibraries,
43
44
  'prefer-primitives': preferPrimitives,
45
+ 'use-button-group-label': useButtonGroupLabel,
44
46
  'use-drawer-label': useDrawerLabel,
45
47
  'use-heading-level-in-spotlight-card': useHeadingLevelInSpotlightCard,
46
48
  'use-href-in-link-item': useHrefInLinkItem,
@@ -0,0 +1,75 @@
1
+ // eslint-disable-next-line import/no-extraneous-dependencies
2
+
3
+ import { isNodeOfType } from 'eslint-codemod-utils';
4
+ import { createLintRule } from '../utils/create-rule';
5
+ const elementsAccessibleNameProps = ['label', 'titleId'];
6
+ const rule = createLintRule({
7
+ meta: {
8
+ name: 'use-button-group-label',
9
+ type: 'suggestion',
10
+ docs: {
11
+ description: 'Ensures button groups are described to assistive technology by a direct label or by another element.',
12
+ recommended: true,
13
+ severity: 'warn'
14
+ },
15
+ messages: {
16
+ missingLabelProp: 'Missing accessible name. If there is no visible content to associate use `label` prop, otherwise pass id of element to `titleId` prop to be associated as label.',
17
+ labelPropShouldHaveContents: 'Define string that labels the interactive element.',
18
+ titleIdShouldHaveValue: '`titleId` should reference the id of element that define accessible name.',
19
+ noBothPropsUsage: 'Do not include both `titleId` and `label` properties. Use `titleId` if the label text is available in the DOM to reference it, otherwise use `label` to provide accessible name explicitly.'
20
+ },
21
+ hasSuggestions: true
22
+ },
23
+ create(context) {
24
+ const contextLocalIdentifier = [];
25
+ return {
26
+ ImportDeclaration(node) {
27
+ if (node.source.value === '@atlaskit/button') {
28
+ if (node.specifiers.length) {
29
+ const defaultImport = node.specifiers.filter(spec => spec.type === 'ImportSpecifier');
30
+ if (defaultImport && defaultImport.length) {
31
+ const {
32
+ local
33
+ } = defaultImport[0];
34
+ contextLocalIdentifier.push(local.name);
35
+ }
36
+ }
37
+ }
38
+ },
39
+ JSXElement(node) {
40
+ if (!isNodeOfType(node, 'JSXElement')) {
41
+ return;
42
+ }
43
+ if (!isNodeOfType(node.openingElement.name, 'JSXIdentifier')) {
44
+ return;
45
+ }
46
+ const name = node.openingElement.name.name;
47
+ if (contextLocalIdentifier.includes(name)) {
48
+ const componentLabelProps = node.openingElement.attributes.filter(attr => isNodeOfType(attr, 'JSXAttribute') && isNodeOfType(attr.name, 'JSXIdentifier') && elementsAccessibleNameProps.includes(attr.name.name));
49
+ if (componentLabelProps.length === 1) {
50
+ const prop = componentLabelProps[0];
51
+ if ('value' in prop && prop.value) {
52
+ if (isNodeOfType(prop.value, 'Literal') && !prop.value.value || isNodeOfType(prop.value, 'JSXExpressionContainer') && !prop.value.expression) {
53
+ context.report({
54
+ node: prop,
55
+ messageId: prop.name.name === 'label' ? 'labelPropShouldHaveContents' : 'titleIdShouldHaveValue'
56
+ });
57
+ }
58
+ }
59
+ } else if (componentLabelProps.length > 1) {
60
+ context.report({
61
+ node: node.openingElement,
62
+ messageId: 'noBothPropsUsage'
63
+ });
64
+ } else {
65
+ context.report({
66
+ node: node.openingElement,
67
+ messageId: 'missingLabelProp'
68
+ });
69
+ }
70
+ }
71
+ }
72
+ };
73
+ }
74
+ });
75
+ export default rule;
@@ -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::6efa1e48692b3e287d6dfcd500a5f0ab>>
3
+ * @codegen <<SignedSource::d1a459e1ea71650f65b2890dc86cc398>>
4
4
  * @codegenCommand yarn workspace @atlaskit/eslint-plugin-design-system codegen
5
5
  */
6
6
  export default {
@@ -22,6 +22,7 @@ export default {
22
22
  '@atlaskit/design-system/no-unsafe-style-overrides': 'warn',
23
23
  '@atlaskit/design-system/no-unsupported-drag-and-drop-libraries': 'error',
24
24
  '@atlaskit/design-system/prefer-primitives': 'warn',
25
+ '@atlaskit/design-system/use-button-group-label': 'warn',
25
26
  '@atlaskit/design-system/use-drawer-label': 'warn',
26
27
  '@atlaskit/design-system/use-heading-level-in-spotlight-card': 'warn',
27
28
  '@atlaskit/design-system/use-href-in-link-item': 'warn',
@@ -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::be810d87ec2d253e3b053dc06ff1b99a>>
3
+ * @codegen <<SignedSource::3b93cfbbe0ea14514b9600509632394b>>
4
4
  * @codegenCommand yarn workspace @atlaskit/eslint-plugin-design-system codegen
5
5
  */
6
6
  export default {
@@ -17,6 +17,7 @@ export default {
17
17
  '@atlaskit/design-system/no-unsafe-design-token-usage': 'error',
18
18
  '@atlaskit/design-system/no-unsafe-style-overrides': 'warn',
19
19
  '@atlaskit/design-system/no-unsupported-drag-and-drop-libraries': 'error',
20
+ '@atlaskit/design-system/use-button-group-label': 'warn',
20
21
  '@atlaskit/design-system/use-drawer-label': 'warn',
21
22
  '@atlaskit/design-system/use-heading-level-in-spotlight-card': 'warn',
22
23
  '@atlaskit/design-system/use-href-in-link-item': 'warn',
@@ -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
  }
@@ -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::ab1f5b129d07027c228dbd79da5f3572>>
3
+ * @codegen <<SignedSource::14cdfdcbd8b999ee097a1a5b245d7117>>
4
4
  * @codegenCommand yarn workspace @atlaskit/eslint-plugin-design-system codegen
5
5
  */
6
6
  import consistentCssPropUsage from './consistent-css-prop-usage';
@@ -19,6 +19,7 @@ import noUnsafeDesignTokenUsage from './no-unsafe-design-token-usage';
19
19
  import noUnsafeStyleOverrides from './no-unsafe-style-overrides';
20
20
  import noUnsupportedDragAndDropLibraries from './no-unsupported-drag-and-drop-libraries';
21
21
  import preferPrimitives from './prefer-primitives';
22
+ import useButtonGroupLabel from './use-button-group-label';
22
23
  import useDrawerLabel from './use-drawer-label';
23
24
  import useHeadingLevelInSpotlightCard from './use-heading-level-in-spotlight-card';
24
25
  import useHrefInLinkItem from './use-href-in-link-item';
@@ -41,6 +42,7 @@ export default {
41
42
  'no-unsafe-style-overrides': noUnsafeStyleOverrides,
42
43
  'no-unsupported-drag-and-drop-libraries': noUnsupportedDragAndDropLibraries,
43
44
  'prefer-primitives': preferPrimitives,
45
+ 'use-button-group-label': useButtonGroupLabel,
44
46
  'use-drawer-label': useDrawerLabel,
45
47
  'use-heading-level-in-spotlight-card': useHeadingLevelInSpotlightCard,
46
48
  'use-href-in-link-item': useHrefInLinkItem,
@@ -0,0 +1,77 @@
1
+ // eslint-disable-next-line import/no-extraneous-dependencies
2
+
3
+ import { isNodeOfType } from 'eslint-codemod-utils';
4
+ import { createLintRule } from '../utils/create-rule';
5
+ var elementsAccessibleNameProps = ['label', 'titleId'];
6
+ var rule = createLintRule({
7
+ meta: {
8
+ name: 'use-button-group-label',
9
+ type: 'suggestion',
10
+ docs: {
11
+ description: 'Ensures button groups are described to assistive technology by a direct label or by another element.',
12
+ recommended: true,
13
+ severity: 'warn'
14
+ },
15
+ messages: {
16
+ missingLabelProp: 'Missing accessible name. If there is no visible content to associate use `label` prop, otherwise pass id of element to `titleId` prop to be associated as label.',
17
+ labelPropShouldHaveContents: 'Define string that labels the interactive element.',
18
+ titleIdShouldHaveValue: '`titleId` should reference the id of element that define accessible name.',
19
+ noBothPropsUsage: 'Do not include both `titleId` and `label` properties. Use `titleId` if the label text is available in the DOM to reference it, otherwise use `label` to provide accessible name explicitly.'
20
+ },
21
+ hasSuggestions: true
22
+ },
23
+ create: function create(context) {
24
+ var contextLocalIdentifier = [];
25
+ return {
26
+ ImportDeclaration: function ImportDeclaration(node) {
27
+ if (node.source.value === '@atlaskit/button') {
28
+ if (node.specifiers.length) {
29
+ var defaultImport = node.specifiers.filter(function (spec) {
30
+ return spec.type === 'ImportSpecifier';
31
+ });
32
+ if (defaultImport && defaultImport.length) {
33
+ var local = defaultImport[0].local;
34
+ contextLocalIdentifier.push(local.name);
35
+ }
36
+ }
37
+ }
38
+ },
39
+ JSXElement: function JSXElement(node) {
40
+ if (!isNodeOfType(node, 'JSXElement')) {
41
+ return;
42
+ }
43
+ if (!isNodeOfType(node.openingElement.name, 'JSXIdentifier')) {
44
+ return;
45
+ }
46
+ var name = node.openingElement.name.name;
47
+ if (contextLocalIdentifier.includes(name)) {
48
+ var componentLabelProps = node.openingElement.attributes.filter(function (attr) {
49
+ return isNodeOfType(attr, 'JSXAttribute') && isNodeOfType(attr.name, 'JSXIdentifier') && elementsAccessibleNameProps.includes(attr.name.name);
50
+ });
51
+ if (componentLabelProps.length === 1) {
52
+ var prop = componentLabelProps[0];
53
+ if ('value' in prop && prop.value) {
54
+ if (isNodeOfType(prop.value, 'Literal') && !prop.value.value || isNodeOfType(prop.value, 'JSXExpressionContainer') && !prop.value.expression) {
55
+ context.report({
56
+ node: prop,
57
+ messageId: prop.name.name === 'label' ? 'labelPropShouldHaveContents' : 'titleIdShouldHaveValue'
58
+ });
59
+ }
60
+ }
61
+ } else if (componentLabelProps.length > 1) {
62
+ context.report({
63
+ node: node.openingElement,
64
+ messageId: 'noBothPropsUsage'
65
+ });
66
+ } else {
67
+ context.report({
68
+ node: node.openingElement,
69
+ messageId: 'missingLabelProp'
70
+ });
71
+ }
72
+ }
73
+ }
74
+ };
75
+ }
76
+ });
77
+ export default rule;
@@ -19,6 +19,7 @@ export declare const configs: {
19
19
  '@atlaskit/design-system/no-unsafe-style-overrides': string;
20
20
  '@atlaskit/design-system/no-unsupported-drag-and-drop-libraries': string;
21
21
  '@atlaskit/design-system/prefer-primitives': string;
22
+ '@atlaskit/design-system/use-button-group-label': string;
22
23
  '@atlaskit/design-system/use-drawer-label': string;
23
24
  '@atlaskit/design-system/use-heading-level-in-spotlight-card': string;
24
25
  '@atlaskit/design-system/use-href-in-link-item': string;
@@ -40,6 +41,7 @@ export declare const configs: {
40
41
  '@atlaskit/design-system/no-unsafe-design-token-usage': string;
41
42
  '@atlaskit/design-system/no-unsafe-style-overrides': string;
42
43
  '@atlaskit/design-system/no-unsupported-drag-and-drop-libraries': string;
44
+ '@atlaskit/design-system/use-button-group-label': string;
43
45
  '@atlaskit/design-system/use-drawer-label': string;
44
46
  '@atlaskit/design-system/use-heading-level-in-spotlight-card': string;
45
47
  '@atlaskit/design-system/use-href-in-link-item': string;
@@ -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::6efa1e48692b3e287d6dfcd500a5f0ab>>
3
+ * @codegen <<SignedSource::d1a459e1ea71650f65b2890dc86cc398>>
4
4
  * @codegenCommand yarn workspace @atlaskit/eslint-plugin-design-system codegen
5
5
  */
6
6
  declare const _default: {
@@ -22,6 +22,7 @@ declare const _default: {
22
22
  '@atlaskit/design-system/no-unsafe-style-overrides': string;
23
23
  '@atlaskit/design-system/no-unsupported-drag-and-drop-libraries': string;
24
24
  '@atlaskit/design-system/prefer-primitives': string;
25
+ '@atlaskit/design-system/use-button-group-label': string;
25
26
  '@atlaskit/design-system/use-drawer-label': string;
26
27
  '@atlaskit/design-system/use-heading-level-in-spotlight-card': string;
27
28
  '@atlaskit/design-system/use-href-in-link-item': string;
@@ -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::be810d87ec2d253e3b053dc06ff1b99a>>
3
+ * @codegen <<SignedSource::3b93cfbbe0ea14514b9600509632394b>>
4
4
  * @codegenCommand yarn workspace @atlaskit/eslint-plugin-design-system codegen
5
5
  */
6
6
  declare const _default: {
@@ -17,6 +17,7 @@ declare const _default: {
17
17
  '@atlaskit/design-system/no-unsafe-design-token-usage': string;
18
18
  '@atlaskit/design-system/no-unsafe-style-overrides': string;
19
19
  '@atlaskit/design-system/no-unsupported-drag-and-drop-libraries': string;
20
+ '@atlaskit/design-system/use-button-group-label': string;
20
21
  '@atlaskit/design-system/use-drawer-label': string;
21
22
  '@atlaskit/design-system/use-heading-level-in-spotlight-card': string;
22
23
  '@atlaskit/design-system/use-href-in-link-item': string;
@@ -19,6 +19,7 @@ declare const _default: {
19
19
  'no-unsafe-style-overrides': import("eslint").Rule.RuleModule;
20
20
  'no-unsupported-drag-and-drop-libraries': import("eslint").Rule.RuleModule;
21
21
  'prefer-primitives': import("eslint").Rule.RuleModule;
22
+ 'use-button-group-label': import("eslint").Rule.RuleModule;
22
23
  'use-drawer-label': import("eslint").Rule.RuleModule;
23
24
  'use-heading-level-in-spotlight-card': import("eslint").Rule.RuleModule;
24
25
  'use-href-in-link-item': import("eslint").Rule.RuleModule;
@@ -0,0 +1,3 @@
1
+ import type { Rule } from 'eslint';
2
+ declare const rule: Rule.RuleModule;
3
+ export default rule;