gson-compile/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/valueprocessor/ConstructionSource.java

248 lines
8.6 KiB
Java

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");
}
}