Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

3517-onstruct #3518

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
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
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,13 @@
/jvm.locations
/testenv
/gpg.keyring


#eclipse files

org.eclipse.core.resources.prefs
org.eclipse.jdt.core.prefs
target/
.classpath
.project
.settings
135 changes: 135 additions & 0 deletions src/core/lombok/Onstruct.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package lombok;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* The {@link Onstruct} annotation declares variables based on getters of an
* object.<br />
* The variables names are the one specified. If the annotation has parameters
* for prefix and suffix, those parameters are added to the variable
* names.<br />
* The getter is the existing method in the object's class verifying
* <ol>
* <li>return non-void type</li>
* <li>requires no argument</li>
* <li>match the variable name specified, prefixed by get|is, and ignoring case.
* In the order :
* <ol>
* <li>getName is selected if exists</li>
* <li>isName is selected if exists</li>
* <li>getname (ignoring case) is selected if exists only ONE. compiling error
* if several found</li>
* <li>isname (ignoring case) is selected if exists only ONE. compiling error if
* several found</li>
* <li>name is selected if exists</li>
* <li>name (ignoring case) is selected if exists only ONE. compiling error if
* several found</li>
* <li>if no matching method exists, error</li>
* </ol>
* </li>
* </ol>
*
* <p>
* It MUST only be applied to typed declarations. No garantee is present for var
* declaration.
* </p>
*
*
* <p>
* Before:
*
* <pre>
* &#064;Onstruct(pre = "b_") Object author, name, editiondate, purchasable = mybook;
* </pre>
*
* After:
*
* <pre>
* var b_author = mybook.getAuthor();
* var b_name = mybook.getName();
* var b_editiondate = mybook.getEditionDate();
* var b_purchasable = mybook.isPurchasable();
* </pre>
*
*/
@Target(ElementType.LOCAL_VARIABLE)
@Retention(RetentionPolicy.SOURCE)
public @interface Onstruct {

//
// variable generation
//

/**
* prefix to start the created var name with. Default is empty
*/
String pre() default "";

/**
* suffix to append to created var name. Default is empty
*/
String suf() default "";

/**
* if true, should camel case the variable name. Only applied when prefix is
* non blank. Default is false.
*/
boolean cml() default false;

//
// method generation
//

/**
* how to build the getter for a var name
*/
public enum SourceType {
GET("get", true), BOOL("is", true), FLUENT("", false);

/** prefix to start the getter method with*/
public final String pre;

/** should we uppercase the first letter of the variable in the method name ? */
public final boolean cml;

SourceType(String pre, boolean cml) {
this.pre = pre;
this.cml = cml;
}
}

public SourceType source() default SourceType.GET;

public enum Cml {
CML(true), NOCML(false), SOURCE(null);
;

public final Boolean cml;

Cml(Boolean cml) {
this.cml = cml;
}
}

/**
* can't set default value to null or non-constant values :/
*/
static final String NULLSTRING = "NULLSTRING";

/**
* overwrite the {@link #source()} prefix to start the getter call by.
* Default is {@link #NULLSTRING} to not overwrite
*/
String methodPre() default NULLSTRING;

/**
* Overwrite the {@link #source()}'s method camel case. If
* {@link Cml#SOURCE}(default), don't overwrite. If {@link Cml#CML}, should
* camel case the method call. If {@link Cml#NOCML}, don't camel case it.
*/
Cml methodCml() default Cml.SOURCE;

}
28 changes: 28 additions & 0 deletions src/core/lombok/core/handlers/OnstructUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package lombok.core.handlers;

import lombok.Onstruct;
import lombok.Onstruct.Cml;

