Skip to content

Commit

Permalink
feat(MenuToggle): remove splitButtonOptions prop and interface (#782)
Browse files Browse the repository at this point in the history
* feat(getVariableValue helper): get value of Identifier

- also refactoring of getAttributeValue

* feat(MenuToggle): remove splitButtonOptions prop and interface

* fix undefined error
  • Loading branch information
adamviktora authored Oct 2, 2024
1 parent 3e2ff77 commit 8399fc7
Show file tree
Hide file tree
Showing 6 changed files with 355 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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%
```

Original file line number Diff line number Diff line change
@@ -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: `<MenuToggle splitButtonOptions={{ items: ["Item 1", "Item 2"], variant: "action" }} />`,
},
{
code: `import { MenuToggle } from '@patternfly/react-core'; <MenuToggle someOtherProp />`,
},
],
invalid: [
{
// object expression with "items" property - direct value
code: `import { MenuToggle } from '@patternfly/react-core';
<MenuToggle splitButtonOptions={{ items: ["Item 1", "Item 2"], variant: "action" }} />`,
output: `import { MenuToggle } from '@patternfly/react-core';
<MenuToggle splitButtonItems={["Item 1", "Item 2"]} />`,
errors: [generalError],
},
{
// object expression with "items" property - in a variable
code: `import { MenuToggle } from '@patternfly/react-core';
const sbItems = ["Item 1", "Item 2"];
<MenuToggle splitButtonOptions={{ items: sbItems, variant: "action" }} />`,
output: `import { MenuToggle } from '@patternfly/react-core';
const sbItems = ["Item 1", "Item 2"];
<MenuToggle splitButtonItems={sbItems} />`,
errors: [generalError],
},
{
// identifier
code: `import { MenuToggle } from '@patternfly/react-core'; import { optionsObject } from 'somewhere';
<MenuToggle splitButtonOptions={optionsObject} />`,
output: `import { MenuToggle } from '@patternfly/react-core'; import { optionsObject } from 'somewhere';
<MenuToggle splitButtonItems={optionsObject.items} />`,
errors: [generalError],
},
{
// object expression with a spreaded object
code: `import { MenuToggle } from '@patternfly/react-core'; import { optionsObject } from 'somewhere';
<MenuToggle splitButtonOptions={{ ...optionsObject }} />`,
output: `import { MenuToggle } from '@patternfly/react-core'; import { optionsObject } from 'somewhere';
<MenuToggle splitButtonItems={optionsObject.items} />`,
errors: [generalError],
},
{
// identifier + SplitButtonOptions type
code: `import { MenuToggle, SplitButtonOptions } from '@patternfly/react-core';
const sbOptions: SplitButtonOptions = { items: sbItems, variant: "action" };
<MenuToggle splitButtonOptions={sbOptions} />`,
output: `import { MenuToggle, } from '@patternfly/react-core';
const sbOptions = { items: sbItems, variant: "action" };
<MenuToggle splitButtonItems={sbOptions.items} />`,
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",
},
],
},
],
});
Original file line number Diff line number Diff line change
@@ -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);
},
});
}
},
};
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { MenuToggle, SplitButtonOptions } from "@patternfly/react-core";

const sbOptions: SplitButtonOptions = {
items: ["Item 1", "Item 2"],
variant: "action",
};

export const MenuToggleRemoveSplitButtonOptionsInput = () => (
<>
<MenuToggle
splitButtonOptions={{
items: ["Item 1", "Item 2"],
variant: "action",
}}
></MenuToggle>
<MenuToggle splitButtonOptions={sbOptions}></MenuToggle>
</>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { MenuToggle, } from "@patternfly/react-core";

const sbOptions = {
items: ["Item 1", "Item 2"],
variant: "action",
};

export const MenuToggleRemoveSplitButtonOptionsInput = () => (
<>
<MenuToggle
splitButtonItems={["Item 1", "Item 2"]}
></MenuToggle>
<MenuToggle splitButtonItems={sbOptions.items}></MenuToggle>
</>
);

0 comments on commit 8399fc7

Please sign in to comment.