/* * Copyright 2025 the original author or authors. *

* Licensed under the Moderne Source Available License (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* https://docs.moderne.io/licensing/moderne-source-available-license *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import {Recipe} from "../../../recipe"; import {TreeVisitor} from "../../../visitor"; import {ExecutionContext} from "../../../execution"; import {JavaScriptVisitor} from "../../visitor"; import {J} from "../../../java"; import {JS} from "../../tree"; import {create as produce} from "mutative"; import {ElementRemovalFormatter} from "../../../java/formatting-utils"; export class RemoveDuplicateObjectKeys extends Recipe { name = "org.openrewrite.javascript.migrate.es6.remove-duplicate-object-keys"; displayName = "Remove duplicate object keys"; description = "Remove duplicate keys in object literals, keeping only the last occurrence (last-wins semantics)."; async editor(): Promise> { return new class extends JavaScriptVisitor { protected async visitNewClass(newClass: J.NewClass, ctx: ExecutionContext): Promise { newClass = await super.visitNewClass(newClass, ctx) as J.NewClass; // Only process object literals (NewClass with body but no class or arguments) if (!newClass.body || newClass.class || (newClass.arguments?.elements && newClass.arguments.elements.length > 0)) { return newClass; } const statements = newClass.body.statements; if (!statements || statements.length === 0) { return newClass; } // Build a map of property names to their last occurrence index const propertyNameToLastIndex = new Map(); const propertyNames: (string | null)[] = []; for (let i = 0; i < statements.length; i++) { const stmt = statements[i]; if (stmt.element.kind === JS.Kind.PropertyAssignment) { const prop = stmt.element as JS.PropertyAssignment; const propName = this.getPropertyName(prop); propertyNames.push(propName); if (propName !== null) { propertyNameToLastIndex.set(propName, i); } } else { propertyNames.push(null); } } // Remove duplicate properties and adjust prefixes return produce(newClass, draft => { const filteredStatements: typeof statements = []; const formatter = new ElementRemovalFormatter(); for (let i = 0; i < statements.length; i++) { const propName = propertyNames[i]; // Check if this is a duplicate that should be removed if (propName !== null) { const lastIndex = propertyNameToLastIndex.get(propName)!; if (i < lastIndex) { formatter.markRemoved(statements[i].element); continue; } } const stmt = statements[i]; const adjustedElement = formatter.processKept(stmt.element); filteredStatements.push({ ...stmt, element: adjustedElement }); } if (!formatter.hasRemovals) { return; // No changes needed } draft.body!.statements = filteredStatements; }); } private getPropertyName(prop: JS.PropertyAssignment): string | null { const name = prop.name.element; // Handle identifier: { foo: 1 } if (name.kind === J.Kind.Identifier) { return (name as J.Identifier).simpleName; } // Handle string literal: { "foo": 1 } if (name.kind === J.Kind.Literal) { const literal = name as J.Literal; if (typeof literal.value === 'string') { return literal.value; } } // For computed properties { [expr]: 1 }, we can't statically determine the name // So we return null and don't consider them for deduplication return null; } } } }