@atlaskit/eslint-plugin-design-system 10.15.0 → 10.17.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 (31) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +1 -0
  3. package/dist/cjs/presets/all.codegen.js +2 -1
  4. package/dist/cjs/rules/ensure-icon-color/index.js +57 -0
  5. package/dist/cjs/rules/index.codegen.js +3 -1
  6. package/dist/cjs/rules/no-legacy-icons/checks.js +272 -84
  7. package/dist/cjs/rules/no-legacy-icons/helpers.js +214 -14
  8. package/dist/cjs/rules/no-legacy-icons/index.js +4 -0
  9. package/dist/es2019/presets/all.codegen.js +2 -1
  10. package/dist/es2019/rules/ensure-icon-color/index.js +50 -0
  11. package/dist/es2019/rules/index.codegen.js +3 -1
  12. package/dist/es2019/rules/no-legacy-icons/checks.js +190 -24
  13. package/dist/es2019/rules/no-legacy-icons/helpers.js +220 -6
  14. package/dist/es2019/rules/no-legacy-icons/index.js +4 -0
  15. package/dist/esm/presets/all.codegen.js +2 -1
  16. package/dist/esm/rules/ensure-icon-color/index.js +52 -0
  17. package/dist/esm/rules/index.codegen.js +3 -1
  18. package/dist/esm/rules/no-legacy-icons/checks.js +273 -85
  19. package/dist/esm/rules/no-legacy-icons/helpers.js +214 -14
  20. package/dist/esm/rules/no-legacy-icons/index.js +4 -0
  21. package/dist/types/index.codegen.d.ts +1 -0
  22. package/dist/types/presets/all.codegen.d.ts +2 -1
  23. package/dist/types/rules/ensure-icon-color/index.d.ts +3 -0
  24. package/dist/types/rules/index.codegen.d.ts +1 -0
  25. package/dist/types/rules/no-legacy-icons/helpers.d.ts +88 -3
  26. package/dist/types-ts4.5/index.codegen.d.ts +1 -0
  27. package/dist/types-ts4.5/presets/all.codegen.d.ts +2 -1
  28. package/dist/types-ts4.5/rules/ensure-icon-color/index.d.ts +3 -0
  29. package/dist/types-ts4.5/rules/index.codegen.d.ts +1 -0
  30. package/dist/types-ts4.5/rules/no-legacy-icons/helpers.d.ts +88 -3
  31. package/package.json +4 -4
@@ -1,5 +1,5 @@
1
1
  import { isNodeOfType } from 'eslint-codemod-utils';
2
- import { addToListOfRanges, canAutoMigrateNewIconBasedOnSize, canMigrateColor, createAutoMigrationError, createCantFindSuitableReplacementError, createCantMigrateColorError, createCantMigrateFunctionUnknownError, createCantMigrateIdentifierError, createCantMigrateIdentifierMapOrArrayError, createCantMigrateReExportError, createCantMigrateSizeUnknown, createCantMigrateSpreadPropsError, createGuidance, createHelpers, getMigrationMapObject, getUpcomingIcons, isInRangeList, isSize, locToString } from './helpers';
2
+ import { addToListOfRanges, canAutoMigrateNewIconBasedOnSize, canMigrateColor, checkIfNewIconExist, createAutoMigrationError, createCantFindSuitableReplacementError, createCantMigrateColorError, createCantMigrateFunctionUnknownError, createCantMigrateIdentifierError, createCantMigrateIdentifierMapOrArrayError, createCantMigrateReExportError, createCantMigrateSizeUnknown, createCantMigrateSpreadPropsError, createGuidance, createHelpers, createImportFix, createPropFixes, getMigrationMapObject, getUpcomingIcons, isInRangeList, isInsideLegacyButton, isInsideNewButton, isSize, locToString } from './helpers';
3
3
  export const createChecks = context => {
4
4
  //create global variables to be shared by the checks
5
5
  const {
@@ -7,15 +7,19 @@ export const createChecks = context => {
7
7
  getConfigFlag
8
8
  } = createHelpers(context);
9
9
  const legacyIconImports = {};
10
+ const migrationIconImports = {};
10
11
  const newButtonImports = new Set();
12
+ const legacyButtonImports = new Set();
11
13
  const errorsManual = {};
12
14
  const errorsAuto = {};
13
15
  let guidance = {};
16
+ let autoIconJSXElementOccurrenceCount = 0;
14
17
 
15
18
  // Extract parameters
16
19
  const shouldErrorForManualMigration = getConfigFlag('shouldErrorForManualMigration', true);
17
20
  const shouldErrorForAutoMigration = getConfigFlag('shouldErrorForAutoMigration', true);
18
21
  const isQuietMode = getConfigFlag('quiet', false);
22
+ const shouldUseMigrationPath = getConfigFlag('shouldUseMigrationPath', true);
19
23
 
20
24
  // Sorted list of ranges
21
25
  let errorRanges = [];
@@ -33,12 +37,26 @@ export const createChecks = context => {
33
37
  if (spec.local.name) {
34
38
  legacyIconImports[spec.local.name] = {
35
39
  packageName: moduleSource,
36
- exported: false
40
+ exported: false,
41
+ importNode: node
37
42
  };
38
43
  }
39
44
  }
40
45
  }
41
46
 
47
+ // Find the imports for icons that import from migration path
48
+ if (moduleSource && typeof moduleSource === 'string' && (moduleSource.startsWith('@atlaskit/icon/core/migration/') || moduleSource.startsWith('@atlaskit/icon/utility/migration/')) && node.specifiers.length) {
49
+ node.specifiers.forEach(spec => {
50
+ if (spec.local.name) {
51
+ migrationIconImports[spec.local.name] = {
52
+ packageName: moduleSource,
53
+ exported: false,
54
+ importNode: node
55
+ };
56
+ }
57
+ });
58
+ }
59
+
42
60
  // Find the imports for new button and IconButton
43
61
  if (typeof moduleSource === 'string' && moduleSource.startsWith('@atlaskit/button/new') && node.specifiers.length) {
44
62
  for (const spec of node.specifiers) {
@@ -49,6 +67,15 @@ export const createChecks = context => {
49
67
  }
50
68
  }
51
69
  }
70
+
71
+ // Find the imports for legacy default button
72
+ if (typeof moduleSource === 'string' && (moduleSource === '@atlaskit/button' || moduleSource === '@atlaskit/button/standard-button' || moduleSource === '@atlaskit/button/loading-button' || moduleSource === '@atlaskit/button/custom-theme-button') && node.specifiers.length) {
73
+ for (const spec of node.specifiers) {
74
+ if (spec.type === 'ImportDefaultSpecifier') {
75
+ legacyButtonImports.add(spec.local.name);
76
+ }
77
+ }
78
+ }
52
79
  };
