diff --git a/packages/eslint-plugin-pf-codemods/src/ruleCustomization.ts b/packages/eslint-plugin-pf-codemods/src/ruleCustomization.ts index 98278099..65fbd299 100644 --- a/packages/eslint-plugin-pf-codemods/src/ruleCustomization.ts +++ b/packages/eslint-plugin-pf-codemods/src/ruleCustomization.ts @@ -13,6 +13,7 @@ export const warningRules = [ "aboutModalBoxHero-remove-subcomponent", "accordionItem-warn-update-markup", "applicationLauncher-warn-input", + "button-support-favorite-variant", "card-deprecate-props", "card-warn-component", "charts-warn-tooltip", diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/buttonSupportFavoriteVariant/button-support-favorite-variant.md b/packages/eslint-plugin-pf-codemods/src/rules/v6/buttonSupportFavoriteVariant/button-support-favorite-variant.md new file mode 100644 index 00000000..1069cff9 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/buttonSupportFavoriteVariant/button-support-favorite-variant.md @@ -0,0 +1,17 @@ +### button-support-favorite-variant [(#11853)](https://github.com/patternfly/patternfly-react/pull/11853) + +This rule detects when you're using `StarIcon` with Button components and suggests using the new `isFavorite` and `isFavorited` props instead. This supports the new favorite variant that includes animation styling. + +#### Examples + +In: + +```jsx +%inputExample% +``` + +Out: + +```jsx +%outputExample% +``` diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/buttonSupportFavoriteVariant/button-support-favorite-variant.test.ts b/packages/eslint-plugin-pf-codemods/src/rules/v6/buttonSupportFavoriteVariant/button-support-favorite-variant.test.ts new file mode 100644 index 00000000..cc542abf --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/buttonSupportFavoriteVariant/button-support-favorite-variant.test.ts @@ -0,0 +1,127 @@ +const ruleTester = require("../../ruletester"); +import * as rule from "./button-support-favorite-variant"; + +ruleTester.run("button-support-favorite-variant", rule, { + valid: [ + { + code: ``, + }, + { + code: `import { Button } from '@patternfly/react-core'; import { SomeIcon } from '@patternfly/react-icons'; `, + }, + { + code: `import { Button } from '@patternfly/react-core'; `, + }, + // Button not from PF, StarIcon from PF - should not trigger + { + code: `import { Button } from '@some-other-package'; import { StarIcon } from '@patternfly/react-icons'; `, + }, + // Both Button and StarIcon not from PF - should not trigger + { + code: `import { Button } from '@some-other-package'; import { StarIcon } from '@another-package'; `, + }, + // Button from PF, StarIcon not from PF (in icon prop) - should not trigger + { + code: `import { Button } from '@patternfly/react-core'; import { StarIcon } from '@some-other-package'; `, + errors: [ + { + message: "It looks like you are trying to create a custom favorites button. Use `isFavorite` and `isFavorited` button properties instead to apply the correct styling.", + type: "JSXElement", + }, + ], + }, + // StarIcon in icon prop + { + code: `import { Button } from '@patternfly/react-core'; import { StarIcon } from '@patternfly/react-icons'; `, + errors: [ + { + message: "It looks like you are trying to create a custom favorites button. Use `isFavorite` and `isFavorited` button properties instead to apply the correct styling.", + type: "JSXElement", + }, + ], + }, + // Button with aliased import + { + code: `import { Button as PFButton } from '@patternfly/react-core'; import { StarIcon } from '@patternfly/react-icons'; `, + errors: [ + { + message: "It looks like you are trying to create a custom favorites button. Use `isFavorite` and `isFavorited` button properties instead to apply the correct styling.", + type: "JSXElement", + }, + ], + }, + // StarIcon with text + { + code: `import { Button } from '@patternfly/react-core'; import { StarIcon } from '@patternfly/react-icons'; `, + errors: [ + { + message: "It looks like you are trying to create a custom favorites button. Use `isFavorite` and `isFavorited` button properties instead to apply the correct styling.", + type: "JSXElement", + }, + ], + }, + // StarIcon with other icon + { + code: `import { Button } from '@patternfly/react-core'; import { StarIcon, SomeIcon } from '@patternfly/react-icons'; `, + errors: [ + { + message: "It looks like you are trying to create a custom favorites button. Use `isFavorite` and `isFavorited` button properties instead to apply the correct styling.", + type: "JSXElement", + }, + ], + }, + // StarIcon in fragment with text + { + code: `import { Button } from '@patternfly/react-core'; import { StarIcon } from '@patternfly/react-icons'; `, + errors: [ + { + message: "It looks like you are trying to create a custom favorites button. Use `isFavorite` and `isFavorited` button properties instead to apply the correct styling.", + type: "JSXElement", + }, + ], + }, + // StarIcon with existing props + { + code: `import { Button } from '@patternfly/react-core'; import { StarIcon } from '@patternfly/react-icons'; `, + errors: [ + { + message: "It looks like you are trying to create a custom favorites button. Use `isFavorite` and `isFavorited` button properties instead to apply the correct styling.", + type: "JSXElement", + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/buttonSupportFavoriteVariant/button-support-favorite-variant.ts b/packages/eslint-plugin-pf-codemods/src/rules/v6/buttonSupportFavoriteVariant/button-support-favorite-variant.ts new file mode 100644 index 00000000..f948ef56 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/buttonSupportFavoriteVariant/button-support-favorite-variant.ts @@ -0,0 +1,90 @@ +import { Rule } from "eslint"; +import { JSXElement, JSXIdentifier, JSXFragment } from "estree-jsx"; +import { + getFromPackage, + getAttribute, + getAttributeValue, + isReactIcon, +} from "../../helpers"; + +// https://www.patternfly.org/components/button/#favorite +module.exports = { + meta: {}, + create: function (context: Rule.RuleContext) { + const { imports: buttonImports } = getFromPackage(context, "@patternfly/react-core"); + const { imports: iconImports } = getFromPackage(context, "@patternfly/react-icons"); + + const buttonImport = buttonImports.find( + (specifier) => specifier.imported.name === "Button" + ); + + if (!buttonImport) { + return {}; + } + + // Helper function to check if a JSX element is a StarIcon + const isStarIcon = (element: JSXElement) => { + return isReactIcon(context, element) && + iconImports.some( + (specifier) => specifier.imported.name === "StarIcon" && + specifier.local.name === (element.openingElement.name as JSXIdentifier).name + ); + }; + + // Helper function to recursively check for StarIcon in children + const hasStarIconInChildren = (children: any[]): boolean => { + return children.some((child) => { + if (child.type === "JSXElement") { + return isStarIcon(child); + } + if (child.type === "JSXFragment") { + return hasStarIconInChildren(child.children); + } + return false; + }); + }; + + return { + JSXElement(node: JSXElement) { + if ( + node.openingElement.name.type === "JSXIdentifier" && + buttonImport.local.name === node.openingElement.name.name + ) { + // Check if Button already has isFavorite or isFavorited props + const isFavoriteProp = getAttribute(node.openingElement, "isFavorite"); + const isFavoritedProp = getAttribute(node.openingElement, "isFavorited"); + + if (isFavoriteProp || isFavoritedProp) { + return; // Already using the new props + } + + // Check for StarIcon in the icon prop + const iconProp = getAttribute(node.openingElement, "icon"); + if (iconProp && iconProp.value?.type === "JSXExpressionContainer") { + const iconExpression = iconProp.value.expression; + if (iconExpression.type === "JSXElement" && isStarIcon(iconExpression)) { + context.report({ + node, + message: `It looks like you are trying to create a custom favorites button. Use \`isFavorite\` and \`isFavorited\` button properties instead to apply the correct styling.`, + }); + return; + } + } + + // Check for StarIcon in children (including fragments) + const hasStarIconChild = hasStarIconInChildren(node.children); + + if (!hasStarIconChild) { + return; // No StarIcon found + } + + // Report warning for all cases + context.report({ + node, + message: `It looks like you are trying to create a custom favorites button. Use \`isFavorite\` and \`isFavorited\` button properties instead to apply the correct styling.`, + }); + } + }, + }; + }, +}; diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/buttonSupportFavoriteVariant/buttonSupportFavoriteVariantInput.tsx b/packages/eslint-plugin-pf-codemods/src/rules/v6/buttonSupportFavoriteVariant/buttonSupportFavoriteVariantInput.tsx new file mode 100644 index 00000000..78bed3bb --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/buttonSupportFavoriteVariant/buttonSupportFavoriteVariantInput.tsx @@ -0,0 +1,39 @@ +import { Button } from "@patternfly/react-core"; +import { StarIcon, SomeIcon } from "@patternfly/react-icons"; + +export const ButtonSupportFavoriteVariantInput = () => ( + <> + {/* Simple case - can be auto-fixed */} + + + {/* StarIcon in icon prop - can be auto-fixed */} + + + {/* StarIcon with text - warning only */} + + + {/* StarIcon with other icon - warning only */} + + + {/* StarIcon in fragment - warning only */} + + + {/* Already using new props - should be ignored */} + + + {/* StarIcon in icon prop - warning only */} + + + {/* StarIcon with text - warning only */} + + + {/* StarIcon with other icon - warning only */} + + + {/* StarIcon in fragment - warning only */} + + + {/* Already using new props - no warning */} + +); +``` + +Out: + +```jsx +import { Button } from "@patternfly/react-core"; +import { StarIcon } from "@patternfly/react-icons"; + +export const ButtonSupportFavoriteVariantOutput = () => ( +