From e45001e42d28fdf84da6ec7252dcd2c2f70896e5 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Fri, 16 Aug 2024 10:46:12 -0400 Subject: [PATCH] Added helpers and rule logic --- .../rules/helpers/getNodeForAttributeFixer.ts | 41 ++++++ .../src/rules/helpers/getObjectProperty.ts | 39 ++++++ .../src/rules/helpers/index.ts | 3 + .../removePropertiesFromObjectExpression.ts | 42 ++++++ .../card-updated-clickable-markup.md | 17 +++ .../card-updated-clickable-markup.test.ts | 121 ++++++++++++++++++ .../card-updated-clickable-markup.ts | 118 +++++++++++++++++ .../cardUpdatedClickableMarkupInput.tsx | 31 +++++ .../cardUpdatedClickableMarkupOutput.tsx | 22 ++++ 9 files changed, 434 insertions(+) create mode 100644 packages/eslint-plugin-pf-codemods/src/rules/helpers/getNodeForAttributeFixer.ts create mode 100644 packages/eslint-plugin-pf-codemods/src/rules/helpers/getObjectProperty.ts create mode 100644 packages/eslint-plugin-pf-codemods/src/rules/helpers/removePropertiesFromObjectExpression.ts create mode 100644 packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/card-updated-clickable-markup.md create mode 100644 packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/card-updated-clickable-markup.test.ts create mode 100644 packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/card-updated-clickable-markup.ts create mode 100644 packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/cardUpdatedClickableMarkupInput.tsx create mode 100644 packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/cardUpdatedClickableMarkupOutput.tsx diff --git a/packages/eslint-plugin-pf-codemods/src/rules/helpers/getNodeForAttributeFixer.ts b/packages/eslint-plugin-pf-codemods/src/rules/helpers/getNodeForAttributeFixer.ts new file mode 100644 index 00000000..a55ecd0d --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/getNodeForAttributeFixer.ts @@ -0,0 +1,41 @@ +import { Rule } from "eslint"; +import { JSXAttribute } from "estree-jsx"; +import { getVariableDeclaration } from "./JSXAttributes"; + +/** Used to find the node where a prop value is initially assigned, to then be passed + * as a fixer function's nodeOrToken argument. Useful for when a prop may have an inline value, e.g. ``, or + * is passed an identifier, e.g. `const val = "value"; ` + */ +export function getNodeForAttributeFixer( + context: Rule.RuleContext, + attribute: JSXAttribute +) { + if (!attribute.value) { + return; + } + + if ( + attribute.value.type === "JSXExpressionContainer" && + attribute.value.expression.type === "Identifier" + ) { + const scope = context.getSourceCode().getScope(attribute); + const variableDeclaration = getVariableDeclaration( + attribute.value.expression.name, + scope + ); + + return variableDeclaration && variableDeclaration.defs[0].node.init; + } + + if (attribute.value.type === "Literal") { + return attribute.value; + } + if ( + attribute.value.type === "JSXExpressionContainer" && + ["ObjectExpression", "MemberExpression"].includes( + attribute.value.expression.type + ) + ) { + return attribute.value.expression; + } +} diff --git a/packages/eslint-plugin-pf-codemods/src/rules/helpers/getObjectProperty.ts b/packages/eslint-plugin-pf-codemods/src/rules/helpers/getObjectProperty.ts new file mode 100644 index 00000000..8446eaf2 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/getObjectProperty.ts @@ -0,0 +1,39 @@ +import { Rule } from "eslint"; +import { Property, Identifier } from "estree-jsx"; +import { getVariableDeclaration } from "./JSXAttributes"; + +/** Can be used to run logic on the specified property of an ObjectExpression or + * only if the specified property exists. + */ +export function getObjectProperty( + context: Rule.RuleContext, + properties: Property[], + name: string +) { + if (!properties.length) { + return; + } + + const matchedProperty = properties.find((property) => { + const isIdentifier = property.key.type === "Identifier"; + const { computed } = property; + + // E.g. const key = "key"; {[key]: value} + if (isIdentifier && computed) { + const scope = context.getSourceCode().getScope(property); + const propertyName = (property.key as Identifier).name; + const propertyVariable = getVariableDeclaration(propertyName, scope); + return propertyVariable?.defs[0].node.init.value === name; + } + // E.g. {key: value} + if (isIdentifier && !computed) { + return (property.key as Identifier).name === name; + } + // E.g. {"key": value} or {["key"]: value} + if (property.key.type === "Literal") { + return property.key.value === name; + } + }); + + return matchedProperty; +} 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 dc0bbe6b..19608166 100644 --- a/packages/eslint-plugin-pf-codemods/src/rules/helpers/index.ts +++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/index.ts @@ -10,7 +10,9 @@ export * from "./getEndRange"; export * from "./getFromPackage"; export * from "./getImportedName"; export * from "./getLocalComponentName"; +export * from "./getNodeForAttributeFixer"; export * from "./getNodeName"; +export * from "./getObjectProperty"; export * from "./getSpecifierFromImports"; export * from "./getText"; export * from "./hasCodemodDataTag"; @@ -24,4 +26,5 @@ export * from "./nodeMatches"; export * from "./pfPackageMatches"; export * from "./removeElement"; export * from "./removeEmptyLineAfter"; +export * from "./removePropertiesFromObjectExpression"; export * from "./renameProps"; diff --git a/packages/eslint-plugin-pf-codemods/src/rules/helpers/removePropertiesFromObjectExpression.ts b/packages/eslint-plugin-pf-codemods/src/rules/helpers/removePropertiesFromObjectExpression.ts new file mode 100644 index 00000000..28822924 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/removePropertiesFromObjectExpression.ts @@ -0,0 +1,42 @@ +import { Property } from "estree-jsx"; + +/** Can be used to take the returned array and join it into a replacement string of object + * key:value pairs. + */ +export function removePropertiesFromObjectExpression( + currentProperties: Property[], + propertiesToRemove: (Property | undefined)[] +) { + if (!currentProperties) { + return []; + } + if (!propertiesToRemove) { + return currentProperties; + } + + const propertyNamesToRemove = propertiesToRemove.map((property) => { + if (property?.key.type === "Identifier") { + return property.key.name; + } + + if (property?.key.type === "Literal") { + return property.key.value; + } + + return ""; + }); + + const propertiesToKeep = currentProperties.filter((property) => { + if (property.key.type === "Identifier") { + return !propertyNamesToRemove.includes(property.key.name); + } + + if (property.key.type === "Literal") { + return propertyNamesToRemove.includes(property.key.value); + } + + return false; + }); + + return propertiesToKeep; +} diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/card-updated-clickable-markup.md b/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/card-updated-clickable-markup.md new file mode 100644 index 00000000..3bf9cfbc --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/card-updated-clickable-markup.md @@ -0,0 +1,17 @@ +### card-updated-clickable-markup [(#10859)](https://github.com/patternfly/patternfly-react/pull/10859) + +The markup for clickable-only cards has been updated. Additionally, the `selectableActions.selectableActionId` and `selectableActions.name` props are no longer necessary to pass to CardHeader for clickable-only cards. Passing them in will not cause any errors, but running the fix for this rule will remove them. + +#### Examples + +In: + +```jsx +%inputExample% +``` + +Out: + +```jsx +%outputExample% +``` diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/card-updated-clickable-markup.test.ts b/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/card-updated-clickable-markup.test.ts new file mode 100644 index 00000000..d61619ae --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/card-updated-clickable-markup.test.ts @@ -0,0 +1,121 @@ +const ruleTester = require("../../ruletester"); +import * as rule from "./card-updated-clickable-markup"; + +ruleTester.run("card-updated-clickable-markup", rule, { + valid: [ + { + code: ``, + }, + { + code: ``, + }, + { + code: `import { Card } from '@patternfly/react-core'; `, + }, + { + code: `import { Card } from '@patternfly/react-core'; `, + }, + { + code: `import { CardHeader } from '@patternfly/react-core'; `, + }, + ], + invalid: [ + { + code: `import { Card, CardHeader } from '@patternfly/react-core'; `, + output: `import { Card, CardHeader } from '@patternfly/react-core'; `, + errors: [ + { + message: "The markup for clickable-only cards has been updated.", + type: "JSXElement", + }, + ], + }, + { + code: `import { Card, CardHeader } from '@patternfly/react-core'; `, + output: `import { Card, CardHeader } from '@patternfly/react-core'; `, + errors: [ + { + message: + "The markup for clickable-only cards has been updated.Additionally, the `selectableActions.selectableActionId` and `selectableActions.name` props are no longer necessary to pass to CardHeader for clickable-only cards.", + type: "JSXElement", + }, + ], + }, + { + code: `import { Card, CardHeader } from '@patternfly/react-core'; `, + output: `import { Card, CardHeader } from '@patternfly/react-core'; `, + errors: [ + { + message: + "The markup for clickable-only cards has been updated.Additionally, the `selectableActions.selectableActionId` and `selectableActions.name` props are no longer necessary to pass to CardHeader for clickable-only cards.", + // message: `The markup for clickable-only cards has been updated, now using button and anchor elements for the respective clickable action. The \`selectableActions.selectableActionId\` and \`selectableActions.name\` props are also no longer necessary for clickable-only cards. Passing them in will not cause any errors, but running the fix for this rule will remove them.`, + type: "JSXElement", + }, + ], + }, + { + code: `import { Card, CardHeader } from '@patternfly/react-core'; const obj = {name: 'Test', selectableActionId: 'Id'}; `, + output: `import { Card, CardHeader } from '@patternfly/react-core'; const obj = {}; `, + errors: [ + { + message: + "The markup for clickable-only cards has been updated.Additionally, the `selectableActions.selectableActionId` and `selectableActions.name` props are no longer necessary to pass to CardHeader for clickable-only cards.", + type: "JSXElement", + }, + ], + }, + { + code: `import { Card, CardHeader } from '@patternfly/react-core'; const obj = {to: "#", name: 'Test', extra: "thing", selectableActionId: 'Id'}; `, + output: `import { Card, CardHeader } from '@patternfly/react-core'; const obj = {to: "#", extra: "thing"}; `, + errors: [ + { + message: + "The markup for clickable-only cards has been updated.Additionally, the `selectableActions.selectableActionId` and `selectableActions.name` props are no longer necessary to pass to CardHeader for clickable-only cards.", + type: "JSXElement", + }, + ], + }, + // Aliased + { + code: `import { Card as CustomCard, CardHeader as CustomCardHeader } from '@patternfly/react-core'; `, + output: `import { Card as CustomCard, CardHeader as CustomCardHeader } from '@patternfly/react-core'; `, + errors: [ + { + message: "The markup for clickable-only cards has been updated.", + type: "JSXElement", + }, + ], + }, + // Dist Imports + { + code: `import { Card, CardHeader } from '@patternfly/react-core/dist/esm/components/Card/index.js'; `, + output: `import { Card, CardHeader } from '@patternfly/react-core/dist/esm/components/Card/index.js'; `, + errors: [ + { + message: "The markup for clickable-only cards has been updated.", + type: "JSXElement", + }, + ], + }, + { + code: `import { Card, CardHeader } from '@patternfly/react-core/dist/js/components/Card/index.js'; `, + output: `import { Card, CardHeader } from '@patternfly/react-core/dist/js/components/Card/index.js'; `, + errors: [ + { + message: "The markup for clickable-only cards has been updated.", + type: "JSXElement", + }, + ], + }, + { + code: `import { Card, CardHeader } from '@patternfly/react-core/dist/dynamic/components/Card/index.js'; `, + output: `import { Card, CardHeader } from '@patternfly/react-core/dist/dynamic/components/Card/index.js'; `, + errors: [ + { + message: "The markup for clickable-only cards has been updated.", + type: "JSXElement", + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/card-updated-clickable-markup.ts b/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/card-updated-clickable-markup.ts new file mode 100644 index 00000000..c440f4e8 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/card-updated-clickable-markup.ts @@ -0,0 +1,118 @@ +import { Rule } from "eslint"; +import { JSXElement, Property, Literal } from "estree-jsx"; +import { + getAllImportsFromPackage, + checkMatchingJSXOpeningElement, + getAttribute, + getAttributeValue, + getSpecifierFromImports, + getChildJSXElementByName, + getObjectProperty, + removePropertiesFromObjectExpression, + getNodeForAttributeFixer, +} from "../../helpers"; + +// https://github.com/patternfly/patternfly-react/pull/10859 +module.exports = { + meta: { fixable: "code" }, + create: function (context: Rule.RuleContext) { + const basePackage = "@patternfly/react-core"; + const componentImports = getAllImportsFromPackage(context, basePackage, [ + "Card", + "CardHeader", + ]); + const cardImport = getSpecifierFromImports(componentImports, "Card"); + const cardHeaderImport = getSpecifierFromImports( + componentImports, + "CardHeader" + ); + + // Actionable cards won't work as intended if both components aren't imported, hence + // we shouldn't need to relay any message if that is the case + return !cardImport || !cardHeaderImport + ? {} + : { + JSXElement(node: JSXElement) { + if ( + checkMatchingJSXOpeningElement(node.openingElement, cardImport) + ) { + const isClickableProp = getAttribute(node, "isClickable"); + const isSelectableProp = getAttribute(node, "isSelectable"); + + if ((isClickableProp && isSelectableProp) || !isClickableProp) { + return; + } + + const cardHeaderChild = getChildJSXElementByName( + node, + cardHeaderImport.local.name + ); + const selectableActionsProp = cardHeaderChild + ? getAttribute(cardHeaderChild, "selectableActions") + : undefined; + if (!cardHeaderChild || !selectableActionsProp) { + return; + } + const selectableActionsValue = getAttributeValue( + context, + selectableActionsProp.value + ); + const nameProperty = getObjectProperty( + context, + selectableActionsValue, + "name" + ); + const idProperty = getObjectProperty( + context, + selectableActionsValue, + "selectableActionId" + ); + + const baseMessage = + "The markup for clickable-only cards has been updated."; + const message = `${baseMessage}${ + nameProperty || idProperty + ? "Additionally, the `selectableActions.selectableActionId` and `selectableActions.name` props are no longer necessary to pass to CardHeader for clickable-only cards." + : "" + }`; + context.report({ + node, + message, + fix(fixer) { + const validPropertiesToRemove = [ + nameProperty, + idProperty, + ].filter((property) => !!property); + if ( + !validPropertiesToRemove.length || + !selectableActionsProp.value + ) { + return []; + } + const propertiesToKeep = removePropertiesFromObjectExpression( + selectableActionsValue, + validPropertiesToRemove + ); + const replacementProperties = propertiesToKeep + .map((property: Property) => + context.getSourceCode().getText(property) + ) + .join(", "); + + const nodeToUpdate = getNodeForAttributeFixer( + context, + selectableActionsProp + ); + return fixer.replaceText( + nodeToUpdate, + propertiesToKeep.length + ? `{${replacementProperties}}` + : "{}" + ); + }, + }); + } + }, + }; + }, +}; diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/cardUpdatedClickableMarkupInput.tsx b/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/cardUpdatedClickableMarkupInput.tsx new file mode 100644 index 00000000..5df735c1 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/cardUpdatedClickableMarkupInput.tsx @@ -0,0 +1,31 @@ +import { Card, CardHeader } from "@patternfly/react-core"; + +export const CardUpdatedClickableMarkupInput = () => { + const selectableActionsObj = { name: "Test2", selectableActionId: "Id2" }; + const selectableActionsObjMany = { + to: "#", + name: "Test2", + selectableActionProps: {}, + selectableActionId: "Id2", + }; + + return ( + <> + + {}, + }} + /> + + + + + + + + + ); +}; diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/cardUpdatedClickableMarkupOutput.tsx b/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/cardUpdatedClickableMarkupOutput.tsx new file mode 100644 index 00000000..469780ad --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/cardUpdatedClickableMarkupOutput.tsx @@ -0,0 +1,22 @@ +import { Card, CardHeader } from "@patternfly/react-core"; + +export const CardUpdatedClickableMarkupInput = () => { + const selectableActionsObj = {}; + const selectableActionsObjMany = {to: "#", selectableActionProps: {}}; + + return ( + <> + + + + + + + + + + + ); +};