/* * Copyright © 2009-2011 Reinier Zwitserloot, Roel Spilker and Robbert Jan Grootjans. * * 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.javac.handlers; import static lombok.javac.Javac.annotationTypeMatches; import java.lang.annotation.Annotation; import java.util.regex.Pattern; import lombok.AccessLevel; import lombok.Data; import lombok.Getter; import lombok.core.AnnotationValues; import lombok.core.AST.Kind; import lombok.core.handlers.TransformationsUtil; import lombok.javac.Javac; import lombok.javac.JavacNode; import com.sun.tools.javac.code.Flags; import com.sun.tools.javac.code.TypeTags; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.tree.JCTree.JCLiteral; import com.sun.tools.javac.tree.JCTree.JCModifiers; import com.sun.tools.javac.tree.TreeMaker; import com.sun.tools.javac.tree.JCTree.JCAnnotation; import com.sun.tools.javac.tree.JCTree.JCAssign; import com.sun.tools.javac.tree.JCTree.JCClassDecl; import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; import com.sun.tools.javac.tree.JCTree.JCExpression; import com.sun.tools.javac.tree.JCTree.JCIdent; import com.sun.tools.javac.tree.JCTree.JCImport; import com.sun.tools.javac.tree.JCTree.JCMethodDecl; import com.sun.tools.javac.tree.JCTree.JCMethodInvocation; import com.sun.tools.javac.tree.JCTree.JCNewArray; import com.sun.tools.javac.tree.JCTree.JCStatement; import com.sun.tools.javac.tree.JCTree.JCVariableDecl; import com.sun.tools.javac.util.List; import com.sun.tools.javac.util.ListBuffer; import com.sun.tools.javac.util.Name; /** * Container for static utility methods useful to handlers written for javac. */ public class JavacHandlerUtil { private JavacHandlerUtil() { //Prevent instantiation } /** * Checks if the given expression (that really ought to refer to a type expression) represents a primitive type. */ public static boolean isPrimitive(JCExpression ref) { String typeName = ref.toString(); return TransformationsUtil.PRIMITIVE_TYPE_NAME_PATTERN.matcher(typeName).matches(); } /** * Removes the annotation from javac's AST (it remains in lombok's AST), * then removes any import statement that imports this exact annotation (not star imports). * Only does this if the DeleteLombokAnnotations class is in the context. */ public static void deleteAnnotationIfNeccessary(JavacNode annotation, Class annotationType) { if (!annotation.shouldDeleteLombokAnnotations()) return; JavacNode parentNode = annotation.directUp(); switch (parentNode.getKind()) { case FIELD: case ARGUMENT: case LOCAL: JCVariableDecl variable = (JCVariableDecl) parentNode.get(); variable.mods.annotations = filterList(variable.mods.annotations, annotation.get()); break; case METHOD: JCMethodDecl method = (JCMethodDecl) parentNode.get(); method.mods.annotations = filterList(method.mods.annotations, annotation.get()); break; case TYPE: try { JCClassDecl type = (JCClassDecl) parentNode.get(); type.mods.annotations = filterList(type.mods.annotations, annotation.get()); } catch (ClassCastException e) { //something rather odd has been annotated. Better to just break only delombok instead of everything. } break; default: //This really shouldn't happen, but if it does, better just break delombok instead of breaking everything. return; } deleteImportFromCompilationUnit(annotation, annotationType.getName()); } public static void deleteImportFromCompilationUnit(JavacNode node, String name) { if (!node.shouldDeleteLombokAnnotations()) return; ListBuffer newDefs = ListBuffer.lb(); JCCompilationUnit unit = (JCCompilationUnit) node.top().get(); for (JCTree def : unit.defs) { boolean delete = false; if (def instanceof JCImport) { JCImport imp0rt = (JCImport)def; delete = (!imp0rt.staticImport && imp0rt.qualid.toString().equals(name)); } if (!delete) newDefs.append(def); } unit.defs = newDefs.toList(); } private static List filterList(List annotations, JCTree jcTree) { ListBuffer newAnnotations = ListBuffer.lb(); for (JCAnnotation ann : annotations) { if (jcTree != ann) newAnnotations.append(ann); } return newAnnotations.toList(); } /** * Translates the given field into all possible getter names. * Convenient wrapper around {@link TransformationsUtil#toAllGetterNames(CharSequence, boolean)}. */ public static java.util.List toAllGetterNames(JCVariableDecl field) { CharSequence fieldName = field.name; boolean isBoolean = field.vartype.toString().equals("boolean"); return TransformationsUtil.toAllGetterNames(fieldName, isBoolean); } /** * @return the likely getter name for the stated field. (e.g. private boolean foo; to isFoo). * * Convenient wrapper around {@link TransformationsUtil#toGetterName(CharSequence, boolean)}. */ public static String toGetterName(JCVariableDecl field) { CharSequence fieldName = field.name; boolean isBoolean = field.vartype.toString().equals("boolean"); return TransformationsUtil.toGetterName(fieldName, isBoolean); } /** * Translates the given field into all possible setter names. * Convenient wrapper around {@link TransformationsUtil#toAllSetterNames(CharSequence, boolean)}. */ public static java.util.List toAllSetterNames(JCVariableDecl field) { CharSequence fieldName = field.name; boolean isBoolean = field.vartype.toString().equals("boolean"); return TransformationsUtil.toAllSetterNames(fieldName, isBoolean); } /** * @return the likely setter name for the stated field. (e.g. private boolean foo; to setFoo). * * Convenient wrapper around {@link TransformationsUtil#toSetterName(CharSequence)}. */ public static String toSetterName(JCVariableDecl field) { CharSequence fieldName = field.name; boolean isBoolean = field.vartype.toString().equals("boolean"); return TransformationsUtil.toSetterName(fieldName, isBoolean); } /** Serves as return value for the methods that check for the existence of fields and methods. */ public enum MemberExistsResult { NOT_EXISTS, EXISTS_BY_USER, EXISTS_BY_LOMBOK; } /** * Checks if there is a field with the provided name. * * @param fieldName the field name to check for. * @param node Any node that represents the Type (JCClassDecl) to look in, or any child node thereof. */ public static MemberExistsResult fieldExists(String fieldName, JavacNode node) { while (node != null && !(node.get() instanceof JCClassDecl)) { node = node.up(); } if (node != null && node.get() instanceof JCClassDecl) { for (JCTree def : ((JCClassDecl)node.get()).defs) { if (def instanceof JCVariableDecl) { if (((JCVariableDecl)def).name.contentEquals(fieldName)) { return Javac.getGeneratedBy(def) == null ? MemberExistsResult.EXISTS_BY_USER : MemberExistsResult.EXISTS_BY_LOMBOK; } } } } return MemberExistsResult.NOT_EXISTS; } public static MemberExistsResult methodExists(String methodName, JavacNode node) { return methodExists(methodName, node, true); } /** * Checks if there is a method with the provided name. In case of multiple methods (overloading), only * the first method decides if EXISTS_BY_USER or EXISTS_BY_LOMBOK is returned. * * @param methodName the method name to check for. * @param node Any node that represents the Type (JCClassDecl) to look in, or any child node thereof. * @param caseSensitive If the search should be case sensitive. */ public static MemberExistsResult methodExists(String methodName, JavacNode node, boolean caseSensitive) { while (node != null && !(node.get() instanceof JCClassDecl)) { node = node.up(); } if (node != null && node.get() instanceof JCClassDecl) { for (JCTree def : ((JCClassDecl)node.get()).defs) { if (def instanceof JCMethodDecl) { String name = ((JCMethodDecl)def).name.toString(); boolean matches = caseSensitive ? name.equals(methodName) : name.equalsIgnoreCase(methodName); if (matches) return Javac.getGeneratedBy(def) == null ? MemberExistsResult.EXISTS_BY_USER : MemberExistsResult.EXISTS_BY_LOMBOK; } } } return MemberExistsResult.NOT_EXISTS; } /** * Checks if there is a (non-default) constructor. In case of multiple constructors (overloading), only * the first constructor decides if EXISTS_BY_USER or EXISTS_BY_LOMBOK is returned. * * @param node Any node that represents the Type (JCClassDecl) to look in, or any child node thereof. */ public static MemberExistsResult constructorExists(JavacNode node) { while (node != null && !(node.get() instanceof JCClassDecl)) { node = node.up(); } if (node != null && node.get() instanceof JCClassDecl) { for (JCTree def : ((JCClassDecl)node.get()).defs) { if (def instanceof JCMethodDecl) { if (((JCMethodDecl)def).name.contentEquals("")) { if ((((JCMethodDecl)def).mods.flags & Flags.GENERATEDCONSTR) != 0) continue; return Javac.getGeneratedBy(def) == null ? MemberExistsResult.EXISTS_BY_USER : MemberExistsResult.EXISTS_BY_LOMBOK; } } } } return MemberExistsResult.NOT_EXISTS; } /** * Turns an {@code AccessLevel} instance into the flag bit used by javac. */ public static int toJavacModifier(AccessLevel accessLevel) { switch (accessLevel) { case MODULE: case PACKAGE: return 0; default: case PUBLIC: return Flags.PUBLIC; case NONE: case PRIVATE: return Flags.PRIVATE; case PROTECTED: return Flags.PROTECTED; } } private static class GetterMethod { private final Name name; private final JCExpression type; GetterMethod(Name name, JCExpression type) { this.name = name; this.type = type; } } private static GetterMethod findGetter(JavacNode field) { JCVariableDecl decl = (JCVariableDecl)field.get(); JavacNode typeNode = field.up(); for (String potentialGetterName : toAllGetterNames(decl)) { for (JavacNode potentialGetter : typeNode.down()) { if (potentialGetter.getKind() != Kind.METHOD) continue; JCMethodDecl method = (JCMethodDecl) potentialGetter.get(); if (!method.name.toString().equalsIgnoreCase(potentialGetterName)) continue; /** static getX() methods don't count. */ if ((method.mods.flags & Flags.STATIC) != 0) continue; /** Nor do getters with a non-empty parameter list. */ if (method.params != null && method.params.size() > 0) continue; return new GetterMethod(method.name, method.restype); } } // Check if the field has a @Getter annotation. boolean hasGetterAnnotation = false; for (JavacNode child : field.down()) { if (child.getKind() == Kind.ANNOTATION && annotationTypeMatches(Getter.class, child)) { AnnotationValues ann = Javac.createAnnotation(Getter.class, child); if (ann.getInstance().value() == AccessLevel.NONE) return null; //Definitely WONT have a getter. hasGetterAnnotation = true; } } // Check if the class has a @Getter annotation. if (!hasGetterAnnotation && new HandleGetter().fieldQualifiesForGetterGeneration(field)) { //Check if the class has @Getter or @Data annotation. JavacNode containingType = field.up(); if (containingType != null) for (JavacNode child : containingType.down()) { if (child.getKind() == Kind.ANNOTATION && annotationTypeMatches(Data.class, child)) hasGetterAnnotation = true; if (child.getKind() == Kind.ANNOTATION && annotationTypeMatches(Getter.class, child)) { AnnotationValues ann = Javac.createAnnotation(Getter.class, child); if (ann.getInstance().value() == AccessLevel.NONE) return null; //Definitely WONT have a getter. hasGetterAnnotation = true; } } } if (hasGetterAnnotation) { String getterName = toGetterName(decl); return new GetterMethod(field.toName(getterName), decl.vartype); } return null; } enum FieldAccess { GETTER, PREFER_FIELD, ALWAYS_FIELD; } static boolean lookForGetter(JavacNode field, FieldAccess fieldAccess) { if (fieldAccess == FieldAccess.GETTER) return true; if (fieldAccess == FieldAccess.ALWAYS_FIELD) return false; // If @Getter(lazy = true) is used, then using it is mandatory. for (JavacNode child : field.down()) { if (child.getKind() != Kind.ANNOTATION) continue; if (Javac.annotationTypeMatches(Getter.class, child)) { AnnotationValues ann = Javac.createAnnotation(Getter.class, child); if (ann.getInstance().lazy()) return true; } } return false; } /** * Returns the type of the field, unless a getter exists for this field, in which case the return type of the getter is returned. * * @see #createFieldAccessor(TreeMaker, JavacNode) */ static JCExpression getFieldType(JavacNode field, FieldAccess fieldAccess) { boolean lookForGetter = lookForGetter(field, fieldAccess); GetterMethod getter = lookForGetter ? findGetter(field) : null; if (getter == null) { return ((JCVariableDecl)field.get()).vartype; } return getter.type; } /** * Creates an expression that reads the field. Will either be {@code this.field} or {@code this.getField()} depending on whether or not there's a getter. */ static JCExpression createFieldAccessor(TreeMaker maker, JavacNode field, FieldAccess fieldAccess) { return createFieldAccessor(maker, field, fieldAccess, null); } static JCExpression createFieldAccessor(TreeMaker maker, JavacNode field, FieldAccess fieldAccess, JCExpression receiver) { boolean lookForGetter = lookForGetter(field, fieldAccess); GetterMethod getter = lookForGetter ? findGetter(field) : null; JCVariableDecl fieldDecl = (JCVariableDecl) field.get(); if (getter == null) { if (receiver == null) { if ((fieldDecl.mods.flags & Flags.STATIC) == 0) { receiver = maker.Ident(field.toName("this")); } else { JavacNode containerNode = field.up(); if (containerNode != null && containerNode.get() instanceof JCClassDecl) { JCClassDecl container = (JCClassDecl) field.up().get(); receiver = maker.Ident(container.name); } } } return receiver == null ? maker.Ident(fieldDecl.name) : maker.Select(receiver, fieldDecl.name); } if (receiver == null) receiver = maker.Ident(field.toName("this")); JCMethodInvocation call = maker.Apply(List.nil(), maker.Select(receiver, getter.name), List.nil()); return call; } /** * Adds the given new field declaration to the provided type AST Node. * The field carries the @{@link SuppressWarnings}("all") annotation. * Also takes care of updating the JavacAST. */ public static void injectFieldSuppressWarnings(JavacNode typeNode, JCVariableDecl field) { injectField(typeNode, field, true); } /** * Adds the given new field declaration to the provided type AST Node. * * Also takes care of updating the JavacAST. */ public static void injectField(JavacNode typeNode, JCVariableDecl field) { injectField(typeNode, field, false); } private static void injectField(JavacNode typeNode, JCVariableDecl field, boolean addSuppressWarnings) { JCClassDecl type = (JCClassDecl) typeNode.get(); if (addSuppressWarnings) addSuppressWarningsAll(field.mods, typeNode, field.pos, Javac.getGeneratedBy(field)); type.defs = type.defs.append(field); typeNode.add(field, Kind.FIELD); } /** * Adds the given new method declaration to the provided type AST Node. * Can also inject constructors. * * Also takes care of updating the JavacAST. */ public static void injectMethod(JavacNode typeNode, JCMethodDecl method) { JCClassDecl type = (JCClassDecl) typeNode.get(); if (method.getName().contentEquals("")) { //Scan for default constructor, and remove it. int idx = 0; for (JCTree def : type.defs) { if (def instanceof JCMethodDecl) { if ((((JCMethodDecl)def).mods.flags & Flags.GENERATEDCONSTR) != 0) { JavacNode tossMe = typeNode.getNodeFor(def); if (tossMe != null) tossMe.up().removeChild(tossMe); type.defs = addAllButOne(type.defs, idx); break; } } idx++; } } addSuppressWarningsAll(method.mods, typeNode, method.pos, Javac.getGeneratedBy(method)); type.defs = type.defs.append(method); typeNode.add(method, Kind.METHOD); } private static void addSuppressWarningsAll(JCModifiers mods, JavacNode node, int pos, JCTree source) { TreeMaker maker = node.getTreeMaker(); JCExpression suppressWarningsType = chainDots(maker, node, "java", "lang", "SuppressWarnings"); JCLiteral allLiteral = maker.Literal("all"); suppressWarningsType.pos = pos; allLiteral.pos = pos; JCAnnotation annotation = Javac.recursiveSetGeneratedBy(maker.Annotation(suppressWarningsType, List.of(allLiteral)), source); annotation.pos = pos; mods.annotations = mods.annotations.append(annotation); } private static List addAllButOne(List defs, int idx) { ListBuffer out = ListBuffer.lb(); int i = 0; for (JCTree def : defs) { if (i++ != idx) out.append(def); } return out.toList(); } /** * In javac, dotted access of any kind, from {@code java.lang.String} to {@code var.methodName} * is represented by a fold-left of {@code Select} nodes with the leftmost string represented by * a {@code Ident} node. This method generates such an expression. * * For example, maker.Select(maker.Select(maker.Ident(NAME[java]), NAME[lang]), NAME[String]). * * @see com.sun.tools.javac.tree.JCTree.JCIdent * @see com.sun.tools.javac.tree.JCTree.JCFieldAccess */ public static JCExpression chainDots(TreeMaker maker, JavacNode node, String... elems) { assert elems != null; assert elems.length > 0; JCExpression e = maker.Ident(node.toName(elems[0])); for (int i = 1 ; i < elems.length ; i++) { e = maker.Select(e, node.toName(elems[i])); } return e; } /** * In javac, dotted access of any kind, from {@code java.lang.String} to {@code var.methodName} * is represented by a fold-left of {@code Select} nodes with the leftmost string represented by * a {@code Ident} node. This method generates such an expression. * * For example, maker.Select(maker.Select(maker.Ident(NAME[java]), NAME[lang]), NAME[String]). * * @see com.sun.tools.javac.tree.JCTree.JCIdent * @see com.sun.tools.javac.tree.JCTree.JCFieldAccess */ public static JCExpression chainDotsString(TreeMaker maker, JavacNode node, String elems) { return chainDots(maker, node, elems.split("\\.")); } /** * Searches the given field node for annotations and returns each one that matches the provided regular expression pattern. * * Only the simple name is checked - the package and any containing class are ignored. */ public static List findAnnotations(JavacNode fieldNode, Pattern namePattern) { ListBuffer result = ListBuffer.lb(); for (JavacNode child : fieldNode.down()) { if (child.getKind() == Kind.ANNOTATION) { JCAnnotation annotation = (JCAnnotation) child.get(); String name = annotation.annotationType.toString(); int idx = name.lastIndexOf("."); String suspect = idx == -1 ? name : name.substring(idx + 1); if (namePattern.matcher(suspect).matches()) { result.append(annotation); } } } return result.toList(); } /** * Generates a new statement that checks if the given variable is null, and if so, throws a {@code NullPointerException} with the * variable name as message. */ public static JCStatement generateNullCheck(TreeMaker treeMaker, JavacNode variable) { JCVariableDecl varDecl = (JCVariableDecl) variable.get(); if (isPrimitive(varDecl.vartype)) return null; Name fieldName = varDecl.name; JCExpression npe = chainDots(treeMaker, variable, "java", "lang", "NullPointerException"); JCTree exception = treeMaker.NewClass(null, List.nil(), npe, List.of(treeMaker.Literal(fieldName.toString())), null); JCStatement throwStatement = treeMaker.Throw(exception); return treeMaker.If(treeMaker.Binary(Javac.getCTCint(JCTree.class, "EQ"), treeMaker.Ident(fieldName), treeMaker.Literal(Javac.getCTCint(TypeTags.class, "BOT"), null)), throwStatement, null); } /** * Given a list of field names and a node referring to a type, finds each name in the list that does not match a field within the type. */ public static List createListOfNonExistentFields(List list, JavacNode type, boolean excludeStandard, boolean excludeTransient) { boolean[] matched = new boolean[list.size()]; for (JavacNode child : type.down()) { if (list.isEmpty()) break; if (child.getKind() != Kind.FIELD) continue; JCVariableDecl field = (JCVariableDecl)child.get(); if (excludeStandard) { if ((field.mods.flags & Flags.STATIC) != 0) continue; if (field.name.toString().startsWith("$")) continue; } if (excludeTransient && (field.mods.flags & Flags.TRANSIENT) != 0) continue; int idx = list.indexOf(child.getName()); if (idx > -1) matched[idx] = true; } ListBuffer problematic = ListBuffer.lb(); for (int i = 0 ; i < list.size() ; i++) { if (!matched[i]) problematic.append(i); } return problematic.toList(); } static List getAndRemoveAnnotationParameter(JCAnnotation ast, String parameterName) { ListBuffer params = ListBuffer.lb(); List result = List.nil(); for (JCExpression param : ast.args) { if (param instanceof JCAssign) { JCAssign assign = (JCAssign) param; if (assign.lhs instanceof JCIdent) { JCIdent ident = (JCIdent) assign.lhs; if (parameterName.equals(ident.name.toString())) { if (assign.rhs instanceof JCNewArray) { result = ((JCNewArray) assign.rhs).elems; } else { result = result.append(assign.rhs); } continue; } } } params.append(param); } ast.args = params.toList(); return result; } static List copyAnnotations(List in) { ListBuffer out = ListBuffer.lb(); for (JCExpression expr : in) { if (!(expr instanceof JCAnnotation)) continue; out.append((JCAnnotation) expr.clone()); } return out.toList(); } }