diff --git a/packages/eslint-plugin-pf-codemods/package.json b/packages/eslint-plugin-pf-codemods/package.json index d6229ade..6ec15879 100644 --- a/packages/eslint-plugin-pf-codemods/package.json +++ b/packages/eslint-plugin-pf-codemods/package.json @@ -28,6 +28,7 @@ "devDependencies": { "@types/eslint": "^8.56.0", "@types/estree-jsx": "^1.0.4", + "@typescript-eslint/utils": "^8.12.2", "typescript": "^5.4.2" } } 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 1bdd9b08..df47f4d3 100644 --- a/packages/eslint-plugin-pf-codemods/src/rules/helpers/index.ts +++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/index.ts @@ -28,10 +28,13 @@ export * from "./isReactIcon"; export * from "./JSXAttributes"; export * from "./makeJSXElementSelfClosing"; export * from "./nodeMatches/checkMatchingImportDeclaration"; +export * from "./nodeMatches/checkMatchingImportSpecifier"; export * from "./nodeMatches/checkMatchingJSXOpeningElement"; export * from "./pfPackageMatches"; export * from "./propertyNameMatches"; export * from "./removeElement"; export * from "./removeEmptyLineAfter"; export * from "./removePropertiesFromObjectExpression"; +export * from "./renameComponent"; +export * from "./renameInterface"; export * from "./renameProps"; diff --git a/packages/eslint-plugin-pf-codemods/src/rules/helpers/nodeMatches/checkMatchingImportSpecifier.ts b/packages/eslint-plugin-pf-codemods/src/rules/helpers/nodeMatches/checkMatchingImportSpecifier.ts new file mode 100644 index 00000000..0df936b5 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/nodeMatches/checkMatchingImportSpecifier.ts @@ -0,0 +1,15 @@ +import { ImportSpecifier } from "estree-jsx"; + +/** Used to check whether the current ImportSpecifier node matches at least 1 of the import specifiers. */ +export function checkMatchingImportSpecifier( + node: ImportSpecifier, + imports: ImportSpecifier | ImportSpecifier[] +) { + if (Array.isArray(imports)) { + return imports.some( + (specifier) => specifier.imported.name === node.imported.name + ); + } + + return imports.imported.name === node.imported.name; +} diff --git a/packages/eslint-plugin-pf-codemods/src/rules/helpers/renameInterface.ts b/packages/eslint-plugin-pf-codemods/src/rules/helpers/renameInterface.ts new file mode 100644 index 00000000..9e2d9c01 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/helpers/renameInterface.ts @@ -0,0 +1,107 @@ +import { Rule } from "eslint"; +import { TSESTree } from "@typescript-eslint/utils"; +import { Identifier, ImportDeclaration, ImportSpecifier } from "estree-jsx"; +import { + checkMatchingImportSpecifier, + getFromPackage, + pfPackageMatches, +} from "."; + +interface Renames { + [currentName: string]: string; +} + +function formatDefaultMessage(oldName: string, newName: string) { + return `${oldName} has been renamed to ${newName}.`; +} + +export function renameInterface( + interfaceRenames: Renames, + componentRenames: Renames, + packageName = "@patternfly/react-core" +) { + return function (context: Rule.RuleContext) { + const oldNames = Object.keys(interfaceRenames); + const { imports } = getFromPackage(context, packageName, oldNames); + + if (imports.length === 0) { + return {}; + } + + const shouldRenameIdentifier = (identifier: Identifier) => { + const matchingImport = imports.find( + (specifier) => specifier.local.name === identifier.name + ); + + if (!matchingImport) { + return false; + } + + return matchingImport.local.name === matchingImport.imported.name; + }; + + const replaceIdentifier = (identifier: Identifier) => { + const oldName = identifier.name; + const newName = interfaceRenames[oldName]; + + context.report({ + node: identifier, + message: formatDefaultMessage(oldName, newName), + fix(fixer) { + return fixer.replaceText(identifier, newName); + }, + }); + }; + + return { + ImportDeclaration(node: ImportDeclaration) { + if (!pfPackageMatches(packageName, node.source.value)) { + return; + } + for (const oldName of Object.keys(componentRenames)) { + const newName = componentRenames[oldName]; + const importSource = node.source.raw; + const importSourceHasComponentName = importSource?.includes(oldName); + const newImportDeclaration = importSource?.replace(oldName, newName); + + if (newImportDeclaration && importSourceHasComponentName) { + context.report({ + node, + message: formatDefaultMessage(oldName, newName), + fix: (fixer) => + fixer.replaceText(node.source, newImportDeclaration), + }); + } + } + }, + ImportSpecifier(node: ImportSpecifier) { + if (!checkMatchingImportSpecifier(node, imports)) { + return; + } + + const oldName = node.imported.name; + const newName = interfaceRenames[oldName]; + + context.report({ + node, + message: formatDefaultMessage(oldName, newName), + fix(fixer) { + return fixer.replaceText(node.imported, newName); + }, + }); + }, + TSTypeReference(node: TSESTree.TSTypeReference) { + if (node.typeName.type === "Identifier") { + shouldRenameIdentifier(node.typeName) && + replaceIdentifier(node.typeName); + } + }, + TSInterfaceHeritage(node: TSESTree.TSInterfaceHeritage) { + if (node.expression.type === "Identifier") { + shouldRenameIdentifier(node.expression) && + replaceIdentifier(node.expression); + } + }, + }; + }; +} diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/componentGroupsInvalidObjectPropsRenameToMissingPageProps/component-groups-invalidObjectProps-rename-to-missingPageProps.md b/packages/eslint-plugin-pf-codemods/src/rules/v6/componentGroupsInvalidObjectPropsRenameToMissingPageProps/component-groups-invalidObjectProps-rename-to-missingPageProps.md new file mode 100644 index 00000000..2094e6aa --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/componentGroupsInvalidObjectPropsRenameToMissingPageProps/component-groups-invalidObjectProps-rename-to-missingPageProps.md @@ -0,0 +1,18 @@ +### component-groups-invalidObjectProps-rename-to-missingPageProps [(react-component-groups/#313)](https://github.com/patternfly/react-component-groups/pull/313) + +In react-component-groups, we've renamed InvalidObjectProps interface to MissingPageProps + +#### Examples + +In: + +```jsx +%inputExample% +``` + +Out: + +```jsx +%outputExample% +``` + diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/componentGroupsInvalidObjectPropsRenameToMissingPageProps/component-groups-invalidObjectProps-rename-to-missingPageProps.test.ts b/packages/eslint-plugin-pf-codemods/src/rules/v6/componentGroupsInvalidObjectPropsRenameToMissingPageProps/component-groups-invalidObjectProps-rename-to-missingPageProps.test.ts new file mode 100644 index 00000000..ef3893d7 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/componentGroupsInvalidObjectPropsRenameToMissingPageProps/component-groups-invalidObjectProps-rename-to-missingPageProps.test.ts @@ -0,0 +1,112 @@ +const ruleTester = require("../../ruletester"); +import * as rule from "./component-groups-invalidObjectProps-rename-to-missingPageProps"; + +const message = `InvalidObjectProps has been renamed to MissingPageProps.`; +const componentMessage = `InvalidObject has been renamed to MissingPage.`; + +ruleTester.run( + "component-groups-invalidObjectProps-rename-to-missingPageProps", + rule, + { + valid: [ + // missing import + { + code: `const props: InvalidObjectProps;`, + }, + // import from wrong package + { + code: `import { InvalidObjectProps } from '@patternfly/react-core';`, + }, + // import of other props + { + code: `import { SomeOtherProps } from '@patternfly/react-component-groups';`, + }, + ], + invalid: [ + { + code: `import { InvalidObjectProps, SomethingElse } from '@patternfly/react-component-groups'; + const props: InvalidObjectProps; + const otherProps = props as InvalidObjectProps; + interface CustomProps extends InvalidObjectProps {};`, + output: `import { MissingPageProps, SomethingElse } from '@patternfly/react-component-groups'; + const props: MissingPageProps; + const otherProps = props as MissingPageProps; + interface CustomProps extends MissingPageProps {};`, + errors: [ + { + message, + type: "ImportSpecifier", + }, + { + message, + type: "Identifier", + }, + { + message, + type: "Identifier", + }, + { + message, + type: "Identifier", + }, + ], + }, + // named import with alias + { + code: `import { InvalidObjectProps as InvObjProps } from '@patternfly/react-component-groups'; + const props: InvObjProps;`, + output: `import { MissingPageProps as InvObjProps } from '@patternfly/react-component-groups'; + const props: InvObjProps;`, + errors: [ + { + message, + type: "ImportSpecifier", + }, + ], + }, + // imports from dist + { + code: `import { InvalidObjectProps } from '@patternfly/react-component-groups/dist/cjs/InvalidObject';`, + output: `import { MissingPageProps } from '@patternfly/react-component-groups/dist/cjs/MissingPage';`, + errors: [ + { + message: componentMessage, + type: "ImportDeclaration", + }, + { + message, + type: "ImportSpecifier", + }, + ], + }, + { + code: `import { InvalidObjectProps } from '@patternfly/react-component-groups/dist/esm/InvalidObject';`, + output: `import { MissingPageProps } from '@patternfly/react-component-groups/dist/esm/MissingPage';`, + errors: [ + { + message: componentMessage, + type: "ImportDeclaration", + }, + { + message, + type: "ImportSpecifier", + }, + ], + }, + { + code: `import { InvalidObjectProps } from '@patternfly/react-component-groups/dist/dynamic/InvalidObject';`, + output: `import { MissingPageProps } from '@patternfly/react-component-groups/dist/dynamic/MissingPage';`, + errors: [ + { + message: componentMessage, + type: "ImportDeclaration", + }, + { + message, + type: "ImportSpecifier", + }, + ], + }, + ], + } +); diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/componentGroupsInvalidObjectPropsRenameToMissingPageProps/component-groups-invalidObjectProps-rename-to-missingPageProps.ts b/packages/eslint-plugin-pf-codemods/src/rules/v6/componentGroupsInvalidObjectPropsRenameToMissingPageProps/component-groups-invalidObjectProps-rename-to-missingPageProps.ts new file mode 100644 index 00000000..fb930bd9 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/componentGroupsInvalidObjectPropsRenameToMissingPageProps/component-groups-invalidObjectProps-rename-to-missingPageProps.ts @@ -0,0 +1,15 @@ +import { renameInterface } from "../../helpers"; + +// https://github.com/patternfly/react-component-groups/pull/313 +module.exports = { + meta: { fixable: "code" }, + create: renameInterface( + { + InvalidObjectProps: "MissingPageProps", + }, + { + InvalidObject: "MissingPage", + }, + "@patternfly/react-component-groups" + ), +}; diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/componentGroupsInvalidObjectPropsRenameToMissingPageProps/componentGroupsInvalidObjectPropsRenameToMissingPagePropsInput.tsx b/packages/eslint-plugin-pf-codemods/src/rules/v6/componentGroupsInvalidObjectPropsRenameToMissingPageProps/componentGroupsInvalidObjectPropsRenameToMissingPagePropsInput.tsx new file mode 100644 index 00000000..ec07c205 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/componentGroupsInvalidObjectPropsRenameToMissingPageProps/componentGroupsInvalidObjectPropsRenameToMissingPagePropsInput.tsx @@ -0,0 +1,4 @@ +import { InvalidObjectProps } from "@patternfly/react-component-groups"; + +const props: InvalidObjectProps; +interface CustomProps extends InvalidObjectProps {} diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/componentGroupsInvalidObjectPropsRenameToMissingPageProps/componentGroupsInvalidObjectPropsRenameToMissingPagePropsOutput.tsx b/packages/eslint-plugin-pf-codemods/src/rules/v6/componentGroupsInvalidObjectPropsRenameToMissingPageProps/componentGroupsInvalidObjectPropsRenameToMissingPagePropsOutput.tsx new file mode 100644 index 00000000..c6f2a276 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/componentGroupsInvalidObjectPropsRenameToMissingPageProps/componentGroupsInvalidObjectPropsRenameToMissingPagePropsOutput.tsx @@ -0,0 +1,4 @@ +import { MissingPageProps } from "@patternfly/react-component-groups"; + +const props: MissingPageProps; +interface CustomProps extends MissingPageProps {} diff --git a/yarn.lock b/yarn.lock index 732029ed..df325943 100644 --- a/yarn.lock +++ b/yarn.lock @@ -80,6 +80,17 @@ __metadata: languageName: node linkType: hard +"@eslint-community/eslint-utils@npm:^4.4.0": + version: 4.4.1 + resolution: "@eslint-community/eslint-utils@npm:4.4.1" + dependencies: + eslint-visitor-keys: "npm:^3.4.3" + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + checksum: 10c0/2aa0ac2fc50ff3f234408b10900ed4f1a0b19352f21346ad4cc3d83a1271481bdda11097baa45d484dd564c895e0762a27a8240be7a256b3ad47129e96528252 + languageName: node + linkType: hard + "@eslint-community/regexpp@npm:^4.6.1": version: 4.12.1 resolution: "@eslint-community/regexpp@npm:4.12.1" @@ -984,6 +995,7 @@ __metadata: "@patternfly/shared-codemod-helpers": "workspace:^" "@types/eslint": "npm:^8.56.0" "@types/estree-jsx": "npm:^1.0.4" + "@typescript-eslint/utils": "npm:^8.12.2" typescript: "npm:^5.4.2" peerDependencies: "@typescript-eslint/parser": ">=5.58.0" @@ -1311,6 +1323,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:8.21.0": + version: 8.21.0 + resolution: "@typescript-eslint/scope-manager@npm:8.21.0" + dependencies: + "@typescript-eslint/types": "npm:8.21.0" + "@typescript-eslint/visitor-keys": "npm:8.21.0" + checksum: 10c0/ea405e79dc884ea1c76465604db52f9b0941d6cbb0bde6bce1af689ef212f782e214de69d46503c7c47bfc180d763369b7433f1965e3be3c442b417e8c9f8f75 + languageName: node + linkType: hard + "@typescript-eslint/types@npm:7.3.1": version: 7.3.1 resolution: "@typescript-eslint/types@npm:7.3.1" @@ -1318,6 +1340,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:8.21.0": + version: 8.21.0 + resolution: "@typescript-eslint/types@npm:8.21.0" + checksum: 10c0/67dfd300cc614d7b02e94d0dacfb228a7f4c3fd4eede29c43adb9e9fcc16365ae3df8d6165018da3c123dce65545bef03e3e8183f35e9b3a911ffc727e3274c2 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:7.3.1": version: 7.3.1 resolution: "@typescript-eslint/typescript-estree@npm:7.3.1" @@ -1337,6 +1366,39 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:8.21.0": + version: 8.21.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.21.0" + dependencies: + "@typescript-eslint/types": "npm:8.21.0" + "@typescript-eslint/visitor-keys": "npm:8.21.0" + debug: "npm:^4.3.4" + fast-glob: "npm:^3.3.2" + is-glob: "npm:^4.0.3" + minimatch: "npm:^9.0.4" + semver: "npm:^7.6.0" + ts-api-utils: "npm:^2.0.0" + peerDependencies: + typescript: ">=4.8.4 <5.8.0" + checksum: 10c0/0cf5b0382524f4af54fb5ec71ca7e939ec922711f2d77b383740b28dd4b21407b0ab5dded62df6819d01c12c0b354e95667e3c7025a5d27d05b805161ab94855 + languageName: node + linkType: hard + +"@typescript-eslint/utils@npm:^8.12.2": + version: 8.21.0 + resolution: "@typescript-eslint/utils@npm:8.21.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.4.0" + "@typescript-eslint/scope-manager": "npm:8.21.0" + "@typescript-eslint/types": "npm:8.21.0" + "@typescript-eslint/typescript-estree": "npm:8.21.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ">=4.8.4 <5.8.0" + checksum: 10c0/d8347dbe9176417220aa62902cfc1b2007a9246bb7a8cccdf8590120903eb50ca14cb668efaab4646d086277f2367559985b62230e43ebd8b0723d237eeaa2f2 + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:7.3.1": version: 7.3.1 resolution: "@typescript-eslint/visitor-keys@npm:7.3.1" @@ -1347,6 +1409,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:8.21.0": + version: 8.21.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.21.0" + dependencies: + "@typescript-eslint/types": "npm:8.21.0" + eslint-visitor-keys: "npm:^4.2.0" + checksum: 10c0/b3f1412f550e35c0d7ae0410db616951116b365167539f9b85710d8bc2b36b322c5e637caee84cc1ae5df8f1d961880250d52ffdef352b31e5bdbef74ba6fea9 + languageName: node + linkType: hard + "@ungap/structured-clone@npm:^1.2.0": version: 1.2.1 resolution: "@ungap/structured-clone@npm:1.2.1" @@ -1777,6 +1849,15 @@ __metadata: languageName: node linkType: hard +"braces@npm:^3.0.3": + version: 3.0.3 + resolution: "braces@npm:3.0.3" + dependencies: + fill-range: "npm:^7.1.1" + checksum: 10c0/7c6dfd30c338d2997ba77500539227b9d1f85e388a5f43220865201e407e076783d0881f2d297b9f80951b4c957fcf0b51c1d2d24227631643c3f7c284b0aa04 + languageName: node + linkType: hard + "browser-stdout@npm:1.3.1": version: 1.3.1 resolution: "browser-stdout@npm:1.3.1" @@ -2829,6 +2910,13 @@ __metadata: languageName: node linkType: hard +"eslint-visitor-keys@npm:^4.2.0": + version: 4.2.0 + resolution: "eslint-visitor-keys@npm:4.2.0" + checksum: 10c0/2ed81c663b147ca6f578312919483eb040295bbab759e5a371953456c636c5b49a559883e2677112453728d66293c0a4c90ab11cab3428cf02a0236d2e738269 + languageName: node + linkType: hard + "eslint@npm:^7.3.0 || ^8.56.0": version: 8.57.1 resolution: "eslint@npm:8.57.1" @@ -3022,6 +3110,19 @@ __metadata: languageName: node linkType: hard +"fast-glob@npm:^3.3.2": + version: 3.3.3 + resolution: "fast-glob@npm:3.3.3" + dependencies: + "@nodelib/fs.stat": "npm:^2.0.2" + "@nodelib/fs.walk": "npm:^1.2.3" + glob-parent: "npm:^5.1.2" + merge2: "npm:^1.3.0" + micromatch: "npm:^4.0.8" + checksum: 10c0/f6aaa141d0d3384cf73cbcdfc52f475ed293f6d5b65bfc5def368b09163a9f7e5ec2b3014d80f733c405f58e470ee0cc451c2937685045cddcdeaa24199c43fe + languageName: node + linkType: hard + "fast-json-stable-stringify@npm:^2.0.0": version: 2.1.0 resolution: "fast-json-stable-stringify@npm:2.1.0" @@ -3081,6 +3182,15 @@ __metadata: languageName: node linkType: hard +"fill-range@npm:^7.1.1": + version: 7.1.1 + resolution: "fill-range@npm:7.1.1" + dependencies: + to-regex-range: "npm:^5.0.1" + checksum: 10c0/b75b691bbe065472f38824f694c2f7449d7f5004aa950426a2c28f0306c60db9b880c0b0e4ed819997ffb882d1da02cfcfc819bddc94d71627f5269682edf018 + languageName: node + linkType: hard + "find-up@npm:3.0.0, find-up@npm:^3.0.0": version: 3.0.0 resolution: "find-up@npm:3.0.0" @@ -5013,6 +5123,16 @@ __metadata: languageName: node linkType: hard +"micromatch@npm:^4.0.8": + version: 4.0.8 + resolution: "micromatch@npm:4.0.8" + dependencies: + braces: "npm:^3.0.3" + picomatch: "npm:^2.3.1" + checksum: 10c0/166fa6eb926b9553f32ef81f5f531d27b4ce7da60e5baf8c021d043b27a388fb95e46a8038d5045877881e673f8134122b59624d5cecbd16eb50a42e7a6b5ca8 + languageName: node + linkType: hard + "mime-db@npm:1.44.0": version: 1.44.0 resolution: "mime-db@npm:1.44.0" @@ -6860,7 +6980,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.3": +"semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.6.0": version: 7.6.3 resolution: "semver@npm:7.6.3" bin: @@ -7485,6 +7605,15 @@ __metadata: languageName: node linkType: hard +"ts-api-utils@npm:^2.0.0": + version: 2.0.0 + resolution: "ts-api-utils@npm:2.0.0" + peerDependencies: + typescript: ">=4.8.4" + checksum: 10c0/6165e29a5b75bd0218e3cb0f9ee31aa893dbd819c2e46dbb086c841121eb0436ed47c2c18a20cb3463d74fd1fb5af62e2604ba5971cc48e5b38ebbdc56746dfc + languageName: node + linkType: hard + "ts-node@npm:^10.9.2": version: 10.9.2 resolution: "ts-node@npm:10.9.2"