diff --git a/packages/eslint-plugin-pf-codemods/index.js b/packages/eslint-plugin-pf-codemods/index.js index 1d79586a3..b1f34dc20 100644 --- a/packages/eslint-plugin-pf-codemods/index.js +++ b/packages/eslint-plugin-pf-codemods/index.js @@ -11,7 +11,7 @@ const rules = { "select-rename-isExpanded": require('./lib/rules/select-rename-isExpanded'), "title-require-heading-level": require('./lib/rules/title-require-heading-level'), "title-size": require('./lib/rules/title-size'), - "wizard-text": require('./lib/rules/wizard-text'), + "wizard-rename-text": require('./lib/rules/wizard-rename-text'), "wizard-rename-hasBodyPadding": require('./lib/rules/wizard-rename-hasBodyPadding'), "wizard-remove-props": require('./lib/rules/wizard-remove-props'), "background-image-src-enum": require('./lib/rules/background-image-src-enum'), @@ -30,6 +30,7 @@ const rules = { "remove-isPseudo-props": require('./lib/rules/remove-isPseudo-props'), "label-remove-isCompact": require('./lib/rules/label-remove-isCompact'), "rename-noPadding": require('./lib/rules/rename-noPadding'), + "tab-title-text": require('./lib/rules/tab-title-text'), }; module.exports = { diff --git a/packages/eslint-plugin-pf-codemods/lib/helpers.js b/packages/eslint-plugin-pf-codemods/lib/helpers.js index c6f432e02..b3316d114 100644 --- a/packages/eslint-plugin-pf-codemods/lib/helpers.js +++ b/packages/eslint-plugin-pf-codemods/lib/helpers.js @@ -98,9 +98,22 @@ function renameComponent(componentMap, message) { } } +function addImport(context, fixer, package, currentImport, newImport) { + const specifierToAddTo = getPackageImports(context, package) + .filter(specifier => !currentImport || specifier.imported.name === currentImport) + .pop(); + + if (!specifierToAddTo) { + return []; + } + + return fixer.insertTextAfter(specifierToAddTo, `, ${newImport}`); +} + module.exports = { getPackageImports, renameProp, renameProps, - renameComponent + renameComponent, + addImport } \ No newline at end of file diff --git a/packages/eslint-plugin-pf-codemods/lib/rules/chipgroup-remove-chipgrouptoolbaritem.js b/packages/eslint-plugin-pf-codemods/lib/rules/chipgroup-remove-chipgrouptoolbaritem.js index 875e6ebea..81399fcd6 100644 --- a/packages/eslint-plugin-pf-codemods/lib/rules/chipgroup-remove-chipgrouptoolbaritem.js +++ b/packages/eslint-plugin-pf-codemods/lib/rules/chipgroup-remove-chipgrouptoolbaritem.js @@ -10,7 +10,7 @@ module.exports = { return !chipGroupImport ? {} : { JSXElement(node) { - if (chipGroupToolbarItemImport.local.name === node.openingElement.name.name) { + if (chipGroupToolbarItemImport && chipGroupToolbarItemImport.local.name === node.openingElement.name.name) { const hasSingleChipGroupParent = node.parent && node.parent.openingElement.name.name === chipGroupImport.local.name && node.parent.children.filter(child => child.type === 'JSXElement').length === 1; diff --git a/packages/eslint-plugin-pf-codemods/lib/rules/dropdown-rename-icon.js b/packages/eslint-plugin-pf-codemods/lib/rules/dropdown-rename-icon.js index 0db43b829..9f998747e 100644 --- a/packages/eslint-plugin-pf-codemods/lib/rules/dropdown-rename-icon.js +++ b/packages/eslint-plugin-pf-codemods/lib/rules/dropdown-rename-icon.js @@ -10,7 +10,7 @@ module.exports = { return !imports ? {} : { JSXElement(node) { - if (dropdownItemImport.local.name === node.openingElement.name.name) { + if (dropdownItemImport && dropdownItemImport.local.name === node.openingElement.name.name) { const dropdownIcons = node.children.filter(child => child.openingElement && child.openingElement.name.name === dropdownItemIconImport.local.name diff --git a/packages/eslint-plugin-pf-codemods/lib/rules/tab-title-text.js b/packages/eslint-plugin-pf-codemods/lib/rules/tab-title-text.js new file mode 100644 index 000000000..69e58b9d2 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/lib/rules/tab-title-text.js @@ -0,0 +1,56 @@ +const { getPackageImports, addImport } = require('../helpers'); + +// https://github.com/patternfly/patternfly-react/pull/4146 +module.exports = { + create: function(context) { + const tabImport = getPackageImports(context, '@patternfly/react-core') + .filter(specifier => specifier.imported.name === 'Tab'); + + return tabImport.length ? { + JSXOpeningElement(node) { + if (tabImport.map(imp => imp.local.name).includes(node.name.name)) { + const attribute = node.attributes.find(node => node.name.name === 'title'); + if (attribute) { + const { value } = attribute; + let replacement; + if (value.type === 'Literal') { + // i.e. title="Title" + replacement = fixer => [ + fixer.replaceText(attribute.value, `{${attribute.value.value}}`) + ]; + } else if (value.type === 'JSXExpressionContainer') { + if (value.expression.type === 'Literal') { + // i.e. title={'Title'} or title={100} + replacement = fixer => [ + fixer.replaceText(attribute.value, `{${attribute.value.expression.value}}`) + ]; + } else if (value.expression.type === 'Identifier') { + // i.e. title={myVariable} + replacement = fixer => [ + fixer.replaceText(attribute.value, `{{${attribute.value.expression.name}}}`) + ]; + } else if (value.expression.type === 'JSXElement' && value.expression.openingElement.name.name !== 'TabTitleText') { + // i.e. title={} + replacement = fixer => [ + fixer.insertTextBefore(attribute.value.expression, ``), + fixer.insertTextAfter(attribute.value.expression, ``) + ]; + } + } + if (replacement) { + context.report({ + node, + message: `title needs to be wrapped with the TabTitleText and/or TabTitleIcon component`, + fix(fixer) { + return replacement(fixer).concat( + addImport(context, fixer, '@patternfly/react-core', 'Tab', 'TabTitleText') + ); + } + }); + } + } + } + } + } : {}; + } +}; diff --git a/packages/eslint-plugin-pf-codemods/test/rules/tab-title-text.js b/packages/eslint-plugin-pf-codemods/test/rules/tab-title-text.js new file mode 100644 index 000000000..9199ddfe2 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/test/rules/tab-title-text.js @@ -0,0 +1,24 @@ +const ruleTester = require('./ruletester'); +const rule = require('../../lib/rules/tab-title-text'); + +ruleTester.run("tab-title-text", rule, { + valid: [ + { + code: `import { Tab, TabTitleText } from '@patternfly/react-core'; Title}>Content`, + }, + { + // No @patternfly/react-core import + code: `Content`, + } + ], + invalid: [ + { + code: `import { Tab } from '@patternfly/react-core'; Content`, + output: `import { Tab, TabTitleText } from '@patternfly/react-core'; Title}>Content`, + errors: [{ + message: `title needs to be wrapped with the TabTitleText and/or TabTitleIcon component`, + type: "JSXOpeningElement", + }] + }, + ] +}); diff --git a/test/test.tsx b/test/test.tsx index f2ccec006..5132a1bdf 100644 --- a/test/test.tsx +++ b/test/test.tsx @@ -24,3 +24,13 @@ export const MyButtonWithCustomEnum = (