@atlaskit/eslint-plugin-design-system 4.0.0 → 4.2.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 (65) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/cjs/index.js +14 -3
  3. package/dist/cjs/rules/ensure-design-token-usage/index.js +3 -0
  4. package/dist/cjs/rules/no-deprecated-apis/index.js +91 -0
  5. package/dist/cjs/rules/no-deprecated-design-token-usage/index.js +76 -0
  6. package/dist/cjs/rules/no-deprecated-imports/index.js +45 -33
  7. package/dist/cjs/rules/no-deprecated-imports/paths.js +43 -15
  8. package/dist/cjs/rules/no-unsafe-design-token-usage/index.js +19 -5
  9. package/dist/cjs/rules/use-visually-hidden/constants.js +12 -0
  10. package/dist/cjs/rules/use-visually-hidden/fix-jsx.js +39 -0
  11. package/dist/cjs/rules/use-visually-hidden/fix-vanilla.js +35 -0
  12. package/dist/cjs/rules/use-visually-hidden/index.js +194 -0
  13. package/dist/cjs/rules/use-visually-hidden/utils.js +91 -0
  14. package/dist/cjs/rules/utils/get-import-node-by-source.js +23 -0
  15. package/dist/cjs/rules/utils/is-color.js +3 -2
  16. package/dist/cjs/rules/utils/is-node.js +33 -3
  17. package/dist/cjs/rules/utils/remove-named-import.js +29 -0
  18. package/dist/cjs/version.json +1 -1
  19. package/dist/es2019/index.js +10 -2
  20. package/dist/es2019/rules/ensure-design-token-usage/index.js +3 -0
  21. package/dist/es2019/rules/no-deprecated-apis/index.js +72 -0
  22. package/dist/es2019/rules/no-deprecated-design-token-usage/index.js +58 -0
  23. package/dist/es2019/rules/no-deprecated-imports/index.js +45 -32
  24. package/dist/es2019/rules/no-deprecated-imports/paths.js +41 -14
  25. package/dist/es2019/rules/no-unsafe-design-token-usage/index.js +12 -5
  26. package/dist/es2019/rules/use-visually-hidden/constants.js +3 -0
  27. package/dist/es2019/rules/use-visually-hidden/fix-jsx.js +24 -0
  28. package/dist/es2019/rules/use-visually-hidden/fix-vanilla.js +21 -0
  29. package/dist/es2019/rules/use-visually-hidden/index.js +169 -0
  30. package/dist/es2019/rules/use-visually-hidden/utils.js +62 -0
  31. package/dist/es2019/rules/utils/get-import-node-by-source.js +10 -0
  32. package/dist/es2019/rules/utils/is-color.js +2 -1
  33. package/dist/es2019/rules/utils/is-node.js +19 -2
  34. package/dist/es2019/rules/utils/remove-named-import.js +16 -0
  35. package/dist/es2019/version.json +1 -1
  36. package/dist/esm/index.js +10 -2
  37. package/dist/esm/rules/ensure-design-token-usage/index.js +3 -0
  38. package/dist/esm/rules/no-deprecated-apis/index.js +81 -0
  39. package/dist/esm/rules/no-deprecated-design-token-usage/index.js +66 -0
  40. package/dist/esm/rules/no-deprecated-imports/index.js +45 -32
  41. package/dist/esm/rules/no-deprecated-imports/paths.js +41 -14
  42. package/dist/esm/rules/no-unsafe-design-token-usage/index.js +20 -5
  43. package/dist/esm/rules/use-visually-hidden/constants.js +3 -0
  44. package/dist/esm/rules/use-visually-hidden/fix-jsx.js +26 -0
  45. package/dist/esm/rules/use-visually-hidden/fix-vanilla.js +23 -0
  46. package/dist/esm/rules/use-visually-hidden/index.js +180 -0
  47. package/dist/esm/rules/use-visually-hidden/utils.js +74 -0
  48. package/dist/esm/rules/utils/get-import-node-by-source.js +14 -0
  49. package/dist/esm/rules/utils/is-color.js +2 -1
  50. package/dist/esm/rules/utils/is-node.js +22 -1
  51. package/dist/esm/rules/utils/remove-named-import.js +20 -0
  52. package/dist/esm/version.json +1 -1
  53. package/dist/types/index.d.ts +5 -0
  54. package/dist/types/rules/no-deprecated-apis/index.d.ts +3 -0
  55. package/dist/types/rules/no-deprecated-design-token-usage/index.d.ts +3 -0
  56. package/dist/types/rules/no-deprecated-imports/paths.d.ts +33 -14
  57. package/dist/types/rules/use-visually-hidden/constants.d.ts +3 -0
  58. package/dist/types/rules/use-visually-hidden/fix-jsx.d.ts +3 -0
  59. package/dist/types/rules/use-visually-hidden/fix-vanilla.d.ts +3 -0
  60. package/dist/types/rules/use-visually-hidden/index.d.ts +3 -0
  61. package/dist/types/rules/use-visually-hidden/utils.d.ts +35 -0
  62. package/dist/types/rules/utils/get-import-node-by-source.d.ts +8 -0
  63. package/dist/types/rules/utils/is-node.d.ts +7 -0
  64. package/dist/types/rules/utils/remove-named-import.d.ts +8 -0
  65. package/package.json +6 -2
