/* * 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 {findMarker} from "../../markers"; /** * Simplifies object properties where the key and value have the same name, * in both destructuring patterns and object literals. * * Destructuring examples: * - `const { x: x } = obj` becomes `const { x } = obj` * - `function({ ref: ref, ...props })` becomes `function({ ref, ...props })` * * Object literal examples: * - `{ x: x }` becomes `{ x }` * - `{ foo: foo, bar: bar }` becomes `{ foo, bar }` */ export class UseObjectPropertyShorthand extends Recipe { name = "org.openrewrite.javascript.cleanup.use-object-property-shorthand"; displayName = "Use object property shorthand"; description = "Simplifies object properties where the property name and value/variable name are the same (e.g., `{ x: x }` becomes `{ x }`). Applies to both destructuring patterns and object literals."; async editor(): Promise> { return new class extends JavaScriptVisitor { /** * Handle object binding patterns (destructuring): const { x: x } = obj */ protected async visitObjectBindingPattern(pattern: JS.ObjectBindingPattern, p: ExecutionContext): Promise { const visited = await super.visitObjectBindingPattern(pattern, p) as JS.ObjectBindingPattern; let hasChanges = false; const simplifiedBindings = visited.bindings.elements.map(right => { const element = right.element; if (element.kind === JS.Kind.BindingElement) { const binding = element as JS.BindingElement; if (binding.propertyName?.element.kind === J.Kind.Identifier) { const propName = (binding.propertyName.element as J.Identifier).simpleName; if (binding.name?.kind === J.Kind.Identifier) { const bindingName = (binding.name as J.Identifier).simpleName; if (propName === bindingName) { hasChanges = true; return { ...right, element: { ...binding, propertyName: undefined, name: { ...binding.name, prefix: binding.propertyName.element.prefix } } } as J.RightPadded; } } } } return right; }); if (!hasChanges) { return visited; } return produce(visited, draft => { draft.bindings.elements = simplifiedBindings; }); } /** * Handle object literals: { x: x } * Object literals are represented as J.NewClass with a body containing JS.PropertyAssignment statements. */ protected async visitNewClass(newClass: J.NewClass, ctx: ExecutionContext): Promise { const visited = await super.visitNewClass(newClass, ctx) as J.NewClass; // Only process object literals (NewClass with body but no class or arguments) if (!visited.body || visited.class || (visited.arguments?.elements && visited.arguments.elements.length > 0)) { return visited; } const statements = visited.body.statements; if (!statements || statements.length === 0) { return visited; } let hasChanges = false; const simplifiedStatements = statements.map(stmt => { if (stmt.element.kind === JS.Kind.PropertyAssignment) { const prop = stmt.element as JS.PropertyAssignment; // Check if the property name is an identifier if (prop.name.element.kind === J.Kind.Identifier) { const propName = (prop.name.element as J.Identifier).simpleName; // Check if the initializer is also an identifier with the same name if (prop.initializer?.kind === J.Kind.Identifier) { const init = prop.initializer as J.Identifier; const initName = init.simpleName; // Skip if initializer has non-null assertion marker if (findMarker(init, JS.Markers.NonNullAssertion)) { return stmt; } if (propName === initName) { hasChanges = true; // Remove the initializer to use shorthand syntax return { ...stmt, element: { ...prop, initializer: undefined } } as J.RightPadded; } } } } return stmt; }); if (!hasChanges) { return visited; } return produce(visited, draft => { draft.body!.statements = simplifiedStatements; }); } }; } }