Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace looseSignature with a better and fine grained annotation #8829

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,20 @@
package org.robolectric.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/** Parameters with types that can't be resolved at compile time may be annotated @ClassName. */
@Target({ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface ClassName {

/**
* The class name intended for this parameter.
*
* <p>Use the value as returned from {@link Class#getName()}, not {@link
* Class#getCanonicalName()}; e.g. {@code Foo$Bar} instead of {@code Foo.Bar}.
*/
String value();
}
Expand Up @@ -38,6 +38,7 @@
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;
import org.robolectric.annotation.ClassName;
import org.robolectric.annotation.Implementation;
import org.robolectric.versioning.AndroidVersionInitTools;

Expand Down Expand Up @@ -162,7 +163,7 @@ public String verifyMethod(

MethodExtraInfo sdkMethod = classInfo.findMethod(methodElement, looseSignatures);
if (sdkMethod == null) {
return "No such method in " + className;
return "No such method " + methodElement + " in " + className;
}

MethodExtraInfo implMethod = new MethodExtraInfo(methodElement);
Expand Down Expand Up @@ -335,7 +336,7 @@ public ClassInfo(ClassNode classNode) {
}

MethodExtraInfo findMethod(ExecutableElement methodElement, boolean looseSignatures) {
MethodInfo methodInfo = new MethodInfo(methodElement);
MethodInfo methodInfo = new MethodInfo(methodElement, looseSignatures);

MethodExtraInfo methodExtraInfo = methods.get(methodInfo);
if (looseSignatures && methodExtraInfo == null) {
Expand Down Expand Up @@ -366,14 +367,25 @@ public MethodInfo(String name, int size) {
}

/** Create a MethodInfo from AST (an @Implementation method in a shadow class). */
public MethodInfo(ExecutableElement methodElement) {
public MethodInfo(ExecutableElement methodElement, boolean looseSignatures) {
this.name = cleanMethodName(methodElement);

for (VariableElement variableElement : methodElement.getParameters()) {
TypeMirror varTypeMirror = variableElement.asType();
String paramType = canonicalize(varTypeMirror);
String paramTypeWithoutGenerics = typeWithoutGenerics(paramType);
paramTypes.add(paramTypeWithoutGenerics);
// If one shadow class is marked by looseSignature, we should ignore
// ClassName in its method's signature as it will affect special method
// signatures for looseSignature.
if (!looseSignatures) {
ClassName className = variableElement.getAnnotation(ClassName.class);
// If this parameter has ClassName annotation, we need to save its type
// based on ClassName value.
if (className != null) {
paramTypes.add(typeWithoutGenerics(className.value()));
continue;
}
}
paramTypes.add(typeWithoutGenerics(paramType));
}
}

Expand Down Expand Up @@ -433,7 +445,14 @@ public MethodExtraInfo(MethodNode method) {

public MethodExtraInfo(ExecutableElement methodElement) {
this.isStatic = methodElement.getModifiers().contains(Modifier.STATIC);
this.returnType = typeWithoutGenerics(canonicalize(methodElement.getReturnType()));
ClassName className = methodElement.getAnnotation(ClassName.class);
if (className != null) {
// If this return type has ClassName annotation, we need to save its type
// based on ClassName value.
this.returnType = typeWithoutGenerics(className.value());
} else {
this.returnType = typeWithoutGenerics(canonicalize(methodElement.getReturnType()));
}
}

@Override
Expand Down
Expand Up @@ -8,6 +8,7 @@
import static org.robolectric.util.reflector.Reflector.reflector;

import com.google.auto.service.AutoService;
import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
Expand All @@ -21,6 +22,7 @@
import java.util.List;
import javax.annotation.Nonnull;
import javax.annotation.Priority;
import org.robolectric.annotation.ClassName;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.ReflectorObject;
import org.robolectric.sandbox.ShadowMatcher;
Expand Down Expand Up @@ -305,13 +307,23 @@ private Method findShadowMethod(
Class<?>[] types,
ShadowInfo shadowInfo,
Class<?> shadowClass) {
// Try to find the shadow method with the exact method signature first.
Method method = findShadowMethodDeclaredOnClass(shadowClass, name, types);

// Try to find shadow method with fallback looseSignature mechanism.
if (method == null && shadowInfo.looseSignatures) {
// If user sets looseSignatures for shadow class, we will try to find method with generic
// types by following origin full looseSignatures definition.
Class<?>[] genericTypes = MethodType.genericMethodType(types.length).parameterArray();
method = findShadowMethodDeclaredOnClass(shadowClass, name, genericTypes);
}

// Try to find shadow method with another fallback WithType mechanism with a lower priority.
if (method == null && !shadowInfo.looseSignatures) {
// Otherwise, we will try to find method with WithType annotation that can match signature.
method = findShadowMethodHasWithTypeDeclaredOnClass(shadowClass, name, types);
}

if (method != null) {
return method;
} else {
Expand All @@ -333,6 +345,76 @@ private Method findShadowMethod(
return method;
}

private ClassName findClassNameAnnotation(Annotation[] annotations) {
for (Annotation annotation : annotations) {
if (ClassName.class.isAssignableFrom(annotation.annotationType())) {
return (ClassName) annotation;
}
}
return null;
}

private boolean hasClassNameAnnotation(Annotation[][] annotations) {
for (Annotation[] parameterAnnotations : annotations) {
for (Annotation annotation : parameterAnnotations) {
if (ClassName.class.isAssignableFrom(annotation.annotationType())) {
return true;
}
}
}
return false;
}

private Method findShadowMethodHasWithTypeDeclaredOnClass(
Class<?> shadowClass, String methodName, Class<?>[] paramClasses) {
// We don't process the method without input parameters now.
if (paramClasses == null || paramClasses.length == 0) {
return null;
}
Method[] methods = shadowClass.getDeclaredMethods();
// TODO try to find methods with the same name first
for (Method method : methods) {
if (method == null
|| !method.getName().equals(methodName)
|| method.getParameterCount() != paramClasses.length
|| !isValidShadowMethod(method)) {
continue;
}
Class<?>[] parameterTypes = method.getParameterTypes();
Annotation[][] allAnnotations = method.getParameterAnnotations();
if (!hasClassNameAnnotation(allAnnotations)) {
continue;
}
boolean matched = true;
for (int i = 0; i < parameterTypes.length; i++) {
// If method's parameter type is superclass of input parameter, we can pass checking for
// this parameter.
if (parameterTypes[i].isAssignableFrom(paramClasses[i])) {
continue;
}
if (allAnnotations.length <= i) {
matched = false;
break;
}
ClassName className = findClassNameAnnotation(allAnnotations[i]);
// If developer uses WithType for an input parameter, we need ensure it is the same
// type of the real method to avoid unexpected method override/overwrite result.
if (className != null
&& paramClasses[i] != null
&& className.value().equals(paramClasses[i].getCanonicalName())) {
continue;
}
matched = false;
break;
}
// TODO identify why above logic will affect __constructor__ without WithType
if (matched) {
return method;
}
}
return null;
}

private Method findShadowMethodDeclaredOnClass(
Class<?> shadowClass, String methodName, Class<?>[] paramClasses) {
try {
Expand Down
Expand Up @@ -29,6 +29,7 @@
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import javax.annotation.Nullable;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.ClassName;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowActivity;
import org.robolectric.shadows.ShadowContextThemeWrapper;
Expand All @@ -39,7 +40,6 @@
import org.robolectric.util.ReflectionHelpers.ClassParameter;
import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.ForType;
import org.robolectric.util.reflector.WithType;

/**
* ActivityController provides low-level APIs to control activity's lifecycle.
Expand Down Expand Up @@ -99,7 +99,7 @@ private ActivityController<T> attach(@Nullable Bundle activityOptions) {

private ActivityController<T> attach(
@Nullable Bundle activityOptions,
@Nullable @WithType("android.app.Activity$NonConfigurationInstances")
@Nullable @ClassName("android.app.Activity$NonConfigurationInstances")
Object lastNonConfigurationInstances,
@Nullable Configuration overrideConfig) {
if (attached) {
Expand Down
Expand Up @@ -62,6 +62,7 @@
import javax.annotation.Nullable;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.android.controller.ActivityController;
import org.robolectric.annotation.ClassName;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
Expand All @@ -76,10 +77,9 @@
import org.robolectric.shadows.ShadowLoadedApk._LoadedApk_;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.reflector.ForType;
import org.robolectric.util.reflector.WithType;

@SuppressWarnings("NewApi")
@Implements(value = Activity.class, looseSignatures = true)
@Implements(value = Activity.class)
public class ShadowActivity extends ShadowContextThemeWrapper {

@RealObject protected Activity realActivity;
Expand Down Expand Up @@ -130,7 +130,7 @@ public void callAttach(Intent intent, @Nullable Bundle activityOptions) {
public void callAttach(
Intent intent,
@Nullable Bundle activityOptions,
@Nullable @WithType("android.app.Activity$NonConfigurationInstances")
@Nullable @ClassName("android.app.Activity$NonConfigurationInstances")
Object lastNonConfigurationInstances) {
callAttach(
intent,
Expand All @@ -142,7 +142,7 @@ public void callAttach(
public void callAttach(
Intent intent,
@Nullable Bundle activityOptions,
@Nullable @WithType("android.app.Activity$NonConfigurationInstances")
@Nullable @ClassName("android.app.Activity$NonConfigurationInstances")
Object lastNonConfigurationInstances,
@Nullable Configuration overrideConfig) {
Application application = RuntimeEnvironment.getApplication();
Expand Down Expand Up @@ -418,7 +418,7 @@ protected Window getWindow() {
* @return fake SplashScreen
*/
@Implementation(minSdk = S)
protected synchronized Object getSplashScreen() {
protected synchronized @ClassName("android.window.SplashScreen") Object getSplashScreen() {
if (splashScreen == null) {
splashScreen = new RoboSplashScreen();
}
Expand Down
Expand Up @@ -31,6 +31,7 @@
import java.util.Map;
import javax.annotation.Nonnull;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.ClassName;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
Expand All @@ -42,14 +43,14 @@
import org.robolectric.util.reflector.ForType;
import org.robolectric.util.reflector.Reflector;

@Implements(value = ActivityThread.class, isInAndroidSdk = false, looseSignatures = true)
@Implements(value = ActivityThread.class, isInAndroidSdk = false)
public class ShadowActivityThread {
private static ApplicationInfo applicationInfo;
@RealObject protected ActivityThread realActivityThread;
@ReflectorObject protected _ActivityThread_ activityThreadReflector;

@Implementation
public static Object getPackageManager() {
public static @ClassName("android.content.pm.IPackageManager") Object getPackageManager() {
ClassLoader classLoader = ShadowActivityThread.class.getClassLoader();
Class<?> iPackageManagerClass;
try {
Expand Down Expand Up @@ -111,7 +112,7 @@ public Object invoke(Object proxy, @Nonnull Method method, Object[] args)
}

@Implementation
public static Object currentActivityThread() {
public static @ClassName("android.app.ActivityThread") Object currentActivityThread() {
return RuntimeEnvironment.getActivityThread();
}

Expand All @@ -133,7 +134,7 @@ protected Application getApplication() {
}

@Implementation(minSdk = R)
public static Object getPermissionManager() {
public static @ClassName("android.permission.IPermissionManager") Object getPermissionManager() {
ClassLoader classLoader = ShadowActivityThread.class.getClassLoader();
Class<?> iPermissionManagerClass;
try {
Expand Down
Expand Up @@ -52,6 +52,7 @@
import java.util.Set;
import java.util.stream.IntStream;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.ClassName;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
Expand All @@ -64,7 +65,7 @@
import org.robolectric.util.reflector.ForType;

/** Shadow for {@link AppOpsManager}. */
@Implements(value = AppOpsManager.class, looseSignatures = true)
@Implements(value = AppOpsManager.class)
public class ShadowAppOpsManager {

// OpEntry fields that the shadow doesn't currently allow the test to configure.
Expand Down Expand Up @@ -415,18 +416,18 @@ protected int noteProxyOpNoThrow(
@RequiresApi(api = S)
@Implementation(minSdk = S)
protected int noteProxyOpNoThrow(
Object op, Object attributionSource, Object message, Object ignoredSkipProxyOperation) {
Preconditions.checkArgument(op instanceof Integer);
int op,
@ClassName("android.content.AttributionSource") Object attributionSource,
String message,
boolean ignoredSkipProxyOperation) {
Preconditions.checkArgument(attributionSource instanceof AttributionSource);
Preconditions.checkArgument(message == null || message instanceof String);
Preconditions.checkArgument(ignoredSkipProxyOperation instanceof Boolean);
AttributionSource castedAttributionSource = (AttributionSource) attributionSource;
return noteProxyOpNoThrow(
(int) op,
op,
castedAttributionSource.getNextPackageName(),
castedAttributionSource.getNextUid(),
castedAttributionSource.getNextAttributionTag(),
(String) message);
message);
}

@Implementation
Expand Down