Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Masthead): Update structure #730

Merged
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 2 additions & 0 deletions packages/eslint-plugin-pf-codemods/src/rules/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
@@ -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%
```

Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
const ruleTester = require("../../ruletester");
import * as rule from "./masthead-structure-changes";

ruleTester.run("masthead-structure-changes", rule, {
valid: [
{
code: `<Masthead />`,
},
{
code: `import { Masthead } from '@patternfly/react-core'; <Masthead someOtherProp />`,
},
],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The valid tests should probably be updated. One would be the code from one of the invalid tests (without the import statement), and the other could be the output from one of the invalid tests.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops

invalid: [
// stage one of a pre-renamed file
{
code: `import { Masthead, MastheadBrand, MastheadMain, MastheadToggle } from '@patternfly/react-core'; <Masthead><MastheadToggle>Foo</MastheadToggle><MastheadMain><MastheadBrand>Bar</MastheadBrand></MastheadMain></Masthead>`,
output: `import { Masthead, MastheadBrand, MastheadMain, MastheadToggle } from '@patternfly/react-core'; <Masthead><MastheadMain><MastheadToggle>Foo</MastheadToggle><MastheadBrand>Bar</MastheadBrand></MastheadMain></Masthead>`,
errors: [
{
message: `The structure of Masthead has been updated, MastheadToggle and MastheadBrand should now be wrapped in MastheadMain.`,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit not a blocker: wdyt about updating the message that gets output to only mention the applicable element? Instead of saying both "toggle and brand...", on a MastheadToggle, only mention "toggle...".

type: "JSXOpeningElement",
},
{
message: `The structure of Masthead has been updated, MastheadToggle and MastheadBrand should now be wrapped in MastheadMain.`,
type: "JSXOpeningElement",
},
],
},
// stage two of a pre-renamed file
{
code: `import { Masthead, MastheadBrand, MastheadMain, MastheadToggle } from '@patternfly/react-core'; <Masthead><MastheadMain><MastheadToggle>Foo</MastheadToggle><MastheadBrand>Bar</MastheadBrand></MastheadMain></Masthead>`,
output: `import { Masthead, MastheadBrand, MastheadMain, MastheadToggle } from '@patternfly/react-core'; <Masthead><MastheadMain><MastheadToggle>Foo</MastheadToggle><MastheadBrand data-codemods><MastheadBrand>Bar</MastheadBrand></MastheadBrand></MastheadMain></Masthead>`,
errors: [
{
message: `The structure of Masthead has been updated, MastheadToggle and MastheadBrand should now be wrapped in MastheadMain.`,
type: "JSXOpeningElement",
},
],
},
// stage one of a post-renamed file
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit not a blocker: for these comments for the pre/post renamed file, maybe instead "Stage [one|two] of a file that [has been|has NOT been] fixed by masthead-name-changes rule"

{
code: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle } from '@patternfly/react-core'; <Masthead><MastheadToggle>Foo</MastheadToggle><MastheadMain><MastheadLogo>Bar</MastheadLogo></MastheadMain></Masthead>`,
output: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core'; <Masthead><MastheadToggle>Foo</MastheadToggle><MastheadMain><MastheadBrand data-codemods><MastheadLogo>Bar</MastheadLogo></MastheadBrand></MastheadMain></Masthead>`,
errors: [
{
message: `The structure of Masthead has been updated, MastheadToggle and MastheadBrand should now be wrapped in MastheadMain.`,
type: "JSXOpeningElement",
},
{
message: `The structure of Masthead has been updated, MastheadToggle and MastheadBrand should now be wrapped in MastheadMain.`,
type: "JSXOpeningElement",
},
],
},
// stage two of a post-renamed file
{
code: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core'; <Masthead><MastheadToggle>Foo</MastheadToggle><MastheadMain><MastheadBrand data-codemods><MastheadLogo>Bar</MastheadLogo></MastheadBrand></MastheadMain></Masthead>`,
output: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core'; <Masthead><MastheadMain><MastheadToggle>Foo</MastheadToggle><MastheadBrand data-codemods><MastheadLogo>Bar</MastheadLogo></MastheadBrand></MastheadMain></Masthead>`,
errors: [
{
message: `The structure of Masthead has been updated, MastheadToggle and MastheadBrand 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'; <MH><MT>Foo</MT><MM><MB>Bar</MB></MM></MH>`,
output: `import { Masthead as MH, MastheadBrand as MB, MastheadMain as MM, MastheadToggle as MT } from '@patternfly/react-core'; <MH><MM><MT>Foo</MT><MB>Bar</MB></MM></MH>`,
errors: [
{
message: `The structure of Masthead has been updated, MastheadToggle and MastheadBrand should now be wrapped in MastheadMain.`,
type: "JSXOpeningElement",
},
{
message: `The structure of Masthead has been updated, MastheadToggle and MastheadBrand should now be wrapped in MastheadMain.`,
type: "JSXOpeningElement",
},
],
},
{
code: `import { Masthead as MH, MastheadBrand as MB, MastheadMain as MM, MastheadToggle as MT } from '@patternfly/react-core'; <MH><MM><MT>Foo</MT><MB>Bar</MB></MM></MH>`,
output: `import { Masthead as MH, MastheadBrand as MB, MastheadMain as MM, MastheadToggle as MT } from '@patternfly/react-core'; <MH><MM><MT>Foo</MT><MB data-codemods><MB>Bar</MB></MB></MM></MH>`,
errors: [
{
message: `The structure of Masthead has been updated, MastheadToggle and MastheadBrand should now be wrapped in MastheadMain.`,
type: "JSXOpeningElement",
},
],
},
// with dist imports
{
code: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle } from '@patternfly/react-core/dist/esm/components/Masthead'; <Masthead><MastheadToggle>Foo</MastheadToggle><MastheadMain><MastheadLogo>Bar</MastheadLogo></MastheadMain></Masthead>`,
output: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core/dist/esm/components/Masthead'; <Masthead><MastheadToggle>Foo</MastheadToggle><MastheadMain><MastheadBrand data-codemods><MastheadLogo>Bar</MastheadLogo></MastheadBrand></MastheadMain></Masthead>`,
errors: [
{
message: `The structure of Masthead has been updated, MastheadToggle and MastheadBrand should now be wrapped in MastheadMain.`,
type: "JSXOpeningElement",
},
{
message: `The structure of Masthead has been updated, MastheadToggle and MastheadBrand should now be wrapped in MastheadMain.`,
type: "JSXOpeningElement",
},
],
},
{
code: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core/dist/esm/components/Masthead'; <Masthead><MastheadToggle>Foo</MastheadToggle><MastheadMain><MastheadBrand data-codemods><MastheadLogo>Bar</MastheadLogo></MastheadBrand></MastheadMain></Masthead>`,
output: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core/dist/esm/components/Masthead'; <Masthead><MastheadMain><MastheadToggle>Foo</MastheadToggle><MastheadBrand data-codemods><MastheadLogo>Bar</MastheadLogo></MastheadBrand></MastheadMain></Masthead>`,
errors: [
{
message: `The structure of Masthead has been updated, MastheadToggle and MastheadBrand should now be wrapped in MastheadMain.`,
type: "JSXOpeningElement",
},
],
},
{
code: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle } from '@patternfly/react-core/dist/js/components/Masthead'; <Masthead><MastheadToggle>Foo</MastheadToggle><MastheadMain><MastheadLogo>Bar</MastheadLogo></MastheadMain></Masthead>`,
output: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core/dist/js/components/Masthead'; <Masthead><MastheadToggle>Foo</MastheadToggle><MastheadMain><MastheadBrand data-codemods><MastheadLogo>Bar</MastheadLogo></MastheadBrand></MastheadMain></Masthead>`,
errors: [
{
message: `The structure of Masthead has been updated, MastheadToggle and MastheadBrand should now be wrapped in MastheadMain.`,
type: "JSXOpeningElement",
},
{
message: `The structure of Masthead has been updated, MastheadToggle and MastheadBrand should now be wrapped in MastheadMain.`,
type: "JSXOpeningElement",
},
],
},
{
code: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core/dist/js/components/Masthead'; <Masthead><MastheadToggle>Foo</MastheadToggle><MastheadMain><MastheadBrand data-codemods><MastheadLogo>Bar</MastheadLogo></MastheadBrand></MastheadMain></Masthead>`,
output: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core/dist/js/components/Masthead'; <Masthead><MastheadMain><MastheadToggle>Foo</MastheadToggle><MastheadBrand data-codemods><MastheadLogo>Bar</MastheadLogo></MastheadBrand></MastheadMain></Masthead>`,
errors: [
{
message: `The structure of Masthead has been updated, MastheadToggle and MastheadBrand should now be wrapped in MastheadMain.`,
type: "JSXOpeningElement",
},
],
},
{
code: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle } from '@patternfly/react-core/dist/dynamic/components/Masthead'; <Masthead><MastheadToggle>Foo</MastheadToggle><MastheadMain><MastheadLogo>Bar</MastheadLogo></MastheadMain></Masthead>`,
output: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core/dist/dynamic/components/Masthead'; <Masthead><MastheadToggle>Foo</MastheadToggle><MastheadMain><MastheadBrand data-codemods><MastheadLogo>Bar</MastheadLogo></MastheadBrand></MastheadMain></Masthead>`,
errors: [
{
message: `The structure of Masthead has been updated, MastheadToggle and MastheadBrand should now be wrapped in MastheadMain.`,
type: "JSXOpeningElement",
},
{
message: `The structure of Masthead has been updated, MastheadToggle and MastheadBrand should now be wrapped in MastheadMain.`,
type: "JSXOpeningElement",
},
],
},
{
code: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core/dist/dynamic/components/Masthead'; <Masthead><MastheadToggle>Foo</MastheadToggle><MastheadMain><MastheadBrand data-codemods><MastheadLogo>Bar</MastheadLogo></MastheadBrand></MastheadMain></Masthead>`,
output: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core/dist/dynamic/components/Masthead'; <Masthead><MastheadMain><MastheadToggle>Foo</MastheadToggle><MastheadBrand data-codemods><MastheadLogo>Bar</MastheadLogo></MastheadBrand></MastheadMain></Masthead>`,
errors: [
{
message: `The structure of Masthead has been updated, MastheadToggle and MastheadBrand should now be wrapped in MastheadMain.`,
type: "JSXOpeningElement",
},
],
},
],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
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, `</${localMastheadBrand}>`));

if (!namedImports.some((imp) => imp.imported.name === "MastheadBrand")) {
fixes.push(fixer.insertTextAfter(lastImport, ", MastheadBrand"));
}

return fixes;
}

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"
);
// TS isn't properly resolving that namedImports is just ImportSpecifiers, hence this seemingly unneeded assertion
const namedImports = _namedImports as ImportSpecifier[];

const message =
"The structure of Masthead has been updated, MastheadToggle and MastheadBrand should now be wrapped in MastheadMain.";

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,
fix: (fixer) =>
moveNodeIntoMastheadMain(context, fixer, node, namedImports),
});
return;
}

const isPreRenameMastheadBrand =
nodeImportedName === "MastheadBrand" &&
parentImportedName === "MastheadMain" &&
!hasCodeModDataTag(node);

const isPostRenameMastheadBrand =
nodeImportedName === "MastheadLogo" &&
parentImportedName !== "MastheadBrand";

if (isPreRenameMastheadBrand || isPostRenameMastheadBrand) {
context.report({
node,
message,
fix: (fixer) =>
wrapNodeInMastheadBrand(fixer, node, namedImports),
});
}
},
};
},
};
Loading
Loading