53
80
 
54
81
  /**
@@ -91,7 +118,10 @@ export const createChecks = context => {
91
118
  }
92
119
  createCantMigrateReExportError(node, packageName, exportName, errorsManual);
93
120
  addToListOfRanges(node, errorRanges);
94
- guidance[locToString(node)] = createGuidance(packageName);
121
+ guidance[locToString(node)] = createGuidance({
122
+ iconPackage: packageName,
123
+ shouldUseMigrationPath
124
+ });
95
125
  };
96
126
 
97
127
  /**
@@ -106,7 +136,10 @@ export const createChecks = context => {
106
136
  for (const spec of node.specifiers) {
107
137
  createCantMigrateReExportError(spec, moduleSource, spec.exported.name, errorsManual);
108
138
  addToListOfRanges(spec, errorRanges);
109
- guidance[locToString(spec)] = createGuidance(moduleSource);
139
+ guidance[locToString(spec)] = createGuidance({
140
+ iconPackage: moduleSource,
141
+ shouldUseMigrationPath
142
+ });
110
143
  }
111
144
  }
112
145
  } else if (node.declaration && isNodeOfType(node.declaration, 'VariableDeclaration')) {
@@ -115,7 +148,10 @@ export const createChecks = context => {
115
148
  if (isNodeOfType(decl, 'VariableDeclarator') && Object.keys(decl).includes('init') && decl.init && isNodeOfType(decl.init, 'Identifier') && Object.keys(legacyIconImports).includes(decl.init.name)) {
116
149
  createCantMigrateReExportError(node, legacyIconImports[decl.init.name].packageName, decl.init.name, errorsManual);
117
150
  addToListOfRanges(node, errorRanges);
118
- guidance[locToString(node)] = createGuidance(legacyIconImports[decl.init.name].packageName);
151
+ guidance[locToString(node)] = createGuidance({
152
+ iconPackage: legacyIconImports[decl.init.name].packageName,
153
+ shouldUseMigrationPath
154
+ });
119
155
  }
120
156
  }
121
157
  } else if (!node.source && node.specifiers && node.specifiers.length > 0) {
@@ -134,7 +170,10 @@ export const createChecks = context => {
134
170
  };
135
171
  createCantMigrateReExportError(spec, legacyIconImports[spec.local.name].packageName, spec.exported.name, errorsManual);
136
172
  addToListOfRanges(spec, errorRanges);
137
- guidance[locToString(spec)] = createGuidance(legacyIconImports[spec.local.name].packageName);
173
+ guidance[locToString(spec)] = createGuidance({
174
+ iconPackage: legacyIconImports[spec.local.name].packageName,
175
+ shouldUseMigrationPath
176
+ });
138
177
  }
139
178
  }
140
179
  }
@@ -151,7 +190,10 @@ export const createChecks = context => {
151
190
  if (node.name && Object.keys(legacyIconImports).includes(node.name) && legacyIconImports[node.name].packageName) {
152
191
  createCantMigrateIdentifierMapOrArrayError(node, legacyIconImports[node.name].packageName, node.name, errorsManual);
153
192
  addToListOfRanges(node, errorRanges);
154
- guidance[locToString(node)] = createGuidance(legacyIconImports[node.name].packageName);
193
+ guidance[locToString(node)] = createGuidance({
194
+ iconPackage: legacyIconImports[node.name].packageName,
195
+ shouldUseMigrationPath
196
+ });
155
197
  }
156
198
  };
157
199
 
@@ -177,29 +219,59 @@ export const createChecks = context => {
177
219
  const isNewIconMigratable = canAutoMigrateNewIconBasedOnSize(upcomingIcon ? upcomingIcon.sizeGuidance.medium : migrationMapObject === null || migrationMapObject === void 0 ? void 0 : (_migrationMapObject$s = migrationMapObject.sizeGuidance) === null || _migrationMapObject$s === void 0 ? void 0 : _migrationMapObject$s.medium);
178
220
  const isInNewButton = isNodeOfType(node.parent.parent.parent.name, 'JSXIdentifier') && newButtonImports.has(node.parent.parent.parent.name.name);
179
221
  if (newIcon && isInNewButton && isNewIconMigratable || upcomingIcon && isInNewButton && isNewIconMigratable) {
180
- createAutoMigrationError(node, legacyIconImports[node.name].packageName, node.name, errorsAuto);
222
+ createAutoMigrationError({
223
+ node,
224
+ importSource: legacyIconImports[node.name].packageName,
225
+ iconName: node.name,
226
+ errors: errorsAuto
227
+ });
181
228
  addToListOfRanges(node, errorRanges);
182
- guidance[locToString(node)] = createGuidance(legacyIconImports[node.name].packageName, isInNewButton, 'medium');
229
+ guidance[locToString(node)] = createGuidance({
230
+ iconPackage: legacyIconImports[node.name].packageName,
231
+ insideNewButton: true,
232
+ size: 'medium',
233
+ shouldUseMigrationPath
234
+ });
183
235
  } else if (!newIcon && !upcomingIcon || !isNewIconMigratable) {
184
236
  createCantFindSuitableReplacementError(node, legacyIconImports[node.name].packageName, node.name, errorsManual);
185
237
  addToListOfRanges(node, errorRanges);
186
- guidance[locToString(node)] = createGuidance(legacyIconImports[node.name].packageName, isInNewButton);
238
+ guidance[locToString(node)] = createGuidance({
239
+ iconPackage: legacyIconImports[node.name].packageName,
240
+ insideNewButton: isInNewButton,
241
+ shouldUseMigrationPath
242
+ });
187
243
  } else if (!isInNewButton) {
188
244
  createCantMigrateFunctionUnknownError(node, legacyIconImports[node.name].packageName, node.name, errorsManual);
189
245
  addToListOfRanges(node, errorRanges);
190
- guidance[locToString(node)] = createGuidance(legacyIconImports[node.name].packageName, isInNewButton);
246
+ guidance[locToString(node)] = createGuidance({
247
+ iconPackage: legacyIconImports[node.name].packageName,
248
+ insideNewButton: false,
249
+ shouldUseMigrationPath
250
+ });
191
251
  }
192
252
  }
193
253
  };
194
254
  const checkIconReference = node => {
195
- //check the reference to see if it's a legacy icon, if not exit early
196
- if (!Object.keys(legacyIconImports).includes(node.name)) {
197
- return;
198
- }
199
255
  //if this is an import statement then exit early
200
256
  if (node.parent && (isNodeOfType(node.parent, 'ImportSpecifier') || isNodeOfType(node.parent, 'ImportDefaultSpecifier'))) {
201
257
  return;
202
258
  }
259
+
260
+ // Flag icons imported from migration path
261
+ if (!shouldUseMigrationPath && Object.keys(migrationIconImports).includes(node.name)) {
262
+ createAutoMigrationError({
263
+ node,
264
+ importSource: migrationIconImports[node.name].packageName,
265
+ iconName: node.name,
266
+ errors: errorsAuto
267
+ });
268
+ }
269
+
270
+ //check the reference to see if it's a legacy icon, if not exit early
271
+ if (!Object.keys(legacyIconImports).includes(node.name)) {
272
+ return;
273
+ }
274
+
203
275
  //if in Fallback prop, do not error
204
276
  if (node.parent && node.parent.parent && isNodeOfType(node.parent.parent, 'JSXAttribute') && isNodeOfType(node.parent.parent.name, 'JSXIdentifier') && node.parent.parent.name.name === 'LEGACY_fallbackIcon') {
205
277
  return;
@@ -223,15 +295,36 @@ export const createChecks = context => {
223
295
  return;
224
296
  }
225
297
  const name = node.openingElement.name.name;
298
+
299
+ // Flag icons imported from migration path
300
+ if (!shouldUseMigrationPath && Object.keys(migrationIconImports).includes(name)) {
301
+ var _sizeProp$value;
302
+ const sizeProp = node.openingElement.attributes.find(attribute => attribute.type === 'JSXAttribute' && (attribute.name.name === 'size' || attribute.name.name === 'LEGACY_size'));
303
+ const insideNewButton = isInsideNewButton(node, newButtonImports);
304
+ // Add spacious spacing if:
305
+ // 1. size is medium, or not set (default is medium)
306
+ // 2. not inside a new or legacy button
307
+ const shouldAddSpaciousSpacing = (sizeProp && sizeProp.type === 'JSXAttribute' && ((_sizeProp$value = sizeProp.value) === null || _sizeProp$value === void 0 ? void 0 : _sizeProp$value.type) === 'Literal' && sizeProp.value.value === 'medium' || !sizeProp) && !isInsideNewButton(node, newButtonImports) && !isInsideLegacyButton(node, legacyButtonImports);
308
+ createAutoMigrationError({
309
+ node,
310
+ importSource: migrationIconImports[name].packageName,
311
+ iconName: name,
312
+ errors: errorsAuto,
313
+ shouldAddSpaciousSpacing,
314
+ insideNewButton
315
+ });
316
+ }
317
+
226
318
  // Legacy icons rendered as JSX elements
227
319
  if (Object.keys(legacyIconImports).includes(name)) {
228
- var _node$parent2, _node$parent2$parent, _node$parent3, _node$parent3$parent, _node$parent3$parent$, _size, _size2;
320
+ var _size, _size2, _sizeProp$value2;
229
321
  // Determine if inside a new button - if so:
230
322
  // - Assume spread props are safe - still error if props explicitly set to unmigratable values
231
- let insideNewButton = false;
232
- if (node.parent && isNodeOfType(node.parent, 'ArrowFunctionExpression') && (_node$parent2 = node.parent) !== null && _node$parent2 !== void 0 && (_node$parent2$parent = _node$parent2.parent) !== null && _node$parent2$parent !== void 0 && _node$parent2$parent.parent && isNodeOfType(node.parent.parent.parent, 'JSXAttribute') && isNodeOfType(node.parent.parent.parent.name, 'JSXIdentifier') && (_node$parent3 = node.parent) !== null && _node$parent3 !== void 0 && (_node$parent3$parent = _node$parent3.parent) !== null && _node$parent3$parent !== void 0 && (_node$parent3$parent$ = _node$parent3$parent.parent) !== null && _node$parent3$parent$ !== void 0 && _node$parent3$parent$.parent && isNodeOfType(node.parent.parent.parent.parent, 'JSXOpeningElement') && isNodeOfType(node.parent.parent.parent.parent.name, 'JSXIdentifier') && newButtonImports.has(node.parent.parent.parent.parent.name.name)) {
233
- insideNewButton = true;
234
- }
323
+ const insideNewButton = isInsideNewButton(node, newButtonImports);
324
+
325
+ // Determine if inside a legacy default button - if so:
326
+ // the auto fixer will add spacing prop to the medium size icon
327
+ const insideLegacyButton = isInsideLegacyButton(node, legacyButtonImports);
235
328
 
236
329
  // Find size prop on node
237
330
  let size = 'medium';
@@ -299,13 +392,32 @@ export const createChecks = context => {
299
392
  const upcomingIcon = getUpcomingIcons(legacyIconImports[name].packageName);
300
393
  const newIcon = migrationMapObject === null || migrationMapObject === void 0 ? void 0 : migrationMapObject.newIcon;
301
394
  const isNewIconMigratable = canAutoMigrateNewIconBasedOnSize(upcomingIcon ? upcomingIcon.sizeGuidance[(_size = size) !== null && _size !== void 0 ? _size : 'medium'] : migrationMapObject === null || migrationMapObject === void 0 ? void 0 : migrationMapObject.sizeGuidance[(_size2 = size) !== null && _size2 !== void 0 ? _size2 : 'medium']);
395
+
396
+ // Add spacious spacing if:
397
+ // 1. size is medium, or not set (default is medium)
398
+ // 2. not inside a new or legacy button
399
+ const sizeProp = node.openingElement.attributes.find(attribute => attribute.type === 'JSXAttribute' && (attribute.name.name === 'size' || attribute.name.name === 'LEGACY_size'));
400
+ const shouldAddSpaciousSpacing = (sizeProp && sizeProp.type === 'JSXAttribute' && ((_sizeProp$value2 = sizeProp.value) === null || _sizeProp$value2 === void 0 ? void 0 : _sizeProp$value2.type) === 'Literal' && sizeProp.value.value === 'medium' || !sizeProp) && !insideNewButton && !insideLegacyButton;
302
401
  if (!hasManualMigration && (newIcon || upcomingIcon) && isNewIconMigratable) {
303
- createAutoMigrationError(node, legacyIconImports[name].packageName, name, errorsAuto);
402
+ autoIconJSXElementOccurrenceCount++;
403
+ createAutoMigrationError({
404
+ node,
405
+ importSource: legacyIconImports[name].packageName,
406
+ iconName: name,
407
+ errors: errorsAuto,
408
+ shouldAddSpaciousSpacing,
409
+ insideNewButton
410
+ });
304
411
  } else if ((!newIcon && !upcomingIcon || !isNewIconMigratable) && size) {
305
412
  createCantFindSuitableReplacementError(node, legacyIconImports[name].packageName, name, errorsManual, upcomingIcon ? true : migrationMapObject ? true : false);
306
413
  }
307
414
  addToListOfRanges(node, errorRanges);
308
- guidance[locToString(node)] = createGuidance(legacyIconImports[name].packageName, insideNewButton, size && isSize(size) ? size : undefined);
415
+ guidance[locToString(node)] = createGuidance({
416
+ iconPackage: legacyIconImports[name].packageName,
417
+ insideNewButton,
418
+ size: size && isSize(size) ? size : undefined,
419
+ shouldUseMigrationPath
420
+ });
309
421
  }
310
422
  };
311
423
 
@@ -319,7 +431,10 @@ export const createChecks = context => {
319
431
  if (isNodeOfType(arg, 'Identifier') && Object.keys(legacyIconImports).includes(arg.name) && legacyIconImports[arg.name].packageName) {
320
432
  createCantMigrateFunctionUnknownError(node, legacyIconImports[arg.name].packageName, arg.name, errorsManual);
321
433
  addToListOfRanges(node, errorRanges);
322
- guidance[locToString(node)] = createGuidance(legacyIconImports[arg.name].packageName);
434
+ guidance[locToString(node)] = createGuidance({
435
+ iconPackage: legacyIconImports[arg.name].packageName,
436
+ shouldUseMigrationPath
437
+ });
323
438
  }
324
439
  }
325
440
  }
@@ -377,7 +492,58 @@ export const createChecks = context => {
377
492
  if (Object.keys(error).includes('data') && error.data) {
378
493
  error.data.guidance = guidanceMessage;
379
494
  }
380
- context.report(error);
495
+ context.report({
496
+ ...error,
497
+ fix: fixer => {
498
+ var _legacyIconImports$er, _migrationIconImports;
499
+ // don't migration if the new icon is not available
500
+ if (!error.data || shouldUseMigrationPath && !checkIfNewIconExist(error)) {
501
+ return [];
502
+ }
503
+ const fixArguments = {
504
+ metadata: error.data,
505
+ legacyImportNode: (_legacyIconImports$er = legacyIconImports[error.data.iconName]) === null || _legacyIconImports$er === void 0 ? void 0 : _legacyIconImports$er.importNode,
506
+ migrationImportNode: (_migrationIconImports = migrationIconImports[error.data.iconName]) === null || _migrationIconImports === void 0 ? void 0 : _migrationIconImports.importNode,
507
+ shouldUseMigrationPath
508
+ };
509
+ const propsFixes = createPropFixes({
510
+ ...fixArguments,
511
+ node,
512
+ fixer
513
+ });
514
+ let importFixes = [];
515
+ // Otherwise if there are multiple occurrences of the icon, import path will be handled after the prop fix
516
+ if (autoIconJSXElementOccurrenceCount <= 1) {
517
+ importFixes = createImportFix({
518
+ ...fixArguments,
519
+ fixer
520
+ });
521
+ }
522
+ return [...propsFixes, ...importFixes];
523
+ }
524
+ });
525
+ }
526
+ }
527
+
528
+ // Update import path at the end if there are multiple occurrences of the icon
529
+ if (autoIconJSXElementOccurrenceCount > 1) {
530
+ for (const [_, error] of Object.entries(errorsAuto)) {
531
+ context.report({
532
+ ...error,
533
+ fix: fixer => {
534
+ var _legacyIconImports$er2, _migrationIconImports2;
535
+ if (!error.data || shouldUseMigrationPath && !checkIfNewIconExist(error)) {
536
+ return [];
537
+ }
538
+ return createImportFix({
539
+ metadata: error.data,
540
+ fixer,
541
+ legacyImportNode: (_legacyIconImports$er2 = legacyIconImports[error.data.iconName]) === null || _legacyIconImports$er2 === void 0 ? void 0 : _legacyIconImports$er2.importNode,
542
+ migrationImportNode: (_migrationIconImports2 = migrationIconImports[error.data.iconName]) === null || _migrationIconImports2 === void 0 ? void 0 : _migrationIconImports2.importNode,
543
+ shouldUseMigrationPath
544
+ });
545
+ }
546
+ });
381
547
  }
382
548
  }
383
549
  }
@@ -1,4 +1,4 @@
1
- import { isNodeOfType } from 'eslint-codemod-utils';
1
+ import { isNodeOfType, literal } from 'eslint-codemod-utils';
2
2
  import baseMigrationMap, { migrationOutcomeDescriptionMap } from '@atlaskit/icon/UNSAFE_migration-map';
3
3
  import { getImportName } from '../utils/get-import-name';
4
4
  import { upcomingIcons } from './upcoming-icons';
@@ -50,10 +50,36 @@ export const canAutoMigrateNewIconBasedOnSize = guidance => {
50
50
  return ['swap', 'swap-slight-visual-change', 'swap-visual-change', 'swap-size-shift-utility'].includes(guidance || '');
51
51
  };
52
52
 
53
+ /**
54
+ *
55
+ * @param iconPackage string
56
+ * @returns object of new icon name and import path
57
+ */
58
+ export const getNewIconNameAndImportPath = (iconPackage, shouldUseMigrationPath) => {
59
+ const legacyIconName = getIconKey(iconPackage);
60
+ const migrationMapObject = getMigrationMapObject(iconPackage);
61
+ if (!migrationMapObject || !migrationMapObject.newIcon) {
62
+ return {};
63
+ }
64
+ const {
65
+ newIcon
66
+ } = migrationMapObject;
67
+ const migrationPath = newIcon.name === legacyIconName ? `${newIcon.package}/${newIcon.type}/migration/${newIcon.name}` : `${newIcon.package}/${newIcon.type}/migration/${newIcon.name}--${legacyIconName.replaceAll('/', '-')}`;
68
+ return {
69
+ iconName: newIcon.name,
70
+ importPath: shouldUseMigrationPath ? migrationPath : `${newIcon.package}/${newIcon.type}/${newIcon.name}`
71
+ };
72
+ };
73
+
53
74
  /**
54
75
  * Creates the written guidance for migrating a legacy icon to a new icon
55
76
  */
