-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
0fb616b
commit 7ae6100
Showing
9 changed files
with
434 additions
and
0 deletions.
There are no files selected for viewing
41 changes: 41 additions & 0 deletions
41
packages/eslint-plugin-pf-codemods/src/rules/helpers/getNodeForAttributeFixer.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. `<Comp prop="value" />`, or | ||
* is passed an identifier, e.g. `const val = "value"; <Comp prop={val} />` | ||
*/ | ||
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; | ||
} | ||
} |
39 changes: 39 additions & 0 deletions
39
packages/eslint-plugin-pf-codemods/src/rules/helpers/getObjectProperty.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
42 changes: 42 additions & 0 deletions
42
packages/eslint-plugin-pf-codemods/src/rules/helpers/removePropertiesFromObjectExpression.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
17 changes: 17 additions & 0 deletions
17
...demods/src/rules/v6/cardUpdatedClickableMarkup/card-updated-clickable-markup.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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% | ||
``` |
121 changes: 121 additions & 0 deletions
121
...pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/card-updated-clickable-markup.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: `<Card isClickable />`, | ||
}, | ||
{ | ||
code: `<Card isClickable><CardHeader selectableActions={{name: 'Test', selectableActionId: 'Id' }} /></Card>`, | ||
}, | ||
{ | ||
code: `import { Card } from '@patternfly/react-core'; <Card someOtherProp />`, | ||
}, | ||
{ | ||
code: `import { Card } from '@patternfly/react-core'; <Card isClickable><CardHeader selectableActions={{name: 'Test', selectableActionId: 'Id'}} /></Card>`, | ||
}, | ||
{ | ||
code: `import { CardHeader } from '@patternfly/react-core'; <Card isClickable><CardHeader selectableActions={{name: 'Test', selectableActionId: 'Id'}} /></Card>`, | ||
}, | ||
], | ||
invalid: [ | ||
{ | ||
code: `import { Card, CardHeader } from '@patternfly/react-core'; <Card isClickable><CardHeader selectableActions={{to: "#"}} /></Card>`, | ||
output: `import { Card, CardHeader } from '@patternfly/react-core'; <Card isClickable><CardHeader selectableActions={{to: "#"}} /></Card>`, | ||
errors: [ | ||
{ | ||
message: "The markup for clickable-only cards has been updated.", | ||
type: "JSXElement", | ||
}, | ||
], | ||
}, | ||
{ | ||
code: `import { Card, CardHeader } from '@patternfly/react-core'; <Card isClickable><CardHeader selectableActions={{name: 'Test', selectableActionId: 'Id'}} /></Card>`, | ||
output: `import { Card, CardHeader } from '@patternfly/react-core'; <Card isClickable><CardHeader selectableActions={{}} /></Card>`, | ||
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'; <Card isClickable><CardHeader selectableActions={{to: "#", name: 'Test', selectableActionId: 'Id'}} /></Card>`, | ||
output: `import { Card, CardHeader } from '@patternfly/react-core'; <Card isClickable><CardHeader selectableActions={{to: "#"}} /></Card>`, | ||
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'}; <Card isClickable><CardHeader selectableActions={obj} /></Card>`, | ||
output: `import { Card, CardHeader } from '@patternfly/react-core'; const obj = {}; <Card isClickable><CardHeader selectableActions={obj} /></Card>`, | ||
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'}; <Card isClickable><CardHeader selectableActions={obj} /></Card>`, | ||
output: `import { Card, CardHeader } from '@patternfly/react-core'; const obj = {to: "#", extra: "thing"}; <Card isClickable><CardHeader selectableActions={obj} /></Card>`, | ||
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'; <CustomCard isClickable><CustomCardHeader selectableActions={{to: "#"}} /></CustomCard>`, | ||
output: `import { Card as CustomCard, CardHeader as CustomCardHeader } from '@patternfly/react-core'; <CustomCard isClickable><CustomCardHeader selectableActions={{to: "#"}} /></CustomCard>`, | ||
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'; <Card isClickable><CardHeader selectableActions={{to: "#"}} /></Card>`, | ||
output: `import { Card, CardHeader } from '@patternfly/react-core/dist/esm/components/Card/index.js'; <Card isClickable><CardHeader selectableActions={{to: "#"}} /></Card>`, | ||
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'; <Card isClickable><CardHeader selectableActions={{to: "#"}} /></Card>`, | ||
output: `import { Card, CardHeader } from '@patternfly/react-core/dist/js/components/Card/index.js'; <Card isClickable><CardHeader selectableActions={{to: "#"}} /></Card>`, | ||
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'; <Card isClickable><CardHeader selectableActions={{to: "#"}} /></Card>`, | ||
output: `import { Card, CardHeader } from '@patternfly/react-core/dist/dynamic/components/Card/index.js'; <Card isClickable><CardHeader selectableActions={{to: "#"}} /></Card>`, | ||
errors: [ | ||
{ | ||
message: "The markup for clickable-only cards has been updated.", | ||
type: "JSXElement", | ||
}, | ||
], | ||
}, | ||
], | ||
}); |
118 changes: 118 additions & 0 deletions
118
...ugin-pf-codemods/src/rules/v6/cardUpdatedClickableMarkup/card-updated-clickable-markup.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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}}` | ||
: "{}" | ||
); | ||
}, | ||
}); | ||
} | ||
}, | ||
}; | ||
}, | ||
}; |
Oops, something went wrong.