Skip to content

Commit

Permalink
Replace looseSignature with ClassName
Browse files Browse the repository at this point in the history
Signed-off-by: utzcoz <utzcoz@outlook.com>
  • Loading branch information
utzcoz committed Feb 14, 2024
1 parent 61d8c19 commit b78b789
Show file tree
Hide file tree
Showing 40 changed files with 318 additions and 198 deletions.
@@ -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 @@ -372,8 +373,14 @@ public MethodInfo(ExecutableElement methodElement) {
for (VariableElement variableElement : methodElement.getParameters()) {
TypeMirror varTypeMirror = variableElement.asType();
String paramType = canonicalize(varTypeMirror);
String paramTypeWithoutGenerics = typeWithoutGenerics(paramType);
paramTypes.add(paramTypeWithoutGenerics);
ClassName className = variableElement.getAnnotation(ClassName.class);
if (className != null) {
// If this parameter has ClassName annotation, we need to save its type
// based on ClassName value.
paramTypes.add(typeWithoutGenerics(className.value()));
} else {
paramTypes.add(typeWithoutGenerics(paramType));
}
}
}

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

public MethodExtraInfo(ExecutableElement methodElement) {
this.isStatic = methodElement.getModifiers().contains(Modifier.STATIC);
this.returnType = typeWithoutGenerics(canonicalize(methodElement.getReturnType()));
TypeMirror typeMirror = methodElement.getReturnType();
WithType withType = methodElement.getAnnotation(WithType.class);
if (withType != null) {
// If this return type has WithType annotation, we need to save its type
// based on WithType value.
this.returnType = typeWithoutGenerics(withType.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.NativeMethodNotFoundException;
Expand Down Expand Up @@ -320,13 +322,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 @@ -348,6 +360,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 @@ -12,7 +12,6 @@
import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.Constructor;
import org.robolectric.util.reflector.ForType;
import org.robolectric.util.reflector.WithType;

/** Builder for {@link android.telephony.CellInfoLte}. */
public class CellInfoLteBuilder {
Expand Down Expand Up @@ -113,7 +112,7 @@ CellInfoLte newCellInfoLte(
long timeStamp,
CellIdentityLte cellIdentity,
CellSignalStrengthLte cellSignalStrength,
@WithType("android.telephony.CellConfigLte") Object cellConfigLte);
@ClassName("android.telephony.CellConfigLte") Object cellConfigLte);

@Accessor("mCellIdentityLte")
void setCellIdentity(CellIdentityLte cellIdentity);
Expand Down
Expand Up @@ -76,10 +76,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 +129,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 +141,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 @@ -42,7 +42,7 @@
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;
Expand Down
Expand Up @@ -64,7 +64,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 +415,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(value = "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
Expand Up @@ -37,8 +37,7 @@
value = ApkAssets.class,
minSdk = P,
shadowPicker = Picker.class,
isInAndroidSdk = false,
looseSignatures = true)
isInAndroidSdk = false)
public class ShadowArscApkAssets9 extends ShadowApkAssets {
// #define ATRACE_TAG ATRACE_TAG_RESOURCES
//
Expand Down Expand Up @@ -149,12 +148,16 @@ protected static long nativeLoad(
}

@Implementation(minSdk = R)
protected static Object nativeLoad(
Object format, Object javaPath, Object flags, Object assetsProvider) throws IOException {
boolean system = ((int) flags & PROPERTY_SYSTEM) == PROPERTY_SYSTEM;
boolean overlay = ((int) flags & PROPERTY_OVERLAY) == PROPERTY_OVERLAY;
boolean forceSharedLib = ((int) flags & PROPERTY_DYNAMIC) == PROPERTY_DYNAMIC;
return nativeLoad((String) javaPath, system, forceSharedLib, overlay);
protected static long nativeLoad(
int format,
String javaPath,
int flags,
@ClassName(value = "android.content.res.loader.AssetsProvider") Object assetsProvider)
throws IOException {
boolean system = (flags & PROPERTY_SYSTEM) == PROPERTY_SYSTEM;
boolean overlay = (flags & PROPERTY_OVERLAY) == PROPERTY_OVERLAY;
boolean forceSharedLib = (flags & PROPERTY_DYNAMIC) == PROPERTY_DYNAMIC;
return nativeLoad(javaPath, system, forceSharedLib, overlay);
}

// static jlong NativeLoadFromFd(JNIEnv* env, jclass /*clazz*/, jobject file_descriptor,
Expand Down
Expand Up @@ -50,7 +50,7 @@
import org.robolectric.util.reflector.ForType;

@SuppressWarnings({"UnusedDeclaration"})
@Implements(value = AudioManager.class, looseSignatures = true)
@Implements(value = AudioManager.class)
public class ShadowAudioManager {
@RealObject AudioManager realAudioManager;

Expand Down Expand Up @@ -883,7 +883,8 @@ public AudioRecordingConfiguration createActiveRecordingConfiguration(
@HiddenApi
@Implementation(minSdk = P)
@RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
protected int registerAudioPolicy(@NonNull Object audioPolicy) {
protected int registerAudioPolicy(
@NonNull @ClassName(value = "android.media.audiopolicy.AudioPolicy") Object audioPolicy) {
Preconditions.checkNotNull(audioPolicy, "Illegal null AudioPolicy argument");
AudioPolicy policy = (AudioPolicy) audioPolicy;
String id = getIdForAudioPolicy(audioPolicy);
Expand All @@ -897,7 +898,8 @@ protected int registerAudioPolicy(@NonNull Object audioPolicy) {

@HiddenApi
@Implementation(minSdk = Q)
protected void unregisterAudioPolicy(@NonNull Object audioPolicy) {
protected void unregisterAudioPolicy(
@NonNull @ClassName(value = "android.media.audiopolicy.AudioPolicy") Object audioPolicy) {
Preconditions.checkNotNull(audioPolicy, "Illegal null AudioPolicy argument");
AudioPolicy policy = (AudioPolicy) audioPolicy;
registeredAudioPolicies.remove(getIdForAudioPolicy(policy));
Expand Down
Expand Up @@ -44,7 +44,7 @@
* other methods are expected run through the real class. The two {@link WriteMode} are treated the
* same.
*/
@Implements(value = AudioTrack.class, looseSignatures = true)
@Implements(value = AudioTrack.class)
public class ShadowAudioTrack {

/**
Expand Down

0 comments on commit b78b789

Please sign in to comment.