Skip to content

Commit

Permalink
refactor(EmptyState): extract small helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
adamviktora committed Mar 5, 2024
1 parent b86d424 commit 7cbbe17
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 117 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { JSXElement, JSXAttribute } from "estree-jsx";

export function getAttribute(node: JSXElement, attributeName: string) {
const attributes = node.openingElement.attributes.filter(
(attr) => attr.type === "JSXAttribute"
) as JSXAttribute[];
return attributes.find((attr) => attr.name.name === attributeName);
}

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;
}
29 changes: 29 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,29 @@
import { Rule } from "eslint";

import { JSXAttribute, 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("");
}
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,16 +1,23 @@
import { AST, Rule } from "eslint";
import {
JSXElement,
ImportDeclaration,
ImportSpecifier,
JSXAttribute,
Node,
JSXText,
JSXElement,
JSXExpressionContainer,
JSXSpreadChild,
JSXFragment,
Node,
} from "estree-jsx";
import { getFromPackage, pfPackageMatches } from "../../helpers";
import { getFromPackage } from "../../helpers";
import {
getAttributeText,
getAttributeValueText,
getNodesText,
} from "../../helpers/getText";
import { includesImport } from "../../helpers/includesImport";
import {
getChildElementByName,
nodeIsComponentNamed,
} from "../../helpers/JSXElements";
import { getAttribute, getExpression } from "../../helpers/JSXAttributes";

const baseMessage =
"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.";
Expand All @@ -22,121 +29,17 @@ module.exports = {
const pkg = "@patternfly/react-core";
const { imports } = getFromPackage(context, pkg);

const allOfType = (nodes: Node[], type: string) =>
nodes.every((specifier) => specifier.type === type);

const includesImport = (
arr: ImportDeclaration["specifiers"],
targetImport: string
) => {
if (!allOfType(arr, "ImportSpecifier")) {
return false;
}

return arr.some(
(specifier) =>
(specifier as ImportSpecifier).imported.name === targetImport
);
};

if (!includesImport(imports, "EmptyState")) {
return {};
}

const getChildElementByName = (name: string, node: JSXElement) =>
node.children?.find(
(child) =>
child.type === "JSXElement" &&
child.openingElement.name.type === "JSXIdentifier" &&
child.openingElement.name.name === name
);

const isComponentNode = (node: JSXElement, componentName: string) => {
if (node.openingElement.name.type === "JSXIdentifier") {
return node.openingElement.name.name === componentName;
}

return false;
};

const getAttribute = (
node: JSXElement,
attributeName: string
): JSXAttribute | undefined => {
const attributes = node.openingElement.attributes.filter(
(attr) => attr.type === "JSXAttribute"
) as JSXAttribute[];
return attributes.find((attr) => attr.name.name === attributeName);
};

const getExpressionValue = (node?: JSXAttribute["value"]) => {
if (!node) {
return;
}

if (node.type === "JSXExpressionContainer") {
return node.expression;
}
};

const getAttributeText = (attribute?: JSXAttribute) => {
if (!attribute) {
return "";
}

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

const getAttributeValueText = (attribute?: JSXAttribute) => {
if (!attribute || !attribute.value) {
return "";
}

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

const getNodesText = (nodes: Node[]) => {
return nodes
.map((node) => context.getSourceCode().getText(node))
.join("");
};

const getChildrenText = (children: JSXElement["children"]) => {
if (!children.length) {
return "";
}

if (children.length === 1 && children[0].type === "JSXText") {
return `"${children[0].value.trim()}"`;
}

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

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

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

return `{<>${getNodesText(children as Node[])}</>}`;
};

return {
JSXElement(node: JSXElement) {
if (!isComponentNode(node, "EmptyState")) {
if (!nodeIsComponentNamed(node, "EmptyState")) {
return;
}

const header = getChildElementByName("EmptyStateHeader", node);
const header = getChildElementByName(node, "EmptyStateHeader");
const emptyStateTitleTextAttribute = getAttribute(node, "titleText");

if (!header && !emptyStateTitleTextAttribute) {
Expand Down Expand Up @@ -176,20 +79,56 @@ module.exports = {
}

const headingClassNameValue = getAttributeValueText(
context,
headingClassNameAttribute
);

const headingClassName = headingClassNameValue
? `headerClassName=${headingClassNameValue}`
: "";
const headingLevel = getAttributeText(headingLevelAttribute);
const titleClassName = getAttributeText(titleClassNameAttribute);
const titleTextPropValue = getAttributeText(titleTextAttribute);
const headingLevel = getAttributeText(context, headingLevelAttribute);
const titleClassName = getAttributeText(
context,
titleClassNameAttribute
);
const titleTextPropValue = getAttributeText(
context,
titleTextAttribute
);

const getChildrenText = (children: JSXElement["children"]) => {
if (!children.length) {
return "";
}

if (children.length === 1 && children[0].type === "JSXText") {
return `"${children[0].value.trim()}"`;
}

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

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

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

return `{<>${getNodesText(context, children as Node[])}</>}`;
};

const titleText =
titleTextPropValue || `titleText=${getChildrenText(headerChildren)}`;

const iconPropValue = getExpressionValue(headerIconAttribute?.value);
const iconPropValue = getExpression(headerIconAttribute?.value);

const emptyStateIconComponent =
iconPropValue?.type === "JSXElement" ? iconPropValue : undefined;
Expand All @@ -202,6 +141,7 @@ module.exports = {
emptyStateIconComponent &&
getAttribute(emptyStateIconComponent, "color");
const emptyStateIconComponentColor = getAttributeText(
context,
emptyStateIconComponentColorAttribute
);

Expand Down

0 comments on commit 7cbbe17

Please sign in to comment.