From cbb89f70bb0588f8b74c15297db52e88da0ff46e Mon Sep 17 00:00:00 2001 From: adamviktora Date: Wed, 28 Aug 2024 17:20:50 +0200 Subject: [PATCH] fix(buttonMoveIconsIconProp): make elements self-closing - also fixes Icon imported with alias - supports Icon / react-icons being used in children attribute --- .../src/rules/helpers/JSXAttributes.ts | 7 ++- .../src/rules/helpers/childrenIsEmpty.ts | 11 ++++ .../rules/helpers/getChildJSXElementByName.ts | 10 +++- .../src/rules/helpers/index.ts | 2 + .../helpers/makeJSXElementSelfClosing.ts | 47 ++++++++++++++++ .../button-moveIcons-icon-prop.test.ts | 47 +++++++++++++--- .../button-moveIcons-icon-prop.ts | 54 ++++++++++++------- 7 files changed, 148 insertions(+), 30 deletions(-) create mode 100644 packages/eslint-plugin-pf-codemods/src/rules/helpers/childrenIsEmpty.ts create mode 100644 packages/eslint-plugin-pf-codemods/src/rules/helpers/makeJSXElementSelfClosing.ts diff --git a/packages/eslint-plugin-pf-codemods/src/rules/helpers/JSXAttributes.ts b/packages/eslint-plugin-pf-codemods/src/rules/helpers/JSXAttributes.ts index 3d0f26ca6..b9b783c9e 100644 --- a/packages/eslint-plugin-pf-codemods/src/rules/helpers/JSXAttributes.ts +++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/JSXAttributes.ts @@ -1,7 +1,10 @@ import { Rule, Scope } from "eslint"; import { - JSXElement, + Expression, JSXAttribute, + JSXElement, + JSXEmptyExpression, + JSXFragment, JSXOpeningElement, MemberExpression, } from "estree-jsx"; @@ -68,7 +71,7 @@ export function getExpression(node?: JSXAttribute["value"]) { } if (node.type === "JSXExpressionContainer") { - return node.expression; + return node.expression as Expression | JSXEmptyExpression | JSXFragment; } } diff --git a/packages/eslint-plugin-pf-codemods/src/rules/helpers/childrenIsEmpty.ts b/packages/eslint-plugin-pf-codemods/src/rules/helpers/childrenIsEmpty.ts new file mode 100644 index 000000000..0523fbf17 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/childrenIsEmpty.ts @@ -0,0 +1,11 @@ +import { JSXElement } from "estree-jsx"; + +/** Checks whether children is empty (no children or only whitespaces) */ +export function childrenIsEmpty(children: JSXElement["children"]) { + return ( + !children.length || + (children.length === 1 && + children[0].type === "JSXText" && + children[0].value.trim() === "") + ); +} diff --git a/packages/eslint-plugin-pf-codemods/src/rules/helpers/getChildJSXElementByName.ts b/packages/eslint-plugin-pf-codemods/src/rules/helpers/getChildJSXElementByName.ts index 4f00e951b..54f750fad 100644 --- a/packages/eslint-plugin-pf-codemods/src/rules/helpers/getChildJSXElementByName.ts +++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/getChildJSXElementByName.ts @@ -25,7 +25,10 @@ function getChildJSXElementCallback( /** Can be used to run logic if the specific child element exists, or to run logic on the * specified element. */ -export function getChildJSXElementByName(node: JSXElement, name: string) { +export function getChildJSXElementByName( + node: JSXElement | JSXFragment, + name: string +) { return node.children?.find((child) => getChildJSXElementCallback(child, name) ) as JSXElement | undefined; @@ -34,7 +37,10 @@ export function getChildJSXElementByName(node: JSXElement, name: string) { /** Can be used to run logic if the specific child elements exist, or to run logic on the * specified elements. */ -export function getAllChildJSXElementsByName(node: JSXElement, name: string) { +export function getAllChildJSXElementsByName( + node: JSXElement | JSXFragment, + name: string +) { return node.children?.filter((child) => getChildJSXElementCallback(child, name) ) as JSXElement[]; 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 5ef2822a9..e719e884d 100644 --- a/packages/eslint-plugin-pf-codemods/src/rules/helpers/index.ts +++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/index.ts @@ -1,3 +1,4 @@ +export * from "./childrenIsEmpty"; export * from "./contextReports"; export * from "./findAncestor"; export * from "./fixers"; @@ -22,6 +23,7 @@ export * from "./includesImport"; export * from "./interfaces"; export * from "./isReactIcon"; export * from "./JSXAttributes"; +export * from "./makeJSXElementSelfClosing"; export * from "./nodeMatches"; export * from "./pfPackageMatches"; export * from "./removeElement"; diff --git a/packages/eslint-plugin-pf-codemods/src/rules/helpers/makeJSXElementSelfClosing.ts b/packages/eslint-plugin-pf-codemods/src/rules/helpers/makeJSXElementSelfClosing.ts new file mode 100644 index 000000000..44944780f --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/makeJSXElementSelfClosing.ts @@ -0,0 +1,47 @@ +import { Rule } from "eslint"; +import { JSXElement, JSXText } from "estree-jsx"; +import { childrenIsEmpty } from "."; + +/** Transforms JSXElement to a self-closing tag. + * Works only on elements without children by default, but you can overwrite this behaviour with the removeChildren parameter. + */ +export function makeJSXElementSelfClosing( + node: JSXElement, + context: Rule.RuleContext, + fixer: Rule.RuleFixer, + removeChildren: boolean = false +): Rule.Fix[] { + if (!node.closingElement) { + return []; + } + + const fixes = []; + + const emptyChildren = childrenIsEmpty(node.children); + + if (removeChildren || emptyChildren) { + const closingSymbol = context + .getSourceCode() + .getLastToken(node.openingElement)!; + + fixes.push( + fixer.replaceText(closingSymbol, " />"), + fixer.remove(node.closingElement) + ); + + if (node.children.length) { + if (removeChildren) { + fixes.push( + fixer.removeRange([ + node.children[0].range![0], + node.children[node.children.length - 1].range![1], + ]) + ); + } else if (emptyChildren) { + fixes.push(fixer.remove(node.children[0] as JSXText)); + } + } + } + + return fixes; +} diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/buttonMoveIconsIconProp/button-moveIcons-icon-prop.test.ts b/packages/eslint-plugin-pf-codemods/src/rules/v6/buttonMoveIconsIconProp/button-moveIcons-icon-prop.test.ts index 957aeee01..b7aa8e51f 100644 --- a/packages/eslint-plugin-pf-codemods/src/rules/v6/buttonMoveIconsIconProp/button-moveIcons-icon-prop.test.ts +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/buttonMoveIconsIconProp/button-moveIcons-icon-prop.test.ts @@ -17,7 +17,7 @@ ruleTester.run("button-moveIcons-icon-prop", rule, { invalid: [ { code: `import { Button } from '@patternfly/react-core'; const icon = Some icon; `, - output: `import { Button } from '@patternfly/react-core'; const icon = Some icon; `, + output: `import { Button } from '@patternfly/react-core'; const icon = Some icon; `, - output: `import { Button } from '@patternfly/react-core'; `, + output: `import { Button } from '@patternfly/react-core'; `, - output: `import { Button, ButtonVariant } from '@patternfly/react-core'; `, + output: `import { Button, ButtonVariant } from '@patternfly/react-core'; `, - output: `import { Button } from '@patternfly/react-core/dist/esm/components/Button/index.js'; `, + output: `import { Button } from '@patternfly/react-core/dist/esm/components/Button/index.js'; `, - output: `import { Button } from '@patternfly/react-core/dist/js/components/Button/index.js'; `, + output: `import { Button } from '@patternfly/react-core/dist/js/components/Button/index.js'; `, - output: `import { Button } from '@patternfly/react-core/dist/dynamic/components/Button/index.js'; `, + output: `import { Button } from '@patternfly/react-core/dist/dynamic/components/Button/index.js'; `, + output: `import { Button, Icon as PFIcon } from '@patternfly/react-core'; `, + errors: [ + { + message: `Icons must now be passed to the \`icon\` prop of Button instead of as children. If you are passing anything other than an icon as children, ignore this rule when running fixes.`, + type: "JSXElement", + }, + ], + }, // with react-icons icon child { code: `import { Button } from '@patternfly/react-core'; import { SomeIcon } from "@patternfly/react-icons"; `, @@ -129,5 +140,27 @@ ruleTester.run("button-moveIcons-icon-prop", rule, { }, ], }, + // with react-icons icon child in children prop (inside JSXFragment) + { + code: `import { Button } from '@patternfly/react-core'; import { SomeIcon } from "@patternfly/react-icons";