/* * Copyright (C) 2015-2021 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 lombok.javac.Javac.*; import static lombok.javac.handlers.JavacHandlerUtil.*; import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Map; import com.sun.source.tree.Tree.Kind; import com.sun.tools.javac.code.BoundKind; import com.sun.tools.javac.code.Flags; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.tree.JCTree.JCAnnotation; import com.sun.tools.javac.tree.JCTree.JCBlock; import com.sun.tools.javac.tree.JCTree.JCExpression; import com.sun.tools.javac.tree.JCTree.JCMethodDecl; import com.sun.tools.javac.tree.JCTree.JCModifiers; import com.sun.tools.javac.tree.JCTree.JCStatement; 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.util.List; import com.sun.tools.javac.util.ListBuffer; import com.sun.tools.javac.util.Name; import lombok.AccessLevel; import lombok.ConfigurationKeys; import lombok.core.LombokImmutableList; import lombok.core.SpiLoadUtil; import lombok.core.TypeLibrary; import lombok.core.configuration.CheckerFrameworkVersion; import lombok.core.handlers.HandlerUtil; import lombok.javac.JavacNode; import lombok.javac.JavacTreeMaker; import lombok.javac.handlers.HandleBuilder.BuilderJob; public class JavacSingularsRecipes { public interface ExpressionMaker { JCExpression make(); } public interface StatementMaker { JCStatement make(); } private static final JavacSingularsRecipes INSTANCE = new JavacSingularsRecipes(); private final Map singularizers = new HashMap(); private final TypeLibrary singularizableTypes = new TypeLibrary(); private JavacSingularsRecipes() { try { loadAll(singularizableTypes, singularizers); singularizableTypes.lock(); } catch (IOException e) { System.err.println("Lombok's @Singularizable feature is broken due to misconfigured SPI files: " + e); } } private static void loadAll(TypeLibrary library, Map map) throws IOException { for (JavacSingularizer handler : SpiLoadUtil.findServices(JavacSingularizer.class, JavacSingularizer.class.getClassLoader())) { for (String type : handler.getSupportedTypes()) { JavacSingularizer existingSingularizer = map.get(type); if (existingSingularizer != null) { JavacSingularizer toKeep = existingSingularizer.getClass().getName().compareTo(handler.getClass().getName()) > 0 ? handler : existingSingularizer; System.err.println("Multiple singularizers found for type " + type + "; the alphabetically first class is used: " + toKeep.getClass().getName()); map.put(type, toKeep); } else { map.put(type, handler); library.addType(type); } } } } public static JavacSingularsRecipes get() { return INSTANCE; } public String toQualified(String typeReference) { java.util.List q = singularizableTypes.toQualifieds(typeReference); if (q.isEmpty()) return null; return q.get(0); } public JavacSingularizer getSingularizer(String fqn, JavacNode node) { final JavacSingularizer singularizer = singularizers.get(fqn); final boolean useGuavaInstead = Boolean.TRUE.equals(node.getAst().readConfiguration(ConfigurationKeys.SINGULAR_USE_GUAVA)); return useGuavaInstead ? singularizer.getGuavaInstead(node) : singularizer; } public static final class SingularData { private final JavacNode annotation; private final Name singularName; private final Name pluralName; private final List typeArgs; private final String targetFqn; private final JavacSingularizer singularizer; private final String setterPrefix; private final boolean ignoreNullCollections; public SingularData(JavacNode annotation, Name singularName, Name pluralName, List typeArgs, String targetFqn, JavacSingularizer singularizer, boolean ignoreNullCollections) { this(annotation, singularName, pluralName, typeArgs, targetFqn, singularizer, ignoreNullCollections, ""); } public SingularData(JavacNode annotation, Name singularName, Name pluralName, List typeArgs, String targetFqn, JavacSingularizer singularizer, boolean ignoreNullCollections, String setterPrefix) { this.annotation = annotation; this.singularName = singularName; this.pluralName = pluralName; this.typeArgs = typeArgs; this.targetFqn = targetFqn; this.singularizer = singularizer; this.setterPrefix = setterPrefix; this.ignoreNullCollections = ignoreNullCollections; } public JavacNode getAnnotation() { return annotation; } public Name getSingularName() { return singularName; } public Name getPluralName() { return pluralName; } public String getSetterPrefix() { return setterPrefix; } public List getTypeArgs() { return typeArgs; } public String getTargetFqn() { return targetFqn; } public JavacSingularizer getSingularizer() { return singularizer; } public boolean isIgnoreNullCollections() { return ignoreNullCollections; } public String getTargetSimpleType() { int idx = targetFqn.lastIndexOf("."); return idx == -1 ? targetFqn : targetFqn.substring(idx + 1); } } public static abstract class JavacSingularizer { public abstract LombokImmutableList getSupportedTypes(); protected JavacSingularizer getGuavaInstead(JavacNode node) { return this; } protected JCModifiers makeMods(JavacTreeMaker maker, JavacNode node, boolean deprecate, AccessLevel access, List methodAnnotations) { JCAnnotation deprecateAnn = deprecate ? maker.Annotation(genJavaLangTypeRef(node, "Deprecated"), List.nil()) : null; List annsOnMethod = (deprecateAnn != null) ? List.of(deprecateAnn) : List.nil(); annsOnMethod = mergeAnnotations(annsOnMethod,methodAnnotations); return maker.Modifiers(toJavacModifier(access), annsOnMethod); } /** Checks if any of the to-be-generated nodes (fields, methods) already exist. If so, errors on these (singulars don't support manually writing some of it, and returns true). */ public boolean checkForAlreadyExistingNodesAndGenerateError(JavacNode builderType, SingularData data) { for (JavacNode child : builderType.down()) { switch (child.getKind()) { case FIELD: { JCVariableDecl field = (JCVariableDecl) child.get(); Name name = field.name; if (name == null) break; if (getGeneratedBy(field) != null) continue; for (Name fieldToBeGenerated : listFieldsToBeGenerated(data, builderType)) { if (!fieldToBeGenerated.equals(name)) continue; child.addError("Manually adding a field that @Singular @Builder would generate is not supported. If you want to manually manage the builder aspect for this field/parameter, don't use @Singular."); return true; } break; } case METHOD: { JCMethodDecl method = (JCMethodDecl) child.get(); Name name = method.name; if (name == null) break; if (getGeneratedBy(method) != null) continue; for (Name methodToBeGenerated : listMethodsToBeGenerated(data, builderType)) { if (!methodToBeGenerated.equals(name)) continue; child.addError("Manually adding a method that @Singular @Builder would generate is not supported. If you want to manually manage the builder aspect for this field/parameter, don't use @Singular."); return true; } break; }} } return false; } public java.util.List listFieldsToBeGenerated(SingularData data, JavacNode builderType) { return Collections.singletonList(data.pluralName); } public java.util.List listMethodsToBeGenerated(SingularData data, JavacNode builderType) { Name p = data.pluralName; Name s = data.singularName; if (p.equals(s)) return Collections.singletonList(p); return Arrays.asList(p, s); } public abstract java.util.List generateFields(SingularData data, JavacNode builderType, JavacNode source); /** * Generates the singular, plural, and clear methods for the given {@link SingularData}. * Uses the given {@code builderType} as return type if {@code chain == true}, {@code void} otherwise. * If you need more control over the return type and value, use * {@link #generateMethods(SingularData, boolean, JavacNode, JCTree, boolean, ExpressionMaker, StatementMaker)}. */ public void generateMethods(final BuilderJob job, SingularData data, boolean deprecate) { //job.checkerFramework, job.builderType, job.source, job.oldFluent, job.oldChain, job.accessInners //CheckerFrameworkVersion cfv, final JavacNode builderType, JavacNode source, boolean fluent, final boolean chain, AccessLevel access) { final JavacTreeMaker maker = job.builderType.getTreeMaker(); ExpressionMaker returnTypeMaker = new ExpressionMaker() { @Override public JCExpression make() { return job.oldChain ? cloneSelfType(job.builderType) : maker.Type(createVoidType(job.builderType.getSymbolTable(), CTC_VOID)); }}; StatementMaker returnStatementMaker = new StatementMaker() { @Override public JCStatement make() { return job.oldChain ? maker.Return(maker.Ident(job.builderType.toName("this"))) : null; }}; generateMethods(job.checkerFramework, data, deprecate, job.builderType, job.sourceNode, job.oldFluent, returnTypeMaker, returnStatementMaker, job.accessInners); } /** * Generates the singular, plural, and clear methods for the given {@link SingularData}. * Uses the given {@code returnTypeMaker} and {@code returnStatementMaker} for the generated methods. */ public abstract void generateMethods(CheckerFrameworkVersion cfv, SingularData data, boolean deprecate, JavacNode builderType, JavacNode source, boolean fluent, ExpressionMaker returnTypeMaker, StatementMaker returnStatementMaker, AccessLevel access); protected void doGenerateMethods(CheckerFrameworkVersion cfv, SingularData data, boolean deprecate, JavacNode builderType, JavacNode source, boolean fluent, ExpressionMaker returnTypeMaker, StatementMaker returnStatementMaker, AccessLevel access) { JavacTreeMaker maker = builderType.getTreeMaker(); generateSingularMethod(cfv, deprecate, maker, returnTypeMaker.make(), returnStatementMaker.make(), data, builderType, source, fluent, access); generatePluralMethod(cfv, deprecate, maker, returnTypeMaker.make(), returnStatementMaker.make(), data, builderType, source, fluent, access); generateClearMethod(cfv, deprecate, maker, returnTypeMaker.make(), returnStatementMaker.make(), data, builderType, source, access); } private void finishAndInjectMethod(CheckerFrameworkVersion cfv, JavacTreeMaker maker, JCExpression returnType, JCStatement returnStatement, SingularData data, JavacNode builderType, JavacNode source, boolean deprecate, ListBuffer statements, Name methodName, List jcVariableDecls, List methodAnnotations, AccessLevel access, Boolean ignoreNullCollections) { if (returnStatement != null) statements.append(returnStatement); JCBlock body = maker.Block(0, statements.toList()); JCModifiers mods = makeMods(maker, builderType, deprecate, access, methodAnnotations); List typeParams = List.nil(); List thrown = List.nil(); if (ignoreNullCollections != null) { if (ignoreNullCollections.booleanValue()) { for (JCVariableDecl d : jcVariableDecls) createRelevantNullableAnnotation(builderType, d); } else { for (JCVariableDecl d : jcVariableDecls) createRelevantNonNullAnnotation(builderType, d); } } returnType = addCheckerFrameworkReturnsReceiver(returnType, maker, builderType, cfv); JCMethodDecl method = maker.MethodDef(mods, methodName, returnType, typeParams, jcVariableDecls, thrown, body, null); if (returnStatement != null) createRelevantNonNullAnnotation(builderType, method); recursiveSetGeneratedBy(method, source); injectMethod(builderType, method); } private void generateClearMethod(CheckerFrameworkVersion cfv, boolean deprecate, JavacTreeMaker maker, JCExpression returnType, JCStatement returnStatement, SingularData data, JavacNode builderType, JavacNode source, AccessLevel access) { JCStatement clearStatement = generateClearStatements(maker, data, builderType); ListBuffer statements = new ListBuffer(); statements.append(clearStatement); Name methodName = builderType.toName(HandlerUtil.buildAccessorName(source, "clear", data.getPluralName().toString())); finishAndInjectMethod(cfv, maker, returnType, returnStatement, data, builderType, source, deprecate, statements, methodName, List.nil(), List.nil(), access, null); } protected abstract JCStatement generateClearStatements(JavacTreeMaker maker, SingularData data, JavacNode builderType); private void generateSingularMethod(CheckerFrameworkVersion cfv, boolean deprecate, JavacTreeMaker maker, JCExpression returnType, JCStatement returnStatement, SingularData data, JavacNode builderType, JavacNode source, boolean fluent, AccessLevel access) { ListBuffer statements = generateSingularMethodStatements(maker, data, builderType, source); List params = generateSingularMethodParameters(maker, data, builderType, source); Name name = data.getSingularName(); String setterPrefix = data.getSetterPrefix(); if (setterPrefix.isEmpty() && !fluent) setterPrefix = getAddMethodName(); if (!setterPrefix.isEmpty()) name = builderType.toName(HandlerUtil.buildAccessorName(source, setterPrefix, name.toString())); statements.prepend(createConstructBuilderVarIfNeeded(maker, data, builderType, source)); List methodAnnotations = copyAnnotations(findCopyableToBuilderSingularSetterAnnotations(data.annotation.up())); finishAndInjectMethod(cfv, maker, returnType, returnStatement, data, builderType, source, deprecate, statements, name, params, methodAnnotations, access, null); } protected JCVariableDecl generateSingularMethodParameter(int typeIndex, JavacTreeMaker maker, SingularData data, JavacNode builderType, JavacNode source, Name name) { long flags = JavacHandlerUtil.addFinalIfNeeded(Flags.PARAMETER, builderType.getContext()); JCExpression type = cloneParamType(typeIndex, maker, data.getTypeArgs(), builderType, source); List typeUseAnns = getTypeUseAnnotations(type); type = removeTypeUseAnnotations(type); JCModifiers mods = typeUseAnns.isEmpty() ? maker.Modifiers(flags) : maker.Modifiers(flags, typeUseAnns); return maker.VarDef(mods, name, type, null); } protected JCStatement generateSingularMethodAddStatement(JavacTreeMaker maker, JavacNode builderType, Name argumentName, String builderFieldName) { JCExpression thisDotFieldDotAdd = chainDots(builderType, "this", builderFieldName, "add"); JCExpression invokeAdd = maker.Apply(List.nil(), thisDotFieldDotAdd, List.of(maker.Ident(argumentName))); return maker.Exec(invokeAdd); } protected abstract ListBuffer generateSingularMethodStatements(JavacTreeMaker maker, SingularData data, JavacNode builderType, JavacNode source); protected abstract List generateSingularMethodParameters(JavacTreeMaker maker, SingularData data, JavacNode builderType, JavacNode source); private void generatePluralMethod(CheckerFrameworkVersion cfv, boolean deprecate, JavacTreeMaker maker, JCExpression returnType, JCStatement returnStatement, SingularData data, JavacNode builderType, JavacNode source, boolean fluent, AccessLevel access) { ListBuffer statements = generatePluralMethodStatements(maker, data, builderType, source); Name name = data.getPluralName(); String setterPrefix = data.getSetterPrefix(); if (setterPrefix.isEmpty() && !fluent) setterPrefix = getAddMethodName() + "All"; if (!setterPrefix.isEmpty()) name = builderType.toName(HandlerUtil.buildAccessorName(source, setterPrefix, name.toString())); JCExpression paramType = getPluralMethodParamType(builderType); paramType = addTypeArgs(getTypeArgumentsCount(), true, builderType, paramType, data.getTypeArgs(), source); long paramFlags = JavacHandlerUtil.addFinalIfNeeded(Flags.PARAMETER, builderType.getContext()); boolean ignoreNullCollections = data.isIgnoreNullCollections(); JCModifiers paramMods = maker.Modifiers(paramFlags); JCVariableDecl param = maker.VarDef(paramMods, data.getPluralName(), paramType, null); statements.prepend(createConstructBuilderVarIfNeeded(maker, data, builderType, source)); if (ignoreNullCollections) { JCExpression incomingIsNotNull = maker.Binary(CTC_NOT_EQUAL, maker.Ident(data.getPluralName()), maker.Literal(CTC_BOT, null)); JCStatement onNotNull = maker.Block(0, statements.toList()); statements = new ListBuffer(); statements.append(maker.If(incomingIsNotNull, onNotNull, null)); } else { statements.prepend(JavacHandlerUtil.generateNullCheck(maker, null, data.getPluralName(), builderType, "%s cannot be null")); } List methodAnnotations = copyAnnotations(findCopyableToSetterAnnotations(data.annotation.up())); finishAndInjectMethod(cfv, maker, returnType, returnStatement, data, builderType, source, deprecate, statements, name, List.of(param), methodAnnotations, access, ignoreNullCollections); } protected ListBuffer generatePluralMethodStatements(JavacTreeMaker maker, SingularData data, JavacNode builderType, JavacNode source) { ListBuffer statements = new ListBuffer(); JCExpression thisDotFieldDotAdd = chainDots(builderType, "this", data.getPluralName().toString(), getAddMethodName() + "All"); JCExpression invokeAdd = maker.Apply(List.nil(), thisDotFieldDotAdd, List.of(maker.Ident(data.getPluralName()))); statements.append(maker.Exec(invokeAdd)); return statements; } protected abstract JCExpression getPluralMethodParamType(JavacNode builderType); protected abstract JCStatement createConstructBuilderVarIfNeeded(JavacTreeMaker maker, SingularData data, JavacNode builderType, JavacNode source); public abstract void appendBuildCode(SingularData data, JavacNode builderType, JavacNode source, ListBuffer statements, Name targetVariableName, String builderVariable); public boolean shadowedDuringBuild() { return true; } public boolean requiresCleaning() { try { return !getClass().getMethod("appendCleaningCode", SingularData.class, JavacNode.class, JCTree.class, ListBuffer.class).getDeclaringClass().equals(JavacSingularizer.class); } catch (NoSuchMethodException e) { return false; } } public void appendCleaningCode(SingularData data, JavacNode builderType, JavacNode source, ListBuffer statements) { } // -- Utility methods -- /** * Adds the requested number of type arguments to the provided type, copying each argument in {@code typeArgs}. If typeArgs is too long, the extra elements are ignored. * If {@code typeArgs} is null or too short, {@code java.lang.Object} will be substituted for each missing type argument. * * @param count The number of type arguments requested. * @param addExtends If {@code true}, all bounds are either '? extends X' or just '?'. If false, the reverse is applied, and '? extends Foo' is converted to Foo, '?' to Object, etc. * @param node Some node in the same AST. Just used to obtain makers and contexts and such. * @param type The type to add generics to. * @param typeArgs the list of type args to clone. * @param source The source annotation that is the root cause of this code generation. */ protected JCExpression addTypeArgs(int count, boolean addExtends, JavacNode node, JCExpression type, List typeArgs, JavacNode source) { JavacTreeMaker maker = node.getTreeMaker(); List clonedAndFixedTypeArgs = createTypeArgs(count, addExtends, node, typeArgs, source); return maker.TypeApply(type, clonedAndFixedTypeArgs); } protected List createTypeArgs(int count, boolean addExtends, JavacNode node, List typeArgs, JavacNode source) { JavacTreeMaker maker = node.getTreeMaker(); if (count < 0) throw new IllegalArgumentException("count is negative"); if (count == 0) return List.nil(); ListBuffer arguments = new ListBuffer(); if (typeArgs != null) for (JCExpression orig : typeArgs) { if (!addExtends) { if (orig.getKind() == Kind.UNBOUNDED_WILDCARD || orig.getKind() == Kind.SUPER_WILDCARD) { arguments.append(genJavaLangTypeRef(node, "Object")); } else if (orig.getKind() == Kind.EXTENDS_WILDCARD) { JCExpression inner; try { inner = (JCExpression) ((JCWildcard) orig).inner; } catch (Exception e) { inner = genJavaLangTypeRef(node, "Object"); } arguments.append(cloneType(maker, inner, source)); } else { arguments.append(cloneType(maker, orig, source)); } } else { if (orig.getKind() == Kind.UNBOUNDED_WILDCARD || orig.getKind() == Kind.SUPER_WILDCARD) { arguments.append(maker.Wildcard(maker.TypeBoundKind(BoundKind.UNBOUND), null)); } else if (orig.getKind() == Kind.EXTENDS_WILDCARD) { arguments.append(cloneType(maker, orig, source)); } else { arguments.append(maker.Wildcard(maker.TypeBoundKind(BoundKind.EXTENDS), cloneType(maker, orig, source))); } } if (--count == 0) break; } while (count-- > 0) { if (addExtends) { arguments.append(maker.Wildcard(maker.TypeBoundKind(BoundKind.UNBOUND), null)); } else { arguments.append(genJavaLangTypeRef(node, "Object")); } } return arguments.toList(); } /** Generates 'builderVariable.name.size()' as an expression; if nullGuard is true, it's this.name == null ? 0 : this.name.size(). */ protected JCExpression getSize(JavacTreeMaker maker, JavacNode builderType, Name name, boolean nullGuard, boolean parens, String builderVariable) { Name thisName = builderType.toName(builderVariable); JCExpression fn = maker.Select(maker.Select(maker.Ident(thisName), name), builderType.toName("size")); JCExpression sizeInvoke = maker.Apply(List.nil(), fn, List.nil()); if (nullGuard) { JCExpression isNull = maker.Binary(CTC_EQUAL, maker.Select(maker.Ident(thisName), name), maker.Literal(CTC_BOT, null)); JCExpression out = maker.Conditional(isNull, maker.Literal(CTC_INT, 0), sizeInvoke); if (parens) return maker.Parens(out); return out; } return sizeInvoke; } protected JCExpression cloneParamType(int index, JavacTreeMaker maker, List typeArgs, JavacNode builderType, JavacNode source) { if (typeArgs == null || typeArgs.size() <= index) { return genJavaLangTypeRef(builderType, "Object"); } else { JCExpression originalType = typeArgs.get(index); if (originalType.getKind() == Kind.UNBOUNDED_WILDCARD || originalType.getKind() == Kind.SUPER_WILDCARD) { return genJavaLangTypeRef(builderType, "Object"); } else if (originalType.getKind() == Kind.EXTENDS_WILDCARD) { try { return cloneType(maker, (JCExpression) ((JCWildcard) originalType).inner, source); } catch (Exception e) { return genJavaLangTypeRef(builderType, "Object"); } } else { return cloneType(maker, originalType, source); } } } protected abstract String getAddMethodName(); protected abstract int getTypeArgumentsCount(); protected abstract String getEmptyMaker(String target); public JCExpression getEmptyExpression(String target, JavacTreeMaker maker, SingularData data, JavacNode builderType, JavacNode source) { String emptyMaker = getEmptyMaker(target); List typeArgs = createTypeArgs(getTypeArgumentsCount(), false, builderType, data.getTypeArgs(), source); return maker.Apply(typeArgs, chainDots(builderType, emptyMaker.split("\\.")), List.nil()); } } }