diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateNonExportedComponents/emptyState-nonExported-components.md b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateNonExportedComponents/emptyState-nonExported-components.md new file mode 100644 index 000000000..b71e17181 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateNonExportedComponents/emptyState-nonExported-components.md @@ -0,0 +1,17 @@ +### emptyState-nonExported-components [(#10364)](https://github.com/patternfly/patternfly-react/pull/10364) + +EmptyStateHeader and EmptyStateIcon are no longer exported by PatternFly. This rule will only apply fixes for exports of these components, as our rule for unused imports will handle applying fixes for imports. + +#### Examples + +In: + +```jsx +%inputExample% +``` + +Out: + +```jsx +%outputExample% +``` diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateNonExportedComponents/emptyState-nonExported-components.test.ts b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateNonExportedComponents/emptyState-nonExported-components.test.ts new file mode 100644 index 000000000..bd2aa5084 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateNonExportedComponents/emptyState-nonExported-components.test.ts @@ -0,0 +1,121 @@ +const ruleTester = require("../../ruletester"); +import * as rule from "./emptyState-nonExported-components"; + +ruleTester.run("emptyState-nonExported-components", rule, { + valid: [ + { + code: `import { EmptyStateHeader } from '@some/other/package';`, + }, + { + code: `import { EmptyStateIcon } from '@some/other/package';`, + }, + ], + invalid: [ + { + code: `import { EmptyStateHeader } from '@patternfly/react-core';`, + output: `import { EmptyStateHeader } from '@patternfly/react-core';`, + errors: [ + { + message: `EmptyStateHeader is no longer exported by PatternFly. This rule will not fix any imports, as our cleanup rule handles removal of unused imports.`, + type: "ImportSpecifier", + }, + ], + }, + { + code: `import { EmptyStateHeader as CustomHeader } from '@patternfly/react-core';`, + output: `import { EmptyStateHeader as CustomHeader } from '@patternfly/react-core';`, + errors: [ + { + message: `EmptyStateHeader is no longer exported by PatternFly. This rule will not fix any imports, as our cleanup rule handles removal of unused imports.`, + type: "ImportSpecifier", + }, + ], + }, + { + code: `export { EmptyStateHeader } from '@patternfly/react-core';`, + output: ``, + errors: [ + { + message: `EmptyStateHeader is no longer exported by PatternFly.`, + type: "ExportNamedDeclaration", + }, + ], + }, + { + code: `export { EmptyStateHeader as CustomHeader } from '@patternfly/react-core';`, + output: ``, + errors: [ + { + message: `EmptyStateHeader is no longer exported by PatternFly.`, + type: "ExportNamedDeclaration", + }, + ], + }, + { + code: `export { EmptyStateHeader, EmptyStateIcon } from '@patternfly/react-core';`, + output: ``, + errors: [ + { + message: `EmptyStateHeader and EmptyStateIcon are no longer exported by PatternFly.`, + type: "ExportNamedDeclaration", + }, + ], + }, + { + code: `export { EmptyStateHeader, Button, EmptyStateIcon } from '@patternfly/react-core';`, + output: `export { Button } from '@patternfly/react-core';`, + errors: [ + { + message: `EmptyStateHeader is no longer exported by PatternFly.`, + type: "ExportNamedDeclaration", + }, + { + message: `EmptyStateIcon is no longer exported by PatternFly.`, + type: "ExportNamedDeclaration", + }, + ], + }, + { + code: `import { EmptyStateHeader } from '@patternfly/react-core';export { EmptyStateHeader };`, + output: `import { EmptyStateHeader } from '@patternfly/react-core';`, + errors: [ + { + message: `EmptyStateHeader is no longer exported by PatternFly. This rule will not fix any imports, as our cleanup rule handles removal of unused imports.`, + type: "ImportSpecifier", + }, + { + message: `EmptyStateHeader is no longer exported by PatternFly.`, + type: "ExportNamedDeclaration", + }, + ], + }, + { + code: `import { EmptyStateHeader as CustomHeader } from '@patternfly/react-core';export { CustomHeader };`, + output: `import { EmptyStateHeader as CustomHeader } from '@patternfly/react-core';`, + errors: [ + { + message: `EmptyStateHeader is no longer exported by PatternFly. This rule will not fix any imports, as our cleanup rule handles removal of unused imports.`, + type: "ImportSpecifier", + }, + { + message: `CustomHeader is no longer exported by PatternFly.`, + type: "ExportNamedDeclaration", + }, + ], + }, + { + code: `import { EmptyStateHeader } from '@patternfly/react-core';export { EmptyStateHeader as CustomHeader };`, + output: `import { EmptyStateHeader } from '@patternfly/react-core';`, + errors: [ + { + message: `EmptyStateHeader is no longer exported by PatternFly. This rule will not fix any imports, as our cleanup rule handles removal of unused imports.`, + type: "ImportSpecifier", + }, + { + message: `EmptyStateHeader is no longer exported by PatternFly.`, + type: "ExportNamedDeclaration", + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateNonExportedComponents/emptyState-nonExported-components.ts b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateNonExportedComponents/emptyState-nonExported-components.ts new file mode 100644 index 000000000..9b4a40bf4 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateNonExportedComponents/emptyState-nonExported-components.ts @@ -0,0 +1,117 @@ +import { Rule } from "eslint"; +import { + ImportSpecifier, + ExportSpecifier, + ExportNamedDeclaration, + ExportDefaultDeclaration, +} from "estree-jsx"; +import { getFromPackage } from "../../helpers"; + +// https://github.com/patternfly/patternfly-react/pull/10364 +module.exports = { + meta: { fixable: "code" }, + create: function (context: Rule.RuleContext) { + const { imports, exports } = getFromPackage( + context, + "@patternfly/react-core" + ); + const emptyStateComponents = ["EmptyStateHeader", "EmptyStateIcon"]; + const emptyStateImports = imports.filter((specifier) => + emptyStateComponents.includes(specifier.imported.name) + ); + const emptyStateExports = exports.filter((specifier) => + emptyStateComponents.includes(specifier.local.name) + ); + + if (!emptyStateImports.length && !emptyStateExports.length) { + return {}; + } + + return { + ImportSpecifier(node: ImportSpecifier) { + if (!emptyStateImports.length) { + return; + } + + emptyStateImports.forEach((esImport) => { + context.report({ + node, + message: `${esImport.imported.name} is no longer exported by PatternFly. This rule will not fix any imports, as our cleanup rule handles removal of unused imports.`, + }); + }); + }, + ExportNamedDeclaration(node: ExportNamedDeclaration) { + const exportsToRemove = node.specifiers.filter( + (spec) => + (emptyStateComponents.includes(spec.local.name) && + node.source?.value === "@patternfly/react-core") || + emptyStateImports.some((imp) => imp.local.name === spec.local.name) + ); + if (!exportsToRemove.length) { + return; + } + + if (exportsToRemove.length === node.specifiers.length) { + const validExportNames = exportsToRemove + .map((exportToRemove) => exportToRemove.local.name) + .join(" and "); + context.report({ + node, + message: `${validExportNames} ${ + exportsToRemove.length > 1 ? "are" : "is" + } no longer exported by PatternFly.`, + fix(fixer) { + return fixer.remove(node); + }, + }); + + return; + } + + exportsToRemove.forEach((exportToRemove) => { + const { range } = exportToRemove; + const tokenBefore = context + .getSourceCode() + .getTokenBefore(exportToRemove); + const isCommaBefore = tokenBefore?.value === ","; + const tokenAfter = context + .getSourceCode() + .getTokenAfter(exportToRemove); + const isCommaAfter = tokenAfter?.value === ","; + const rangeStartValue = isCommaBefore + ? tokenBefore.range[0] + : range?.[0]; + const rangeEndValue = isCommaAfter ? tokenAfter.range[1] : range?.[1]; + context.report({ + node, + message: `${exportToRemove.local.name} is no longer exported by PatternFly.`, + fix(fixer) { + return rangeStartValue && rangeEndValue + ? fixer.removeRange([rangeStartValue, rangeEndValue]) + : []; + }, + }); + }); + }, + ExportDefaultDeclaration(node: ExportDefaultDeclaration) { + const exportName = + node.declaration.type === "Identifier" && node.declaration.name; + const isEmptyStateDefaultExport = emptyStateImports.some( + (imp) => + node.declaration.type === "Identifier" && + imp.local.name === exportName + ); + + if (isEmptyStateDefaultExport) { + context.report({ + node, + message: `${exportName} is no longer exported by PatternFly.`, + fix(fixer) { + return fixer.remove(node); + }, + }); + } + }, + }; + }, +}; diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateNonExportedComponents/emptyStateNonExportedComponentsInput.tsx b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateNonExportedComponents/emptyStateNonExportedComponentsInput.tsx new file mode 100644 index 000000000..017f72a73 --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateNonExportedComponents/emptyStateNonExportedComponentsInput.tsx @@ -0,0 +1,7 @@ +import { EmptyStateHeader, EmptyStateIcon } from "@patternfly/react-core"; + +export { EmptyStateHeader, EmptyStateIcon }; +export { + EmptyStateHeader as CustomESHeader, + EmptyStateIcon as CustomESIcon, +} from "@patternfly/react-core"; diff --git a/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateNonExportedComponents/emptyStateNonExportedComponentsOutput.tsx b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateNonExportedComponents/emptyStateNonExportedComponentsOutput.tsx new file mode 100644 index 000000000..44ff04c0a --- /dev/null +++ b/packages/eslint-plugin-pf-codemods/src/rules/v6/emptyStateNonExportedComponents/emptyStateNonExportedComponentsOutput.tsx @@ -0,0 +1 @@ +import { EmptyStateHeader, EmptyStateIcon } from "@patternfly/react-core";