From a42bcf8c262d357cd7b122069288cd048b221fdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pet=C5=99=C3=ADk?= Date: Tue, 26 Mar 2024 18:25:48 +0100 Subject: [PATCH] rebase --- .../src/rules/helpers/helpers.js | 243 +++++------------- .../src/rules/helpers/index.ts | 17 +- .../src/rules/helpers/renameProps.ts | 34 +++ .../src/rules/helpers/renamePropsOnNode.ts | 108 ++++++++ 4 files changed, 220 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 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 80a263541..576ca51c8 100644 --- a/packages/eslint-plugin-pf-codemods/src/rules/helpers/index.ts +++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/index.ts @@ -1,8 +1,9 @@ -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 './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'; 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..21bd3c9e3 --- /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 './renamePropsOnNode'; +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..7a15ddcb9 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/renamePropsOnNode.ts @@ -0,0 +1,108 @@ +import { Rule } from 'eslint'; +import { + JSXOpeningElement, + ImportSpecifier, + JSXIdentifier, + JSXAttribute, +} from 'estree-jsx'; + +interface RenameConfig { + newName: string; + message?: string | ((node: any) => string); + replace?: boolean; +} + +interface ComponentRenames { + [propName: string]: RenameConfig | string | Record; +} + +export interface Renames { + [componentName: string]: ComponentRenames; +} + +function isRenameConfig(obj: any): obj is RenameConfig { + return ( + typeof obj === 'object' && + obj !== null && + typeof obj.newName === 'string' && + (obj.message === undefined || + typeof obj.message === 'string' || + typeof obj.message === 'function') && + (obj.replace === undefined || typeof obj.replace === 'boolean') + ); +} + +export function renamePropsOnNode( + context: Rule.RuleContext, + imports: ImportSpecifier[], + node: JSXOpeningElement, + renames: Renames +) { + node.name = node.name as JSXIdentifier; + const componentName = imports.find( + (imp) => + node.name.type === 'JSXIdentifier' && imp.local.name === node.name.name + )?.imported.name; + + if (componentName) { + const renamedProps = renames[componentName]; + node.attributes + .filter( + (attribute) => + attribute.type === 'JSXAttribute' && + attribute.name.type === 'JSXIdentifier' && + renamedProps.hasOwnProperty(attribute.name?.name) + ) + .forEach((attribute) => { + attribute = attribute as JSXAttribute; + attribute.name = attribute.name as JSXIdentifier; + const newPropObject = renamedProps[attribute.name.name]; + + const message = + isRenameConfig(newPropObject) && newPropObject.message + ? newPropObject.message instanceof Function + ? newPropObject.message(node) + : newPropObject.message + : undefined; + + if ( + (isRenameConfig(newPropObject) && newPropObject.newName !== '') || + (typeof newPropObject === 'string' && newPropObject !== '') + ) { + const newName = isRenameConfig(newPropObject) + ? newPropObject.newName + : newPropObject; + context.report({ + node, + message: + message || + `${attribute.name.name} prop for ${ + (node.name as JSXIdentifier).name + } has been ${ + newPropObject.replace ? 'replaced with' : 'renamed to' + } ${newName}`, + fix(fixer) { + return fixer.replaceText( + newPropObject.replace + ? attribute + : (attribute as JSXAttribute).name, + newName + ); + }, + }); + } else { + context.report({ + node, + message: + message || + `${attribute.name.name} prop for ${ + (node.name as JSXIdentifier).name + } has been removed`, + fix(fixer) { + return fixer.replaceText(attribute, ''); + }, + }); + } + }); + } +}