public class OnstructUtils {

public static String varName(String requestedName, Onstruct instance) {
String prefix = instance.pre();
String suffix = instance.suf();
boolean cml = instance.cml() && prefix != null && !prefix.isEmpty();
return (prefix != null ? prefix : "") + (cml ? cml(requestedName) : requestedName) + (suffix != null ? suffix : "");
}

public static String methodName(String requestedName, Onstruct instance) {
String methodPrefix = instance.source().pre;
if (!instance.methodPre().equals(Onstruct.NULLSTRING)) methodPrefix = instance.methodPre();
boolean cml = instance.source().cml;
if (instance.methodCml() != Cml.SOURCE) cml = instance.methodCml().cml;
if (methodPrefix == null || methodPrefix.isEmpty()) methodPrefix = "";
return methodPrefix + (cml ? cml(requestedName) : requestedName);
}

public static String cml(String name) {
return name.substring(0, 1).toUpperCase() + name.substring(1);
}

}
30 changes: 30 additions & 0 deletions src/core/lombok/eclipse/handlers/HandleOnstruct.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package lombok.eclipse.handlers;

import java.io.PrintStream;

import org.eclipse.jdt.internal.compiler.ast.Annotation;

import lombok.Onstruct;
import lombok.core.AnnotationValues;
import lombok.core.HandlerPriority;
import lombok.eclipse.DeferUntilPostDiet;
import lombok.eclipse.EclipseASTVisitor;
import lombok.eclipse.EclipseAnnotationHandler;
import lombok.eclipse.EclipseNode;
import lombok.spi.Provides;

@Provides
@DeferUntilPostDiet
@HandlerPriority(65536) // same as HandleValue // TODO
public class HandleOnstruct extends EclipseAnnotationHandler<Onstruct> {

public static final HandleOnstruct INSTANCE = new HandleOnstruct();

@Override public void handle(AnnotationValues<Onstruct> annotation, Annotation ast, EclipseNode annotationNode) {
PrintStream stream = System.out;
stream.println("got annotation on " + ast);
annotationNode.up().traverse(new EclipseASTVisitor.Printer(true, stream, true));
}


}
134 changes: 134 additions & 0 deletions src/core/lombok/javac/handlers/HandleOnstruct.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package lombok.javac.handlers;

import static lombok.javac.handlers.JavacHandlerUtil.deleteAnnotationIfNeccessary;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCAnnotation;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import com.sun.tools.javac.tree.JCTree.JCIdent;
import com.sun.tools.javac.tree.JCTree.JCVariableDecl;

import lombok.Onstruct;
import lombok.core.AST.Kind;
import lombok.core.AnnotationValues;
import lombok.core.HandlerPriority;
import lombok.core.LombokNode;
import lombok.core.handlers.OnstructUtils;
import lombok.eclipse.DeferUntilPostDiet;
import lombok.javac.JavacAnnotationHandler;
import lombok.javac.JavacNode;
import lombok.javac.JavacTreeMaker;
import lombok.spi.Provides;