@@ -44,17 +44,24 @@ const rule = {
44
44
  },
45
45
 
46
46
  create(context) {
47
- const sourceCode = context.getSourceCode();
48
47
  const restrictedPathMessages = restrictedPaths.reduce((memo, importSource) => {
49
48
  if (typeof importSource === 'string') {
50
49
  memo[importSource] = {
51
50
  message: ''
52
51
  };
53
52
  } else {
54
- memo[importSource.name] = {
55
- message: importSource.message // importNames: importSource.importNames
53
+ if ('message' in importSource) {
54
+ memo[importSource.path] = {
55
+ message: importSource.message
56
+ };
57
+ }
56
58
 
57
- };
59
+ if ('imports' in importSource) {
60
+ memo[importSource.path] = {
61
+ // @ts-ignore
62
+ imports: importSource.imports
63
+ };
64
+ }
58
65
  }
59
66
 
60
67
  return memo;
@@ -62,26 +69,45 @@ const rule = {
62
69
  /**
63
70
  * Report a restricted path.
64
71
  * @param {string} importSource path of the import
65
- * @param {Map<string,Object[]>} importNames Map of import names that are being imported
66
72
  * @param {node} node representing the restricted path reference
73
+ * @param {Map<string,Rule.Node>} importNames Map of import names that are being imported
67
74
  * @returns {void}
68
75
  * @private
69
76
  */
70
77
 
71
- function checkRestrictedPathAndReport(importSource, importNames, node) {
78
+ function checkRestrictedPathAndReport(importSource, node, importNames) {
72
79
  if (!Object.prototype.hasOwnProperty.call(restrictedPathMessages, importSource)) {
73
80
  return;
74
81
  }
75
82
 
76
- const customMessage = restrictedPathMessages[importSource].message;
77
- context.report({
78
- node,
79
- messageId: customMessage ? 'pathWithCustomMessage' : 'path',
80
- data: {
81
- importSource,
82
- customMessage
83
- }
84
- });
83
+ const config = restrictedPathMessages[importSource]; // The message will only exist if the import is completely banned,
84
+ // eg a deprecated package
85
+
86
+ if ('message' in config) {
87
+ context.report({
88
+ node,
89
+ messageId: config.message ? 'pathWithCustomMessage' : 'path',
90
+ data: {
91
+ importSource,
92
+ customMessage: config.message
93
+ }
94
+ });
95
+ } // if there are specific named exports that are banned,
96
+ // iterate through and check if they're being imported
97
+
98
+
99
+ if ('imports' in config) {
100
+ var _config$imports;
101
+
102
+ (_config$imports = config.imports) === null || _config$imports === void 0 ? void 0 : _config$imports.forEach(restrictedImport => {
103
+ if (importNames.has(restrictedImport.importName)) {
104
+ context.report({
105
+ node: importNames.get(restrictedImport.importName),
106
+ message: restrictedImport.message
107
+ });
108
+ }
109
+ });
110
+ }
85
111
  }
86
112
  /**
87
113
  * Checks a node to see if any problems should be reported.
@@ -95,18 +121,10 @@ const rule = {
95
121
  const importSource = node.source.value.trim();
96
122
  const importNames = new Map();
97
123
 
98
- if (node.type === 'ExportAllDeclaration') {
99
- const starToken = sourceCode.getFirstToken(node, 1);
100
- importNames.set('*', [{
101
- loc: starToken === null || starToken === void 0 ? void 0 : starToken.loc
102
- }]);
103
- } else if ('specifiers' in node) {
124
+ if ('specifiers' in node) {
104
125
  // @ts-ignore
105
126
  for (const specifier of node.specifiers) {
106
127
  let name;
107
- const specifierData = {
108
- loc: specifier.loc
109
- };
110
128
 
111
129
  if (specifier.type === 'ImportDefaultSpecifier') {
112
130
  name = 'default';
@@ -119,16 +137,12 @@ const rule = {
119
137
  }
120
138
 
121
139
  if (name) {
122
- if (importNames.has(name)) {
123
- importNames.get(name).push(specifierData);
124
- } else {
125
- importNames.set(name, [specifierData]);
126
- }
140
+ importNames.set(name, specifier);
127
141
  }
128
142
  }
129
143
  }
130
144
 
131
- checkRestrictedPathAndReport(importSource, importNames, node);
145
+ checkRestrictedPathAndReport(importSource, node, importNames);
132
146
  };
133
147
 
134
148
  return {
@@ -138,9 +152,8 @@ const rule = {
138
152
  if (node.source) {
139
153
  checkNode(node);
140
154
  }
141
- },
155
+ }
142
156
 
143
- ExportAllDeclaration: checkNode
144
157
  };
145
158
  }
146
159
 
@@ -1,43 +1,70 @@
1
+ export const namedThemeExports = [{
2
+ importName: 'assistive',
3
+ message: 'The assistive mixin is deprecated. Please use `@atlaskit/visually-hidden` instead.'
4
+ }, {
5
+ importName: 'visuallyHidden',
6
+ message: 'The visuallyHidden mixin is deprecated. Please use `@atlaskit/visually-hidden` instead.'
7
+ }, {
8
+ importName: 'focusRing',
9
+ message: 'The focusRing mixin is deprecated. Please use `@atlaskit/visually-hidden` instead.'
10
+ }];
1
11
  export const restrictedPaths = [{
2
- name: '@atlaskit/navigation-next',
12
+ path: '@atlaskit/navigation-next',
3
13
  message: `navigation-next is deprecated. Please use '@atlaskit/atlassian-navigation' instead.`
4
14
  }, {
5
- name: '@atlaskit/field-base',
15
+ path: '@atlaskit/field-base',
6
16
  message: `field-base is deprecated. Please use the '@atlaskit/form' package instead.`
7
17
  }, {
8
- name: '@atlaskit/field-radio-group',
18
+ path: '@atlaskit/field-radio-group',
9
19
  message: `field-radio-group is deprecated. Please use '@atlaskit/radio' instead, and check the migration guide.`
10
20
  }, {
11
- name: '@atlaskit/field-range',
21
+ path: '@atlaskit/field-range',
12
22
  message: `field-range is deprecated. Please use '@atlaskit/range' instead.`
13
23
  }, {
14
- name: '@atlaskit/field-text',
24
+ path: '@atlaskit/field-text',
15
25
  message: `field-text is deprecated. Please use '@atlaskit/textfield' instead.`
16
26
  }, {
17
- name: '@atlaskit/field-text-area',
27
+ path: '@atlaskit/field-text-area',
18
28
  message: `field-text-area is deprecated. Please use '@atlaskit/textarea' instead.`
19
29
  }, {
20
- name: '@atlaskit/navigation',
30
+ path: '@atlaskit/navigation',
21
31
  message: `navigation is deprecated. Please use '@atlaskit/atlassian-navigation' instead.`
22
32
  }, {
23
- name: '@atlaskit/global-navigation',
33
+ path: '@atlaskit/global-navigation',
24
34
  message: `global-navigation is deprecated. Please use '@atlaskit/atlassian-navigation' for the horizontal nav bar, '@atlaskit/side-navigation' for the side nav, and '@atlaskit/page-layout' to layout your application.`
25
35
  }, {
26
- name: '@atlaskit/input',
36
+ path: '@atlaskit/input',
27
37
  message: 'input is deprecated. This was an internal component and should not be used directly.'
28
38
  }, {
29
- name: '@atlaskit/layer',
39
+ path: '@atlaskit/layer',
30
40
  message: 'layer is deprecated. This was an internal component and should not be used directly.'
31
41
  }, {
32
- name: '@atlaskit/single-select',
42
+ path: '@atlaskit/single-select',
33
43
  message: `single-select is deprecated. Please use '@atlaskit/select' instead.`
34
44
  }, {
35
- name: '@atlaskit/multi-select',
45
+ path: '@atlaskit/multi-select',
36
46
  message: `multi-select is deprecated. Please use '@atlaskit/select' instead.`
37
47
  }, {
38
- name: '@atlaskit/droplist',
48
+ path: '@atlaskit/droplist',
39
49
  message: `droplist is deprecated. For the pop-up behaviour please use '@atlaskit/popup' and for common menu components please use '@atlaskit/menu'.`
40
50
  }, {
41
- name: '@atlaskit/item',
51
+ path: '@atlaskit/item',
42
52
  message: `item is deprecated. Please use '@atlaskit/menu' instead.`
53
+ }, // TODO uncomment me when we formally deprecate typography
54
+ // {
55
+ // path: '@atlaskit/theme/typography',
56
+ // message: 'The typography mixins are deprecated. Please use `@atlaskit/heading` instead.',
57
+ // },
58
+ {
59
+ path: '@atlaskit/theme/constants',
60
+ imports: namedThemeExports
61
+ }, {
62
+ path: '@atlaskit/theme',
63
+ imports: namedThemeExports // .concat(
64
+ // { importName: 'typography', message: 'The typography mixins are deprecated. Please use `@atlaskit/heading` instead.',}
65
+ // )
66
+
67
+ }, {
68
+ path: '@atlaskit/icon-priority',
69
+ message: `icon-priority is deprecated due to limited usage in Cloud products. It will be deleted after 21 April 2022.`
43
70
  }];
@@ -2,6 +2,9 @@ import renameMapping from '@atlaskit/tokens/rename-mapping';
2
2
  import tokens from '@atlaskit/tokens/token-names';
3
3
  import { isDecendantOfStyleBlock, isDecendantOfStyleJsxAttribute } from '../utils/is-node';
4
4
  import { isToken } from '../utils/is-token';
5
+
6
+ const getCleanPathId = path => path.split('.').filter(el => el !== '[default]').join('.');
7
+
5
8
  const defaultConfig = {
6
9
  shouldEnforceFallbacks: false
7
10
  };
@@ -28,7 +31,7 @@ token('color.background.blanket');
28
31
  \`\`\`
29
32
  `,
30
33
  invalidToken: 'The token "{{name}}" does not exist.',
31
- tokenRenamed: 'The token "{{name}}" has been renamed.',
34
+ tokenRemoved: 'The token "{{name}}" is removed in favour of "{{replacement}}".',
32
35
  tokenFallbackEnforced: `Token function requires a fallback, preferably something that best matches the light/default theme in case tokens aren't present.
33
36
 
34
37
  \`\`\`
@@ -136,14 +139,18 @@ token('color.background.blanket');
136
139
  return;
137
140
  }
138
141
 
139
- if (typeof tokenKey === 'string' && tokenKey in renameMapping) {
142
+ const migrationMeta = renameMapping.filter(t => t.state === 'deleted').find(t => t.path === tokenKey);
143
+
144
+ if (typeof tokenKey === 'string' && migrationMeta) {
145
+ const cleanTokenKey = getCleanPathId(migrationMeta.replacement);
140
146
  context.report({
141
- messageId: 'tokenRenamed',
147
+ messageId: 'tokenRemoved',
142
148
  node,
143
149
  data: {
144
- name: tokenKey
150
+ name: tokenKey,
151
+ replacement: cleanTokenKey
145
152
  },
146
- fix: fixer => fixer.replaceText(node.arguments[0], `'${renameMapping[tokenKey]}'`)
153
+ fix: fixer => fixer.replaceText(node.arguments[0], `'${cleanTokenKey}'`)
147
154
  });
148
155
  return;
149
156
  }
@@ -0,0 +1,3 @@
1
+ export const IMPORT_NAME = 'AKVisuallyHidden';
2
+ export const VISUALLY_HIDDEN_SOURCE = '@atlaskit/visually-hidden';
3
+ export const VISUALLY_HIDDEN_IMPORT = `import ${IMPORT_NAME} from '${VISUALLY_HIDDEN_SOURCE}';\n`;
@@ -0,0 +1,24 @@
1
+ import { getImportedNodeBySource } from '../utils/get-import-node-by-source';
2
+ import { getClosestNodeOfType } from '../utils/is-node';
3
+ import { IMPORT_NAME, VISUALLY_HIDDEN_IMPORT, VISUALLY_HIDDEN_SOURCE } from './constants';
4
+ import { getFirstImport } from './utils';
5
+ export default ((source, node) => fixer => {
6
+ const fixes = [];
7
+ const importedNode = getFirstImport(source);
8
+ const visuallyHiddenNode = getImportedNodeBySource(source, VISUALLY_HIDDEN_SOURCE);
9
+
10
+ if (!importedNode) {
11
+ return [];
12
+ }
13
+
14
+ const jsxOpeningElement = getClosestNodeOfType(node, 'JSXOpeningElement');
15
+
16
+ if (visuallyHiddenNode) {
17
+ fixes.push(fixer.replaceText(jsxOpeningElement, visuallyHiddenNode.specifiers[0].local.name));
18
+ } else {
19
+ fixes.push(fixer.insertTextBefore(importedNode, VISUALLY_HIDDEN_IMPORT));
20
+ fixes.push(fixer.replaceText(jsxOpeningElement, `<${IMPORT_NAME} />`));
21
+ }
22
+
23
+ return fixes;
24
+ });
@@ -0,0 +1,21 @@
1
+ import { getImportedNodeBySource } from '../utils/get-import-node-by-source';
2
+ import { IMPORT_NAME, VISUALLY_HIDDEN_IMPORT, VISUALLY_HIDDEN_SOURCE } from './constants';
3
+ import { getFirstImport } from './utils';
4
+ export default ((source, node) => fixer => {
5
+ const fixes = [];
6
+ const importedNode = getFirstImport(source);
7
+ const visuallyHiddenNode = getImportedNodeBySource(source, VISUALLY_HIDDEN_SOURCE);
8
+
9
+ if (!importedNode) {
10
+ return [];
11
+ }
12
+
13
+ if (visuallyHiddenNode) {
14
+ fixes.push(fixer.replaceText(node, visuallyHiddenNode.specifiers[0].local.name));
15
+ } else {
16
+ fixes.push(fixer.insertTextBefore(importedNode, VISUALLY_HIDDEN_IMPORT));
17
+ fixes.push(fixer.replaceText(node, IMPORT_NAME));
18
+ }
19
+
20
+ return fixes;
21
+ });
@@ -0,0 +1,169 @@
1
+ /* eslint-disable @atlaskit/design-system/use-visually-hidden */
2
+ import { getClosestNodeOfType, isStyledObjectNode, isStyledTemplateNode } from '../utils/is-node';
3
+ import fixJsx from './fix-jsx';
4
+ import fixVanilla from './fix-vanilla';
5
+ import { countMatchingKeyValues, getObjectLikeness, makeTemplateLiteralIntoEntries } from './utils';
6
+ const THEME_IMPORT_NAMES = ['visuallyHidden', 'assistive'];
7
+ const rule = {
8
+ meta: {
9
+ type: 'suggestion',
10
+ fixable: 'code',
11
+ docs: {
12
+ description: 'Suggest usage of the `@atlaskit/visually-hidden` component instead of either the `@atlaskit/theme` mixins or just something the user has rolled themselves.',
13
+ recommended: true
14
+ },
15
+ messages: {
16
+ noDeprecatedUsage: 'Using the export `{{local}}` from `{{import}}` as a mixin is discouraged. Please use `@atlaskit/visually-hidden` instead.',
17
+ noDeprecated: 'The export `{{local}}` from `{{import}}` is deprecated. Please use `@atlaskit/visually-hidden` instead.',
18
+ suggestion: 'This CSS closely matches the implementation of a visually hidden element. You should consider using the `@atlaskit/visually-hidden` component instead.'
19
+ }
20
+ },
21
+
22
+ create(context) {
23
+ const source = context.getSourceCode();
24
+ return {
25
+ ImportDeclaration(node) {
26
+ const isThemeNode = node.source.value === '@atlaskit/theme' || node.source.value === '@atlaskit/theme/constants';
27
+
28
+ if (!isThemeNode) {
29
+ return;
30
+ }
31
+
32
+ const visuallyHiddenOrAssistive = node.specifiers.filter(specifier => specifier.type === 'ImportSpecifier').find(specifier => THEME_IMPORT_NAMES.includes(specifier.imported.name));
33
+
34
+ if (!visuallyHiddenOrAssistive) {
35
+ return;
36
+ }
37
+
38
+ context.getDeclaredVariables(visuallyHiddenOrAssistive).forEach(someNode => {
39
+ someNode.references.map(innerNode => innerNode.identifier).forEach(idNode => {
40
+ // @ts-ignore JSX is not typed correctly in eslint
41
+ if ((idNode === null || idNode === void 0 ? void 0 : idNode.parent.type) === 'JSXExpressionContainer') {
42
+ context.report({
43
+ node: idNode.parent,
44
+ messageId: 'noDeprecatedUsage',
45
+ data: {
46
+ import: `${node.source.value}`,
47
+ local: visuallyHiddenOrAssistive.local.name
48
+ },
49
+ fix: fixJsx(source, idNode)
50
+ }); // this is either a styled usage OR mixin usage in a styled usage
51
+ } else if (idNode.parent.type === 'CallExpression') {
52
+ if (isStyledObjectNode(idNode.parent) || isStyledTemplateNode(idNode.parent)) {
53
+ context.report({
54
+ node: idNode.parent,
55
+ messageId: 'noDeprecatedUsage',
56
+ data: {
57
+ import: `${node.source.value}`,
58
+ local: visuallyHiddenOrAssistive.local.name
59
+ },
60
+ fix: fixVanilla(source, idNode.parent)
61
+ });
62
+ }
63
+
64
+ if (idNode.parent.callee === idNode) {
65
+ context.report({
66
+ node: idNode.parent,
67
+ messageId: 'noDeprecatedUsage',
68
+ data: {
69
+ import: `${node.source.value}`,
70
+ local: visuallyHiddenOrAssistive.local.name
71
+ },
72
+ fix: fixVanilla(source, getClosestNodeOfType(idNode.parent, 'TaggedTemplateExpression'))
73
+ });
74
+ }
75
+ }
76
+ });
77
+ });
78
+ return context.report({
79
+ node: visuallyHiddenOrAssistive,
80
+ messageId: 'noDeprecated',
81
+ data: {
82
+ import: `${node.source.value}`,
83
+ local: visuallyHiddenOrAssistive.local.name
84
+ }
85
+ });
86
+ },
87
+
88
+ CallExpression(node) {
89
+ var _node$arguments$;
90
+
91
+ if (node.type !== 'CallExpression') {
92
+ return;
93
+ }
94
+
95
+ if (!(node.callee.type === 'MemberExpression' || node.callee.type === 'Identifier')) {
96
+ return;
97
+ }
98
+
99
+ const isStyled = isStyledObjectNode(node);
100
+
101
+ if (node.callee.type === 'MemberExpression' && node.callee.object.type === 'Identifier' && node.callee.object.name !== 'styled') {
102
+ return;
103
+ }
104
+
105
+ if (node.callee.type === 'Identifier' && node.callee.name !== 'css') {
106
+ return;
107
+ } // This is an object style (probably)
108
+
109
+
110
+ if (node.arguments && ((_node$arguments$ = node.arguments[0]) === null || _node$arguments$ === void 0 ? void 0 : _node$arguments$.type) === 'ObjectExpression') {
111
+ const matchingScore = getObjectLikeness(node.arguments[0]);
112
+
113
+ if (matchingScore > 0.8) {
114
+ return context.report({
115
+ node: node.parent,
116
+ messageId: 'suggestion',
117
+ fix: isStyled ? fixVanilla(source, node) : undefined
118
+ });
119
+ }
120
+ }
121
+
122
+ return null;
123
+ },
124
+
125
+ ObjectExpression(node) {
126
+ if (node.parent.type === 'CallExpression') {
127
+ return;
128
+ }
129
+
130
+ const matchingScore = getObjectLikeness(node);
131
+
132
+ if (matchingScore > 0.8) {
133
+ return context.report({
134
+ node: node,
135
+ messageId: 'suggestion'
136
+ });
137
+ }
138
+ },
139
+
140
+ 'TaggedTemplateExpression[tag.name="css"],TaggedTemplateExpression[tag.object.name="styled"]': node => {
141
+ if (node.type !== 'TaggedTemplateExpression') {
142
+ return;
143
+ }
144
+
145
+ const templateString = node.quasi.quasis.map(q => q.value.raw).join('');
146
+ const styleEntries = makeTemplateLiteralIntoEntries(templateString);
147
+
148
+ if (!styleEntries) {
149
+ return;
150
+ }
151
+
152
+ const count = countMatchingKeyValues(styleEntries.map(([key, value]) => ({
153
+ key,
154
+ value
155
+ })));
156
+
157
+ if (count > 0.8) {
158
+ return context.report({
159
+ node,
160
+ messageId: 'suggestion',
161
+ fix: node.tag.type !== 'Identifier' ? fixVanilla(source, node) : undefined
162
+ });
163
+ }
164
+ }
165
+ };
166
+ }
167
+
168
+ };
169
+ export default rule;
@@ -0,0 +1,62 @@
1
+ // eslint-disable-next-line import/no-unresolved
2
+ // eslint-disable-next-line @atlaskit/design-system/use-visually-hidden
3
+ const referenceObject = {
4
+ width: '1px',
5
+ height: '1px',
6
+ padding: '0',
7
+ position: 'absolute',
8
+ border: '0',
9
+ clip: 'rect(1px, 1px, 1px, 1px)',
10
+ overflow: 'hidden',
11
+ whiteSpace: 'nowrap'
12
+ };
13
+
14
+ /**
15
+ * Returns the first import in the esprima AST.
16
+ */
17
+ export const getFirstImport = source => {
18
+ return source.ast.body.find(node => node.type === 'ImportDeclaration');
19
+ };
20
+ /**
21
+ * Takes a template literal and returns [key, value] array of the css properties
22
+ */
23
+
24
+ export const makeTemplateLiteralIntoEntries = templateString => {
25
+ return templateString.replace(/\n/g, '').split(/;|{|}/).filter(el => !el.match(/\@/)).map(el => el.trim().split(':').map(e => e.trim()));
26
+ };
27
+ /**
28
+ * Given a node, translate the node into css key-value pairs and
29
+ * compare the output to the reference styles required to make a
30
+ * visually hidden element.
31
+ *
32
+ * @returns {number} A fraction between 0-1 depending on the object's likeness.
33
+ */
34
+
35
+ export const getObjectLikeness = node => {
36
+ const styleEntries = node.properties.map(({
37
+ type,
38
+ key,
39
+ value
40
+ }) => {
41
+ if (type === 'Property' && key.type === 'Identifier') {
42
+ return {
43
+ key: key.name,
44
+ value: value.type === 'Literal' && value.value
45
+ };
46
+ }
47
+
48
+ return null;
49
+ }).filter(node => Boolean(node));
50
+ return countMatchingKeyValues(styleEntries);
51
+ };
52
+ export const countMatchingKeyValues = styleEntries => {
53
+ const matchingStyleEntries = styleEntries.filter(entry => {
54
+ return entry.key in referenceObject;
55
+ });
56
+
57
+ if (styleEntries.length < 5) {
58
+ return 0;
59
+ }
60
+
61
+ return matchingStyleEntries.reduce((acc, curr) => acc + (referenceObject[curr === null || curr === void 0 ? void 0 : curr.key] === (curr === null || curr === void 0 ? void 0 : curr.value) ? 1.5 : 0.75), 0) / styleEntries.length;
62
+ };
@@ -0,0 +1,10 @@
1
+ /* eslint-disable import/no-unresolved */
2
+
3
+ /**
4
+ * @param {SourceCode} source The eslint source
5
+ * @param {string} path The path specified to find
6
+ * @returns {ImportDeclaration}
7
+ */
8
+ export const getImportedNodeBySource = (source, path) => {
9
+ return source.ast.body.filter(node => node.type === 'ImportDeclaration').find(node => node.source.value === path);
10
+ };
@@ -37,7 +37,8 @@ export const isHardCodedColor = value => {
37
37
  return true;
38
38
  }
39
39
 
40
- if (value.startsWith('#') && (value.length === 4 || value.length === 7 || value.length === 9)) {
40
+ if (value.startsWith('#') && ( // short hex, hex, or hex with alpha
41
+ value.length === 4 || value.length === 7 || value.length === 9)) {
41
42
  return true;
42
43
  }
43
44
 
@@ -32,6 +32,8 @@ export const isDecendantOfStyleJsxAttribute = node => {
32
32
 
33
33
  return false;
34
34
  };
35
+ export const isStyledTemplateNode = node => (node === null || node === void 0 ? void 0 : node.type) === 'TaggedTemplateExpression' && node.tag.type === 'MemberExpression' && node.tag.object.type === 'Identifier' && node.tag.object.name === 'styled';
36
+ export const isStyledObjectNode = node => (node === null || node === void 0 ? void 0 : node.type) === 'CallExpression' && node.callee.type === 'MemberExpression' && node.callee.object.type === 'Identifier' && node.callee.object.name === 'styled';
35
37
  export const isDecendantOfStyleBlock = node => {
36
38
  if (node.type === 'VariableDeclarator') {
37
39
  if (node.id.type !== 'Identifier') {
@@ -59,7 +61,7 @@ export const isDecendantOfStyleBlock = node => {
59
61
  return true;
60
62
  }
61
63
 
62
- if (node.type === 'TaggedTemplateExpression' && node.tag.type === 'MemberExpression' && node.tag.object.type === 'Identifier' && node.tag.object.name === 'styled') {
64
+ if (isStyledTemplateNode(node)) {
63
65
  return true;
64
66
  }
65
67
 
@@ -73,4 +75,19 @@ export const isDecendantOfStyleBlock = node => {
73
75
 
74
76
  return false;
75
77
  };
76
- export const isChildOfType = (node, type) => node.parent.type === type;
78
+ export const isChildOfType = (node, type) => node.parent.type === type;
79
+ /**
80
+ * Given a node, walk up the tree until there is no parent OR a common ancestor of the correct type is found
81
+ */
82
+
83
+ export const getClosestNodeOfType = (node, type) => {
84
+ if (!node) {
85
+ return node;
86
+ }
87
+
88
+ if (node.type === type) {
89
+ return node;
90
+ }
91
+
92
+ return getClosestNodeOfType(node.parent, type);
93
+ };
@@ -0,0 +1,16 @@
1
+ /* eslint-disable import/no-unresolved */
2
+
3
+ /**
4
+ * @param {SourceCode} source The eslint source
5
+ * @param {string} path The path specified to find
6
+ * @returns FixerObject
7
+ */
8
+ export const removeNamedImport = (fixer, importNode, name) => {
9
+ const filteredSpecifers = importNode.specifiers.filter(node => node.type === 'ImportSpecifier' && node.imported.name !== name);
10
+
11
+ if (filteredSpecifers.length) {
12
+ return fixer.remove(importNode.specifiers.find(node => node.type === 'ImportSpecifier' && node.imported.name === name));
13
+ }
14
+
15
+ return fixer.remove(importNode);
16
+ };
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/eslint-plugin-design-system",
3
- "version": "4.0.0",
3
+ "version": "4.2.0",
4
4
  "sideEffects": false
5
5
  }
package/dist/esm/index.js CHANGED
@@ -1,16 +1,24 @@
1
1
  import ensureTokenUsage from './rules/ensure-design-token-usage';
2
+ import noDeprecatedAPIs from './rules/no-deprecated-apis';
3
+ import noDeprecatedUsage from './rules/no-deprecated-design-token-usage';
2
4
  import noDeprecatedImports from './rules/no-deprecated-imports';
3
5
  import noUnsafeUsage from './rules/no-unsafe-design-token-usage';
6
+ import useVisuallyHidden from './rules/use-visually-hidden';
4
7
  export var rules = {
5
8
  'ensure-design-token-usage': ensureTokenUsage,
6
9
  'no-unsafe-design-token-usage': noUnsafeUsage,
7
- 'no-deprecated-imports': noDeprecatedImports
10
+ 'no-deprecated-design-token-usage': noDeprecatedUsage,
11
+ 'no-deprecated-imports': noDeprecatedImports,
12
+ 'no-deprecated-apis': noDeprecatedAPIs,
13
+ 'use-visually-hidden': useVisuallyHidden
8
14
  };
9
15
  export var configs = {
10
16
  recommended: {
11
17
  plugins: ['@atlaskit/design-system'],
12
18
  rules: {
13
- '@atlaskit/design-system/no-deprecated-imports': 'error'
19
+ '@atlaskit/design-system/no-deprecated-imports': 'error',
20
+ '@atlaskit/design-system/use-visually-hidden': 'error',
21
+ '@atlaskit/design-system/no-deprecated-apis': 'warn'
14
22
  }
15
23
  }
16
24
  };
@@ -33,6 +33,9 @@ var filterSuggestion = function filterSuggestion(_ref) {
33
33
 
34
34
  var rule = {
35
35
  meta: {
36
+ // We need to upgrade the version of ESLint.
37
+ // @ts-expect-error
38
+ hasSuggestions: true,
36
39
  docs: {
37
40
  recommended: true
38
41
  },