package io.gitlab.jfronny.gson.compile.processor.util.valueprocessor; import org.jetbrains.annotations.ApiStatus; import javax.lang.model.element.*; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementFilter; import javax.lang.model.util.Types; import java.util.Locale; import java.util.Objects; /** * How a [Value] can be constructed. Either a constructor, factory method, or builder. */ public sealed interface ConstructionSource { /** * The target [Value] class to construct. */ TypeElement getTargetClass(); /** * The executable element to construct the [Value]. This may be a constructor, factory method, or builder. */ ExecutableElement getConstructionElement(); /** * If this source is a constructor (either of the value or the builder). */ boolean isConstructor(); /** * If this source is a builder. */ boolean isBuilder(); final class Constructor implements ConstructionSource { private final ExecutableElement constructor; private TypeElement targetClass; public Constructor(ExecutableElement constructor) { this.constructor = Objects.requireNonNull(constructor); } @Override public TypeElement getTargetClass() { return targetClass != null ? targetClass : (targetClass = (TypeElement) constructor.getEnclosingElement()); } @Override public ExecutableElement getConstructionElement() { return constructor; } @Override public boolean isConstructor() { return true; } @Override public boolean isBuilder() { return false; } } final class Factory implements ConstructionSource { private final Types types; private final ExecutableElement method; private TypeElement targetClass; public Factory(Types types, ExecutableElement method) { this.types = types; this.method = method; } @Override public TypeElement getTargetClass() { return targetClass != null ? targetClass : (targetClass = (TypeElement) types.asElement(method.getReturnType())); } @Override public ExecutableElement getConstructionElement() { return method; } @Override public boolean isConstructor() { return false; } @Override public boolean isBuilder() { return false; } } sealed abstract class Builder implements ConstructionSource { public abstract TypeElement getBuilderClass(); public abstract ExecutableElement getBuildMethod(); @Override public boolean isBuilder() { return true; } } final class BuilderConstructor extends Builder { private final Types types; private final ExecutableElement constructor; private TypeElement targetClass; private TypeElement builderClass; private ExecutableElement buildMethod; public BuilderConstructor(Types types, ExecutableElement constructor) { this.types = types; this.constructor = constructor; } @Override public TypeElement getTargetClass() { return targetClass != null ? targetClass : (targetClass = (TypeElement) types.asElement(getBuildMethod().getReturnType())); } @Override public ExecutableElement getConstructionElement() { return constructor; } @Override public boolean isConstructor() { return true; } @Override public TypeElement getBuilderClass() { return builderClass != null ? builderClass : (builderClass = (TypeElement) constructor.getEnclosingElement()); } @Override public ExecutableElement getBuildMethod() { return buildMethod != null ? buildMethod : (buildMethod = findBuildMethod((TypeElement) constructor.getEnclosingElement())); } } final class BuilderFactory extends Builder { private final Types types; private final ExecutableElement method; private TypeElement targetClass; private TypeElement builderClass; private ExecutableElement buildMethod; public BuilderFactory(Types types, ExecutableElement method) { this.types = types; this.method = method; } @Override public TypeElement getTargetClass() { return targetClass != null ? targetClass : (targetClass = (TypeElement) types.asElement(getBuildMethod().getReturnType())); } @Override public ExecutableElement getConstructionElement() { return method; } @Override public boolean isConstructor() { return false; } @Override public TypeElement getBuilderClass() { return builderClass != null ? builderClass : (builderClass = (TypeElement) types.asElement(method.getReturnType())); } @Override public ExecutableElement getBuildMethod() { return buildMethod != null ? buildMethod : (buildMethod = findBuildMethod((TypeElement) types.asElement(method.getReturnType()))); } } @ApiStatus.Internal static ExecutableElement findBuildMethod(TypeElement builderClass) { // Ok, maybe there is just one possible builder method. { ExecutableElement candidate = null; boolean foundMultipleCandidates = false; boolean isCandidateReasonableBuilderMethodName = false; for (ExecutableElement method : ElementFilter.methodsIn(builderClass.getEnclosedElements())) { if (isPossibleBuilderMethod(method, builderClass)) { if (candidate == null) { candidate = method; } else { // Multiple possible methods, keep the one with a reasonable builder name if possible. foundMultipleCandidates = true; isCandidateReasonableBuilderMethodName = isCandidateReasonableBuilderMethodName || isReasonableBuilderMethodName(candidate); if (isCandidateReasonableBuilderMethodName) { if (isReasonableBuilderMethodName(method)) { // both reasonable, too ambiguous. candidate = null; break; } } else { candidate = method; } } } } if (candidate != null && (!foundMultipleCandidates || isCandidateReasonableBuilderMethodName)) { return candidate; } } // Last try, check to see if the immediate parent class makes sense. { Element candidate = builderClass.getEnclosingElement(); if (candidate.getKind() == ElementKind.CLASS) { for (ExecutableElement method : ElementFilter.methodsIn(builderClass.getEnclosedElements())) { if (method.getReturnType().equals(candidate.asType()) && method.getParameters().isEmpty()) { return method; } } } } // Well, I give up. return null; } /** * A possible builder method has no parameters and a return type of the class we want to * construct. Therefore, the return type is not going to be void, primitive, or a platform * class. */ @ApiStatus.Internal static boolean isPossibleBuilderMethod(ExecutableElement method, TypeElement builderClass) { if (!method.getParameters().isEmpty()) return false; TypeMirror returnType = method.getReturnType(); if (returnType.getKind() == TypeKind.VOID) return false; if (returnType.getKind().isPrimitive()) return false; if (returnType.equals(builderClass.asType())) return false; String returnTypeName = returnType.toString(); return !(returnTypeName.startsWith("java.") || returnTypeName.startsWith("javax.") || returnTypeName.startsWith("android.")); } @ApiStatus.Internal static boolean isReasonableBuilderMethodName(ExecutableElement method) { String methodName = method.getSimpleName().toString().toLowerCase(Locale.ROOT); return methodName.startsWith("build") || methodName.startsWith("create"); } }