@atlaskit/eslint-plugin-design-system 10.16.0 → 10.17.1

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.
@@ -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 +1 @@
1
- export const upcomingIcons = ['app-switcher', 'check-circle-outline', 'export', 'file', 'flag-filled', 'lightbulb-filled', 'lock-filled', 'menu-expand', 'menu', 'notification-direct', 'notification', 'people-group', 'people', 'refresh', 'switcher', 'editor/align-image-center', 'editor/align-image-left', 'editor/align-image-right', 'editor/background-color', 'editor/bold', 'editor/indent', 'editor/italic', 'editor/horizontal-rule', 'editor-layout-three-equal', 'editor-layout-three-with-sidebars', 'editor/layout-two-equal', 'editor/layout-two-left-sidebars', 'editor/layout-two-right-sidebar', 'editor/media-center', 'editor/media-full-width', 'editor/media-wide', 'editor/number-list', 'emoji/activity', 'editor/file', 'emoji/flags', 'emoji/food', 'emoji/nature', 'emoji/people', 'emoji/productivity', 'emoji/travel', 'bitbucket/repos'];
1
+ export const upcomingIcons = [];