Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions src/core/lombok/CopyWithConstructo.java
Original file line number Diff line number Diff line change
@@ -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 <em>copy-with</em> method for the annotated type.
* <p>
* A <em>copy-with</em> 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.
*
* <h2>Example</h2>
* <pre>
* {@code
* @CopyWith
* class Person {
* private final String name;
* private final int age;
* }
* }
* </pre>
*
* Will generate (conceptually):
* <pre>
* {@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
* );
* }
* }
* }
* </pre>
*
* <p>
* The access level of the generated method can be customized with {@link #access()}.
*
* <p>
* Complete documentation is found at
* <a href="https://projectlombok.org/features/copywith">the project lombok features page for &#64;CopyWith</a>.
*
* @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;
}
124 changes: 124 additions & 0 deletions src/core/lombok/eclipse/handlers/HandleCopyWith.java
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* Generates a <code>copyWith(...)</code> method for the annotated class
* that creates a new instance with optionally replaced field values.
*
* <h2>Example</h2>
* <pre>
* {@code
* @CopyWith
* class Person {
* private final String name;
* private final int age;
* }
* }
* </pre>
*
* Generates (conceptually):
* <pre>
* {@code
* public Person copyWith(String name, int age) {
* return new Person(
* name != null ? name : this.name,
* age != 0 ? age : this.age
* );
* }
* }
* </pre>
*/
@lombok.core.AnnotationHandlerFor(CopyWith.class)
public class HandleCopyWith extends EclipseAnnotationHandler<CopyWith> {

@Override
public void handle(AnnotationValues<CopyWith> 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<EclipseNode> 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);
}
}
124 changes: 124 additions & 0 deletions src/core/lombok/javac/handlers/HandleCopyWith.java
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* Generates a <code>copyWith(...)</code> method for the annotated class
* that creates a new instance with optionally replaced field values.
*
* <h2>Example</h2>
* <pre>
* {@code
* @CopyWith
* class Person {
* private final String name;
* private final int age;
* }
* }
* </pre>
*
* Generates (conceptually):
* <pre>
* {@code
* public Person copyWith(String name, int age) {
* return new Person(
* name != null ? name : this.name,
* age != 0 ? age : this.age
* );
* }
* }
* </pre>
*/
@lombok.core.AnnotationHandlerFor(CopyWith.class)
public class HandleCopyWith extends JavacAnnotationHandler<CopyWith> {

@Override
public void handle(AnnotationValues<CopyWith> 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<JavacNode> fields = fieldsOf(typeNode);

if (fields.isEmpty()) {
source.addWarning("No fields found in class, no copyWith generated.");
return;
}

// build method parameters and constructor arguments
ListBuffer<JCVariableDecl> params = new ListBuffer<>();
ListBuffer<JCExpression> 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);
}
}
23 changes: 23 additions & 0 deletions test/transform/resource/after-ecj/CopyWithTest.java
Original file line number Diff line number Diff line change
@@ -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
);
}
}
ی
18 changes: 18 additions & 0 deletions test/transform/resource/before/CopyWithTest.java
Original file line number Diff line number Diff line change
@@ -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; }
}