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

248 lines
8.5 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.enclosingElement);
}
@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.returnType));
}
@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().returnType));
}
@Override
public ExecutableElement getConstructionElement() {
return constructor;
}
@Override
public boolean isConstructor() {
return true;
}
@Override
public TypeElement getBuilderClass() {
return builderClass != null ? builderClass : (builderClass = (TypeElement) constructor.enclosingElement);
}
@Override
public ExecutableElement getBuildMethod() {
return buildMethod != null ? buildMethod : (buildMethod = findBuildMethod((TypeElement) constructor.enclosingElement));
}
}
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().returnType));
}
@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.returnType));
}
@Override
public ExecutableElement getBuildMethod() {
return buildMethod != null ? buildMethod : (buildMethod = findBuildMethod((TypeElement) types.asElement(method.returnType)));
}
}
@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.enclosedElements)) {
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.enclosingElement;
if (candidate.kind == ElementKind.CLASS) {
for (ExecutableElement method : ElementFilter.methodsIn(builderClass.enclosedElements)) {
if (method.returnType.equals(candidate.asType()) && method.parameters.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.parameters.isEmpty) return false;
TypeMirror returnType = method.returnType;
if (returnType.kind == TypeKind.VOID) return false;
if (returnType.kind.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.simpleName.toString().toLowerCase(Locale.ROOT);
return methodName.startsWith("build") || methodName.startsWith("create");
}
}