@Provides
@DeferUntilPostDiet
@HandlerPriority(65536) // same as HandleValue // TODO
public class HandleOnstruct extends JavacAnnotationHandler<Onstruct> {


/**
* find the siblings with same kind and annotation. Copy of
* {@link LombokNode#upFromAnnotationToFields()} with same kind and no check
* on the parent.
*
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public static Collection<JavacNode> upFromAnnotationToSameKind(JavacNode node) {
if (node.getKind() != Kind.ANNOTATION) return Collections.emptyList();
JavacNode declaration = node.up();
if (declaration == null) return Collections.emptyList();

List<JavacNode> fields = new ArrayList();

for (JavacNode potentialField : declaration.up().down()) {
if (potentialField.getKind() != declaration.getKind()) continue;
for (JavacNode child : potentialField.down()) {
if (child.getKind() != Kind.ANNOTATION) continue;
if (child.get() == node.get()) fields.add(potentialField);
}
}

return fields;
}

/**
* retrieve the children statements from a list of node
*/
@SuppressWarnings({"unchecked", "rawtypes"})
protected static List<JCTree> findChildrenStatements(Collection<JavacNode> fields) {
List<JCTree> ret = new ArrayList();
for (JavacNode f : fields) {
for (JavacNode potentialStatement : f.down()) {
if (potentialStatement.getKind() == Kind.STATEMENT) {
ret.add(potentialStatement.get());
}
}
}
return ret;
}

@Override public void handle(AnnotationValues<Onstruct> annotation, JCAnnotation ast, JavacNode annotationNode) {
Collection<JavacNode> annotatedVariables = upFromAnnotationToSameKind(annotationNode);
JavacNode parentNode = annotationNode.up();
Onstruct annotationInstance = annotation.getInstance();
deleteAnnotationIfNeccessary(annotationNode, Onstruct.class);

List<JCTree> statements = findChildrenStatements(annotatedVariables);
// sanity checks on statements. Among the variables declaration, there
// must be exactly one statement.
if (statements.isEmpty()) {
annotationNode.addError("no assignment. Requires one identifier assignment.");
return;
}
if (statements.size() > 1) {
annotationNode.addError("Too many assignments:" + statements + " Requires exactly one identifier assignment.");
return;
}

JCTree tree = statements.get(0);
JCTree.JCIdent ident;
String varName = null;
// sanity checks on the assignment. It must be an identifier.
if (tree instanceof JCTree.JCIdent) {
ident = (JCTree.JCIdent) tree;
varName = (ident.name.toString());
} else {
annotationNode.addError("invalid assignment" + tree + " : must be an identifier");
return;
}
if (varName == null) {
annotationNode.addError("assignement is null . Must be an identifier");
return;
}

for (JavacNode f : annotatedVariables) {
handleVarDeclaration(f, annotationInstance, parentNode, ident);
}
// parentNode.rebuild();
}

private void handleVarDeclaration(JavacNode varNode, Onstruct annotationInstance, JavacNode parentNode, JCIdent ident) {
String varName = OnstructUtils.varName(varNode.getName(), annotationInstance);
String methName = OnstructUtils.methodName(varNode.getName(), annotationInstance);
JCTree elem = varNode.get();
JavacTreeMaker maker = varNode.getTreeMaker();
// System.out.println("create : var " + varName + " = " + ident.name + "." + methName + "();");
if (elem instanceof JCVariableDecl) {
JCVariableDecl variable = (JCVariableDecl) elem;
variable.type = JavacHandlerUtil.chainDots(varNode, "lombok", "var").type;
JCExpression methCall = maker.Select(ident, varNode.toName(methName));
variable.init = maker.Apply(com.sun.tools.javac.util.List.<JCExpression>nil(),
methCall,
com.sun.tools.javac.util.List.<JCExpression>nil());
variable.name = varNode.toName(varName);
System.out.println("replaced with " + variable);
} else
System.err.println(varNode.get());
}

}
49 changes: 49 additions & 0 deletions test/transform/resource/before/OnstructBook.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import java.util.Date;
import java.util.Objects;

import lombok.AllArgsConstructor;
import lombok.Onstruct;
import lombok.Onstruct.Cml;
import lombok.Onstruct.SourceType;
import lombok.core.PrintAST;
import lombok.experimental.Accessors;

public class OnstructBook {

@lombok.Getter
@AllArgsConstructor
public static class Book {

private String author;
@Accessors(fluent = true)
private String name;
private Date editionDate;
private boolean purchasable;

}

void test() {
Book mybook = new Book("author0", "bookname0", new Date(), true);
@Onstruct
Object author, editionDate = mybook;// var author = mybook.getAuthor()
@Onstruct(source=SourceType.FLUENT)
Object name = mybook;// var name = mybook.name()
@Onstruct(source=SourceType.BOOL)
Object purchasable = mybook;// var purchasable = mybook.isPurchasable()
}

void testVarPrefix() {
Book mybook = new Book("author0", "bookname0", new Date(), true);
@Onstruct(pre = "b_")
Object author, editionDate = mybook;
@Onstruct(pre="b_", methodPre = "is")
Object purchasable = mybook;
}

void testOverWriteSourceType() {
Book mybook = new Book("author0", "bookname0", new Date(), true);
@Onstruct(source = SourceType.FLUENT, methodCml = Cml.CML)
Object name=mybook; // var name = mybook.Name()
}

}