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 b9b783c9..9ae2c1c9 100644 --- a/packages/eslint-plugin-pf-codemods/src/rules/helpers/JSXAttributes.ts +++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/JSXAttributes.ts @@ -49,18 +49,21 @@ export function getAttributeValue( return node.value; } - const isExpressionContainer = valueType === "JSXExpressionContainer"; - if (isExpressionContainer && node.expression.type === "Identifier") { + if (valueType !== "JSXExpressionContainer") { + return; + } + + if (node.expression.type === "Identifier") { const variableScope = context.getSourceCode().getScope(node); - return getVariableValue(node.expression.name, variableScope); + return getVariableValue(node.expression.name, variableScope, context); } - if (isExpressionContainer && node.expression.type === "MemberExpression") { + if (node.expression.type === "MemberExpression") { return getMemberExpression(node.expression); } - if (isExpressionContainer && node.expression.type === "Literal") { + if (node.expression.type === "Literal") { return node.expression.value; } - if (isExpressionContainer && node.expression.type === "ObjectExpression") { + if (node.expression.type === "ObjectExpression") { return node.expression.properties; } } @@ -100,7 +103,11 @@ export function getVariableDeclaration( return undefined; } -export function getVariableValue(name: string, scope: Scope.Scope | null) { +export function getVariableValue( + name: string, + scope: Scope.Scope | null, + context: Rule.RuleContext +) { const variableDeclaration = getVariableDeclaration(name, scope); if (!variableDeclaration) { return; @@ -113,6 +120,13 @@ export function getVariableValue(name: string, scope: Scope.Scope | null) { if (!variableInit) { return; } + if (variableInit.type === "Identifier") { + return getVariableValue( + variableInit.name, + context.getSourceCode().getScope(variableInit), + context + ); + } if (variableInit.type === "Literal") { return variableInit.value; } diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/menuToggleRemoveSplitButtonOptions/menuToggle-remove-splitButtonOptions.md b/packages/eslint-plugin-pf-codemods/src/rules/v6/menuToggleRemoveSplitButtonOptions/menuToggle-remove-splitButtonOptions.md new file mode 100644 index 00000000..ddcebb0c --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/menuToggleRemoveSplitButtonOptions/menuToggle-remove-splitButtonOptions.md @@ -0,0 +1,18 @@ +### menuToggle-remove-splitButtonOptions [(#11096)](https://github.com/patternfly/patternfly-react/pull/11096) + +We have replaced `splitButtonOptions` prop on MenuToggle with `splitButtonItems`. SplitButtonOptions interface has been deleted, because its `variant` prop no longer supports the "action" option. The `items` prop of SplitButtonOptions will be passed directly to MenuToggle's new `splitButtonItems` prop. + +#### Examples + +In: + +```jsx +%inputExample% +``` + +Out: + +```jsx +%outputExample% +``` + diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/menuToggleRemoveSplitButtonOptions/menuToggle-remove-splitButtonOptions.test.ts b/packages/eslint-plugin-pf-codemods/src/rules/v6/menuToggleRemoveSplitButtonOptions/menuToggle-remove-splitButtonOptions.test.ts new file mode 100644 index 00000000..1bbcd79e --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/menuToggleRemoveSplitButtonOptions/menuToggle-remove-splitButtonOptions.test.ts @@ -0,0 +1,112 @@ +const ruleTester = require("../../ruletester"); +import * as rule from "./menuToggle-remove-splitButtonOptions"; + +const message = + "We have replaced `splitButtonOptions` prop on MenuToggle with `splitButtonItems`. SplitButtonOptions interface has been removed, because its `variant` prop no longer supports the 'action' option. The `items` prop of SplitButtonOptions will be passed directly to MenuToggle's new `splitButtonItems` prop."; +const interfaceRemovedMessage = `The SplitButtonOptions interface has been removed.`; + +const generalError = { + message, + type: "JSXOpeningElement", +}; + +ruleTester.run("menuToggle-remove-splitButtonOptions", rule, { + valid: [ + { + code: ``, + }, + { + code: `import { MenuToggle } from '@patternfly/react-core'; `, + }, + ], + invalid: [ + { + // object expression with "items" property - direct value + code: `import { MenuToggle } from '@patternfly/react-core'; + `, + output: `import { MenuToggle } from '@patternfly/react-core'; + `, + errors: [generalError], + }, + { + // object expression with "items" property - in a variable + code: `import { MenuToggle } from '@patternfly/react-core'; + const sbItems = ["Item 1", "Item 2"]; + `, + output: `import { MenuToggle } from '@patternfly/react-core'; + const sbItems = ["Item 1", "Item 2"]; + `, + errors: [generalError], + }, + { + // identifier + code: `import { MenuToggle } from '@patternfly/react-core'; import { optionsObject } from 'somewhere'; + `, + output: `import { MenuToggle } from '@patternfly/react-core'; import { optionsObject } from 'somewhere'; + `, + errors: [generalError], + }, + { + // object expression with a spreaded object + code: `import { MenuToggle } from '@patternfly/react-core'; import { optionsObject } from 'somewhere'; + `, + output: `import { MenuToggle } from '@patternfly/react-core'; import { optionsObject } from 'somewhere'; + `, + errors: [generalError], + }, + { + // identifier + SplitButtonOptions type + code: `import { MenuToggle, SplitButtonOptions } from '@patternfly/react-core'; + const sbOptions: SplitButtonOptions = { items: sbItems, variant: "action" }; + `, + output: `import { MenuToggle, } from '@patternfly/react-core'; + const sbOptions = { items: sbItems, variant: "action" }; + `, + errors: [ + { + message: interfaceRemovedMessage, + type: "ImportSpecifier", + }, + { + message: interfaceRemovedMessage, + type: "Identifier", + }, + generalError, + ], + }, + { + // SplitButtonOptions named export + code: `import { SplitButtonOptions } from '@patternfly/react-core'; + export { SplitButtonOptions as SBO };`, + output: ` + `, + errors: [ + { + message: interfaceRemovedMessage, + type: "ImportSpecifier", + }, + { + message: interfaceRemovedMessage, + type: "ExportNamedDeclaration", + }, + ], + }, + { + // SplitButtonOptions default export + code: `import { SplitButtonOptions } from '@patternfly/react-core'; + export default SplitButtonOptions;`, + output: ` + `, + errors: [ + { + message: interfaceRemovedMessage, + type: "ImportSpecifier", + }, + { + message: interfaceRemovedMessage, + type: "ExportDefaultDeclaration", + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/menuToggleRemoveSplitButtonOptions/menuToggle-remove-splitButtonOptions.ts b/packages/eslint-plugin-pf-codemods/src/rules/v6/menuToggleRemoveSplitButtonOptions/menuToggle-remove-splitButtonOptions.ts new file mode 100644 index 00000000..1b3ca0bf --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/menuToggleRemoveSplitButtonOptions/menuToggle-remove-splitButtonOptions.ts @@ -0,0 +1,171 @@ +import { Rule } from "eslint"; +import { + ExportDefaultDeclaration, + ExportNamedDeclaration, + Identifier, + ImportSpecifier, + JSXOpeningElement, + Property, + SpreadElement, +} from "estree-jsx"; +import { + checkMatchingJSXOpeningElement, + getAttribute, + getFromPackage, + getObjectProperty, + ImportSpecifierWithParent, + removeSpecifierFromDeclaration, +} from "../../helpers"; + +// https://github.com/patternfly/patternfly-react/pull/11096 +module.exports = { + meta: { fixable: "code" }, + create: function (context: Rule.RuleContext) { + const message = + "We have replaced `splitButtonOptions` prop on MenuToggle with `splitButtonItems`. SplitButtonOptions interface has been removed, because its `variant` prop no longer supports the 'action' option. The `items` prop of SplitButtonOptions will be passed directly to MenuToggle's new `splitButtonItems` prop."; + const interfaceRemovedMessage = `The SplitButtonOptions interface has been removed.`; + + const basePackage = "@patternfly/react-core"; + const { imports: menuToggleImports } = getFromPackage( + context, + basePackage, + ["MenuToggle"] + ); + const { imports: splitButtonOptionsImports } = getFromPackage( + context, + basePackage, + ["SplitButtonOptions"] + ); + const splitButtonOptionsLocalNames = splitButtonOptionsImports.map( + (specifier) => specifier.local.name + ); + + if (!menuToggleImports && !splitButtonOptionsImports) { + return; + } + + return { + JSXOpeningElement(node: JSXOpeningElement) { + if (!checkMatchingJSXOpeningElement(node, menuToggleImports)) { + return; + } + + const splitButtonOptionsProp = getAttribute(node, "splitButtonOptions"); + + if ( + !splitButtonOptionsProp || + splitButtonOptionsProp.value?.type !== "JSXExpressionContainer" + ) { + return; + } + + const reportAndFix = (splitButtonItemsValue: string) => { + context.report({ + node, + message, + fix(fixer) { + return fixer.replaceText( + splitButtonOptionsProp, + `splitButtonItems={${splitButtonItemsValue}}` + ); + }, + }); + }; + + const reportAndFixIdentifier = (identifier: Identifier) => { + reportAndFix(`${identifier.name}.items`); + }; + + const propValue = splitButtonOptionsProp.value.expression; + if (propValue.type === "Identifier") { + reportAndFixIdentifier(propValue); + } + + if (propValue.type === "ObjectExpression") { + const properties = propValue.properties.filter( + (prop) => prop.type === "Property" + ) as Property[]; + const itemsProperty = getObjectProperty(context, properties, "items"); + + if (itemsProperty) { + const itemsPropertyValueString = context + .getSourceCode() + .getText(itemsProperty.value); + + reportAndFix(itemsPropertyValueString); + } else { + const spreadElement = propValue.properties.find( + (prop) => prop.type === "SpreadElement" + ) as SpreadElement | undefined; + if (spreadElement && spreadElement.argument.type === "Identifier") { + reportAndFixIdentifier(spreadElement.argument); + } + } + } + }, + Identifier(node: Identifier) { + const typeName = (node as any).typeAnnotation?.typeAnnotation?.typeName + ?.name; + + if (splitButtonOptionsLocalNames.includes(typeName)) { + context.report({ + node, + message: interfaceRemovedMessage, + fix(fixer) { + return fixer.remove((node as any).typeAnnotation); + }, + }); + } + }, + ImportSpecifier(node: ImportSpecifier) { + if (splitButtonOptionsImports.includes(node)) { + context.report({ + node, + message: interfaceRemovedMessage, + fix(fixer) { + return removeSpecifierFromDeclaration( + fixer, + context, + (node as ImportSpecifierWithParent).parent!, + node + ); + }, + }); + } + }, + ExportNamedDeclaration(node: ExportNamedDeclaration) { + const specifierToRemove = node.specifiers.find((specifier) => + splitButtonOptionsLocalNames.includes(specifier.local.name) + ); + if (specifierToRemove) { + context.report({ + node, + message: interfaceRemovedMessage, + fix(fixer) { + return removeSpecifierFromDeclaration( + fixer, + context, + node, + specifierToRemove + ); + }, + }); + } + }, + ExportDefaultDeclaration(node: ExportDefaultDeclaration) { + if ( + node.declaration.type === "Identifier" && + splitButtonOptionsLocalNames.includes(node.declaration.name) + ) { + context.report({ + node, + message: interfaceRemovedMessage, + fix(fixer) { + return fixer.remove(node); + }, + }); + } + }, + }; + }, +}; diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/menuToggleRemoveSplitButtonOptions/menuToggleRemoveSplitButtonOptionsInput.tsx b/packages/eslint-plugin-pf-codemods/src/rules/v6/menuToggleRemoveSplitButtonOptions/menuToggleRemoveSplitButtonOptionsInput.tsx new file mode 100644 index 00000000..cd783f7a --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/menuToggleRemoveSplitButtonOptions/menuToggleRemoveSplitButtonOptionsInput.tsx @@ -0,0 +1,18 @@ +import { MenuToggle, SplitButtonOptions } from "@patternfly/react-core"; + +const sbOptions: SplitButtonOptions = { + items: ["Item 1", "Item 2"], + variant: "action", +}; + +export const MenuToggleRemoveSplitButtonOptionsInput = () => ( + <> + + + +); diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/menuToggleRemoveSplitButtonOptions/menuToggleRemoveSplitButtonOptionsOutput.tsx b/packages/eslint-plugin-pf-codemods/src/rules/v6/menuToggleRemoveSplitButtonOptions/menuToggleRemoveSplitButtonOptionsOutput.tsx new file mode 100644 index 00000000..6e10b13d --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/menuToggleRemoveSplitButtonOptions/menuToggleRemoveSplitButtonOptionsOutput.tsx @@ -0,0 +1,15 @@ +import { MenuToggle, } from "@patternfly/react-core"; + +const sbOptions = { + items: ["Item 1", "Item 2"], + variant: "action", +}; + +export const MenuToggleRemoveSplitButtonOptionsInput = () => ( + <> + + + +);