Skip to content

Commit

Permalink
refactor(EmptyState): extract helpers, fix(EmptyState): getElementChi…
Browse files Browse the repository at this point in the history
…ldText fix (#602)

* fix(EmptyState): getChildrenText works on all kinds of children

- previously the switch would unintentionally handle most cases under JSXText, as the first few whitespaces count as JSXText of children

* refactor(EmptyState): extract small helpers

* refactor(imports): export helpers and clean imports

* refactor(EmptyState): extract getChildrenAsAttributeValueText helper

* refactor: getAttribute helper

Co-authored-by: Dominik Petřík <77832970+Dominik-Petrik@users.noreply.github.com>

---------

Co-authored-by: Dominik Petřík <77832970+Dominik-Petrik@users.noreply.github.com>
  • Loading branch information
adamviktora and Dominik-Petrik authored Mar 21, 2024
1 parent f42bf72 commit 0cbb6b1
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 119 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { JSXElement, JSXAttribute } from "estree-jsx";

export function getAttribute(node: JSXElement, attributeName: string) {
return node.openingElement.attributes.find(
(attr) => attr.type === "JSXAttribute" && attr.name.name === attributeName
) as JSXAttribute | undefined;
}

export function getExpression(node?: JSXAttribute["value"]) {
if (!node) {
return;
}

if (node.type === "JSXExpressionContainer") {
return node.expression;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { JSXElement } from "estree-jsx";

export function getChildElementByName(node: JSXElement, name: string) {
return node.children?.find(
(child) =>
child.type === "JSXElement" &&
child.openingElement.name.type === "JSXIdentifier" &&
child.openingElement.name.name === name
) as JSXElement | undefined;
}

export function nodeIsComponentNamed(node: JSXElement, componentName: string) {
if (node.openingElement.name.type === "JSXIdentifier") {
return node.openingElement.name.name === componentName;
}

return false;
}
74 changes: 74 additions & 0 deletions packages/eslint-plugin-pf-codemods/src/rules/helpers/getText.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Rule } from "eslint";

import {
JSXAttribute,
JSXElement,
JSXExpressionContainer,
JSXFragment,
Node,
} from "estree-jsx";

export function getAttributeText(
context: Rule.RuleContext,
attribute?: JSXAttribute
) {
if (!attribute) {
return "";
}

return context.getSourceCode().getText(attribute);
}

export function getAttributeValueText(
context: Rule.RuleContext,
attribute?: JSXAttribute
) {
if (!attribute || !attribute.value) {
return "";
}

return context.getSourceCode().getText(attribute.value);
}

export function getNodesText(context: Rule.RuleContext, nodes: Node[]) {
return nodes.map((node) => context.getSourceCode().getText(node)).join("");
}

export function getChildrenAsAttributeValueText(
context: Rule.RuleContext,
children: JSXElement["children"]
) {
if (!children.length) {
return `""`;
}

// is a single text-only child
if (children.length === 1 && children[0].type === "JSXText") {
const childText = children[0].value.trim();

if (childText.includes(`"`)) {
return `{<>${childText}</>}`;
}

return `"${childText}"`;
}

const nonEmptyChildrenNodes = children.filter(
(child) => !(child.type === "JSXText" && child.value.trim() === "")
);

if (nonEmptyChildrenNodes.length === 1) {
const singleChild = nonEmptyChildrenNodes[0];
const singleChildText = context
.getSourceCode()
.getText(
singleChild as JSXExpressionContainer | JSXElement | JSXFragment
);

return singleChild.type === "JSXExpressionContainer"
? singleChildText
: `{${singleChildText}}`;
}

return `{<>${getNodesText(context, children as Node[])}</>}`;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ImportSpecifier } from "estree-jsx";

export function includesImport(
importSpecifiers: ImportSpecifier[],
targetImport: string
) {
return importSpecifiers.some(
(specifier) => specifier.imported.name === targetImport
);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export * from "./findAncestor";
export * from "./helpers";
export * from "./pfPackageMatches";
export * from "./getFromPackage";
export * from "./getText";
export * from "./includesImport";
export * from "./JSXAttributes";
export * from "./JSXElements";
export * from "./pfPackageMatches";
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ ruleTester.run("emptyStateHeader-move-into-emptyState", rule, {
],
},
{
// without any EmptyStateHeader props but with children
// without any EmptyStateHeader props but with children as text
code: `import {
EmptyState,
EmptyStateHeader,
Expand Down Expand Up @@ -148,6 +148,112 @@ ruleTester.run("emptyStateHeader-move-into-emptyState", rule, {
},
],
},
{
// without any EmptyStateHeader props but with children as single JSXElement
code: `import {
EmptyState,
EmptyStateHeader,
} from "@patternfly/react-core";
export const EmptyStateHeaderMoveIntoEmptyStateInput = () => (
<EmptyState>
<EmptyStateHeader>
<h1>Foo bar</h1>
</EmptyStateHeader>
</EmptyState>
);
`,
output: `import {
EmptyState,
EmptyStateHeader,
} from "@patternfly/react-core";
export const EmptyStateHeaderMoveIntoEmptyStateInput = () => (
<EmptyState titleText={<h1>Foo bar</h1>}>
</EmptyState>
);
`,
errors: [
{
message: `EmptyStateHeader has been moved inside of the EmptyState component and is now only customizable using props, and the EmptyStateIcon component now wraps content passed to the icon prop automatically. Additionally, the titleText prop is now required on EmptyState.`,
type: "JSXElement",
},
],
},
{
// without any EmptyStateHeader props but with children as single JSXExpressionContainer
code: `import {
EmptyState,
EmptyStateHeader,
} from "@patternfly/react-core";
const title = "Some title";
export const EmptyStateHeaderMoveIntoEmptyStateInput = () => (
<EmptyState>
<EmptyStateHeader>
{title}
</EmptyStateHeader>
</EmptyState>
);
`,
output: `import {
EmptyState,
EmptyStateHeader,
} from "@patternfly/react-core";
const title = "Some title";
export const EmptyStateHeaderMoveIntoEmptyStateInput = () => (
<EmptyState titleText={title}>
</EmptyState>
);
`,
errors: [
{
message: `EmptyStateHeader has been moved inside of the EmptyState component and is now only customizable using props, and the EmptyStateIcon component now wraps content passed to the icon prop automatically. Additionally, the titleText prop is now required on EmptyState.`,
type: "JSXElement",
},
],
},
{
// without any EmptyStateHeader props but with children consisting of multiple elements
code: `import {
EmptyState,
EmptyStateHeader,
} from "@patternfly/react-core";
const title = "Some title";
export const EmptyStateHeaderMoveIntoEmptyStateInput = () => (
<EmptyState>
<EmptyStateHeader>
{title} followed by some text
</EmptyStateHeader>
</EmptyState>
);
`,
output: `import {
EmptyState,
EmptyStateHeader,
} from "@patternfly/react-core";
const title = "Some title";
export const EmptyStateHeaderMoveIntoEmptyStateInput = () => (
<EmptyState titleText={<>
{title} followed by some text
</>}>
</EmptyState>
);
`,
errors: [
{
message: `EmptyStateHeader has been moved inside of the EmptyState component and is now only customizable using props, and the EmptyStateIcon component now wraps content passed to the icon prop automatically. Additionally, the titleText prop is now required on EmptyState.`,
type: "JSXElement",
},
],
},
{
// without an EmptyStateHeader or titleText
code: `import { EmptyState } from "@patternfly/react-core";
Expand Down
Loading

0 comments on commit 0cbb6b1

Please sign in to comment.