diff --git a/.gitignore b/.gitignore index c9dcccb544..7325f384dd 100644 --- a/.gitignore +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/src/core/lombok/Onstruct.java b/src/core/lombok/Onstruct.java new file mode 100644 index 0000000000..acb641c7ba --- /dev/null +++ b/src/core/lombok/Onstruct.java @@ -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.
+ * The variables names are the one specified. If the annotation has parameters + * for prefix and suffix, those parameters are added to the variable + * names.
+ * The getter is the existing method in the object's class verifying + *
    + *
  1. return non-void type
  2. + *
  3. requires no argument
  4. + *
  5. match the variable name specified, prefixed by get|is, and ignoring case. + * In the order : + *
      + *
    1. getName is selected if exists
    2. + *
    3. isName is selected if exists
    4. + *
    5. getname (ignoring case) is selected if exists only ONE. compiling error + * if several found
    6. + *
    7. isname (ignoring case) is selected if exists only ONE. compiling error if + * several found
    8. + *
    9. name is selected if exists
    10. + *
    11. name (ignoring case) is selected if exists only ONE. compiling error if + * several found
    12. + *
    13. if no matching method exists, error
    14. + *
    + *
  6. + *
+ * + *

+ * It MUST only be applied to typed declarations. No garantee is present for var + * declaration. + *

+ * + * + *

+ * Before: + * + *

+ * @Onstruct(pre = "b_") Object author, name, editiondate, purchasable = mybook;
+ * 
+ * + * After: + * + *
+ * var b_author = mybook.getAuthor();
+ * var b_name = mybook.getName();
+ * var b_editiondate = mybook.getEditionDate();
+ * var b_purchasable = mybook.isPurchasable();
+ * 
+ * + */ +@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; + +} diff --git a/src/core/lombok/core/handlers/OnstructUtils.java b/src/core/lombok/core/handlers/OnstructUtils.java new file mode 100644 index 0000000000..eae92e8505 --- /dev/null +++ b/src/core/lombok/core/handlers/OnstructUtils.java @@ -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); + } + +} diff --git a/src/core/lombok/eclipse/handlers/HandleOnstruct.java b/src/core/lombok/eclipse/handlers/HandleOnstruct.java new file mode 100644 index 0000000000..059c6d34f8 --- /dev/null +++ b/src/core/lombok/eclipse/handlers/HandleOnstruct.java @@ -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 { + + public static final HandleOnstruct INSTANCE = new HandleOnstruct(); + + @Override public void handle(AnnotationValues annotation, Annotation ast, EclipseNode annotationNode) { + PrintStream stream = System.out; + stream.println("got annotation on " + ast); + annotationNode.up().traverse(new EclipseASTVisitor.Printer(true, stream, true)); + } + + +} diff --git a/src/core/lombok/javac/handlers/HandleOnstruct.java b/src/core/lombok/javac/handlers/HandleOnstruct.java new file mode 100644 index 0000000000..1cdadc66eb --- /dev/null +++ b/src/core/lombok/javac/handlers/HandleOnstruct.java @@ -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 { + + + /** + * 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 upFromAnnotationToSameKind(JavacNode node) { + if (node.getKind() != Kind.ANNOTATION) return Collections.emptyList(); + JavacNode declaration = node.up(); + if (declaration == null) return Collections.emptyList(); + + List 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 findChildrenStatements(Collection fields) { + List 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 annotation, JCAnnotation ast, JavacNode annotationNode) { + Collection annotatedVariables = upFromAnnotationToSameKind(annotationNode); + JavacNode parentNode = annotationNode.up(); + Onstruct annotationInstance = annotation.getInstance(); + deleteAnnotationIfNeccessary(annotationNode, Onstruct.class); + + List 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.nil(), + methCall, + com.sun.tools.javac.util.List.nil()); + variable.name = varNode.toName(varName); + System.out.println("replaced with " + variable); + } else + System.err.println(varNode.get()); + } + +} diff --git a/test/transform/resource/before/OnstructBook.java b/test/transform/resource/before/OnstructBook.java new file mode 100644 index 0000000000..a834852ca6 --- /dev/null +++ b/test/transform/resource/before/OnstructBook.java @@ -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() + } + +}