From 113caaaad170c79e75de522ae375c6c10849126e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pet=C5=99=C3=ADk?= <77832970+Dominik-Petrik@users.noreply.github.com> Date: Tue, 23 Apr 2024 22:05:11 +0200 Subject: [PATCH] chore(devex) Add types for renameProps helper (#607) * rebase * refactor * small refactoring * readability and naming improvements --------- Co-authored-by: Austin Sullivan --- .../src/rules/helpers/helpers.js | 243 +++++------------- .../src/rules/helpers/index.ts | 17 +- .../src/rules/helpers/renameProps.ts | 34 +++ .../src/rules/helpers/renamePropsOnNode.ts | 42 +++ .../rules/helpers/renameSinglePropOnNode.ts | 124 +++++++++ 5 files changed, 278 insertions(+), 182 deletions(-) create mode 100644 packages/eslint-plugin-pf-codemods/src/rules/helpers/renameProps.ts create mode 100644 packages/eslint-plugin-pf-codemods/src/rules/helpers/renamePropsOnNode.ts create mode 100644 packages/eslint-plugin-pf-codemods/src/rules/helpers/renameSinglePropOnNode.ts diff --git a/packages/eslint-plugin-pf-codemods/src/rules/helpers/helpers.js b/packages/eslint-plugin-pf-codemods/src/rules/helpers/helpers.js index c369cb084..a9c3bfb74 100644 --- a/packages/eslint-plugin-pf-codemods/src/rules/helpers/helpers.js +++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/helpers.js @@ -1,8 +1,8 @@ -import { getFromPackage } from "./getFromPackage"; -import { pfPackageMatches } from "./pfPackageMatches"; -import { findAncestor } from "./findAncestor"; +import { getFromPackage } from './getFromPackage'; +import { pfPackageMatches } from './pfPackageMatches'; +import { findAncestor } from './findAncestor'; -const evk = require("eslint-visitor-keys"); +const evk = require('eslint-visitor-keys'); export function moveSpecifiers( specifiersToMove, @@ -22,7 +22,7 @@ export function moveSpecifiers( return ( !comments?.length || !comments?.find((comment) => - comment?.value?.includes("data-codemods") + comment?.value?.includes('data-codemods') ) ); }); @@ -38,26 +38,26 @@ export function moveSpecifiers( const getModifiedToPackage = (firstSpecifier) => { // expecting @patternfly/{package} or // @patternfly/{package}/{designator} where designator is deprecated - const toParts = toPackage.split("/"); + const toParts = toPackage.split('/'); if ( - !firstSpecifier?.parent?.source?.value?.includes("dist/esm") || - toParts[0] !== "@patternfly" + !firstSpecifier?.parent?.source?.value?.includes('dist/esm') || + toParts[0] !== '@patternfly' ) { return; } - const fromParts = firstSpecifier.parent.source.value.split("/"); + const fromParts = firstSpecifier.parent.source.value.split('/'); //expecting @patternfly/{package}/dist/esm/components/{Component}/index.js //needing toPath to look like fromPath with the designator before /components if (toParts.length === 3) { fromParts.splice(4, 0, toParts[2]); - return fromParts.join("/"); + return fromParts.join('/'); } // Expecting @patternfly/{package}/dist/esm/next/components/{Component}/index.js // Needing toPath to look like fromPath *without* the designator before /components if (toParts.length === 2) { - return fromParts.filter((part) => part !== "next").join("/"); + return fromParts.filter((part) => part !== 'next').join('/'); } }; const modifiedToPackageImport = getModifiedToPackage( @@ -70,7 +70,7 @@ export function moveSpecifiers( const getExistingDeclaration = (nodeType, modifiedPackage) => { return src.ast.body.find((node) => { const specifierReference = - nodeType === "ImportDeclaration" + nodeType === 'ImportDeclaration' ? importSpecifiersToMove[0] : exportSpecifiersToMove[0]; @@ -83,11 +83,11 @@ export function moveSpecifiers( }); }; const existingToPackageImportDeclaration = getExistingDeclaration( - "ImportDeclaration", + 'ImportDeclaration', modifiedToPackageImport ); const existingToPackageExportDeclaration = getExistingDeclaration( - "ExportNamedDeclaration", + 'ExportNamedDeclaration', modifiedToPackageExport ); @@ -97,9 +97,9 @@ export function moveSpecifiers( return `${specifierText}${ specifierComments.length - ? " " + - specifierComments.map((comment) => `/*${comment.value}*/`).join("") - : "" + ? ' ' + + specifierComments.map((comment) => `/*${comment.value}*/`).join('') + : '' }`; }; const getExistingSpecifiersFromDeclaration = (declaration) => @@ -137,7 +137,7 @@ export function moveSpecifiers( } ); const newToPackageImportDeclaration = `import${ - node.importKind === "type" ? " type" : "" + node.importKind === 'type' ? ' type' : '' } {\n\t${[ ...existingToPackageImportSpecifiers, ...newAliasToPackageSpecifiers, @@ -148,11 +148,11 @@ export function moveSpecifiers( message: `${newToPackageSpecifiers .map((s) => s.imported.name) - .join(", ") - .replace(/, ([^,]+)$/, ", and $1")}` + - `${newToPackageSpecifiers.length > 1 ? " have " : " has "}${ + .join(', ') + .replace(/, ([^,]+)$/, ', and $1')}` + + `${newToPackageSpecifiers.length > 1 ? ' have ' : ' has '}${ messageAfterSpecifierPathChange || - "been moved. Running the fix flag will update your imports." + 'been moved. Running the fix flag will update your imports.' }`, fix(fixer) { //no other imports left, replace the fromPackage @@ -185,10 +185,10 @@ export function moveSpecifiers( fixer.replaceText( node, `import${ - node.importKind === "type" ? " type" : "" + node.importKind === 'type' ? ' type' : '' } {\n\t${fromPackageSpecifiers .map((specifier) => createSpecifierString(specifier)) - .join(",\n\t")}\n} from '${node.source.value}';` + .join(',\n\t')}\n} from '${node.source.value}';` ) ); } @@ -210,7 +210,7 @@ export function moveSpecifiers( return; } const newToPackageExportDeclaration = `export${ - node.exportKind === "type" ? " type" : "" + node.exportKind === 'type' ? ' type' : '' } {\n\t${[ ...existingToPackageExportSpecifiers, ...newToPackageSpecifiers.map((exportSpecifier) => { @@ -229,11 +229,11 @@ export function moveSpecifiers( message: `${newToPackageSpecifiers .map((s) => s.local.name) - .join(", ") - .replace(/, ([^,]+)$/, ", and $1")}` + - `${newToPackageSpecifiers.length > 1 ? " have " : " has "}${ + .join(', ') + .replace(/, ([^,]+)$/, ', and $1')}` + + `${newToPackageSpecifiers.length > 1 ? ' have ' : ' has '}${ messageAfterSpecifierPathChange || - "been moved. Running the fix flag will update your exports." + 'been moved. Running the fix flag will update your exports.' }`, fix(fixer) { //no other exports left, replace the fromPackage @@ -266,7 +266,7 @@ export function moveSpecifiers( fixer.replaceText( node, `export${ - node.exportKind === "type" ? " type" : "" + node.exportKind === 'type' ? ' type' : '' } {\n\t${fromPackageSpecifiers .map((specifier) => { const specifierText = src.getText(specifier); @@ -275,10 +275,10 @@ export function moveSpecifiers( .getCommentsAfter(specifier) .map((comment) => comment?.value) || []; return specifierComments.length - ? `${specifierText} /* ${specifierComments.join("")} */` + ? `${specifierText} /* ${specifierComments.join('')} */` : specifierText; }) - .join(",\n\t")}\n} from '${node?.source?.value}';` + .join(',\n\t')}\n} from '${node?.source?.value}';` ) ); } @@ -322,117 +322,12 @@ export function createAliasImportSpecifiers(specifiers) { }); } -export function renamePropsOnNode(context, imports, node, renames) { - const componentName = imports.find((imp) => imp.local.name === node.name.name) - ?.imported.name; - - if (componentName) { - const renamedProps = renames[componentName]; - - node.attributes - .filter((attribute) => renamedProps.hasOwnProperty(attribute.name?.name)) - .forEach((attribute) => { - const newPropObject = renamedProps[attribute.name.name]; - - const message = newPropObject.message - ? newPropObject.message instanceof Function - ? newPropObject.message(node) - : newPropObject.message - : undefined; - - if ( - newPropObject.newName === undefined || - newPropObject.newName === "" - ) { - context.report({ - node, - message: - message || - `${attribute.name.name} prop for ${node.name.name} has been removed`, - fix(fixer) { - return fixer.replaceText(attribute, ""); - }, - }); - } else { - context.report({ - node, - message: - message || - `${attribute.name.name} prop for ${node.name.name} has been ${ - newPropObject.replace ? "replaced with" : "renamed to" - } ${newPropObject.newName}`, - fix(fixer) { - return fixer.replaceText( - newPropObject.replace ? attribute : attribute.name, - newPropObject.newName - ); - }, - }); - } - }); - } -} - -/** - * - * @param {*} renames - * structure of the renames object: - * { - * ComponentOne: { - propA: { - newName: "newPropA", - message: (node) => `propA prop has been renamed to newPropA for ${node.name.name}, some custom message`, // message is optional, default message will always be provided - }, - isSmall: { - newName: 'size="sm"', - replace // when replace is present, it will replace the entire prop, including its value (e.g. isSmall={true} will be replaced with size="sm") - } - }, - ComponentTwo: { - propToDelete: { - newName: "" // removing a prop is done by an empty string newName - message: "propToDelete has been deleted on ComponentTwo" // message can also be a string, but function is preferable to keep the node name, when using aliased import of a component - }, - propToDeleteShort: "", // shorter way to remove a prop - propToRenameShort: "someNewPropName", // shorter way to rename a prop - propToDeleteAlternativeWay: {}, // this will also work for removing a prop - } - * } - * @param {string} packageName - * @returns - */ -export function renameProps(renames, packageName = "@patternfly/react-core") { - return function (context) { - const imports = getFromPackage(context, packageName).imports.filter( - (specifier) => Object.keys(renames).includes(specifier.imported.name) - ); - - if (imports.length === 0) { - return {}; - } - - Object.keys(renames).forEach((component) => { - Object.entries(renames[component]).forEach(([oldName, value]) => { - if (typeof value === "string") { - renames[component][oldName] = { newName: value }; - } - }); - }); - - return { - JSXOpeningElement(node) { - renamePropsOnNode(context, imports, node, renames); - }, - }; - }; -} - export function renameComponents( componentMap, condition = (_context, _package) => true, message = (prevName, newName) => `${prevName} has been replaced with ${newName}`, - packageName = "@patternfly/react-core" + packageName = '@patternfly/react-core' ) { return function (context) { const { imports, exports } = getFromPackage(context, packageName); @@ -453,12 +348,12 @@ export function renameComponents( } if (filteredImports.length) { - renameComponentFunctions["ImportDeclaration"] = function (node) { + renameComponentFunctions['ImportDeclaration'] = function (node) { ensureImports(context, node, packageName, [ ...new Set(Object.values(componentMap)), ]); }; - renameComponentFunctions["JSXIdentifier"] = function (node) { + renameComponentFunctions['JSXIdentifier'] = function (node) { const nodeName = node?.name; const importedNode = filteredImports.find( (imp) => imp?.local?.name === nodeName @@ -469,14 +364,14 @@ export function renameComponents( ) { // if data-codemods attribute, do nothing const parentNode = node?.parent; - const isOpeningTag = parentNode?.type === "JSXOpeningElement"; + const isOpeningTag = parentNode?.type === 'JSXOpeningElement'; const openingTagAttributes = isOpeningTag ? parentNode?.attributes : parentNode?.parent?.openingElement?.attributes; const hasDataAttr = openingTagAttributes && openingTagAttributes.filter( - (attr) => attr.name?.name === "data-codemods" + (attr) => attr.name?.name === 'data-codemods' ).length; if (hasDataAttr) { return; @@ -488,7 +383,7 @@ export function renameComponents( context.getSourceCode().getText(node).replace(nodeName, newName); const addDataAttr = (jsxStr) => `${jsxStr.slice(0, -1)} data-codemods="true">`; - const newOpeningParentTag = newName.includes("Toolbar") + const newOpeningParentTag = newName.includes('Toolbar') ? addDataAttr(updateTagName(parentNode)) : updateTagName(parentNode); context.report({ @@ -505,7 +400,7 @@ export function renameComponents( } if (filteredExports.length) { - renameComponentFunctions["ExportNamedDeclaration"] = function (node) { + renameComponentFunctions['ExportNamedDeclaration'] = function (node) { const exportedNode = node?.specifiers?.find((specifier) => filteredExports .map((exp) => exp?.local?.name) @@ -542,7 +437,7 @@ export function ensureImports(context, node, packageName, imports) { const missingImports = imports .filter((imp) => !patternflyImportNames.includes(imp)) // not added by consumer .filter((imp) => !myImports.includes(imp)) // not added by this rule - .join(", "); + .join(', '); if (missingImports) { const lastSpecifier = node.specifiers[node.specifiers.length - 1]; context.report({ @@ -566,11 +461,11 @@ export function addCallbackParam( return function (context) { const { imports: reactCoreImports } = getFromPackage( context, - "@patternfly/react-core" + '@patternfly/react-core' ); const { imports: deprecatedImports } = getFromPackage( context, - "@patternfly/react-core/deprecated" + '@patternfly/react-core/deprecated' ); const imports = [...reactCoreImports, ...deprecatedImports].filter( (specifier) => componentsArray.includes(specifier?.imported?.name) @@ -594,9 +489,9 @@ export function addCallbackParam( nodeToReplace: attribute.value?.expression, }; - if (propProperties.type === "ArrowFunctionExpression") { + if (propProperties.type === 'ArrowFunctionExpression') { propProperties.params = attribute.value?.expression?.params; - } else if (propProperties.type === "Identifier") { + } else if (propProperties.type === 'Identifier') { const matchingVariable = findVariableDeclaration( propProperties.name, context.getSourceCode().getScope(node) @@ -606,36 +501,36 @@ export function addCallbackParam( ); propProperties.params = - matchingDefinition?.type === "FunctionName" + matchingDefinition?.type === 'FunctionName' ? matchingDefinition?.node?.params : matchingDefinition?.node?.init?.params; const isPotentialUseStateHook = (definition) => - definition?.type === "Variable" && - definition?.node.id?.type === "ArrayPattern" && - definition?.node.init?.type === "CallExpression"; + definition?.type === 'Variable' && + definition?.node.id?.type === 'ArrayPattern' && + definition?.node.init?.type === 'CallExpression'; if (isPotentialUseStateHook(matchingDefinition)) { const callee = matchingDefinition?.node.init.callee; const reactImports = getFromPackage( context, - "react" + 'react' ).imports; const defaultSpecifierName = reactImports.find( - (spec) => spec.type === "ImportDefaultSpecifier" + (spec) => spec.type === 'ImportDefaultSpecifier' )?.local.name; const useStateLocalName = reactImports.find( - (spec) => spec.imported?.name === "useState" + (spec) => spec.imported?.name === 'useState' )?.local.name; const isStateSetter = (callee) => - (callee.type === "Identifier" && + (callee.type === 'Identifier' && callee.name === useStateLocalName) || - (callee.type === "MemberExpression" && + (callee.type === 'MemberExpression' && callee.object.name === defaultSpecifierName && - callee.property.name === "useState"); + callee.property.name === 'useState'); if (isStateSetter(callee)) { propProperties.isStateSetter = true; @@ -643,17 +538,17 @@ export function addCallbackParam( matchingDefinition?.node.init.typeParameters?.params[0].typeName?.name; } } - } else if (propProperties.type === "MemberExpression") { + } else if (propProperties.type === 'MemberExpression') { const memberExpression = attribute.value?.expression; - if (memberExpression.object.type === "ThisExpression") { + if (memberExpression.object.type === 'ThisExpression') { const parentClass = findAncestor( memberExpression, - (current) => current.type === "ClassDeclaration" + (current) => current.type === 'ClassDeclaration' ); const methods = parentClass?.body?.body; const methodDefinition = methods?.find( (method) => - method.key.type === "Identifier" && + method.key.type === 'Identifier' && method.key.name === memberExpression.property.name ); @@ -664,14 +559,14 @@ export function addCallbackParam( const { type, params } = propProperties; const parameterConfig = propMap[attribute.name.name]; - const isParamAdditionOnly = typeof parameterConfig === "string"; + const isParamAdditionOnly = typeof parameterConfig === 'string'; const newOrDefaultParamName = isParamAdditionOnly ? parameterConfig : parameterConfig.defaultParamName; let potentialParamMatchers = `(^_?${newOrDefaultParamName.replace( /^_+/, - "" + '' )}$)`; const otherMatchersString = parameterConfig.otherMatchers ?.toString() @@ -730,7 +625,7 @@ export function addCallbackParam( if ( (params?.length >= 1 && - ["ArrowFunctionExpression", "Identifier"].includes(type)) || + ['ArrowFunctionExpression', 'Identifier'].includes(type)) || propProperties.isThisExpression || propProperties.isStateSetter ) { @@ -747,7 +642,7 @@ export function addCallbackParam( if (propProperties.isStateSetter) { const typeText = propProperties.stateType ? `: ${propProperties.stateType}` - : ""; + : ''; fixes.push( fixer.replaceText( propProperties.nodeToReplace, @@ -756,7 +651,7 @@ export function addCallbackParam( ); } else if ( propProperties.isThisExpression || - type === "Identifier" + type === 'Identifier' ) { if (!params || params.length === 0) { return fixes; @@ -765,7 +660,7 @@ export function addCallbackParam( const newParamsText = `${newParam}, ${params .filter((p) => p.name !== newParam) .map((p) => context.getSourceCode().getText(p)) - .join(", ")}`; + .join(', ')}`; fixes.push( fixer.replaceText( @@ -774,7 +669,7 @@ export function addCallbackParam( .getSourceCode() .getText(propProperties.nodeToReplace)}(${params .map((p) => p.name) - .join(", ")})` + .join(', ')})` ) ); } else { @@ -786,7 +681,7 @@ export function addCallbackParam( .getText(firstParam)}`; const hasParenthesis = - context.getTokenBefore(firstParam).value === "("; + context.getTokenBefore(firstParam).value === '('; return fixer.replaceText( firstParam, @@ -806,7 +701,7 @@ export function addCallbackParam( currentUseOfNewParam.range[1], ]; - return fixer.replaceTextRange(targetRange, ""); + return fixer.replaceTextRange(targetRange, ''); }; const currentIndexOfNewParam = params?.findIndex( @@ -849,7 +744,7 @@ export function getAllJSXElements(context) { if (!node) { return; } - if (node.type === "JSXElement") { + if (node.type === 'JSXElement') { jsxElements.push(node); } @@ -858,7 +753,7 @@ export function getAllJSXElements(context) { if (Array.isArray(child)) { child.forEach((c) => traverse(c)); - } else if (child && typeof child === "object") { + } else if (child && typeof child === 'object') { traverse(child); } } 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 a46dba5a1..8b3affde2 100644 --- a/packages/eslint-plugin-pf-codemods/src/rules/helpers/index.ts +++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/index.ts @@ -1,9 +1,10 @@ -export * from "./findAncestor"; -export * from "./helpers"; +export * from './findAncestor'; +export * from './helpers'; +export * from './getFromPackage'; +export * from './getText'; +export * from './includesImport'; +export * from './JSXAttributes'; +export * from './JSXElements'; +export * from './pfPackageMatches'; +export * from './renameProps'; export * from "./getImportDeclaration"; -export * from "./getFromPackage"; -export * from "./getText"; -export * from "./includesImport"; -export * from "./JSXAttributes"; -export * from "./JSXElements"; -export * from "./pfPackageMatches"; diff --git a/packages/eslint-plugin-pf-codemods/src/rules/helpers/renameProps.ts b/packages/eslint-plugin-pf-codemods/src/rules/helpers/renameProps.ts new file mode 100644 index 000000000..82f44fdfb --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/renameProps.ts @@ -0,0 +1,34 @@ +import { Rule } from 'eslint'; +import { getFromPackage } from './getFromPackage'; +import { renamePropsOnNode } from './renamePropsOnNode'; +import { Renames } from './renameSinglePropOnNode'; +import { JSXOpeningElement } from 'estree-jsx'; + +export function renameProps( + renames: Renames, + packageName = '@patternfly/react-core' +) { + return function (context: Rule.RuleContext) { + const imports = getFromPackage(context, packageName).imports.filter( + (specifier) => Object.keys(renames).includes(specifier.imported.name) + ); + + if (imports.length === 0) { + return {}; + } + + Object.keys(renames).forEach((component) => { + Object.entries(renames[component]).forEach(([oldName, value]) => { + if (typeof value === 'string') { + renames[component][oldName] = { newName: value }; + } + }); + }); + + return { + JSXOpeningElement(node: JSXOpeningElement) { + renamePropsOnNode(context, imports, node, renames); + }, + }; + }; +} diff --git a/packages/eslint-plugin-pf-codemods/src/rules/helpers/renamePropsOnNode.ts b/packages/eslint-plugin-pf-codemods/src/rules/helpers/renamePropsOnNode.ts new file mode 100644 index 000000000..09768fecf --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/renamePropsOnNode.ts @@ -0,0 +1,42 @@ +import { Rule } from 'eslint'; +import { + JSXOpeningElement, + ImportSpecifier, + JSXAttribute, + JSXIdentifier, +} from 'estree-jsx'; +import { renameSinglePropOnNode, Renames } from './renameSinglePropOnNode'; + +export function renamePropsOnNode( + context: Rule.RuleContext, + imports: ImportSpecifier[], + node: JSXOpeningElement, + renames: Renames +) { + const componentName = imports.find( + (imp) => + node.name.type === 'JSXIdentifier' && imp.local.name === node.name.name + )?.imported.name; + + if (!componentName) { + return; + } + + const renamedProps = renames[componentName]; + + const JSXAttributes = node.attributes.filter( + (attribute) => + attribute.type === 'JSXAttribute' && + attribute.name.type === 'JSXIdentifier' && + renamedProps.hasOwnProperty(attribute.name?.name) + ) as JSXAttribute[]; + + JSXAttributes.forEach((attribute) => + renameSinglePropOnNode( + context, + attribute, + node, + renamedProps[(attribute.name as JSXIdentifier).name] + ) + ); +} diff --git a/packages/eslint-plugin-pf-codemods/src/rules/helpers/renameSinglePropOnNode.ts b/packages/eslint-plugin-pf-codemods/src/rules/helpers/renameSinglePropOnNode.ts new file mode 100644 index 000000000..4f18e1174 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/renameSinglePropOnNode.ts @@ -0,0 +1,124 @@ +import { JSXAttribute, JSXIdentifier, JSXOpeningElement } from 'estree-jsx'; + +import { Rule } from 'eslint'; + +export interface RenameConfig { + newName: string; + message?: string | ((node: any) => string); + replace?: boolean; +} + +type RenameInput = RenameConfig | string | Record; + +export interface ComponentRenames { + [propName: string]: RenameInput; +} + +export interface Renames { + [componentName: string]: ComponentRenames; +} + +function checkIsRenameConfig( + renameInput: RenameInput +): renameInput is RenameConfig { + const hasValidObjectType = + typeof renameInput === 'object' && renameInput !== null; + + if (!hasValidObjectType) { + return false; + } + + if ( + typeof renameInput === 'string' || + Object.keys(renameInput).length === 0 + ) { + return false; + } + + const hasValidNewNameType = typeof renameInput.newName === 'string'; + + const isValidMessage = + renameInput.message === undefined || + typeof renameInput.message === 'string' || + typeof renameInput.message === 'function'; + + const isValidReplace = + renameInput.replace === undefined || + typeof renameInput.replace === 'boolean'; + + return hasValidNewNameType && isValidMessage && isValidReplace; +} + +const getNewName = (replaced: boolean, propRename: RenameInput) => { + const isRenameConfig = checkIsRenameConfig(propRename); + + const isString = typeof propRename === 'string'; + + if (!replaced || (!isRenameConfig && !isString)) { + return ''; + } + return isRenameConfig ? propRename.newName : propRename; +}; + +const getAction = (replaced: boolean, propRename: RenameInput) => { + if (!replaced) { + return 'removed'; + } + return propRename.replace ? 'replaced with ' : 'renamed to '; +}; + +const getMessage = ( + node: JSXOpeningElement, + propRename: RenameInput, + defaultMessage: string +) => { + if (!checkIsRenameConfig(propRename) || !propRename.message) { + return defaultMessage; + } + + return propRename.message instanceof Function + ? propRename.message(node) + : propRename.message; +}; + +const getFixer = ( + attribute: JSXAttribute, + propRename: RenameInput, + newName: string +) => { + const toReplace = + propRename.replace || newName === '' ? attribute : attribute.name; + return (fixer: Rule.RuleFixer) => fixer.replaceText(toReplace, newName); +}; + +export function renameSinglePropOnNode( + context: Rule.RuleContext, + attribute: JSXAttribute, + node: JSXOpeningElement, + propRename: RenameInput +) { + if (attribute.name.type !== 'JSXIdentifier') { + return; + } + + const isRenameConfig = checkIsRenameConfig(propRename); + + const isReplaced = + (isRenameConfig && propRename.newName !== '') || + (typeof propRename === 'string' && propRename !== ''); + + const action = getAction(isReplaced, propRename); + + const newName = getNewName(isReplaced, propRename); + + const defaultMessage = `${attribute.name.name} prop for ${ + (node.name as JSXIdentifier).name + } has been ${action}${newName}`; + const message = getMessage(node, propRename, defaultMessage); + + context.report({ + node, + message, + fix: getFixer(attribute, propRename, newName), + }); +}