diff --git a/packages/eslint-plugin-pf-codemods/src/rules/helpers/getImportedName.ts b/packages/eslint-plugin-pf-codemods/src/rules/helpers/getImportedName.ts new file mode 100644 index 000000000..7ebadddc4 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/getImportedName.ts @@ -0,0 +1,17 @@ +import { ImportSpecifier, JSXOpeningElement } from "estree-jsx"; + +/** Resolves the imported name of a node, even if that node has an aliased local name */ +export function getImportedName( + namedImports: ImportSpecifier[], + node: JSXOpeningElement +) { + if (node.name.type !== "JSXIdentifier") { + return; + } + + const nodeName = node.name.name; + + const nodeImport = namedImports.find((imp) => imp.local.name === nodeName); + + return nodeImport?.imported.name; +} diff --git a/packages/eslint-plugin-pf-codemods/src/rules/helpers/getLocalComponentName.ts b/packages/eslint-plugin-pf-codemods/src/rules/helpers/getLocalComponentName.ts new file mode 100644 index 000000000..7fd25eac9 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/getLocalComponentName.ts @@ -0,0 +1,20 @@ +import { ImportSpecifier } from "estree-jsx"; + +/** Resolves the local name of an import */ +export function getLocalComponentName( + namedImports: ImportSpecifier[], + importedName: string +) { + const componentImport = namedImports.find( + (name) => name.imported.name === importedName + ); + + const isAlias = + componentImport?.imported.name !== componentImport?.local.name; + + if (componentImport && isAlias) { + return componentImport.local.name; + } + + return importedName; +} diff --git a/packages/eslint-plugin-pf-codemods/src/rules/helpers/index.ts b/packages/eslint-plugin-pf-codemods/src/rules/helpers/index.ts index b232d90d7..5e5d6407b 100644 --- a/packages/eslint-plugin-pf-codemods/src/rules/helpers/index.ts +++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/index.ts @@ -7,6 +7,8 @@ export * from "./getComponentImportName"; export * from "./getDefaultDeclarationString"; export * from "./getEndRange"; export * from "./getFromPackage"; +export * from "./getImportedName"; +export * from "./getLocalComponentName"; export * from "./getNodeName"; export * from "./getText"; export * from "./hasCodemodDataTag"; diff --git a/packages/eslint-plugin-pf-codemods/src/rules/helpers/interfaces.ts b/packages/eslint-plugin-pf-codemods/src/rules/helpers/interfaces.ts index 3cf2caedf..4f25975f3 100644 --- a/packages/eslint-plugin-pf-codemods/src/rules/helpers/interfaces.ts +++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/interfaces.ts @@ -20,6 +20,10 @@ export interface ImportDefaultSpecifierWithParent parent?: ImportDeclaration; } -export interface JSXOpeningElementWithParent extends JSXOpeningElement { +export interface JSXElementWithParent extends JSXElement { parent?: JSXElement; } + +export interface JSXOpeningElementWithParent extends JSXOpeningElement { + parent?: JSXElementWithParent; +} diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/mastheadStructureChanges/masthead-structure-changes.md b/packages/eslint-plugin-pf-codemods/src/rules/v6/mastheadStructureChanges/masthead-structure-changes.md new file mode 100644 index 000000000..82f14b8dc --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/mastheadStructureChanges/masthead-structure-changes.md @@ -0,0 +1,18 @@ +### masthead-structure-changes [(#10809)](https://github.com/patternfly/patternfly-react/pull/10809) + +The structure of Masthead has been updated, MastheadToggle and MastheadBrand should now be wrapped in MastheadMain. + +#### Examples + +In: + +```jsx +%inputExample% +``` + +Out: + +```jsx +%outputExample% +``` + diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/mastheadStructureChanges/masthead-structure-changes.test.ts b/packages/eslint-plugin-pf-codemods/src/rules/v6/mastheadStructureChanges/masthead-structure-changes.test.ts new file mode 100644 index 000000000..57dbbeae6 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/mastheadStructureChanges/masthead-structure-changes.test.ts @@ -0,0 +1,175 @@ +const ruleTester = require("../../ruletester"); +import * as rule from "./masthead-structure-changes"; + +ruleTester.run("masthead-structure-changes", rule, { + valid: [ + // no pf import and has NOT had MastheadBrand renamed to MastheadLogo by the masthead-name-changes codemod + { + code: `FooBar`, + }, + // no pf import and has had MastheadBrand renamed to MastheadLogo by the masthead-name-changes codemod + { + code: `FooBar`, + }, + //toggle already wrapped in MastheadMain and brand double wrapped with data-codemods on the top level, has NOT had MastheadBrand renamed to MastheadLogo by the masthead-name-changes codemod + { + code: `import { Masthead, MastheadBrand, MastheadMain, MastheadToggle } from '@patternfly/react-core'; FooBar`, + }, + // toggle already wrapped in MastheadMain and logo wrapped in a brand with data-codemods, has had MastheadBrand renamed to MastheadLogo by the masthead-name-changes codemod + { + code: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core'; FooBar`, + }, + ], + invalid: [ + // stage one of a file that has NOT had MastheadBrand renamed to MastheadLogo by the masthead-name-changes codemod + { + code: `import { Masthead, MastheadBrand, MastheadMain, MastheadToggle } from '@patternfly/react-core'; FooBar`, + output: `import { Masthead, MastheadBrand, MastheadMain, MastheadToggle } from '@patternfly/react-core'; FooBar`, + errors: [ + { + message: `The structure of Masthead has been updated, MastheadToggle should now be wrapped in MastheadMain.`, + type: "JSXOpeningElement", + }, + { + message: `The structure of Masthead has been updated, the PF5 MastheadBrand has been renamed to MastheadLogo (this renaming is handled by our masthead-name-changes codemod) and should now be wrapped in a new MastheadBrand.`, + type: "JSXOpeningElement", + }, + ], + }, + // stage two of a file that has NOT had MastheadBrand renamed to MastheadLogo by the masthead-name-changes codemod + { + code: `import { Masthead, MastheadBrand, MastheadMain, MastheadToggle } from '@patternfly/react-core'; FooBar`, + output: `import { Masthead, MastheadBrand, MastheadMain, MastheadToggle } from '@patternfly/react-core'; FooBar`, + errors: [ + { + message: `The structure of Masthead has been updated, the PF5 MastheadBrand has been renamed to MastheadLogo (this renaming is handled by our masthead-name-changes codemod) and should now be wrapped in a new MastheadBrand.`, + type: "JSXOpeningElement", + }, + ], + }, + // stage one of a file that has had MastheadBrand renamed to MastheadLogo by the masthead-name-changes codemod + { + code: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle } from '@patternfly/react-core'; FooBar`, + output: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core'; FooBar`, + errors: [ + { + message: `The structure of Masthead has been updated, MastheadToggle should now be wrapped in MastheadMain.`, + type: "JSXOpeningElement", + }, + { + message: `The structure of Masthead has been updated, MastheadLogo should now be wrapped in MastheadBrand.`, + type: "JSXOpeningElement", + }, + ], + }, + // stage two of a file that has had MastheadBrand renamed to MastheadLogo by the masthead-name-changes codemod + { + code: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core'; FooBar`, + output: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core'; FooBar`, + errors: [ + { + message: `The structure of Masthead has been updated, MastheadToggle should now be wrapped in MastheadMain.`, + type: "JSXOpeningElement", + }, + ], + }, + // with aliases + { + code: `import { Masthead as MH, MastheadBrand as MB, MastheadMain as MM, MastheadToggle as MT } from '@patternfly/react-core'; FooBar`, + output: `import { Masthead as MH, MastheadBrand as MB, MastheadMain as MM, MastheadToggle as MT } from '@patternfly/react-core'; FooBar`, + errors: [ + { + message: `The structure of Masthead has been updated, MastheadToggle should now be wrapped in MastheadMain.`, + type: "JSXOpeningElement", + }, + { + message: `The structure of Masthead has been updated, the PF5 MastheadBrand has been renamed to MastheadLogo (this renaming is handled by our masthead-name-changes codemod) and should now be wrapped in a new MastheadBrand.`, + type: "JSXOpeningElement", + }, + ], + }, + { + code: `import { Masthead as MH, MastheadBrand as MB, MastheadMain as MM, MastheadToggle as MT } from '@patternfly/react-core'; FooBar`, + output: `import { Masthead as MH, MastheadBrand as MB, MastheadMain as MM, MastheadToggle as MT } from '@patternfly/react-core'; FooBar`, + errors: [ + { + message: `The structure of Masthead has been updated, the PF5 MastheadBrand has been renamed to MastheadLogo (this renaming is handled by our masthead-name-changes codemod) and should now be wrapped in a new MastheadBrand.`, + type: "JSXOpeningElement", + }, + ], + }, + // with dist imports + { + code: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle } from '@patternfly/react-core/dist/esm/components/Masthead'; FooBar`, + output: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core/dist/esm/components/Masthead'; FooBar`, + errors: [ + { + message: `The structure of Masthead has been updated, MastheadToggle should now be wrapped in MastheadMain.`, + type: "JSXOpeningElement", + }, + { + message: `The structure of Masthead has been updated, MastheadLogo should now be wrapped in MastheadBrand.`, + type: "JSXOpeningElement", + }, + ], + }, + { + code: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core/dist/esm/components/Masthead'; FooBar`, + output: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core/dist/esm/components/Masthead'; FooBar`, + errors: [ + { + message: `The structure of Masthead has been updated, MastheadToggle should now be wrapped in MastheadMain.`, + type: "JSXOpeningElement", + }, + ], + }, + { + code: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle } from '@patternfly/react-core/dist/js/components/Masthead'; FooBar`, + output: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core/dist/js/components/Masthead'; FooBar`, + errors: [ + { + message: `The structure of Masthead has been updated, MastheadToggle should now be wrapped in MastheadMain.`, + type: "JSXOpeningElement", + }, + { + message: `The structure of Masthead has been updated, MastheadLogo should now be wrapped in MastheadBrand.`, + type: "JSXOpeningElement", + }, + ], + }, + { + code: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core/dist/js/components/Masthead'; FooBar`, + output: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core/dist/js/components/Masthead'; FooBar`, + errors: [ + { + message: `The structure of Masthead has been updated, MastheadToggle should now be wrapped in MastheadMain.`, + type: "JSXOpeningElement", + }, + ], + }, + { + code: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle } from '@patternfly/react-core/dist/dynamic/components/Masthead'; FooBar`, + output: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core/dist/dynamic/components/Masthead'; FooBar`, + errors: [ + { + message: `The structure of Masthead has been updated, MastheadToggle should now be wrapped in MastheadMain.`, + type: "JSXOpeningElement", + }, + { + message: `The structure of Masthead has been updated, MastheadLogo should now be wrapped in MastheadBrand.`, + type: "JSXOpeningElement", + }, + ], + }, + { + code: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core/dist/dynamic/components/Masthead'; FooBar`, + output: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core/dist/dynamic/components/Masthead'; FooBar`, + errors: [ + { + message: `The structure of Masthead has been updated, MastheadToggle should now be wrapped in MastheadMain.`, + type: "JSXOpeningElement", + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/mastheadStructureChanges/masthead-structure-changes.ts b/packages/eslint-plugin-pf-codemods/src/rules/v6/mastheadStructureChanges/masthead-structure-changes.ts new file mode 100644 index 000000000..95f47fe5f --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/mastheadStructureChanges/masthead-structure-changes.ts @@ -0,0 +1,174 @@ +import { Rule } from "eslint"; +import { ImportSpecifier } from "estree-jsx"; +import { JSXOpeningElementWithParent } from "../../helpers"; +import { + getAllImportsFromPackage, + getChildElementByName, + getImportedName, + getLocalComponentName, + hasCodeModDataTag, +} from "../../helpers"; +// https://github.com/patternfly/patternfly-react/pull/10809 + +function moveNodeIntoMastheadMain( + context: Rule.RuleContext, + fixer: Rule.RuleFixer, + node: JSXOpeningElementWithParent, + namedImports: ImportSpecifier[] +) { + if (!node.parent || !node.parent.parent) { + return []; + } + + const localMastheadMain = getLocalComponentName(namedImports, "MastheadMain"); + const mastheadMain = getChildElementByName( + node.parent.parent, + localMastheadMain + ); + + if (!mastheadMain) { + return []; + } + + const fixes = [fixer.remove(node.parent)]; + + const nodeString = context.getSourceCode().getText(node.parent); + + fixes.push(fixer.insertTextAfter(mastheadMain.openingElement, nodeString)); + + return fixes; +} + +function wrapNodeInMastheadBrand( + fixer: Rule.RuleFixer, + node: JSXOpeningElementWithParent, + namedImports: ImportSpecifier[] +) { + if (!node.parent) { + return []; + } + + const fixes = []; + + const closingNode = node.parent?.closingElement + ? node.parent.closingElement + : node; + + const importCount = namedImports.length - 1; + const lastImport = namedImports[importCount]; + + const localMastheadBrand = getLocalComponentName( + namedImports, + "MastheadBrand" + ); + + fixes.push( + fixer.insertTextBefore(node, `<${localMastheadBrand} data-codemods>`) + ); + fixes.push(fixer.insertTextAfter(closingNode, ``)); + + if (!namedImports.some((imp) => imp.imported.name === "MastheadBrand")) { + fixes.push(fixer.insertTextAfter(lastImport, ", MastheadBrand")); + } + + return fixes; +} + +function formatMessage( + component: "MastheadToggle" | "MastheadBrand" | "MastheadLogo" +) { + const baseMessage = "The structure of Masthead has been updated, "; + + const restOfMessage = { + MastheadToggle: "MastheadToggle should now be wrapped in MastheadMain.", + MastheadBrand: + "the PF5 MastheadBrand has been renamed to MastheadLogo (this renaming is handled by our masthead-name-changes codemod) and should now be wrapped in a new MastheadBrand.", + MastheadLogo: "MastheadLogo should now be wrapped in MastheadBrand.", + }; + + return baseMessage + restOfMessage[component]; +} + +module.exports = { + meta: { fixable: "code" }, + create: function (context: Rule.RuleContext) { + const targetComponents = [ + "MastheadBrand", + "MastheadToggle", + "MastheadLogo", + "Masthead", + "MastheadMain", + ]; + const componentImports = getAllImportsFromPackage( + context, + "@patternfly/react-core", + targetComponents + ); + const namedImports = componentImports.filter( + (imp) => imp.type === "ImportSpecifier" + ) as ImportSpecifier[]; + + return !namedImports.length + ? {} + : { + JSXOpeningElement(node: JSXOpeningElementWithParent) { + const nodeImportedName = getImportedName(namedImports, node); + + if (node.name.type !== "JSXIdentifier" || !nodeImportedName) { + return; + } + const parentOpeningElement = node.parent?.parent?.openingElement; + + if (!parentOpeningElement) { + return; + } + + const parentImportedName = getImportedName( + namedImports, + parentOpeningElement + ); + + if ( + nodeImportedName === "MastheadToggle" && + parentImportedName !== "MastheadMain" + ) { + context.report({ + node, + message: formatMessage("MastheadToggle"), + fix: (fixer) => + moveNodeIntoMastheadMain(context, fixer, node, namedImports), + }); + return; + } + + const isPreRenameMastheadBrand = + nodeImportedName === "MastheadBrand" && + parentImportedName === "MastheadMain" && + !hasCodeModDataTag(node); + + const isPostRenameMastheadBrand = + nodeImportedName === "MastheadLogo" && + parentImportedName !== "MastheadBrand"; + + if (isPreRenameMastheadBrand) { + context.report({ + node, + message: formatMessage("MastheadBrand"), + fix: (fixer) => + wrapNodeInMastheadBrand(fixer, node, namedImports), + }); + return; + } + + if (isPostRenameMastheadBrand) { + context.report({ + node, + message: formatMessage("MastheadLogo"), + fix: (fixer) => + wrapNodeInMastheadBrand(fixer, node, namedImports), + }); + } + }, + }; + }, +}; diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/mastheadStructureChanges/mastheadStructureChangesInput.tsx b/packages/eslint-plugin-pf-codemods/src/rules/v6/mastheadStructureChanges/mastheadStructureChangesInput.tsx new file mode 100644 index 000000000..90f082599 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/mastheadStructureChanges/mastheadStructureChangesInput.tsx @@ -0,0 +1,25 @@ +import { + Masthead, + MastheadBrand, + MastheadMain, + MastheadToggle, + MastheadLogo +} from "@patternfly/react-core"; + +export const MastheadStructureChangesInputPreNameChange = () => ( + + Foo + + Bar + + +); + +export const MastheadStructureChangesInputPostNameChange = () => ( + + Foo + + Bar + + +); diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/mastheadStructureChanges/mastheadStructureChangesOutput.tsx b/packages/eslint-plugin-pf-codemods/src/rules/v6/mastheadStructureChanges/mastheadStructureChangesOutput.tsx new file mode 100644 index 000000000..d7ab84e35 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/mastheadStructureChanges/mastheadStructureChangesOutput.tsx @@ -0,0 +1,29 @@ +import { + Masthead, + MastheadBrand, + MastheadMain, + MastheadToggle, + MastheadLogo, +} from "@patternfly/react-core"; + +export const MastheadStructureChangesInputPreNameChange = () => ( + + + Foo + + Bar + + + +); + +export const MastheadStructureChangesInputPostNameChange = () => ( + + + Foo + + Bar + + + +);