56
- export const createGuidance = (iconPackage, insideNewButton = false, size) => {
77
+ export const createGuidance = ({
78
+ iconPackage,
79
+ insideNewButton,
80
+ size,
81
+ shouldUseMigrationPath
82
+ }) => {
57
83
  const migrationMapObject = getMigrationMapObject(iconPackage);
58
84
  const upcomingIcon = getUpcomingIcons(iconPackage);
59
85
  if (upcomingIcon) {
@@ -82,17 +108,21 @@ export const createGuidance = (iconPackage, insideNewButton = false, size) => {
82
108
  if (!newIcon) {
83
109
  return 'No equivalent icon in new set. An option is to contribute a custom icon into icon-labs package instead.\n';
84
110
  }
111
+ const {
112
+ iconName,
113
+ importPath
114
+ } = getNewIconNameAndImportPath(iconPackage, shouldUseMigrationPath);
85
115
  const buttonGuidanceStr = "Please set 'spacing' property of the new icon to 'none', to ensure appropriate spacing inside `@atlaskit/button`.\n";
86
116
  let guidance = '';
87
117
  if (size) {
88
118
  if (migrationMapObject.sizeGuidance[size] && canAutoMigrateNewIconBasedOnSize(migrationMapObject.sizeGuidance[size])) {
89
- guidance += `Fix: Use ${newIcon.name} from ${newIcon.package}/${newIcon.type}/${newIcon.name} instead.`;
119
+ guidance += `Fix: Use ${iconName} from ${importPath} instead.`;
90
120
  } else {
91
121
  guidance += `No equivalent icon for this size, ${size}, in new set.`;
92
122
  }
93
123
  guidance += `${Object.keys(migrationOutcomeDescriptionMap).includes(migrationMapObject.sizeGuidance[size]) ? ` Please: ${migrationOutcomeDescriptionMap[migrationMapObject.sizeGuidance[size]]}` : ' No migration size advice given.'}\n`;
94
124
  } else {
95
- guidance = `Use ${newIcon.name} from ${newIcon.package}/${newIcon.type}/${newIcon.name} instead.\nMigration suggestions, depending on the legacy icon size:\n`;
125
+ guidance = `Use ${iconName} from ${importPath} instead.\nMigration suggestions, depending on the legacy icon size:\n`;
96
126
  Object.entries(migrationMapObject.sizeGuidance).forEach(([size, value]) => {
97
127
  guidance += `\t- ${size}: `;
98
128
  if (!Object.keys(migrationOutcomeDescriptionMap).includes(value)) {
@@ -223,13 +253,23 @@ export const createCantMigrateSizeUnknown = (node, errors, importSource, iconNam
223
253
  };
224
254
  pushManualError(locToString(node), errors, myError, importSource, iconName);
225
255
  };
226
- export const createAutoMigrationError = (node, importSource, iconName, errors) => {
256
+ export const createAutoMigrationError = ({
257
+ node,
258
+ importSource,
259
+ iconName,
260
+ errors,
261
+ shouldAddSpaciousSpacing,
262
+ insideNewButton
263
+ }) => {
227
264
  const myError = {
228
265
  node,
229
266
  messageId: 'noLegacyIconsAutoMigration',
230
267
  data: {
231
268
  importSource,
232
- iconName
269
+ iconName,
270
+ spacing: shouldAddSpaciousSpacing ? 'spacious' : '',
271
+ // value type need to be a string in Rule.ReportDescriptor
272
+ insideNewButton: String(insideNewButton)
233
273
  }
234
274
  };
235
275
  errors[locToString(node)] = myError;
@@ -320,4 +360,178 @@ export const isInRangeList = (node, sortedListOfRangesForErrors) => {
320
360
  }
321
361
  const found = sortedListOfRangesForErrors.find(currRange => range[0] >= currRange.start && range[1] <= currRange.end);
322
362
  return !!found;
363
+ };
364
+
365
+ /**
366
+ *
367
+ * @param node Icon JSXelement
368
+ * @param newButtonImports list of new button import specifiers
369
+ * @returns if Icon is inside a new button
370
+ */
371
+ export const isInsideNewButton = (node, newButtonImports) => {
372
+ var _node$parent, _node$parent$parent, _node$parent2, _node$parent2$parent, _node$parent2$parent$;
373
+ let insideNewButton = false;
374
+ if (node.parent && isNodeOfType(node.parent, 'ArrowFunctionExpression') && (_node$parent = node.parent) !== null && _node$parent !== void 0 && (_node$parent$parent = _node$parent.parent) !== null && _node$parent$parent !== void 0 && _node$parent$parent.parent && isNodeOfType(node.parent.parent.parent, 'JSXAttribute') && isNodeOfType(node.parent.parent.parent.name, 'JSXIdentifier') && (_node$parent2 = node.parent) !== null && _node$parent2 !== void 0 && (_node$parent2$parent = _node$parent2.parent) !== null && _node$parent2$parent !== void 0 && (_node$parent2$parent$ = _node$parent2$parent.parent) !== null && _node$parent2$parent$ !== void 0 && _node$parent2$parent$.parent && isNodeOfType(node.parent.parent.parent.parent, 'JSXOpeningElement') && isNodeOfType(node.parent.parent.parent.parent.name, 'JSXIdentifier') && newButtonImports.has(node.parent.parent.parent.parent.name.name)) {
375
+ insideNewButton = true;
376
+ }
377
+ return insideNewButton;
378
+ };
379
+
380
+ /**
381
+ *
382
+ * @param node Icon JSXelement
383
+ * @param newButtonImports list of legacy button import specifiers
384
+ * @returns if Icon is inside a legacy button
385
+ */
386
+ export const isInsideLegacyButton = (node, legacyButtonImports) => {
387
+ var _node$parent3, _node$parent4, _node$parent4$parent, _node$parent5, _node$parent5$parent, _node$parent6, _node$parent6$parent;
388
+ let insideLegacyButton = false;
389
+ if (node.parent && isNodeOfType(node.parent, 'JSXExpressionContainer') && (_node$parent3 = node.parent) !== null && _node$parent3 !== void 0 && _node$parent3.parent && isNodeOfType(node.parent.parent, 'JSXAttribute') && (node.parent.parent.name.name === 'iconBefore' || node.parent.parent.name.name === 'iconAfter') && isNodeOfType((_node$parent4 = node.parent) === null || _node$parent4 === void 0 ? void 0 : (_node$parent4$parent = _node$parent4.parent) === null || _node$parent4$parent === void 0 ? void 0 : _node$parent4$parent.parent, 'JSXOpeningElement') && isNodeOfType((_node$parent5 = node.parent) === null || _node$parent5 === void 0 ? void 0 : (_node$parent5$parent = _node$parent5.parent) === null || _node$parent5$parent === void 0 ? void 0 : _node$parent5$parent.parent.name, 'JSXIdentifier') && legacyButtonImports.has((_node$parent6 = node.parent) === null || _node$parent6 === void 0 ? void 0 : (_node$parent6$parent = _node$parent6.parent) === null || _node$parent6$parent === void 0 ? void 0 : _node$parent6$parent.parent.name.name)) {
390
+ insideLegacyButton = true;
391
+ }
392
+ return insideLegacyButton;
393
+ };
394
+ const findProp = (attributes, propName) => attributes.find(attr => attr.type === 'JSXAttribute' && attr.name.name === propName);
395
+
396
+ /**
397
+ *
398
+ * Creates a list of fixers to update the icon import path
399
+ * @param metadata Metadata including the import source and spacing
400
+ * @param fixer The original fix function
401
+ * @param legacyImportNode The import declaration node to replace
402
+ * @param shouldUseMigrationPath The eslint rule config, whether to use migration entrypoint or not
403
+ * @param migrationImportNode The migration import declaration node to replace, only present if shouldUseMigrationPath is false
404
+ * @returns A list of fixers to migrate the icon
405
+ */
406
+ export const createImportFix = ({
407
+ fixer,
408
+ legacyImportNode,
409
+ metadata,
410
+ shouldUseMigrationPath,
411
+ migrationImportNode
412
+ }) => {
413
+ const fixes = [];
414
+ const {
415
+ importSource
416
+ } = metadata;
417
+ const importPath = migrationImportNode ? importSource.replace('/migration', '').split('--')[0] : getNewIconNameAndImportPath(importSource, shouldUseMigrationPath).importPath;
418
+
419
+ // replace old icon import with icon import
420
+ if (legacyImportNode && importPath) {
421
+ fixes.push(fixer.replaceText(legacyImportNode.source, `'${literal(importPath)}'`));
422
+ }
423
+ if (migrationImportNode && !shouldUseMigrationPath && importPath) {
424
+ fixes.push(fixer.replaceText(migrationImportNode.source, `'${literal(importPath)}'`));
425
+ }
426
+ return fixes;
427
+ };
428
+
429
+ /**
430
+ * Creates a list of fixers to update the icon props
431
+ * @param node The Icon element to migrate
432
+ * @param metadata Metadata including the import source and spacing
433
+ * @param fixer The original fix function
434
+ * @param legacyImportNode The import declaration node to replace
435
+ * @param shouldUseMigrationPath The eslint rule config, whether to use migration entrypoint or not
436
+ * @param migrationImportNode The migration import declaration node to replace, only present if shouldUseMigrationPath is false
437
+ * @returns A list of fixers to migrate the icon
438
+ */
439
+ export const createPropFixes = ({
440
+ node,
441
+ fixer,
442
+ legacyImportNode,
443
+ metadata,
444
+ shouldUseMigrationPath,
445
+ migrationImportNode
446
+ }) => {
447
+ const fixes = [];
448
+ const {
449
+ importSource,
450
+ spacing,
451
+ insideNewButton
452
+ } = metadata;
453
+ if (shouldUseMigrationPath && !legacyImportNode) {
454
+ return fixes;
455
+ }
456
+ const importPath = migrationImportNode ? importSource.replace('/migration', '').split('--')[0] : getNewIconNameAndImportPath(importSource, shouldUseMigrationPath).importPath;
457
+ const iconType = importPath !== null && importPath !== void 0 && importPath.startsWith('@atlaskit/icon/core') ? 'core' : 'utility';
458
+ if (node.type === 'JSXElement') {
459
+ const {
460
+ openingElement
461
+ } = node;
462
+ const {
463
+ attributes
464
+ } = openingElement;
465
+
466
+ // replace primaryColor prop with color
467
+ const primaryColor = findProp(attributes, 'primaryColor');
468
+ if (primaryColor && primaryColor.type === 'JSXAttribute') {
469
+ fixes.push(fixer.replaceText(primaryColor.name, 'color'));
470
+ }
471
+
472
+ // add color="currentColor" if
473
+ // 1. primaryColor prop is not set
474
+ // 2. icon is not imported from migration entrypoint
475
+ // 3. icon element is not inside a new button
476
+ if (legacyImportNode && !primaryColor && !migrationImportNode &&
477
+ // value type need to be a string in Rule.ReportDescriptor
478
+ insideNewButton !== 'true') {
479
+ fixes.push(fixer.insertTextAfter(openingElement.name, ` color="currentColor"`));
480
+ }
481
+
482
+ // rename or remove size prop based on shouldUseMigrationPath,
483
+ // add spacing="spacious" if
484
+ // 1. it's in error metadata, which means size is medium
485
+ // 2. no existing spacing prop
486
+ // 3. iconType is "core"
487
+ // 4. icon is not imported from migration entrypoint
488
+ const sizeProp = findProp(attributes, 'size');
489
+ const spacingProp = findProp(attributes, 'spacing');
490
+ if (spacing && !spacingProp && iconType === 'core' && !migrationImportNode) {
491
+ fixes.push(fixer.insertTextAfter(sizeProp || openingElement.name, ` spacing="${spacing}"`));
492
+ }
493
+ if (sizeProp && sizeProp.type === 'JSXAttribute') {
494
+ fixes.push(shouldUseMigrationPath ?
495
+ // replace size prop with LEGACY_size,
496
+ fixer.replaceText(sizeProp.name, 'LEGACY_size') :
497
+ // remove size prop if shouldUseMigrationPath is false
498
+ fixer.remove(sizeProp));
499
+ }
500
+
501
+ // rename or remove secondaryColor prop based on shouldUseMigrationPath
502
+ const secondaryColorProp = findProp(attributes, 'secondaryColor');
503
+ if (secondaryColorProp && secondaryColorProp.type === 'JSXAttribute') {
504
+ fixes.push(shouldUseMigrationPath ?
505
+ // replace secondaryColor prop with LEGACY_secondaryColor
506
+ fixer.replaceText(secondaryColorProp.name, 'LEGACY_secondaryColor') :
507
+ // remove secondaryColor prop if shouldUseMigrationPath is false
508
+ fixer.remove(secondaryColorProp));
509
+ }
510
+
511
+ // remove LEGACY props
512
+ if (!shouldUseMigrationPath) {
513
+ ['LEGACY_size', 'LEGACY_margin', 'LEGACY_fallbackIcon', 'LEGACY_secondaryColor'].forEach(propName => {
514
+ const legacyProp = findProp(attributes, propName);
515
+ if (legacyProp && legacyProp.type === 'JSXAttribute') {
516
+ fixes.push(fixer.remove(legacyProp));
517
+ }
518
+ });
519
+ }
520
+ }
521
+ return fixes;
522
+ };
523
+
524
+ /**
525
+ * Check if the new icon exists in the migration map
526
+ */
527
+ export const checkIfNewIconExist = error => {
528
+ var _error$data;
529
+ if (!((_error$data = error.data) !== null && _error$data !== void 0 && _error$data.importSource)) {
530
+ return false;
531
+ }
532
+ const iconKey = getIconKey(error.data.importSource);
533
+ const {
534
+ newIcon
535
+ } = baseMigrationMap[iconKey] || {};
536
+ return Boolean(newIcon);
323
537
  };
@@ -5,6 +5,7 @@ import { createHelpers } from './helpers';
5
5
  const rule = createLintRule({
6
6
  meta: {
7
7
  name: 'no-legacy-icons',
8
+ fixable: 'code',
8
9
  type: 'problem',
9
10
  docs: {
10
11
  description: 'Enforces no legacy icons are used.',
@@ -22,6 +23,9 @@ const rule = createLintRule({
22
23
  },
23
24
  quiet: {
24
25
  type: 'boolean'
26
+ },
27
+ shouldUseMigrationPath: {
28
+ type: 'boolean'
25
29
  }
26
30
  },
27
31
  additionalProperties: false
@@ -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::b6947ac630ea512fe3f4e3f44abb6783>>
3
+ * @codegen <<SignedSource::16613cb3962ed68af0bffd70eef5f5e2>>
4
4
  * @codegenCommand yarn workspace @atlaskit/eslint-plugin-design-system codegen
5
5
  */
6
6
  export default {
@@ -9,6 +9,7 @@ export default {
9
9
  '@atlaskit/design-system/consistent-css-prop-usage': 'error',
10
10
  '@atlaskit/design-system/ensure-design-token-usage': 'error',
11
11
  '@atlaskit/design-system/ensure-design-token-usage/preview': 'warn',
12
+ '@atlaskit/design-system/ensure-icon-color': 'error',
12
13
  '@atlaskit/design-system/icon-label': 'warn',
13
14
  '@atlaskit/design-system/no-banned-imports': 'error',
14
15
  '@atlaskit/design-system/no-css-tagged-template-expression': 'error',