/* * Copyright (C) 2009-2020 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.javac.handlers; import static com.sun.tools.javac.code.Flags.GENERATEDCONSTR; import static lombok.core.handlers.HandlerUtil.*; import static lombok.javac.Javac.*; import static lombok.javac.JavacAugments.JCTree_generatedNode; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.lang.model.element.Element; import com.sun.tools.javac.code.BoundKind; import com.sun.tools.javac.code.Flags; import com.sun.tools.javac.code.Scope; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Symbol.ClassSymbol; import com.sun.tools.javac.code.Symbol.MethodSymbol; import com.sun.tools.javac.code.Symbol.VarSymbol; import com.sun.tools.javac.code.Symtab; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.code.Type.MethodType; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.tree.JCTree.JCAnnotation; import com.sun.tools.javac.tree.JCTree.JCArrayTypeTree; import com.sun.tools.javac.tree.JCTree.JCAssign; import com.sun.tools.javac.tree.JCTree.JCBlock; 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.JCExpressionStatement; import com.sun.tools.javac.tree.JCTree.JCFieldAccess; import com.sun.tools.javac.tree.JCTree.JCIdent; import com.sun.tools.javac.tree.JCTree.JCImport; import com.sun.tools.javac.tree.JCTree.JCLiteral; import com.sun.tools.javac.tree.JCTree.JCMethodDecl; import com.sun.tools.javac.tree.JCTree.JCMethodInvocation; import com.sun.tools.javac.tree.JCTree.JCModifiers; import com.sun.tools.javac.tree.JCTree.JCNewArray; import com.sun.tools.javac.tree.JCTree.JCPrimitiveTypeTree; import com.sun.tools.javac.tree.JCTree.JCStatement; import com.sun.tools.javac.tree.JCTree.JCTypeApply; import com.sun.tools.javac.tree.JCTree.JCTypeParameter; import com.sun.tools.javac.tree.JCTree.JCVariableDecl; import com.sun.tools.javac.tree.JCTree.JCWildcard; import com.sun.tools.javac.tree.JCTree.TypeBoundKind; import com.sun.tools.javac.tree.TreeMaker; import com.sun.tools.javac.tree.TreeScanner; import com.sun.tools.javac.util.Context; import com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition; import com.sun.tools.javac.util.List; import com.sun.tools.javac.util.ListBuffer; import com.sun.tools.javac.util.Name; import com.sun.tools.javac.util.Options; import lombok.AccessLevel; import lombok.ConfigurationKeys; import lombok.Data; import lombok.Getter; import lombok.core.AST.Kind; import lombok.core.AnnotationValues; import lombok.core.AnnotationValues.AnnotationValue; import lombok.core.CleanupTask; import lombok.core.LombokImmutableList; import lombok.core.TypeResolver; import lombok.core.configuration.CheckerFrameworkVersion; import lombok.core.configuration.NullCheckExceptionType; import lombok.core.configuration.TypeName; import lombok.core.handlers.HandlerUtil; import lombok.core.handlers.HandlerUtil.FieldAccess; import lombok.delombok.LombokOptionsFactory; import lombok.experimental.Accessors; import lombok.experimental.Tolerate; import lombok.javac.Javac; import lombok.javac.JavacNode; import lombok.javac.JavacTreeMaker; import lombok.permit.Permit; /** * Container for static utility methods useful to handlers written for javac. */ public class JavacHandlerUtil { private JavacHandlerUtil() { //Prevent instantiation } private static class MarkingScanner extends TreeScanner { private final JCTree source; private final Context context; MarkingScanner(JCTree source, Context context) { this.source = source; this.context = context; } @Override public void scan(JCTree tree) { if (tree == null) return; setGeneratedBy(tree, source, context); super.scan(tree); } } /** * Contributed by Jan Lahoda; many lombok transformations should not be run (or a lite version should be run) when the netbeans editor * is running javac on the open source file to find inline errors and such. As class files are compiled separately this does not affect * actual runtime behaviour or file output of the netbeans IDE. */ public static boolean inNetbeansEditor(JavacNode node) { return inNetbeansEditor(node.getContext()); } public static boolean inNetbeansEditor(Context context) { Options options = Options.instance(context); return (options.keySet().contains("ide") && !options.keySet().contains("backgroundCompilation")); } public static boolean inNetbeansCompileOnSave(Context context) { Options options = Options.instance(context); return (options.keySet().contains("ide") && options.keySet().contains("backgroundCompilation")); } public static JCTree getGeneratedBy(JCTree node) { return JCTree_generatedNode.get(node); } public static boolean isGenerated(JCTree node) { return getGeneratedBy(node) != null; } public static T recursiveSetGeneratedBy(T node, JCTree source, Context context) { if (node == null) return null; setGeneratedBy(node, source, context); node.accept(new MarkingScanner(source, context)); return node; } public static T setGeneratedBy(T node, JCTree source, Context context) { if (node == null) return null; if (source == null) JCTree_generatedNode.clear(node); else JCTree_generatedNode.set(node, source); if (source != null && (!inNetbeansEditor(context) || (node instanceof JCVariableDecl && (((JCVariableDecl) node).mods.flags & Flags.PARAMETER) != 0))) node.pos = source.pos; return node; } public static boolean hasAnnotation(String type, JavacNode node) { return hasAnnotation(type, node, false); } public static boolean hasAnnotation(Class type, JavacNode node) { return hasAnnotation(type, node, false); } public static boolean hasAnnotationAndDeleteIfNeccessary(Class type, JavacNode node) { return hasAnnotation(type, node, true); } private static boolean hasAnnotation(Class type, JavacNode node, boolean delete) { if (node == null) return false; if (type == null) return false; switch (node.getKind()) { case ARGUMENT: case FIELD: case LOCAL: case TYPE: case METHOD: for (JavacNode child : node.down()) { if (annotationTypeMatches(type, child)) { if (delete) deleteAnnotationIfNeccessary(child, type); return true; } } // intentional fallthrough default: return false; } } private static boolean hasAnnotation(String type, JavacNode node, boolean delete) { if (node == null) return false; if (type == null) return false; switch (node.getKind()) { case ARGUMENT: case FIELD: case LOCAL: case TYPE: case METHOD: for (JavacNode child : node.down()) { if (annotationTypeMatches(type, child)) { if (delete) deleteAnnotationIfNeccessary(child, type); return true; } } // intentional fallthrough default: return false; } } public static JavacNode findInnerClass(JavacNode parent, String name) { for (JavacNode child : parent.down()) { if (child.getKind() != Kind.TYPE) continue; JCClassDecl td = (JCClassDecl) child.get(); if (td.name.contentEquals(name)) return child; } return null; } public static JavacNode findAnnotation(Class type, JavacNode node) { return findAnnotation(type, node, false); } public static JavacNode findAnnotation(Class type, JavacNode node, boolean delete) { if (node == null) return null; if (type == null) return null; switch (node.getKind()) { case ARGUMENT: case FIELD: case LOCAL: case TYPE: case METHOD: for (JavacNode child : node.down()) { if (annotationTypeMatches(type, child)) { if (delete) deleteAnnotationIfNeccessary(child, type); return child; } } // intentional fallthrough default: return null; } } /** * Checks if the Annotation AST Node provided is likely to be an instance of the provided annotation type. * * @param type An actual annotation type, such as {@code lombok.Getter.class}. * @param node A Lombok AST node representing an annotation in source code. */ public static boolean annotationTypeMatches(Class type, JavacNode node) { if (node.getKind() != Kind.ANNOTATION) return false; return typeMatches(type, node, ((JCAnnotation) node.get()).annotationType); } /** * Checks if the Annotation AST Node provided is likely to be an instance of the provided annotation type. * * @param type An actual annotation type, such as {@code lombok.Getter.class}. * @param node A Lombok AST node representing an annotation in source code. */ public static boolean annotationTypeMatches(String type, JavacNode node) { if (node.getKind() != Kind.ANNOTATION) return false; return typeMatches(type, node, ((JCAnnotation) node.get()).annotationType); } /** * Checks if the given TypeReference node is likely to be a reference to the provided class. * * @param type An actual type. This method checks if {@code typeNode} is likely to be a reference to this type. * @param node A Lombok AST node. Any node in the appropriate compilation unit will do (used to get access to import statements). * @param typeNode A type reference to check. */ public static boolean typeMatches(Class type, JavacNode node, JCTree typeNode) { return typeMatches(type.getName(), node, typeNode); } /** * Checks if the given TypeReference node is likely to be a reference to the provided class. * * @param type An actual type. This method checks if {@code typeNode} is likely to be a reference to this type. * @param node A Lombok AST node. Any node in the appropriate compilation unit will do (used to get access to import statements). * @param typeNode A type reference to check. */ public static boolean typeMatches(String type, JavacNode node, JCTree typeNode) { String typeName = typeNode == null ? null : typeNode.toString(); if (typeName == null || typeName.length() == 0) return false; int lastIndexA = typeName.lastIndexOf('.') + 1; int lastIndexB = Math.max(type.lastIndexOf('.'), type.lastIndexOf('$')) + 1; int len = typeName.length() - lastIndexA; if (len != type.length() - lastIndexB) return false; for (int i = 0; i < len; i++) if (typeName.charAt(i + lastIndexA) != type.charAt(i + lastIndexB)) return false; TypeResolver resolver = node.getImportListAsTypeResolver(); return resolver.typeMatches(node, type, typeName); } /** * Returns if a field is marked deprecated, either by {@code @Deprecated} or in javadoc * @param field the field to check * @return {@code true} if a field is marked deprecated, either by {@code @Deprecated} or in javadoc, otherwise {@code false} */ public static boolean isFieldDeprecated(JavacNode field) { if (!(field.get() instanceof JCVariableDecl)) return false; JCVariableDecl fieldNode = (JCVariableDecl) field.get(); if ((fieldNode.mods.flags & Flags.DEPRECATED) != 0) { return true; } for (JavacNode child : field.down()) { if (annotationTypeMatches(Deprecated.class, child)) { return true; } } return false; } public static CheckerFrameworkVersion getCheckerFrameworkVersion(JavacNode node) { CheckerFrameworkVersion cfv = node.getAst().readConfiguration(ConfigurationKeys.CHECKER_FRAMEWORK); return cfv == null ? CheckerFrameworkVersion.NONE : cfv; } /** * Returns if a node is marked deprecated (as picked up on by the parser). * @param node the node to check (type, method, or field decl). */ public static boolean nodeHasDeprecatedFlag(JCTree node) { if (node instanceof JCVariableDecl) return (((JCVariableDecl) node).mods.flags & Flags.DEPRECATED) != 0; if (node instanceof JCMethodDecl) return (((JCMethodDecl) node).mods.flags & Flags.DEPRECATED) != 0; if (node instanceof JCClassDecl) return (((JCClassDecl) node).mods.flags & Flags.DEPRECATED) != 0; return false; } /** * Creates an instance of {@code AnnotationValues} for the provided AST Node. * * @param type An annotation class type, such as {@code lombok.Getter.class}. * @param node A Lombok AST node representing an annotation in source code. */ public static AnnotationValues createAnnotation(Class type, final JavacNode node) { return createAnnotation(type, (JCAnnotation) node.get(), node); } /** * Creates an instance of {@code AnnotationValues} for the provided AST Node * and Annotation expression. * * @param type An annotation class type, such as {@code lombok.Getter.class}. * @param anno the annotation expression * @param node A Lombok AST node representing an annotation in source code. */ public static AnnotationValues createAnnotation(Class type, JCAnnotation anno, final JavacNode node) { Map values = new HashMap(); List arguments = anno.getArguments(); for (JCExpression arg : arguments) { String mName; JCExpression rhs; java.util.List raws = new ArrayList(); java.util.List guesses = new ArrayList(); java.util.List expressions = new ArrayList(); final java.util.List positions = new ArrayList(); if (arg instanceof JCAssign) { JCAssign assign = (JCAssign) arg; mName = assign.lhs.toString(); rhs = assign.rhs; } else { rhs = arg; mName = "value"; } if (rhs instanceof JCNewArray) { List elems = ((JCNewArray) rhs).elems; for (JCExpression inner : elems) { raws.add(inner.toString()); expressions.add(inner); if (inner instanceof JCAnnotation) { try { @SuppressWarnings("unchecked") Class innerClass = (Class) Class.forName(inner.type.toString()); guesses.add(createAnnotation(innerClass, (JCAnnotation) inner, node)); } catch (ClassNotFoundException ex) { guesses.add(calculateGuess(inner)); } } else { guesses.add(calculateGuess(inner)); } positions.add(inner.pos()); } } else { raws.add(rhs.toString()); expressions.add(rhs); if (rhs instanceof JCAnnotation) { try { @SuppressWarnings("unchecked") Class innerClass = (Class) Class.forName(rhs.type.toString()); guesses.add(createAnnotation(innerClass, (JCAnnotation) rhs, node)); } catch (ClassNotFoundException ex) { guesses.add(calculateGuess(rhs)); } } else { guesses.add(calculateGuess(rhs)); } positions.add(rhs.pos()); } values.put(mName, new AnnotationValue(node, raws, expressions, guesses, true) { @Override public void setError(String message, int valueIdx) { if (valueIdx < 0) node.addError(message); else node.addError(message, positions.get(valueIdx)); } @Override public void setWarning(String message, int valueIdx) { if (valueIdx < 0) node.addWarning(message); else node.addWarning(message, positions.get(valueIdx)); } }); } for (Method m : type.getDeclaredMethods()) { if (!Modifier.isPublic(m.getModifiers())) continue; String name = m.getName(); if (!values.containsKey(name)) { values.put(name, new AnnotationValue(node, new ArrayList(), new ArrayList(), new ArrayList(), false) { @Override public void setError(String message, int valueIdx) { node.addError(message); } @Override public void setWarning(String message, int valueIdx) { node.addWarning(message); } }); } } return new AnnotationValues(type, values, node); } /** * 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, String annotationType) { deleteAnnotationIfNeccessary0(annotation, annotationType); } /** * 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) { deleteAnnotationIfNeccessary0(annotation, annotationType.getName()); } /** * 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 annotationType1, Class annotationType2) { deleteAnnotationIfNeccessary0(annotation, annotationType1.getName(), annotationType2.getName()); } /** * 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 annotationType1, String annotationType2) { deleteAnnotationIfNeccessary0(annotation, annotationType1.getName(), annotationType2); } private static void deleteAnnotationIfNeccessary0(JavacNode annotation, String... annotationTypes) { if (inNetbeansEditor(annotation)) return; 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; } parentNode.getAst().setChanged(); for (String annotationType : annotationTypes) { deleteImportFromCompilationUnit(annotation, annotationType); } } public static void deleteImportFromCompilationUnit(JavacNode node, String name) { if (inNetbeansEditor(node)) return; if (!node.shouldDeleteLombokAnnotations()) return; ListBuffer newDefs = new ListBuffer(); 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 = new ListBuffer(); for (JCAnnotation ann : annotations) { if (jcTree != ann) newAnnotations.append(ann); } return newAnnotations.toList(); } /** Serves as return value for the methods that check for the existence of fields and methods. */ public enum MemberExistsResult { NOT_EXISTS, EXISTS_BY_LOMBOK, EXISTS_BY_USER; } /** * Translates the given field into all possible getter names. * Convenient wrapper around {@link TransformationsUtil#toAllGetterNames(lombok.core.AnnotationValues, CharSequence, boolean)}. */ public static java.util.List toAllGetterNames(JavacNode field) { return HandlerUtil.toAllGetterNames(field.getAst(), getAccessorsForField(field), field.getName(), isBoolean(field)); } /** * @return the likely getter name for the stated field. (e.g. private boolean foo; to isFoo). * * Convenient wrapper around {@link TransformationsUtil#toGetterName(lombok.core.AnnotationValues, CharSequence, boolean)}. */ public static String toGetterName(JavacNode field) { return HandlerUtil.toGetterName(field.getAst(), getAccessorsForField(field), field.getName(), isBoolean(field)); } /** * Translates the given field into all possible setter names. * Convenient wrapper around {@link TransformationsUtil#toAllSetterNames(lombok.core.AnnotationValues, CharSequence, boolean)}. */ public static java.util.List toAllSetterNames(JavacNode field) { return HandlerUtil.toAllSetterNames(field.getAst(), getAccessorsForField(field), field.getName(), isBoolean(field)); } /** * @return the likely setter name for the stated field. (e.g. private boolean foo; to setFoo). * * Convenient wrapper around {@link TransformationsUtil#toSetterName(lombok.core.AnnotationValues, CharSequence, boolean)}. */ public static String toSetterName(JavacNode field) { return HandlerUtil.toSetterName(field.getAst(), getAccessorsForField(field), field.getName(), isBoolean(field)); } /** * Translates the given field into all possible with names. * Convenient wrapper around {@link TransformationsUtil#toAllWithNames(lombok.core.AnnotationValues, CharSequence, boolean)}. */ public static java.util.List toAllWithNames(JavacNode field) { return HandlerUtil.toAllWithNames(field.getAst(), getAccessorsForField(field), field.getName(), isBoolean(field)); } /** * @return the likely with name for the stated field. (e.g. private boolean foo; to withFoo). * * Convenient wrapper around {@link TransformationsUtil#toWithName(lombok.core.AnnotationValues, CharSequence, boolean)}. */ public static String toWithName(JavacNode field) { return HandlerUtil.toWithName(field.getAst(), getAccessorsForField(field), field.getName(), isBoolean(field)); } /** * When generating a setter, the setter either returns void (beanspec) or Self (fluent). * This method scans for the {@code Accessors} annotation to figure that out. */ public static boolean shouldReturnThis(JavacNode field) { if ((((JCVariableDecl) field.get()).mods.flags & Flags.STATIC) != 0) return false; AnnotationValues accessors = JavacHandlerUtil.getAccessorsForField(field); return HandlerUtil.shouldReturnThis0(accessors, field.getAst()); } public static JCExpression cloneSelfType(JavacNode childOfType) { JavacNode typeNode = childOfType; JavacTreeMaker maker = childOfType.getTreeMaker(); while (typeNode != null && typeNode.getKind() != Kind.TYPE) typeNode = typeNode.up(); return JavacHandlerUtil.namePlusTypeParamsToTypeReference(maker, typeNode, ((JCClassDecl) typeNode.get()).typarams); } public static boolean isBoolean(JavacNode field) { JCExpression varType = ((JCVariableDecl) field.get()).vartype; return isBoolean(varType); } public static boolean isBoolean(JCExpression varType) { return varType != null && varType.toString().equals("boolean"); } public static Name removePrefixFromField(JavacNode field) { java.util.List prefixes = null; for (JavacNode node : field.down()) { if (annotationTypeMatches(Accessors.class, node)) { AnnotationValues ann = createAnnotation(Accessors.class, node); if (ann.isExplicit("prefix")) prefixes = Arrays.asList(ann.getInstance().prefix()); break; } } if (prefixes == null) { JavacNode current = field.up(); outer: while (current != null) { for (JavacNode node : current.down()) { if (annotationTypeMatches(Accessors.class, node)) { AnnotationValues ann = createAnnotation(Accessors.class, node); if (ann.isExplicit("prefix")) prefixes = Arrays.asList(ann.getInstance().prefix()); break outer; } } current = current.up(); } } if (prefixes == null) prefixes = field.getAst().readConfiguration(ConfigurationKeys.ACCESSORS_PREFIX); if (!prefixes.isEmpty()) { CharSequence newName = removePrefix(field.getName(), prefixes); if (newName != null) return field.toName(newName.toString()); } return ((JCVariableDecl) field.get()).name; } public static AnnotationValues getAccessorsForField(JavacNode field) { for (JavacNode node : field.down()) { if (annotationTypeMatches(Accessors.class, node)) { return createAnnotation(Accessors.class, node); } } JavacNode current = field.up(); while (current != null) { for (JavacNode node : current.down()) { if (annotationTypeMatches(Accessors.class, node)) { return createAnnotation(Accessors.class, node); } } current = current.up(); } return AnnotationValues.of(Accessors.class, field); } /** * 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) { node = upToTypeNode(node); 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 getGeneratedBy(def) == null ? MemberExistsResult.EXISTS_BY_USER : MemberExistsResult.EXISTS_BY_LOMBOK; } } } } return MemberExistsResult.NOT_EXISTS; } public static MemberExistsResult methodExists(String methodName, JavacNode node, int params) { return methodExists(methodName, node, true, params); } /** * 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. * @param params The number of parameters the method should have; varargs count as 0-*. Set to -1 to find any method with the appropriate name regardless of parameter count. */ public static MemberExistsResult methodExists(String methodName, JavacNode node, boolean caseSensitive, int params) { node = upToTypeNode(node); if (node != null && node.get() instanceof JCClassDecl) { top: for (JCTree def : ((JCClassDecl)node.get()).defs) { if (def instanceof JCMethodDecl) { JCMethodDecl md = (JCMethodDecl) def; String name = md.name.toString(); boolean matches = caseSensitive ? name.equals(methodName) : name.equalsIgnoreCase(methodName); if (matches) { if (params > -1) { List ps = md.params; int minArgs = 0; int maxArgs = 0; if (ps != null && ps.length() > 0) { minArgs = ps.length(); if ((ps.last().mods.flags & Flags.VARARGS) != 0) { maxArgs = Integer.MAX_VALUE; minArgs--; } else { maxArgs = minArgs; } } if (params < minArgs || params > maxArgs) continue; } if (isTolerate(node, md)) continue top; return getGeneratedBy(def) == null ? MemberExistsResult.EXISTS_BY_USER : MemberExistsResult.EXISTS_BY_LOMBOK; } } } } return MemberExistsResult.NOT_EXISTS; } public static boolean isTolerate(JavacNode node, JCTree.JCMethodDecl md) { List annotations = md.getModifiers().getAnnotations(); if (annotations != null) for (JCTree.JCAnnotation anno : annotations) { if (typeMatches(Tolerate.class, node, anno.getAnnotationType())) return true; } return false; } /** * 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) { node = upToTypeNode(node); if (node != null && node.get() instanceof JCClassDecl) { for (JCTree def : ((JCClassDecl)node.get()).defs) { if (def instanceof JCMethodDecl) { JCMethodDecl md = (JCMethodDecl) def; if (md.name.contentEquals("")) { if ((md.mods.flags & Flags.GENERATEDCONSTR) != 0) continue; if (isTolerate(node, md)) continue; return getGeneratedBy(def) == null ? MemberExistsResult.EXISTS_BY_USER : MemberExistsResult.EXISTS_BY_LOMBOK; } } } } return MemberExistsResult.NOT_EXISTS; } public static boolean isConstructorCall(final JCStatement statement) { if (!(statement instanceof JCExpressionStatement)) return false; JCExpression expr = ((JCExpressionStatement) statement).expr; if (!(expr instanceof JCMethodInvocation)) return false; JCExpression invocation = ((JCMethodInvocation) expr).meth; String name; if (invocation instanceof JCFieldAccess) { name = ((JCFieldAccess) invocation).name.toString(); } else if (invocation instanceof JCIdent) { name = ((JCIdent) invocation).name.toString(); } else { name = ""; } return "super".equals(name) || "this".equals(name); } /** * 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(field)) { 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 = 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 && 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 = createAnnotation(Getter.class, child); if (ann.getInstance().value() == AccessLevel.NONE) return null; //Definitely WONT have a getter. hasGetterAnnotation = true; } } } if (hasGetterAnnotation) { String getterName = toGetterName(field); if (getterName == null) return null; return new GetterMethod(field.toName(getterName), decl.vartype); } return null; } 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 (annotationTypeMatches(Getter.class, child)) { AnnotationValues ann = 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, FieldAccess) */ static JCExpression getFieldType(JavacNode field, FieldAccess fieldAccess) { if (field.getKind() == Kind.METHOD) return ((JCMethodDecl) field.get()).restype; 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(JavacTreeMaker maker, JavacNode field, FieldAccess fieldAccess) { return createFieldAccessor(maker, field, fieldAccess, null); } static JCExpression createFieldAccessor(JavacTreeMaker 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; } static JCExpression createMethodAccessor(JavacTreeMaker maker, JavacNode method) { return createMethodAccessor(maker, method, null); } static JCExpression createMethodAccessor(JavacTreeMaker maker, JavacNode method, JCExpression receiver) { JCMethodDecl methodDecl = (JCMethodDecl) method.get(); if (receiver == null && (methodDecl.mods.flags & Flags.STATIC) == 0) { receiver = maker.Ident(method.toName("this")); } else if (receiver == null) { JavacNode containerNode = method.up(); if (containerNode != null && containerNode.get() instanceof JCClassDecl) { JCClassDecl container = (JCClassDecl) method.up().get(); receiver = maker.Ident(container.name); } } JCMethodInvocation call = maker.Apply(List.nil(), receiver == null ? maker.Ident(methodDecl.name) : maker.Select(receiver, methodDecl.name), List.nil()); return call; } public static Type getMirrorForFieldType(JavacNode fieldNode) { Element fieldElement = fieldNode.getElement(); if (fieldElement instanceof VarSymbol) return ((VarSymbol) fieldElement).type; return null; } /** * 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 JavacNode injectFieldAndMarkGenerated(JavacNode typeNode, JCVariableDecl field) { return 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 JavacNode injectField(JavacNode typeNode, JCVariableDecl field) { return injectField(typeNode, field, false); } public static JavacNode injectField(JavacNode typeNode, JCVariableDecl field, boolean addGenerated) { return injectField(typeNode, field, addGenerated, false); } public static JavacNode injectField(JavacNode typeNode, JCVariableDecl field, boolean addGenerated, boolean specialEnumHandling) { JCClassDecl type = (JCClassDecl) typeNode.get(); if (addGenerated) { addSuppressWarningsAll(field.mods, typeNode, field.pos, getGeneratedBy(field), typeNode.getContext()); addGenerated(field.mods, typeNode, field.pos, getGeneratedBy(field), typeNode.getContext()); } List insertAfter = null; List insertBefore = type.defs; while (true) { boolean skip = false; if (insertBefore.head instanceof JCVariableDecl) { JCVariableDecl f = (JCVariableDecl) insertBefore.head; if ((!specialEnumHandling && isEnumConstant(f)) || isGenerated(f)) skip = true; } else if (insertBefore.head instanceof JCMethodDecl) { if ((((JCMethodDecl) insertBefore.head).mods.flags & GENERATEDCONSTR) != 0) skip = true; } if (skip) { insertAfter = insertBefore; insertBefore = insertBefore.tail; continue; } break; } List fieldEntry = List.of(field); fieldEntry.tail = insertBefore; if (insertAfter == null) { type.defs = fieldEntry; } else { insertAfter.tail = fieldEntry; } return typeNode.add(field, Kind.FIELD); } public static boolean isEnumConstant(final JCVariableDecl field) { return (field.mods.flags & Flags.ENUM) != 0; } static class JCAnnotatedTypeReflect { private static Class TYPE; private static Constructor CONSTRUCTOR; private static Field ANNOTATIONS, UNDERLYING_TYPE; private static void init(Class in) { if (TYPE != null) return; if (!in.getName().equals("com.sun.tools.javac.tree.JCTree$JCAnnotatedType")) return; try { CONSTRUCTOR = Permit.getConstructor(in, List.class, JCExpression.class); ANNOTATIONS = Permit.getField(in, "annotations"); UNDERLYING_TYPE = Permit.getField(in, "underlyingType"); TYPE = in; } catch (Exception ignore) {} } static boolean is(JCTree obj) { if (obj == null) return false; init(obj.getClass()); return obj.getClass() == TYPE; } @SuppressWarnings("unchecked") static List getAnnotations(JCTree obj) { init(obj.getClass()); try { return (List) ANNOTATIONS.get(obj); } catch (Exception e) { return List.nil(); } } static JCExpression getUnderlyingType(JCTree obj) { init(obj.getClass()); try { return (JCExpression) UNDERLYING_TYPE.get(obj); } catch (Exception e) { return null; } } static JCExpression create(List annotations, JCExpression underlyingType) { try { return (JCExpression) CONSTRUCTOR.newInstance(annotations, underlyingType); } catch (Exception e) { return null; } } } // jdk9 support, types have changed, names stay the same static class ClassSymbolMembersField { private static final Field membersField; private static final Method removeMethod; private static final Method enterMethod; static { Field f = null; Method r = null; Method e = null; try { f = Permit.getField(ClassSymbol.class, "members_field"); r = Permit.getMethod(f.getType(), "remove", Symbol.class); e = Permit.getMethod(f.getType(), "enter", Symbol.class); } catch (Exception ex) {} membersField = f; removeMethod = r; enterMethod = e; } static void remove(ClassSymbol from, Symbol toRemove) { if (from == null) return; try { Scope scope = (Scope) membersField.get(from); if (scope == null) return; removeMethod.invoke(scope, toRemove); } catch (Exception e) {} } static void enter(ClassSymbol from, Symbol toEnter) { if (from == null) return; try { Scope scope = (Scope) membersField.get(from); if (scope == null) return; enterMethod.invoke(scope, toEnter); } catch (Exception e) {} } } public static void injectMethod(JavacNode typeNode, JCMethodDecl method) { injectMethod(typeNode, method, null, null); } /** * 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, List paramTypes, Type returnType) { 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); ClassSymbolMembersField.remove(type.sym, ((JCMethodDecl) def).sym); break; } } idx++; } } addSuppressWarningsAll(method.mods, typeNode, method.pos, getGeneratedBy(method), typeNode.getContext()); addGenerated(method.mods, typeNode, method.pos, getGeneratedBy(method), typeNode.getContext()); type.defs = type.defs.append(method); fixMethodMirror(typeNode.getContext(), typeNode.getElement(), method.getModifiers().flags, method.getName(), paramTypes, returnType); typeNode.add(method, Kind.METHOD); } private static void fixMethodMirror(Context context, Element typeMirror, long access, Name methodName, List paramTypes, Type returnType) { if (typeMirror == null || paramTypes == null || returnType == null) return; ClassSymbol cs = (ClassSymbol) typeMirror; MethodSymbol methodSymbol = new MethodSymbol(access, methodName, new MethodType(paramTypes, returnType, List.nil(), Symtab.instance(context).methodClass), cs); ClassSymbolMembersField.enter(cs, methodSymbol); } /** * Adds an inner type (class, interface, enum) to the given type. Cannot inject top-level types. * * @param typeNode parent type to inject new type into * @param type New type (class, interface, etc) to inject. * @return */ public static JavacNode injectType(JavacNode typeNode, final JCClassDecl type) { JCClassDecl typeDecl = (JCClassDecl) typeNode.get(); addSuppressWarningsAll(type.mods, typeNode, type.pos, getGeneratedBy(type), typeNode.getContext()); addGenerated(type.mods, typeNode, type.pos, getGeneratedBy(type), typeNode.getContext()); typeDecl.defs = typeDecl.defs.append(type); return typeNode.add(type, Kind.TYPE); } public static long addFinalIfNeeded(long flags, Context context) { boolean addFinal = LombokOptionsFactory.getDelombokOptions(context).getFormatPreferences().generateFinalParams(); if (addFinal) flags |= Flags.FINAL; return flags; } public static JCExpression genTypeRef(JavacNode node, String complexName) { String[] parts = complexName.split("\\."); if (parts.length > 2 && parts[0].equals("java") && parts[1].equals("lang")) { String[] subParts = new String[parts.length - 2]; System.arraycopy(parts, 2, subParts, 0, subParts.length); return genJavaLangTypeRef(node, subParts); } return chainDots(node, parts); } public static JCExpression genJavaLangTypeRef(JavacNode node, String... simpleNames) { if (LombokOptionsFactory.getDelombokOptions(node.getContext()).getFormatPreferences().javaLangAsFqn()) { return chainDots(node, "java", "lang", simpleNames); } else { return chainDots(node, null, null, simpleNames); } } public static JCExpression genJavaLangTypeRef(JavacNode node, int pos, String... simpleNames) { if (LombokOptionsFactory.getDelombokOptions(node.getContext()).getFormatPreferences().javaLangAsFqn()) { return chainDots(node, pos, "java", "lang", simpleNames); } else { return chainDots(node, pos, null, null, simpleNames); } } public static void addSuppressWarningsAll(JCModifiers mods, JavacNode node, int pos, JCTree source, Context context) { if (!LombokOptionsFactory.getDelombokOptions(context).getFormatPreferences().generateSuppressWarnings()) return; addAnnotation(mods, node, pos, source, context, "java.lang.SuppressWarnings", node.getTreeMaker().Literal("all")); if (Boolean.TRUE.equals(node.getAst().readConfiguration(ConfigurationKeys.ADD_FINDBUGS_SUPPRESSWARNINGS_ANNOTATIONS))) { JavacTreeMaker maker = node.getTreeMaker(); JCExpression arg = maker.Assign(maker.Ident(node.toName("justification")), maker.Literal("generated code")); addAnnotation(mods, node, pos, source, context, "edu.umd.cs.findbugs.annotations.SuppressFBWarnings", arg); } } public static void addGenerated(JCModifiers mods, JavacNode node, int pos, JCTree source, Context context) { if (!LombokOptionsFactory.getDelombokOptions(context).getFormatPreferences().generateGenerated()) return; if (HandlerUtil.shouldAddGenerated(node)) { addAnnotation(mods, node, pos, source, context, "javax.annotation.Generated", node.getTreeMaker().Literal("lombok")); } if (Boolean.TRUE.equals(node.getAst().readConfiguration(ConfigurationKeys.ADD_LOMBOK_GENERATED_ANNOTATIONS))) { addAnnotation(mods, node, pos, source, context, "lombok.Generated", null); } } public static void addAnnotation(JCModifiers mods, JavacNode node, int pos, JCTree source, Context context, String annotationTypeFqn, JCExpression arg) { boolean isJavaLangBased; String simpleName; { int idx = annotationTypeFqn.lastIndexOf('.'); simpleName = idx == -1 ? annotationTypeFqn : annotationTypeFqn.substring(idx + 1); isJavaLangBased = idx == 9 && annotationTypeFqn.regionMatches(0, "java.lang.", 0, 10); } for (JCAnnotation ann : mods.annotations) { JCTree annType = ann.getAnnotationType(); if (annType instanceof JCIdent) { Name lastPart = ((JCIdent) annType).name; if (lastPart.contentEquals(simpleName)) return; } if (annType instanceof JCFieldAccess) { if (annType.toString().equals(annotationTypeFqn)) return; } } JavacTreeMaker maker = node.getTreeMaker(); JCExpression annType = isJavaLangBased ? genJavaLangTypeRef(node, simpleName) : chainDotsString(node, annotationTypeFqn); annType.pos = pos; if (arg != null) { arg.pos = pos; if (arg instanceof JCAssign) { ((JCAssign) arg).lhs.pos = pos; ((JCAssign) arg).rhs.pos = pos; } } List argList = arg != null ? List.of(arg) : List.nil(); JCAnnotation annotation = recursiveSetGeneratedBy(maker.Annotation(annType, argList), source, context); annotation.pos = pos; mods.annotations = mods.annotations.append(annotation); } private static List addAllButOne(List defs, int idx) { ListBuffer out = new ListBuffer(); 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. *

* The position of the generated node(s) will be unpositioned (-1). * * 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(JavacNode node, String elem1, String elem2, String... elems) { return chainDots(node, -1, elem1, elem2, elems); } public static JCExpression chainDots(JavacNode node, String[] elems) { return chainDots(node, -1, null, null, elems); } public static JCExpression chainDots(JavacNode node, LombokImmutableList elems) { assert elems != null; JavacTreeMaker maker = node.getTreeMaker(); JCExpression e = null; for (String elem : elems) { if (e == null) e = maker.Ident(node.toName(elem)); else e = maker.Select(e, node.toName(elem)); } 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. *

* The position of the generated node(s) will be equal to the {@code pos} parameter. * * 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(JavacNode node, int pos, String elem1, String elem2, String... elems) { assert elems != null; JavacTreeMaker maker = node.getTreeMaker(); if (pos != -1) maker = maker.at(pos); JCExpression e = null; if (elem1 != null) e = maker.Ident(node.toName(elem1)); if (elem2 != null) e = e == null ? maker.Ident(node.toName(elem2)) : maker.Select(e, node.toName(elem2)); for (int i = 0 ; i < elems.length ; i++) { e = e == null ? maker.Ident(node.toName(elems[i])) : maker.Select(e, node.toName(elems[i])); } assert e != null; 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(JavacNode node, String elems) { return chainDots(node, null, null, 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 = new ListBuffer(); 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(); } public static String scanForNearestAnnotation(JavacNode node, String... anns) { while (node != null) { for (JavacNode ann : node.down()) { if (ann.getKind() != Kind.ANNOTATION) continue; JCAnnotation a = (JCAnnotation) ann.get(); for (String annToFind : anns) if (typeMatches(annToFind, node, a.annotationType)) return annToFind; } node = node.up(); } return null; } public static boolean hasNonNullAnnotations(JavacNode node) { for (JavacNode child : node.down()) { if (child.getKind() == Kind.ANNOTATION) { JCAnnotation annotation = (JCAnnotation) child.get(); for (String nn : NONNULL_ANNOTATIONS) if (typeMatches(nn, node, annotation.annotationType)) return true; } } return false; } public static boolean hasNonNullAnnotations(JavacNode node, List anns) { if (anns == null) return false; for (JCAnnotation ann : anns) { for (String nn : NONNULL_ANNOTATIONS) if (typeMatches(nn, node, ann)) return true; } return false; } /** * Searches the given field node for annotations and returns each one that is 'copyable' (either via configuration or from the base list). */ public static List findCopyableAnnotations(JavacNode node) { JCAnnotation anno = null; String annoName = null; for (JavacNode child : node.down()) { if (child.getKind() == Kind.ANNOTATION) { if (anno != null) { annoName = ""; break; } JCAnnotation annotation = (JCAnnotation) child.get(); annoName = annotation.annotationType.toString(); anno = annotation; } } if (annoName == null) return List.nil(); java.util.List configuredCopyable = node.getAst().readConfiguration(ConfigurationKeys.COPYABLE_ANNOTATIONS); if (!annoName.isEmpty()) { for (TypeName cn : configuredCopyable) if (cn != null && typeMatches(cn.toString(), node, anno.annotationType)) return List.of(anno); for (String bn : BASE_COPYABLE_ANNOTATIONS) if (typeMatches(bn, node, anno.annotationType)) return List.of(anno); } ListBuffer result = new ListBuffer(); for (JavacNode child : node.down()) { if (child.getKind() == Kind.ANNOTATION) { JCAnnotation annotation = (JCAnnotation) child.get(); boolean match = false; for (TypeName cn : configuredCopyable) if (cn != null && typeMatches(cn.toString(), node, annotation.annotationType)) { result.append(annotation); match = true; break; } if (!match) for (String bn : BASE_COPYABLE_ANNOTATIONS) if (typeMatches(bn, node, annotation.annotationType)) { result.append(annotation); break; } } } return result.toList(); } /** * Searches the given field node for annotations that are specifically intentioned to be copied to the setter. */ public static List findCopyableToSetterAnnotations(JavacNode node) { JCAnnotation anno = null; String annoName = null; for (JavacNode child : node.down()) { if (child.getKind() == Kind.ANNOTATION) { if (anno != null) { annoName = ""; break; } JCAnnotation annotation = (JCAnnotation) child.get(); annoName = annotation.annotationType.toString(); anno = annotation; } } if (annoName == null) return List.nil(); if (!annoName.isEmpty()) { for (String bn : COPY_TO_SETTER_ANNOTATIONS) if (typeMatches(bn, node, anno.annotationType)) return List.of(anno); } ListBuffer result = new ListBuffer(); for (JavacNode child : node.down()) { if (child.getKind() == Kind.ANNOTATION) { JCAnnotation annotation = (JCAnnotation) child.get(); boolean match = false; if (!match) for (String bn : COPY_TO_SETTER_ANNOTATIONS) if (typeMatches(bn, node, annotation.annotationType)) { result.append(annotation); break; } } } return result.toList(); } /** * Generates a new statement that checks if the given variable is null, and if so, throws a configured exception with the * variable name as message. */ public static JCStatement generateNullCheck(JavacTreeMaker maker, JavacNode variable, JavacNode source) { return generateNullCheck(maker, (JCVariableDecl) variable.get(), source); } /** * Generates a new statement that checks if the given local is null, and if so, throws a configured exception with the * local variable name as message. */ public static JCStatement generateNullCheck(JavacTreeMaker maker, JCExpression typeNode, Name varName, JavacNode source) { NullCheckExceptionType exceptionType = source.getAst().readConfiguration(ConfigurationKeys.NON_NULL_EXCEPTION_TYPE); if (exceptionType == null) exceptionType = NullCheckExceptionType.NULL_POINTER_EXCEPTION; if (isPrimitive(typeNode)) return null; JCLiteral message = maker.Literal(exceptionType.toExceptionMessage(varName.toString())); LombokImmutableList method = exceptionType.getMethod(); if (method != null) { return maker.Exec(maker.Apply(List.nil(), chainDots(source, method), List.of(maker.Ident(varName), message))); } if (exceptionType == NullCheckExceptionType.ASSERTION) { return maker.Assert(maker.Binary(CTC_NOT_EQUAL, maker.Ident(varName), maker.Literal(CTC_BOT, null)), message); } JCExpression exType = genTypeRef(source, exceptionType.getExceptionType()); JCExpression exception = maker.NewClass(null, List.nil(), exType, List.of(message), null); JCStatement throwStatement = maker.Throw(exception); JCBlock throwBlock = maker.Block(0, List.of(throwStatement)); return maker.If(maker.Binary(CTC_EQUAL, maker.Ident(varName), maker.Literal(CTC_BOT, null)), throwBlock, null); } /** * Generates a new statement that checks if the given variable is null, and if so, throws a configured exception with the * variable name as message. * * This is a special case method reserved for use when the provided declaration differs from the * variable's declaration, i.e. in a constructor or setter where the local parameter is named the same but with the prefix * stripped as a result of @Accessors.prefix. */ public static JCStatement generateNullCheck(JavacTreeMaker maker, JCVariableDecl varDecl, JavacNode source) { return generateNullCheck(maker, varDecl.vartype, varDecl.name, source); } /** * 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 = new ListBuffer(); for (int i = 0 ; i < list.size() ; i++) { if (!matched[i]) problematic.append(i); } return problematic.toList(); } static List unboxAndRemoveAnnotationParameter(JCAnnotation ast, String parameterName, String errorName, JavacNode annotationNode) { ListBuffer params = new ListBuffer(); ListBuffer result = new ListBuffer(); outer: for (JCExpression param : ast.args) { boolean allowRaw; String nameOfParam = "value"; JCExpression valueOfParam = null; if (param instanceof JCAssign) { JCAssign assign = (JCAssign) param; if (assign.lhs instanceof JCIdent) { JCIdent ident = (JCIdent) assign.lhs; nameOfParam = ident.name.toString(); } valueOfParam = assign.rhs; } /* strip trailing underscores */ { int lastIdx; for (lastIdx = nameOfParam.length() ; lastIdx > 0; lastIdx--) { if (nameOfParam.charAt(lastIdx - 1) != '_') break; } allowRaw = lastIdx < nameOfParam.length(); nameOfParam = nameOfParam.substring(0, lastIdx); } if (!parameterName.equals(nameOfParam)) { params.append(param); continue outer; } int endPos = Javac.getEndPosition(param.pos(), (JCCompilationUnit) annotationNode.top().get()); annotationNode.getAst().removeFromDeferredDiagnostics(param.pos, endPos); if (valueOfParam instanceof JCAnnotation) { String dummyAnnotationName = ((JCAnnotation) valueOfParam).annotationType.toString(); dummyAnnotationName = dummyAnnotationName.replace("_", "").replace("$", "").replace("x", "").replace("X", ""); if (dummyAnnotationName.length() > 0) { if (allowRaw) { result.append((JCAnnotation) valueOfParam); } else { addError(errorName, annotationNode); continue outer; } } else { for (JCExpression expr : ((JCAnnotation) valueOfParam).args) { if (expr instanceof JCAssign && ((JCAssign) expr).lhs instanceof JCIdent) { JCIdent id = (JCIdent) ((JCAssign) expr).lhs; if ("value".equals(id.name.toString())) { expr = ((JCAssign) expr).rhs; } else { addError(errorName, annotationNode); } } if (expr instanceof JCAnnotation) { result.append((JCAnnotation) expr); } else if (expr instanceof JCNewArray) { for (JCExpression expr2 : ((JCNewArray) expr).elems) { if (expr2 instanceof JCAnnotation) { result.append((JCAnnotation) expr2); } else { addError(errorName, annotationNode); continue outer; } } } else { addError(errorName, annotationNode); continue outer; } } } } else if (valueOfParam instanceof JCNewArray) { JCNewArray arr = (JCNewArray) valueOfParam; if (arr.elems.isEmpty()) { // Just remove it, this is always fine. } else if (allowRaw) { for (JCExpression jce : arr.elems) { if (jce instanceof JCAnnotation) result.append((JCAnnotation) jce); else addError(errorName, annotationNode); } } else { addError(errorName, annotationNode); } } else { addError(errorName, annotationNode); } } ast.args = params.toList(); return result.toList(); } private static void addError(String errorName, JavacNode node) { if (node.getLatestJavaSpecSupported() < 8) { node.addError("The correct format up to JDK7 is " + errorName + "=@__({@SomeAnnotation, @SomeOtherAnnotation}))"); } else { node.addError("The correct format for JDK8+ is " + errorName + "_={@SomeAnnotation, @SomeOtherAnnotation})"); } } public static List copyTypeParams(JavacNode source, List params) { if (params == null || params.isEmpty()) return params; ListBuffer out = new ListBuffer(); JavacTreeMaker maker = source.getTreeMaker(); Context context = source.getContext(); for (JCTypeParameter tp : params) { List bounds = tp.bounds; if (bounds != null && !bounds.isEmpty()) { ListBuffer boundsCopy = new ListBuffer(); for (JCExpression expr : tp.bounds) { boundsCopy.append(cloneType(maker, expr, source.get(), context)); } bounds = boundsCopy.toList(); } out.append(maker.TypeParameter(tp.name, bounds)); } return out.toList(); } public static List getTypeUseAnnotations(JCExpression from) { if (!JCAnnotatedTypeReflect.is(from)) return List.nil(); return JCAnnotatedTypeReflect.getAnnotations(from); } public static JCExpression removeTypeUseAnnotations(JCExpression from) { if (!JCAnnotatedTypeReflect.is(from)) return from; return JCAnnotatedTypeReflect.getUnderlyingType(from); } public static JCExpression namePlusTypeParamsToTypeReference(JavacTreeMaker maker, JavacNode type, List params) { JCClassDecl td = (JCClassDecl) type.get(); boolean instance = (td.mods.flags & Flags.STATIC) == 0; return namePlusTypeParamsToTypeReference(maker, type.up(), td.name, instance, params); } public static JCExpression namePlusTypeParamsToTypeReference(JavacTreeMaker maker, JavacNode parentType, Name typeName, boolean instance, List params) { JCExpression r = null; if (parentType != null && parentType.getKind() == Kind.TYPE) { JCClassDecl td = (JCClassDecl) parentType.get(); boolean outerInstance = instance && ((td.mods.flags & Flags.STATIC) == 0); List outerParams = instance ? td.typarams : List.nil(); r = namePlusTypeParamsToTypeReference(maker, parentType.up(), td.name, outerInstance, outerParams); } r = r == null ? maker.Ident(typeName) : maker.Select(r, typeName); if (!params.isEmpty()) r = maker.TypeApply(r, typeParameterNames(maker, params)); return r; } public static List typeParameterNames(JavacTreeMaker maker, List params) { ListBuffer typeArgs = new ListBuffer(); for (JCTypeParameter param : params) { typeArgs.append(maker.Ident(param.name)); } return typeArgs.toList(); } public static void sanityCheckForMethodGeneratingAnnotationsOnBuilderClass(JavacNode typeNode, JavacNode errorNode) { List disallowed = List.nil(); for (JavacNode child : typeNode.down()) { for (String annType : INVALID_ON_BUILDERS) { if (annotationTypeMatches(annType, child)) { int lastIndex = annType.lastIndexOf('.'); disallowed = disallowed.append(lastIndex == -1 ? annType : annType.substring(lastIndex + 1)); } } } int size = disallowed.size(); if (size == 0) return; if (size == 1) { errorNode.addError("@" + disallowed.head + " is not allowed on builder classes."); return; } StringBuilder out = new StringBuilder(); for (String a : disallowed) out.append("@").append(a).append(", "); out.setLength(out.length() - 2); errorNode.addError(out.append(" are not allowed on builder classes.").toString()); } static List copyAnnotations(List in) { ListBuffer out = new ListBuffer(); for (JCExpression expr : in) { if (!(expr instanceof JCAnnotation)) continue; out.append((JCAnnotation) expr.clone()); } return out.toList(); } static List mergeAnnotations(List a, List b) { if (a == null || a.isEmpty()) return b; if (b == null || b.isEmpty()) return a; ListBuffer out = new ListBuffer(); for (JCAnnotation ann : a) out.append(ann); for (JCAnnotation ann : b) out.append(ann); return out.toList(); } static boolean isClass(JavacNode typeNode) { return isClassAndDoesNotHaveFlags(typeNode, Flags.INTERFACE | Flags.ENUM | Flags.ANNOTATION); } static boolean isClassOrEnum(JavacNode typeNode) { return isClassAndDoesNotHaveFlags(typeNode, Flags.INTERFACE | Flags.ANNOTATION); } public static boolean isClassAndDoesNotHaveFlags(JavacNode typeNode, int flags) { JCClassDecl typeDecl = null; if (typeNode.get() instanceof JCClassDecl) typeDecl = (JCClassDecl)typeNode.get(); else return false; long typeDeclflags = typeDecl == null ? 0 : typeDecl.mods.flags; return (typeDeclflags & flags) == 0; } public static JavacNode upToTypeNode(JavacNode node) { if (node == null) throw new NullPointerException("node"); while ((node != null) && !(node.get() instanceof JCClassDecl)) node = node.up(); return node; } public static List cloneTypes(JavacTreeMaker maker, List in, JCTree source, Context context) { if (in.isEmpty()) return List.nil(); if (in.size() == 1) return List.of(cloneType(maker, in.get(0), source, context)); ListBuffer lb = new ListBuffer(); for (JCExpression expr : in) lb.append(cloneType(maker, expr, source, context)); return lb.toList(); } /** * Creates a full clone of a given javac AST type node. Every part is cloned (every identifier, every select, every wildcard, every type apply, every type_use annotation). * * If there's any node in the tree that we don't know how to clone, that part isn't cloned. However, we wouldn't know what could possibly show up that we * can't currently clone; that's just a safeguard. * * This should be used if the type looks the same in the code, but resolves differently. For example, a static method that has some generics in it named after * the class's own parameter, but as its a static method, the static method's notion of {@code T} is different from the class notion of {@code T}. If you're duplicating * a type used in the class context, you need to use this method. */ public static JCExpression cloneType(JavacTreeMaker maker, JCExpression in, JCTree source, Context context) { JCExpression out = cloneType0(maker, in); if (out != null) recursiveSetGeneratedBy(out, source, context); return out; } private static JCExpression cloneType0(JavacTreeMaker maker, JCTree in) { if (in == null) return null; if (in instanceof JCPrimitiveTypeTree) return (JCExpression) in; if (in instanceof JCIdent) { return maker.Ident(((JCIdent) in).name); } if (in instanceof JCFieldAccess) { JCFieldAccess fa = (JCFieldAccess) in; return maker.Select(cloneType0(maker, fa.selected), fa.name); } if (in instanceof JCArrayTypeTree) { JCArrayTypeTree att = (JCArrayTypeTree) in; return maker.TypeArray(cloneType0(maker, att.elemtype)); } if (in instanceof JCTypeApply) { JCTypeApply ta = (JCTypeApply) in; ListBuffer lb = new ListBuffer(); for (JCExpression typeArg : ta.arguments) { lb.append(cloneType0(maker, typeArg)); } return maker.TypeApply(cloneType0(maker, ta.clazz), lb.toList()); } if (in instanceof JCWildcard) { JCWildcard w = (JCWildcard) in; JCExpression newInner = cloneType0(maker, w.inner); TypeBoundKind newKind; switch (w.getKind()) { case SUPER_WILDCARD: newKind = maker.TypeBoundKind(BoundKind.SUPER); break; case EXTENDS_WILDCARD: newKind = maker.TypeBoundKind(BoundKind.EXTENDS); break; default: case UNBOUNDED_WILDCARD: newKind = maker.TypeBoundKind(BoundKind.UNBOUND); break; } return maker.Wildcard(newKind, newInner); } if (JCAnnotatedTypeReflect.is(in)) { JCExpression underlyingType = cloneType0(maker, JCAnnotatedTypeReflect.getUnderlyingType(in)); List anns = copyAnnotations(JCAnnotatedTypeReflect.getAnnotations(in)); return JCAnnotatedTypeReflect.create(anns, underlyingType); } // This is somewhat unsafe, but it's better than outright throwing an exception here. Returning null will just cause an exception down the pipeline. return (JCExpression) in; } private static final Pattern SECTION_FINDER = Pattern.compile("^\\s*\\**\\s*[-*][-*]+\\s*([GS]ETTER|WITH(?:ER)?)\\s*[-*][-*]+\\s*\\**\\s*$", Pattern.MULTILINE | Pattern.CASE_INSENSITIVE); public static String stripLinesWithTagFromJavadoc(String javadoc, String regexpFragment) { Pattern p = Pattern.compile("^\\s*\\**\\s*" + regexpFragment + "\\s*\\**\\s*$", Pattern.MULTILINE | Pattern.CASE_INSENSITIVE); Matcher m = p.matcher(javadoc); return m.replaceAll(""); } public static String stripSectionsFromJavadoc(String javadoc) { Matcher m = SECTION_FINDER.matcher(javadoc); if (!m.find()) return javadoc; return javadoc.substring(0, m.start()); } public static String getJavadocSection(String javadoc, String sectionNameSpec) { String[] sectionNames = sectionNameSpec.split("\\|"); Matcher m = SECTION_FINDER.matcher(javadoc); int sectionStart = -1; int sectionEnd = -1; while (m.find()) { boolean found = false; for (String sectionName : sectionNames) if (m.group(1).equalsIgnoreCase(sectionName)) { found = true; break; } if (found) { sectionStart = m.end() + 1; } else if (sectionStart != -1) { sectionEnd = m.start(); } } if (sectionStart != -1) { if (sectionEnd != -1) return javadoc.substring(sectionStart, sectionEnd); return javadoc.substring(sectionStart); } return null; } public static enum CopyJavadoc { VERBATIM { @Override public String apply(final JCCompilationUnit cu, final JavacNode node) { return Javac.getDocComment(cu, node.get()); } }, GETTER { @Override public String apply(final JCCompilationUnit cu, final JavacNode node) { final JCTree n = node.get(); String javadoc = Javac.getDocComment(cu, n); // step 1: Check if there is a 'GETTER' section. If yes, that becomes the new method's javadoc. String out = getJavadocSection(javadoc, "GETTER"); final boolean sectionBased = out != null; if (!sectionBased) { out = stripLinesWithTagFromJavadoc(stripSectionsFromJavadoc(javadoc), "@param(?:eter)?\\s+.*"); } node.getAst().cleanupTask("javadocfilter-getter", n, new CleanupTask() { @Override public void cleanup() { String javadoc = Javac.getDocComment(cu, n); if (javadoc == null || javadoc.isEmpty()) return; javadoc = stripSectionsFromJavadoc(javadoc); if (!sectionBased) { javadoc = stripLinesWithTagFromJavadoc(stripSectionsFromJavadoc(javadoc), "@returns?\\s+.*"); } Javac.setDocComment(cu, n, javadoc); } }); return out; } }, SETTER { @Override public String apply(final JCCompilationUnit cu, final JavacNode node) { return applySetter(cu, node, "SETTER"); } }, WITH { @Override public String apply(final JCCompilationUnit cu, final JavacNode node) { return addReturnsUpdatedSelfIfNeeded(applySetter(cu, node, "WITH|WITHER")); } }; public abstract String apply(final JCCompilationUnit cu, final JavacNode node); private static String applySetter(final JCCompilationUnit cu, JavacNode node, String sectionName) { final JCTree n = node.get(); String javadoc = Javac.getDocComment(cu, n); // step 1: Check if there is a 'SETTER' section. If yes, that becomes the new method's javadoc. String out = getJavadocSection(javadoc, sectionName); final boolean sectionBased = out != null; if (!sectionBased) { out = stripLinesWithTagFromJavadoc(stripSectionsFromJavadoc(javadoc), "@returns?\\s+.*"); } node.getAst().cleanupTask("javadocfilter-setter", n, new CleanupTask() { @Override public void cleanup() { String javadoc = Javac.getDocComment(cu, n); if (javadoc == null || javadoc.isEmpty()) return; javadoc = stripSectionsFromJavadoc(javadoc); if (!sectionBased) { javadoc = stripLinesWithTagFromJavadoc(stripSectionsFromJavadoc(javadoc), "@param(?:eter)?\\s+.*"); } Javac.setDocComment(cu, n, javadoc); } }); return shouldReturnThis(node) ? addReturnsThisIfNeeded(out) : out; } } public static void copyJavadoc(JavacNode from, JCTree to, CopyJavadoc copyMode) { copyJavadoc(from, to, copyMode, false); } /** * Copies javadoc on one node to the other. * * in 'GETTER' copyMode, first a 'GETTER' segment is searched for. If it exists, that will become the javadoc for the 'to' node, and this section is * stripped out of the 'from' node. If no 'GETTER' segment is found, then the entire javadoc is taken minus any {@code @param} lines and other sections. * any {@code @return} lines are stripped from 'from'. * * in 'SETTER' mode, stripping works similarly to 'GETTER' mode, except {@code param} are copied and stripped from the original and {@code @return} are skipped. */ public static void copyJavadoc(JavacNode from, JCTree to, CopyJavadoc copyMode, boolean forceAddReturn) { if (copyMode == null) copyMode = CopyJavadoc.VERBATIM; try { JCCompilationUnit cu = ((JCCompilationUnit) from.top().get()); String newJavadoc = copyMode.apply(cu, from); if (newJavadoc != null) { if (forceAddReturn) newJavadoc = addReturnsThisIfNeeded(newJavadoc); Javac.setDocComment(cu, to, newJavadoc); } } catch (Exception ignore) {} } private static final Pattern FIND_RETURN = Pattern.compile("^\\s*\\**\\s*@returns?\\s+.*$", Pattern.MULTILINE | Pattern.CASE_INSENSITIVE); static String addReturnsThisIfNeeded(String in) { if (FIND_RETURN.matcher(in).find()) return in; return addJavadocLine(in, "@return {@code this}."); } static String addReturnsUpdatedSelfIfNeeded(String in) { if (FIND_RETURN.matcher(in).find()) return in; return addJavadocLine(in, "@return a clone of this object, except with this updated property (returns {@code this} if an identical value is passed)."); } static String addJavadocLine(String in, String line) { if (in.endsWith("\n")) return in + line + "\n"; return in + "\n" + line; } public static boolean isDirectDescendantOfObject(JavacNode typeNode) { if (!(typeNode.get() instanceof JCClassDecl)) throw new IllegalArgumentException("not a type node"); JCTree extending = Javac.getExtendsClause((JCClassDecl) typeNode.get()); if (extending == null) return true; String p = extending.toString(); return p.equals("Object") || p.equals("java.lang.Object"); } }