Invoke defaults constructor in kotlin code gen (#896)

* Add Util#invokeDefaultConstructor

* Add defaultPrimitiveValue

This will be used to initialize required properties for later invocation of the default constructor

* Move isTransient into PropertyGenerator

We will need this in order to know how to invoke constructors even if they have transient parameters

* Add notion of hasLocalIsPresentName to PropertyGenerator

* Switch to using invokeDefaultConstructor for any default property types

* Add code gen versions of default constructor test

* Fix mismatched names

* Use Arrays.copyOf

* Unwrap InvocationTargetException

* Use name allocator

* Rename createMask to createDefaultValuesParametersMask, use it directly

* Opportunistically clean up result variable holder

Only needs to be made if we have non-parameter instances, otherwise we can just return directly

* Fix mask name

* Remove unnecessary mod

* Switch to local lazily-initialized constructor reference

Not working because of some issue in kotlinpoet I don't understand

* Fix named usage

* Clean up debugging dots

* Add proguard/R8 rule for keeping defaults constructor in targets

* Make constructor lookup property private

* Add another defensive dot

* Rework invokeDefaultConstructor to accept vararg args

A little more idiomatic

* Update proguard rules
This commit is contained in:
Zac Sweers
2019-09-08 17:24:25 -04:00
committed by GitHub
parent 711de52ae1
commit 329d0e14b0
7 changed files with 344 additions and 72 deletions

View File

@@ -44,6 +44,17 @@ import static com.squareup.moshi.Types.supertypeOf;
public final class Util {
public static final Set<Annotation> NO_ANNOTATIONS = Collections.emptySet();
public static final Type[] EMPTY_TYPE_ARRAY = new Type[] {};
@Nullable private static final Class<?> DEFAULT_CONSTRUCTOR_MARKER;
static {
Class<?> clazz;
try {
clazz = Class.forName("kotlin.jvm.internal.DefaultConstructorMarker");
} catch (ClassNotFoundException e) {
clazz = null;
}
DEFAULT_CONSTRUCTOR_MARKER = clazz;
}
private Util() {
}
@@ -521,4 +532,91 @@ public final class Util {
throw rethrowCause(e);
}
}
/**
* Reflectively looks up the defaults constructor of a kotlin class.
*
* @param targetClass the target kotlin class to instantiate.
* @param <T> the type of {@code targetClass}.
* @return the instantiated {@code targetClass} instance.
* @see #createDefaultValuesParametersMask(boolean...)
*/
public static <T> Constructor<T> lookupDefaultsConstructor(Class<T> targetClass) {
if (DEFAULT_CONSTRUCTOR_MARKER == null) {
throw new IllegalStateException("DefaultConstructorMarker not on classpath. Make sure the "
+ "Kotlin stdlib is on the classpath.");
}
Constructor<T> defaultConstructor = findConstructor(targetClass);
defaultConstructor.setAccessible(true);
return defaultConstructor;
}
/**
* Reflectively invokes the defaults constructor of a kotlin class. This allows indicating which
* arguments are "set" or not, and thus recreate the behavior of named a arguments invocation
* dynamically.
*
* @param targetClass the target kotlin class to instantiate.
* @param defaultsConstructor the target class's defaults constructor in kotlin invoke.
* @param mask an int mask indicating which {@code args} are present.
* @param args the constructor arguments, including "unset" values (set to null or the primitive
* default).
* @param <T> the type of {@code targetClass}.
* @return the instantiated {@code targetClass} instance.
* @see #createDefaultValuesParametersMask(boolean...)
*/
public static <T> T invokeDefaultConstructor(
Class<T> targetClass,
Constructor<T> defaultsConstructor,
int mask,
Object... args) {
Object[] finalArgs = Arrays.copyOf(args, args.length + 2);
finalArgs[finalArgs.length - 2] = mask;
finalArgs[finalArgs.length - 1] = null; // DefaultConstructorMarker param
try {
return defaultsConstructor.newInstance(finalArgs);
} catch (InstantiationException e) {
throw new IllegalStateException("Could not instantiate instance of " + targetClass);
} catch (IllegalAccessException e) {
throw new IllegalStateException("Could not access defaults constructor of " + targetClass);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) throw (RuntimeException) cause;
if (cause instanceof Error) throw (Error) cause;
throw new RuntimeException("Could not invoke defaults constructor of " + targetClass, cause);
}
}
private static <T> Constructor<T> findConstructor(Class<T> targetClass) {
for (Constructor<?> constructor : targetClass.getDeclaredConstructors()) {
Class<?>[] paramTypes = constructor.getParameterTypes();
if (paramTypes.length != 0
&& paramTypes[paramTypes.length - 1].equals(DEFAULT_CONSTRUCTOR_MARKER)) {
//noinspection unchecked
return (Constructor<T>) constructor;
}
}
throw new IllegalStateException("No defaults constructor found for " + targetClass);
}
/**
* Creates an mask with bits set to indicate which indices of a default constructor's parameters
* are set.
*
* @param argPresentValues vararg of all present values (set or unset). Max allowable size is 32.
* @return the created mask.
*/
public static int createDefaultValuesParametersMask(boolean... argPresentValues) {
if (argPresentValues.length > 32) {
throw new IllegalArgumentException("Arg present values exceeds max allowable 32.");
}
int mask = 0;
for (int i = 0; i < argPresentValues.length; ++i) {
if (!argPresentValues[i]) {
mask = mask | (1 << i);
}
}
return mask;
}
}

View File

@@ -16,6 +16,16 @@
# The name of @JsonClass types is used to look up the generated adapter.
-keepnames @com.squareup.moshi.JsonClass class *
# Retain generated target class's synthetic defaults constructor and keep DefaultConstructorMarker's
# name. We will look this up reflectively to invoke the type's constructor.
#
# We can't _just_ keep the defaults constructor because Proguard/R8's spec doesn't allow wildcard
# matching preceding parameters.
-keepnames class kotlin.jvm.internal.DefaultConstructorMarker
-keepclassmembers @com.squareup.moshi.JsonClass class * {
<init>(...);
}
# Retain generated JsonAdapters if annotated type is retained.
-if @com.squareup.moshi.JsonClass class *
-keep class <1>JsonAdapter {