diff --git a/src/core/lombok/CopyWithConstructo.java b/src/core/lombok/CopyWithConstructo.java new file mode 100644 index 0000000000..29d3abcde6 --- /dev/null +++ b/src/core/lombok/CopyWithConstructo.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2010-2025 The Project Lombok Authors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package lombok; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Generates a copy-with method for the annotated type. + *

+ * A copy-with method creates a new instance of the class, copying all + * fields from the original object while allowing selected fields to be replaced + * with new values. + * + *

Example

+ *
+ * {@code
+ * @CopyWith
+ * class Person {
+ *     private final String name;
+ *     private final int age;
+ * }
+ * }
+ * 
+ * + * Will generate (conceptually): + *
+ * {@code
+ * class Person {
+ *     private final String name;
+ *     private final int age;
+ *
+ *     public Person copyWith(String name, int age) {
+ *         return new Person(
+ *             name != null ? name : this.name,
+ *             age != 0 ? age : this.age
+ *         );
+ *     }
+ * }
+ * }
+ * 
+ * + *

+ * The access level of the generated method can be customized with {@link #access()}. + * + *

+ * Complete documentation is found at + * the project lombok features page for @CopyWith. + * + * @see lombok.AllArgsConstructor + * @see lombok.With + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface CopyWith { + /** + * Sets the access level of the generated {@code copyWith(...)} method. + * By default, the method is {@code public}. + * + * @return The access modifier of the generated method. + */ + AccessLevel access() default AccessLevel.PUBLIC; +} diff --git a/src/core/lombok/eclipse/handlers/HandleCopyWith.java b/src/core/lombok/eclipse/handlers/HandleCopyWith.java new file mode 100644 index 0000000000..89534753af --- /dev/null +++ b/src/core/lombok/eclipse/handlers/HandleCopyWith.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2010-2025 The Project Lombok Authors. + * + * Licensed under the MIT License (same as other lombok source files). + */ +package lombok.eclipse.handlers; + +import lombok.AccessLevel; +import lombok.CopyWith; +import lombok.core.AnnotationValues; +import lombok.eclipse.EclipseAnnotationHandler; +import lombok.eclipse.EclipseNode; +import lombok.eclipse.handlers.EclipseHandlerUtil; + +import org.eclipse.jdt.internal.compiler.ast.*; +import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants; + +import static lombok.eclipse.handlers.EclipseHandlerUtil.*; + +/** + * Handles the {@link lombok.CopyWith} annotation for Eclipse. + *

+ * Generates a copyWith(...) method for the annotated class + * that creates a new instance with optionally replaced field values. + * + *

Example

+ *
+ * {@code
+ * @CopyWith
+ * class Person {
+ *     private final String name;
+ *     private final int age;
+ * }
+ * }
+ * 
+ * + * Generates (conceptually): + *
+ * {@code
+ * public Person copyWith(String name, int age) {
+ *     return new Person(
+ *         name != null ? name : this.name,
+ *         age != 0 ? age : this.age
+ *     );
+ * }
+ * }
+ * 
+ */ +@lombok.core.AnnotationHandlerFor(CopyWith.class) +public class HandleCopyWith extends EclipseAnnotationHandler { + + @Override + public void handle(AnnotationValues annotation, Annotation ast, EclipseNode annotationNode) { + EclipseNode typeNode = annotationNode.up(); + if (typeNode == null || typeNode.getKind() != EclipseNode.Kind.TYPE) { + annotationNode.addError("@CopyWith is only supported on types."); + return; + } + + CopyWith copyWith = annotation.getInstance(); + AccessLevel level = copyWith.access(); + + generateCopyWith(typeNode, level, annotationNode, ast); + } + + private void generateCopyWith(EclipseNode typeNode, AccessLevel level, EclipseNode source, Annotation ast) { + // get all fields of the class + java.util.List fields = fieldsOf(typeNode); + if (fields.isEmpty()) { + source.addWarning("No fields found in class, no copyWith generated."); + return; + } + + TypeDeclaration typeDecl = (TypeDeclaration) typeNode.get(); + ASTNode sourceAst = source.get(); + + + //build parameters and constructor arguments + Argument[] params = new Argument[fields.size()]; + Expression[] constructorArgs = new Expression[fields.size()]; + + for (int i = 0; i < fields.size(); i++) { + FieldDeclaration fieldDecl = (FieldDeclaration) fields.get(i).get(); + + char[] paramName = fieldDecl.name; + params[i] = new Argument(paramName, 0, copyType(fieldDecl.type, sourceAst), ClassFileConstants.AccFinal); + + //if not null → use the parameter value + Expression cond = EclipseHandlerUtil.makeNullCheck(paramName, copyType(fieldDecl.type, sourceAst)); + Expression fallback = new FieldReference(fieldDecl.name, 0L); + fallback.receiver = new ThisReference(0, 0); + + constructorArgs[i] = new ConditionalExpression( + new SingleNameReference(paramName, 0L), + new SingleNameReference(paramName, 0L), + fallback + ); + } + + // new ClassName(args...) + AllocationExpression constructorCall = new AllocationExpression(); + constructorCall.type = EclipseHandlerUtil.makeType(typeDecl.name, sourceAst); + constructorCall.arguments = constructorArgs; + + // return statement + ReturnStatement returnStmt = new ReturnStatement(constructorCall, 0, 0); + + // body + Statement[] statements = new Statement[]{ returnStmt }; + + // copyWith method + MethodDeclaration copyWithMethod = new MethodDeclaration(typeDecl.compilationResult); + copyWithMethod.modifiers = toEclipseModifier(level); + copyWithMethod.returnType = EclipseHandlerUtil.makeType(typeDecl.name, sourceAst); + copyWithMethod.selector = "copyWith".toCharArray(); + copyWithMethod.arguments = params; + copyWithMethod.bodyStart = copyWithMethod.declarationSourceStart = sourceAst.sourceStart; + copyWithMethod.bodyEnd = copyWithMethod.declarationSourceEnd = sourceAst.sourceEnd; + copyWithMethod.statements = statements; + + //inject method to class + injectMethod(typeNode, copyWithMethod); + } +} diff --git a/src/core/lombok/javac/handlers/HandleCopyWith.java b/src/core/lombok/javac/handlers/HandleCopyWith.java new file mode 100644 index 0000000000..61b118325b --- /dev/null +++ b/src/core/lombok/javac/handlers/HandleCopyWith.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2010-2025 The Project Lombok Authors. + * + * Licensed under the MIT License (same as other lombok source files). + */ +package lombok.javac.handlers; + +import com.sun.tools.javac.tree.JCTree.*; +import com.sun.tools.javac.util.ListBuffer; + +import lombok.AccessLevel; +import lombok.CopyWith; +import lombok.core.AnnotationValues; +import lombok.javac.JavacAnnotationHandler; +import lombok.javac.JavacNode; +import lombok.javac.handlers.JavacHandlerUtil; + +import static lombok.javac.handlers.JavacHandlerUtil.*; + +/** + * Handles the {@link lombok.CopyWith} annotation for javac. + *

+ * Generates a copyWith(...) method for the annotated class + * that creates a new instance with optionally replaced field values. + * + *

Example

+ *
+ * {@code
+ * @CopyWith
+ * class Person {
+ *     private final String name;
+ *     private final int age;
+ * }
+ * }
+ * 
+ * + * Generates (conceptually): + *
+ * {@code
+ * public Person copyWith(String name, int age) {
+ *     return new Person(
+ *         name != null ? name : this.name,
+ *         age != 0 ? age : this.age
+ *     );
+ * }
+ * }
+ * 
+ */ +@lombok.core.AnnotationHandlerFor(CopyWith.class) +public class HandleCopyWith extends JavacAnnotationHandler { + + @Override + public void handle(AnnotationValues annotation, JCAnnotation ast, JavacNode annotationNode) { + JavacNode typeNode = annotationNode.up(); + + if (typeNode == null || typeNode.getKind() != JavacNode.Kind.TYPE) { + annotationNode.addError("@CopyWith is only supported on types."); + return; + } + + CopyWith copyWithInstance = annotation.getInstance(); + AccessLevel level = copyWithInstance.access(); + + generateCopyWith(typeNode, level, annotationNode, ast); + } + + private void generateCopyWith(JavacNode typeNode, AccessLevel level, JavacNode source, JCAnnotation ast) { + // read the fields of the class + java.util.List fields = fieldsOf(typeNode); + + if (fields.isEmpty()) { + source.addWarning("No fields found in class, no copyWith generated."); + return; + } + + // build method parameters and constructor arguments + ListBuffer params = new ListBuffer<>(); + ListBuffer constructorArgs = new ListBuffer<>(); + + for (JavacNode field : fields) { + JCVariableDecl decl = (JCVariableDecl) field.get(); + params.append(treeMaker(typeNode).VarDef( + treeMaker(typeNode).Modifiers(0), + decl.name, + decl.vartype, + null + )); + + + //If no null/zero value is provided → use the original field value + JCExpression replacement = treeMaker(typeNode).Conditional( + treeMaker(typeNode).Binary(JCTree.Tag.NE, + treeMaker(typeNode).Ident(decl.name), + literalNull(typeNode)), + treeMaker(typeNode).Ident(decl.name), + treeMaker(typeNode).Select(treeMaker(typeNode).Ident(typeNode.toName("this")), decl.name) + ); + + constructorArgs.append(replacement); + } + + // method body → return new ClassName(args...) + JCExpression newClassExpr = treeMaker(typeNode).NewClass( + null, nil(), namePlusType(typeNode), constructorArgs.toList(), null + ); + + JCBlock body = treeMaker(typeNode).Block(0, com.sun.tools.javac.util.List.of( + treeMaker(typeNode).Return(newClassExpr) + )); + + JCMethodDecl copyWithMethod = treeMaker(typeNode).MethodDef( + treeMaker(typeNode).Modifiers(toJavacModifier(level)), + typeNode.toName("copyWith"), + namePlusType(typeNode), + nil(), + params.toList(), + nil(), + body, + null + ); + + injectMethod(typeNode, copyWithMethod); + } +} diff --git a/test/transform/resource/after-ecj/CopyWithTest.java b/test/transform/resource/after-ecj/CopyWithTest.java new file mode 100644 index 0000000000..39dc99f707 --- /dev/null +++ b/test/transform/resource/after-ecj/CopyWithTest.java @@ -0,0 +1,23 @@ +package test; + +class Person { + private final String name; + private final int age; + + public Person(String name, int age) { + this.name = name; + this.age = age; + } + + public String getName() { return name; } + public int getAge() { return age; } + + // generated copyWith method + public Person copyWith(String name, int age) { + return new Person( + name != null ? name : this.name, + age != 0 ? age : this.age + ); + } +} +ی \ No newline at end of file diff --git a/test/transform/resource/before/CopyWithTest.java b/test/transform/resource/before/CopyWithTest.java new file mode 100644 index 0000000000..8a01634ec3 --- /dev/null +++ b/test/transform/resource/before/CopyWithTest.java @@ -0,0 +1,18 @@ +package test; + +import lombok.CopyWith; + +@CopyWith +class Person { + private final String name; + private final int age; + + public Person(String name, int age) { + this.name = name; + this.age = age; + } + + // getters + public String getName() { return name; } + public int getAge() { return age; } +}