diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/textReplaceWithContent/text-replace-with-content.md b/packages/eslint-plugin-pf-codemods/src/rules/v6/textReplaceWithContent/text-replace-with-content.md new file mode 100644 index 000000000..36deeaa41 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/textReplaceWithContent/text-replace-with-content.md @@ -0,0 +1,18 @@ +### text-replace-with-content [(#10643)](https://github.com/patternfly/patternfly-react/pull/10643) + +We have replaced Text, TextContent, TextList and TextListItem with one Content component. Running this fix will change all of those components names to Content and add a "component" prop where necessary. + +#### Examples + +In: + +```jsx +%inputExample% +``` + +Out: + +```jsx +%outputExample% +``` + diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/textReplaceWithContent/text-replace-with-content.test.ts b/packages/eslint-plugin-pf-codemods/src/rules/v6/textReplaceWithContent/text-replace-with-content.test.ts new file mode 100644 index 000000000..33294b28b --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/textReplaceWithContent/text-replace-with-content.test.ts @@ -0,0 +1,121 @@ +const ruleTester = require('../../ruletester'); +import * as rule from './text-replace-with-content'; + +const errorMessage = `We have replaced Text, TextContent, TextList and TextListItem with one Content component. Running this fix will change all of those components names to Content and add a \`component\` prop where necessary.`; +const importDeclarationError = { + message: errorMessage, + type: 'ImportDeclaration', +}; +const jsxElementError = { + message: errorMessage, + type: 'JSXElement', +}; + +ruleTester.run('text-replace-with-content', rule, { + valid: [ + { + code: ``, + }, + { + code: `import { Text } from 'somewhere'; `, + }, + ], + invalid: [ + { + code: `import { Text } from '@patternfly/react-core'; Abc`, + output: `import { Content } from '@patternfly/react-core'; Abc`, + errors: [importDeclarationError, jsxElementError], + }, + { + code: `import { Text } from '@patternfly/react-core'; Abc`, + output: `import { Content } from '@patternfly/react-core'; Abc`, + errors: [importDeclarationError, jsxElementError], + }, + // { + // code: `import { Text, TextContent } from '@patternfly/react-core'; + // + // Abc + // `, + // output: `import { Content, TextContent } from '@patternfly/react-core'; + // + // Abc + // `, + // errors: [importDeclarationError, jsxElementError, jsxElementError], + // }, + { + code: `import { TextContent } from '@patternfly/react-core'; `, + output: `import { Content } from '@patternfly/react-core'; `, + errors: [importDeclarationError, jsxElementError], + }, + // { + // code: `import { TextList, TextListItem } from '@patternfly/react-core'; + // + // A + // B + // C + // `, + // output: `import { Content, TextListItem } from '@patternfly/react-core'; + // + // A + // B + // C + // `, + // errors: [ + // importDeclarationError, + // jsxElementError, + // jsxElementError, + // jsxElementError, + // jsxElementError, + // ], + // }, + // { + // code: `import { TextList, TextListItem } from '@patternfly/react-core'; + // + // A + // letter A + // B + // letter B + // `, + // output: `import { Content, TextListItem } from '@patternfly/react-core'; + // + // A + // letter A + // B + // letter B + // `, + // errors: [ + // importDeclarationError, + // jsxElementError, + // jsxElementError, + // jsxElementError, + // jsxElementError, + // jsxElementError, + // ], + // }, + { + code: `import { TextList } from '@patternfly/react-core'; `, + output: `import { Content } from '@patternfly/react-core'; `, + errors: [importDeclarationError, jsxElementError], + }, + // with alias + { + code: `import { Text as PFText } from '@patternfly/react-core'; Abc`, + output: `import { Content } from '@patternfly/react-core'; Abc`, + errors: [importDeclarationError, jsxElementError], + }, + ], +}); + +/* + + -> + -> + -> + -> + -> + -> + -> + -> + -> + +*/ diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/textReplaceWithContent/text-replace-with-content.ts b/packages/eslint-plugin-pf-codemods/src/rules/v6/textReplaceWithContent/text-replace-with-content.ts new file mode 100644 index 000000000..64137fa89 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/textReplaceWithContent/text-replace-with-content.ts @@ -0,0 +1,127 @@ +import { Rule } from 'eslint'; +import { + ImportDeclaration, + ImportSpecifier, + JSXElement, + JSXIdentifier, +} from 'estree-jsx'; +import { getAttribute, getFromPackage, pfPackageMatches } from '../../helpers'; + +// https://github.com/patternfly/patternfly-react/pull/10643 +module.exports = { + meta: { fixable: 'code' }, + create: function (context: Rule.RuleContext) { + const { imports } = getFromPackage(context, '@patternfly/react-core'); + + const textComponents = ['Text', 'TextContent', 'TextList', 'TextListItem']; + + const componentImports = imports.filter((specifier) => + textComponents.includes(specifier.imported.name) + ); + + const errorMessage = + 'We have replaced Text, TextContent, TextList and TextListItem with one Content component. Running this fix will change all of those components names to Content and add a `component` prop where necessary.'; + + return !componentImports.length + ? {} + : { + ImportDeclaration(node: ImportDeclaration) { + if (pfPackageMatches('@patternfly/react-core', node.source.value)) { + const specifierToReplace = node.specifiers.find( + (specifier) => + specifier.type === 'ImportSpecifier' && + textComponents.includes(specifier.imported.name) + ) as ImportSpecifier; + + if (!specifierToReplace) { + return; + } + + context.report({ + node, + message: errorMessage, + fix(fixer) { + return fixer.replaceText(specifierToReplace, 'Content'); + }, + }); + } + }, + JSXElement(node: JSXElement) { + const openingElement = node.openingElement; + const closingElement = node.closingElement; + + if (openingElement.name.type === 'JSXIdentifier') { + const componentImport = componentImports.find( + (imp) => + imp.local.name === (openingElement.name as JSXIdentifier).name + ); + + if (!componentImport) { + return; + } + + const componentName = componentImport.imported.name as + | 'Text' + | 'TextContent' + | 'TextList' + | 'TextListItem'; + + const componentAttribute = getAttribute(node, 'component'); + + context.report({ + node, + message: errorMessage, + fix(fixer) { + const fixes = []; + + if (!componentAttribute && componentName !== 'TextContent') { + const componentMap = { + Text: 'p', + TextList: 'ul', + TextListItem: 'li', + }; + + fixes.push( + fixer.insertTextAfter( + openingElement.name, + ` component="${componentMap[componentName]}"` + ) + ); + } + + if (componentName === 'TextContent') { + const isVisitedAttribute = getAttribute(node, 'isVisited'); + if (isVisitedAttribute) { + fixes.push( + fixer.replaceText( + isVisitedAttribute.name, + 'isVisitedLink' + ) + ); + } + } + + if (componentName === 'TextList') { + const isPlainAttribute = getAttribute(node, 'isPlain'); + if (isPlainAttribute) { + fixes.push( + fixer.replaceText(isPlainAttribute.name, 'isPlainList') + ); + } + } + + fixes.push(fixer.replaceText(openingElement.name, 'Content')); + if (closingElement) { + fixes.push( + fixer.replaceText(closingElement.name, 'Content') + ); + } + + return fixes; + }, + }); + } + }, + }; + }, +}; diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/textReplaceWithContent/textReplaceWithContentInput.tsx b/packages/eslint-plugin-pf-codemods/src/rules/v6/textReplaceWithContent/textReplaceWithContentInput.tsx new file mode 100644 index 000000000..4a71d68be --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/textReplaceWithContent/textReplaceWithContentInput.tsx @@ -0,0 +1,25 @@ +import { + Text, + TextContent, + TextList, + TextListItem, +} from "@patternfly/react-core"; + +export const TextReplaceWithContentInput = () => ( + <> + Abc + Abc + Abc + Abc + Abc + Abc + Abc + Abc + Abc + + A + B + C + + +); diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/textReplaceWithContent/textReplaceWithContentOutput.tsx b/packages/eslint-plugin-pf-codemods/src/rules/v6/textReplaceWithContent/textReplaceWithContentOutput.tsx new file mode 100644 index 000000000..aa099bb56 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/textReplaceWithContent/textReplaceWithContentOutput.tsx @@ -0,0 +1,25 @@ +import { + Content, + Content, + Content, + Content, +} from "@patternfly/react-core"; + +export const TextReplaceWithContentInput = () => ( + <> + Abc + Abc + Abc + Abc + Abc + Abc + Abc + Abc + Abc + + A + B + C + + +);