@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.
- package/CHANGELOG.md +12 -0
- package/README.md +1 -0
- package/constellation/index/usage.mdx +114 -5
- package/dist/cjs/presets/all.codegen.js +2 -1
- package/dist/cjs/presets/recommended.codegen.js +2 -1
- package/dist/cjs/rules/consistent-css-prop-usage/index.js +98 -9
- package/dist/cjs/rules/index.codegen.js +3 -1
- package/dist/cjs/rules/use-button-group-label/index.js +83 -0
- package/dist/es2019/presets/all.codegen.js +2 -1
- package/dist/es2019/presets/recommended.codegen.js +2 -1
- package/dist/es2019/rules/consistent-css-prop-usage/index.js +95 -8
- package/dist/es2019/rules/index.codegen.js +3 -1
- package/dist/es2019/rules/use-button-group-label/index.js +75 -0
- package/dist/esm/presets/all.codegen.js +2 -1
- package/dist/esm/presets/recommended.codegen.js +2 -1
- package/dist/esm/rules/consistent-css-prop-usage/index.js +98 -9
- package/dist/esm/rules/index.codegen.js +3 -1
- package/dist/esm/rules/use-button-group-label/index.js +77 -0
- package/dist/types/index.codegen.d.ts +2 -0
- package/dist/types/presets/all.codegen.d.ts +2 -1
- package/dist/types/presets/recommended.codegen.d.ts +2 -1
- package/dist/types/rules/index.codegen.d.ts +1 -0
- package/dist/types/rules/use-button-group-label/index.d.ts +3 -0
- package/dist/types-ts4.5/index.codegen.d.ts +2 -0
- package/dist/types-ts4.5/presets/all.codegen.d.ts +2 -1
- package/dist/types-ts4.5/presets/recommended.codegen.d.ts +2 -1
- package/dist/types-ts4.5/rules/index.codegen.d.ts +1 -0
- package/dist/types-ts4.5/rules/use-button-group-label/index.d.ts +3 -0
- 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,
|
|
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::
|
|
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::
|
|
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::
|
|
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,
|
|
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::
|
|
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::
|
|
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::
|
|
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;
|