Supports overrides with co-variant return types in RequestFactory, and
generics in proxies.

Generics in proxies can lead to overrides with co-variant return types
(e.g. if you override a method to set a @PropertyName on it), but also has
its own set of issues (type variables in getter's return type –used as the
property type– have to be "resolved" against the actual interface
parameterization).

Bug: issue 5926
Change-Id: Iba6384a93b866934c6e744643d79f9063e36bb2d
diff --git a/requestfactory/build.xml b/requestfactory/build.xml
index 89c1340..586d155 100755
--- a/requestfactory/build.xml
+++ b/requestfactory/build.xml
@@ -119,6 +119,7 @@
       <arg path="${gwt.build.lib}/requestfactory-test-validated.jar" />
       <arg value="com.google.web.bindery.requestfactory.gwt.client.RequestFactoryChainedContextTest.Factory" />
       <arg value="com.google.web.bindery.requestfactory.gwt.client.RequestFactoryPolymorphicTest.Factory" />
+      <arg value="com.google.web.bindery.requestfactory.gwt.client.RequestFactoryGenericsTest.Factory" />
       <arg value="com.google.web.bindery.requestfactory.shared.BoxesAndPrimitivesTest.Factory" />
       <arg value="com.google.web.bindery.requestfactory.shared.ComplexKeysTest.Factory" />
       <arg value="com.google.web.bindery.requestfactory.shared.LocatorTest.Factory" />
diff --git a/user/src/com/google/web/bindery/autobean/vm/impl/BeanPropertyContext.java b/user/src/com/google/web/bindery/autobean/vm/impl/BeanPropertyContext.java
index a4fe31f..265297e 100644
--- a/user/src/com/google/web/bindery/autobean/vm/impl/BeanPropertyContext.java
+++ b/user/src/com/google/web/bindery/autobean/vm/impl/BeanPropertyContext.java
@@ -15,7 +15,7 @@
  */
 package com.google.web.bindery.autobean.vm.impl;
 
-import java.lang.reflect.Method;
+import java.lang.reflect.Type;
 
 /**
  * A property context that allows setters to be called on a simple peer,
@@ -25,10 +25,11 @@
   private final ProxyAutoBean<?> bean;
   private final String propertyName;
 
-  public BeanPropertyContext(ProxyAutoBean<?> bean, Method getter) {
-    super(getter);
+  public BeanPropertyContext(ProxyAutoBean<?> bean, String name, Type genericType, Class<?> type,
+      Class<?> elementType, Class<?> keyType, Class<?> valueType) {
+    super(genericType, type, elementType, keyType, valueType);
     this.bean = bean;
-    propertyName = BeanMethod.GET.inferName(getter);
+    propertyName = name;
   }
 
   @Override
diff --git a/user/src/com/google/web/bindery/autobean/vm/impl/GetterPropertyContext.java b/user/src/com/google/web/bindery/autobean/vm/impl/GetterPropertyContext.java
index d00b8ed..593a682 100644
--- a/user/src/com/google/web/bindery/autobean/vm/impl/GetterPropertyContext.java
+++ b/user/src/com/google/web/bindery/autobean/vm/impl/GetterPropertyContext.java
@@ -17,6 +17,7 @@
 
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
+import java.lang.reflect.Type;
 
 /**
  * Used by {@link ProxyAutoBean#traverseProperties()}.
@@ -25,23 +26,12 @@
   private final Method setter;
   private final Object shim;
 
-  GetterPropertyContext(ProxyAutoBean<?> bean, Method getter) {
-    super(getter);
-    this.shim = bean.as();
+  GetterPropertyContext(ProxyAutoBean<?> bean, Method setter, Type genericType, Class<?> type,
+      Class<?> elementType, Class<?> keyType, Class<?> valueType) {
+    super(genericType, type, elementType, keyType, valueType);
 
-    // Look for the setter method.
-    Method found = null;
-    String name = BeanMethod.GET.inferName(getter);
-    for (Method m : getter.getDeclaringClass().getMethods()) {
-      if (BeanMethod.SET.matches(m) || BeanMethod.SET_BUILDER.matches(m)) {
-        if (BeanMethod.SET.inferName(m).equals(name)
-            && getter.getReturnType().isAssignableFrom(m.getParameterTypes()[0])) {
-          found = m;
-          break;
-        }
-      }
-    }
-    setter = found;
+    this.setter = setter;
+    this.shim = bean.as();
   }
 
   @Override
diff --git a/user/src/com/google/web/bindery/autobean/vm/impl/MethodPropertyContext.java b/user/src/com/google/web/bindery/autobean/vm/impl/MethodPropertyContext.java
index 10ce579..0a4fca7 100644
--- a/user/src/com/google/web/bindery/autobean/vm/impl/MethodPropertyContext.java
+++ b/user/src/com/google/web/bindery/autobean/vm/impl/MethodPropertyContext.java
@@ -19,78 +19,48 @@
 import com.google.web.bindery.autobean.shared.AutoBeanVisitor.MapPropertyContext;
 import com.google.web.bindery.autobean.shared.AutoBeanVisitor.ParameterizationVisitor;
 
-import java.lang.reflect.Method;
 import java.lang.reflect.Type;
-import java.util.Collection;
-import java.util.Map;
-import java.util.WeakHashMap;
 
 /**
  * A base type to handle analyzing the return value of a getter method. The
  * accessor methods are implemented in subtypes.
  */
-abstract class MethodPropertyContext implements CollectionPropertyContext,
-    MapPropertyContext {
-  private static class Data {
-    Class<?> elementType;
-    Type genericType;
-    Class<?> keyType;
-    Class<?> valueType;
-    Class<?> type;
-  }
+abstract class MethodPropertyContext implements CollectionPropertyContext, MapPropertyContext {
+  private final Class<?> elementType;
+  private final Type genericType;
+  private final Class<?> keyType;
+  private final Class<?> valueType;
+  private final Class<?> type;
 
-  /**
-   * Save prior instances in order to decrease the amount of data computed.
-   */
-  private static final Map<Method, Data> cache = new WeakHashMap<Method, Data>();
-  private final Data data;
-
-  public MethodPropertyContext(Method getter) {
-    synchronized (cache) {
-      Data previous = cache.get(getter);
-      if (previous != null) {
-        this.data = previous;
-        return;
-      }
-
-      this.data = new Data();
-      data.genericType = getter.getGenericReturnType();
-      data.type = getter.getReturnType();
-      // Compute collection element type
-      if (Collection.class.isAssignableFrom(getType())) {
-        data.elementType = TypeUtils.ensureBaseType(TypeUtils.getSingleParameterization(
-            Collection.class, getter.getGenericReturnType(),
-            getter.getReturnType()));
-      } else if (Map.class.isAssignableFrom(getType())) {
-        Type[] types = TypeUtils.getParameterization(Map.class,
-            getter.getGenericReturnType());
-        data.keyType = TypeUtils.ensureBaseType(types[0]);
-        data.valueType = TypeUtils.ensureBaseType(types[1]);
-      }
-      cache.put(getter, data);
-    }
+  protected MethodPropertyContext(Type genericType, Class<?> type, Class<?> elementType,
+      Class<?> keyType, Class<?> valueType) {
+    this.genericType = genericType;
+    this.type = type;
+    this.elementType = elementType;
+    this.keyType = keyType;
+    this.valueType = valueType;
   }
 
   public void accept(ParameterizationVisitor visitor) {
-    traverse(visitor, data.genericType);
+    traverse(visitor, genericType);
   }
 
   public abstract boolean canSet();
 
   public Class<?> getElementType() {
-    return data.elementType;
+    return elementType;
   }
 
   public Class<?> getKeyType() {
-    return data.keyType;
+    return keyType;
   }
 
   public Class<?> getType() {
-    return data.type;
+    return type;
   }
 
   public Class<?> getValueType() {
-    return data.valueType;
+    return valueType;
   }
 
   public abstract void set(Object value);
diff --git a/user/src/com/google/web/bindery/autobean/vm/impl/ProxyAutoBean.java b/user/src/com/google/web/bindery/autobean/vm/impl/ProxyAutoBean.java
index 4c37d1f..fd9816e 100644
--- a/user/src/com/google/web/bindery/autobean/vm/impl/ProxyAutoBean.java
+++ b/user/src/com/google/web/bindery/autobean/vm/impl/ProxyAutoBean.java
@@ -28,9 +28,10 @@
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Proxy;
+import java.lang.reflect.Type;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Iterator;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.WeakHashMap;
@@ -42,16 +43,43 @@
  */
 public class ProxyAutoBean<T> extends AbstractAutoBean<T> {
   private static class Data {
-    final List<Method> getters = new ArrayList<Method>();
-    final List<String> getterNames = new ArrayList<String>();
-    final List<PropertyType> propertyType = new ArrayList<PropertyType>();
+    final Class<?> elementType;
+    final Type genericType;
+    final Method getter;
+    final Class<?> keyType;
+    final PropertyType propertyType;
+    Method setter;
+    final Class<?> type;
+    final Class<?> valueType;
+
+    Data(Method getter, Type genericType, Class<?> type, PropertyType propertyType) {
+      this.getter = getter;
+      this.genericType = genericType;
+      this.type = type;
+      this.propertyType = propertyType;
+
+      if (propertyType == PropertyType.COLLECTION) {
+        elementType =
+            TypeUtils.ensureBaseType(TypeUtils.getSingleParameterization(Collection.class,
+                genericType, type));
+        keyType = valueType = null;
+      } else if (propertyType == PropertyType.MAP) {
+        elementType = null;
+        Type[] types = TypeUtils.getParameterization(Map.class, genericType, type);
+        keyType = TypeUtils.ensureBaseType(types[0]);
+        valueType = TypeUtils.ensureBaseType(types[1]);
+      } else {
+        elementType = keyType = valueType = null;
+      }
+    }
   }
 
   private enum PropertyType {
     VALUE, REFERENCE, COLLECTION, MAP;
   }
 
-  private static final Map<Class<?>, Data> cache = new WeakHashMap<Class<?>, Data>();
+  private static final Map<Class<?>, Map<String, Data>> cache =
+      new WeakHashMap<Class<?>, Map<String, Data>>();
 
   /**
    * Utility method to crete a new {@link Proxy} instance.
@@ -77,37 +105,62 @@
     return intf.cast(Proxy.newProxyInstance(intf.getClassLoader(), intfs, handler));
   }
 
-  private static Data calculateData(Class<?> beanType) {
-    Data toReturn;
+  private static Map<String, Data> calculateData(Class<?> beanType) {
+    Map<String, Data> toReturn;
     synchronized (cache) {
       toReturn = cache.get(beanType);
       if (toReturn == null) {
-        toReturn = new Data();
+        Map<String, Data> getters = new HashMap<String, Data>();
+        List<Method> setters = new ArrayList<Method>();
         for (Method method : beanType.getMethods()) {
           if (BeanMethod.GET.matches(method)) {
-            toReturn.getters.add(method);
+            // match methods on their name for now, to find the most specific
+            // override
+            String name = method.getName();
 
-            String name;
-            PropertyName annotation = method.getAnnotation(PropertyName.class);
-            if (annotation != null) {
-              name = annotation.value();
-            } else {
-              name = BeanMethod.GET.inferName(method);
-            }
-            toReturn.getterNames.add(name);
+            Type genericReturnType = TypeUtils.resolveGenerics(beanType, method.getGenericReturnType());
+            Class<?> returnType = TypeUtils.ensureBaseType(genericReturnType);
 
-            Class<?> returnType = method.getReturnType();
-            if (TypeUtils.isValueType(returnType)) {
-              toReturn.propertyType.add(PropertyType.VALUE);
-            } else if (Collection.class.isAssignableFrom(returnType)) {
-              toReturn.propertyType.add(PropertyType.COLLECTION);
-            } else if (Map.class.isAssignableFrom(returnType)) {
-              toReturn.propertyType.add(PropertyType.MAP);
-            } else {
-              toReturn.propertyType.add(PropertyType.REFERENCE);
+            Data data = getters.get(name);
+            if (data == null || data.type.isAssignableFrom(returnType)) {
+              // no getter seen yet for the property, or a less specific one
+              PropertyType propertyType;
+              if (TypeUtils.isValueType(returnType)) {
+                propertyType = PropertyType.VALUE;
+              } else if (Collection.class.isAssignableFrom(returnType)) {
+                propertyType = PropertyType.COLLECTION;
+              } else if (Map.class.isAssignableFrom(returnType)) {
+                propertyType = PropertyType.MAP;
+              } else {
+                propertyType = PropertyType.REFERENCE;
+              }
+              data = new Data(method, genericReturnType, returnType, propertyType);
+
+              getters.put(name, data);
             }
+          } else if (BeanMethod.SET.matches(method) || BeanMethod.SET_BUILDER.matches(method)) {
+            setters.add(method);
           }
         }
+
+        toReturn = new HashMap<String, Data>(getters.size());
+
+        // Now take @PropertyName into account
+        for (Map.Entry<String, Data> entry : getters.entrySet()) {
+          Data data = entry.getValue();
+          toReturn.put(BeanMethod.GET.inferName(data.getter), data);
+        }
+
+        // Associate setters to getters
+        for (Method setter : setters) {
+          String name = BeanMethod.SET.inferName(setter);
+          Data data = toReturn.get(name);
+          if (data != null && data.setter == null
+              && data.getter.getReturnType().isAssignableFrom(setter.getParameterTypes()[0])) {
+            data.setter = setter;
+          }
+        }
+
         cache.put(beanType, toReturn);
       }
     }
@@ -116,7 +169,7 @@
 
   private final Class<T> beanType;
   private final Configuration configuration;
-  private final Data data;
+  private final Map<String, Data> propertyData;
   /**
    * Because the shim and the ProxyAutoBean are related through WeakMapping, we
    * need to ensure that the ProxyAutoBean doesn't artificially extend the
@@ -150,7 +203,7 @@
     super(factory);
     this.beanType = (Class<T>) beanType;
     this.configuration = configuration;
-    this.data = calculateData(beanType);
+    this.propertyData = calculateData(beanType);
   }
 
   @SuppressWarnings("unchecked")
@@ -159,7 +212,7 @@
     super(toWrap, factory);
     this.beanType = (Class<T>) beanType;
     this.configuration = configuration;
-    this.data = calculateData(beanType);
+    this.propertyData = calculateData(beanType);
   }
 
   @Override
@@ -235,7 +288,7 @@
   @Override
   protected T getWrapped() {
     if (wrapped == null && isUsingSimplePeer()) {
-      wrapped = (T) ProxyAutoBean.makeProxy(beanType, new SimpleBeanHandler<T>(this));
+      wrapped = ProxyAutoBean.<T> makeProxy(beanType, new SimpleBeanHandler<T>(this));
     }
     return super.getWrapped();
   }
@@ -256,15 +309,11 @@
   // TODO: Port to model-based when class-based TypeOracle is available.
   @Override
   protected void traverseProperties(AutoBeanVisitor visitor, OneShotContext ctx) {
-    assert data.getters.size() == data.getterNames.size()
-        && data.getters.size() == data.propertyType.size();
-    Iterator<Method> getterIt = data.getters.iterator();
-    Iterator<String> nameIt = data.getterNames.iterator();
-    Iterator<PropertyType> typeIt = data.propertyType.iterator();
-    while (getterIt.hasNext()) {
-      Method getter = getterIt.next();
-      String name = nameIt.next();
-      PropertyType propertyType = typeIt.next();
+    for (Map.Entry<String, Data> entry : propertyData.entrySet()) {
+      String name = entry.getKey();
+      Data data = entry.getValue();
+      Method getter = data.getter;
+      PropertyType propertyType = data.propertyType;
 
       // Use the shim to handle automatic wrapping
       Object value;
@@ -280,9 +329,16 @@
       }
 
       // Create the context used for the property visitation
-      MethodPropertyContext x =
-          isUsingSimplePeer() ? new BeanPropertyContext(this, getter) : new GetterPropertyContext(
-              this, getter);
+      MethodPropertyContext x;
+      if (isUsingSimplePeer()) {
+        x =
+            new BeanPropertyContext(this, name, data.genericType, data.type, data.elementType,
+                data.keyType, data.valueType);
+      } else {
+        x =
+            new GetterPropertyContext(this, getter, data.genericType, data.type, data.elementType,
+                data.keyType, data.valueType);
+      }
 
       switch (propertyType) {
         case VALUE: {
@@ -333,10 +389,6 @@
     }
   }
 
-  Class<?> getBeanType() {
-    return beanType;
-  }
-
   private T createShim() {
     T toReturn = ProxyAutoBean.makeProxy(beanType, new ShimHandler<T>(this, getWrapped()));
     WeakMapping.setWeak(toReturn, AutoBean.class.getName(), this);
diff --git a/user/src/com/google/web/bindery/autobean/vm/impl/TypeUtils.java b/user/src/com/google/web/bindery/autobean/vm/impl/TypeUtils.java
index 73b977e..b6964b2 100644
--- a/user/src/com/google/web/bindery/autobean/vm/impl/TypeUtils.java
+++ b/user/src/com/google/web/bindery/autobean/vm/impl/TypeUtils.java
@@ -170,6 +170,66 @@
     return autoBoxType == null ? domainType : autoBoxType;
   }
 
+  public static Type resolveGenerics(Class<?> containingType, Type type) {
+    if (type instanceof Class<?>) {
+      return type;
+    } else if (type instanceof ParameterizedType) {
+      final ParameterizedType param = (ParameterizedType) type;
+      Type[] actualTypeArguments = param.getActualTypeArguments();
+      boolean hadTypeVariable = false;
+      final ArrayList<Type> resolvedTypeArguments = new ArrayList<Type>(actualTypeArguments.length);
+      for (Type actualTypeArgument : actualTypeArguments) {
+        Type resolvedTypeArgument = resolveGenerics(containingType, actualTypeArgument);
+        hadTypeVariable = hadTypeVariable || (resolvedTypeArgument != actualTypeArgument);
+        resolvedTypeArguments.add(resolvedTypeArgument);
+      }
+      if (!hadTypeVariable) {
+        return type;
+      }
+      return new ParameterizedType() {
+        @Override
+        public Type getRawType() {
+          return param.getRawType();
+        }
+
+        @Override
+        public Type getOwnerType() {
+          return param.getOwnerType();
+        }
+
+        @Override
+        public Type[] getActualTypeArguments() {
+          return resolvedTypeArguments.toArray(new Type[resolvedTypeArguments.size()]);
+        }
+      };
+    } else if (type instanceof TypeVariable<?>) {
+      TypeVariable<?> variable = (TypeVariable<?>) type;
+      Object genericDeclaration = variable.getGenericDeclaration();
+      if (genericDeclaration instanceof Class<?>) {
+        Class<?> declaration = (Class<?>) genericDeclaration;
+        // could probably optimize, but would involve duplicating a bunch of
+        // getParameterization's code
+        Type[] types = getParameterization(declaration, containingType);
+        TypeVariable<?>[] x = declaration.getTypeParameters();
+        for (int i = 0; i < x.length; i++) {
+          if (variable.equals(x[i])) {
+            return resolveGenerics(containingType, types[i]);
+          }
+        }
+        throw new IllegalStateException(
+            "TypeVariable not found in its generic declaration type; must be a JVM error");
+      } else {
+        // Use "erasure" for method-level type variables; should we throw
+        // instead?
+        return ensureBaseType(type);
+      }
+    } else if (type instanceof WildcardType) {
+      return resolveGenerics(containingType, ((WildcardType) type).getUpperBounds()[0]);
+    } else {
+      throw new RuntimeException("Cannot handle " + type.getClass().getName());
+    }
+  }
+
   private TypeUtils() {
   }
 }
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/ProxyScanner.java b/user/src/com/google/web/bindery/requestfactory/apt/ProxyScanner.java
index 2b72edf..5374d3c 100644
--- a/user/src/com/google/web/bindery/requestfactory/apt/ProxyScanner.java
+++ b/user/src/com/google/web/bindery/requestfactory/apt/ProxyScanner.java
@@ -23,6 +23,7 @@
 import javax.lang.model.element.ExecutableElement;
 import javax.lang.model.element.TypeElement;
 import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.ExecutableType;
 import javax.lang.model.type.MirroredTypeException;
 import javax.lang.model.type.TypeMirror;
 
@@ -32,26 +33,47 @@
  */
 class ProxyScanner extends ScannerBase<Void> {
 
+  private TypeElement checkedElement;
+
   @Override
   public Void visitExecutable(ExecutableElement x, State state) {
     if (shouldIgnore(x, state)) {
       return null;
     }
 
+    ExecutableType xType = viewIn(checkedElement, x, state);
     if (isGetter(x, state)) {
-      TypeMirror returnType = x.getReturnType();
+      TypeMirror returnType = xType.getReturnType();
       if (!state.isTransportableType(returnType)) {
+        // XXX(t.broyer): should we really pass the "resolved" type? that could
+        // result in several errors being reported on the same method, but on
+        // the other hand tells exactly which type it is that isn't
+        // transportable.
+        // For instance, a List<T> might be transportable if T is
+        // java.lang.String in a sub-interface, but not if T is some
+        // untransportable type in another sub-interface
         state.poison(x, Messages.untransportableType(returnType));
       }
     } else if (!isSetter(x, state)) {
       state.poison(x, Messages.proxyOnlyGettersSetters());
     }
-    // Parameters checked by visitVariable
-    return super.visitExecutable(x, state);
+
+    // check parameters (we do not defer to visitVariable, as we need the
+    // resolved generics)
+    int i = 0;
+    for (TypeMirror parameterType : xType.getParameterTypes()) {
+      if (!state.isTransportableType(parameterType)) {
+        // see comments above about the returnType
+        state.poison(x.getParameters().get(i), Messages.untransportableType(parameterType));
+      }
+      i++;
+    }
+    return null;
   }
 
   @Override
   public Void visitType(TypeElement x, State state) {
+    checkedElement = x;
     ProxyFor proxyFor = x.getAnnotation(ProxyFor.class);
     ProxyForName proxyForName = x.getAnnotation(ProxyForName.class);
     JsonRpcProxy jsonRpcProxy = x.getAnnotation(JsonRpcProxy.class);
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/RequestContextScanner.java b/user/src/com/google/web/bindery/requestfactory/apt/RequestContextScanner.java
index 8a07085..614b7c7 100644
--- a/user/src/com/google/web/bindery/requestfactory/apt/RequestContextScanner.java
+++ b/user/src/com/google/web/bindery/requestfactory/apt/RequestContextScanner.java
@@ -23,8 +23,8 @@
 import javax.lang.model.element.ExecutableElement;
 import javax.lang.model.element.TypeElement;
 import javax.lang.model.element.TypeParameterElement;
-import javax.lang.model.element.VariableElement;
 import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.ExecutableType;
 import javax.lang.model.type.MirroredTypeException;
 import javax.lang.model.type.TypeMirror;
 
@@ -34,12 +34,16 @@
  */
 class RequestContextScanner extends ScannerBase<Void> {
 
+  private TypeElement checkedElement;
+
   @Override
   public Void visitExecutable(ExecutableElement x, State state) {
     if (shouldIgnore(x, state)) {
       return null;
     }
-    TypeMirror returnType = x.getReturnType();
+    // resolve type parameters, if any
+    ExecutableType xType = viewIn(checkedElement, x, state);
+    TypeMirror returnType = xType.getReturnType();
     if (state.types.isAssignable(returnType, state.requestType)) {
       // Extract Request<Foo> type
       DeclaredType asRequest = (DeclaredType) State.viewAs(state.requestType, returnType, state);
@@ -65,17 +69,27 @@
           state.poison(x, Messages.untransportableType(requestReturn));
         }
       }
-    } else if (isSetter(x, state)) {
-      // Parameter checked in visitVariable
-    } else {
+    } else if (!isSetter(x, state)) {
       state.poison(x, Messages.contextRequiredReturnTypes(state.requestType.asElement()
           .getSimpleName(), state.instanceRequestType.asElement().getSimpleName()));
     }
-    return super.visitExecutable(x, state);
+
+    // check parameters (we do not defer to visitVariable, as we need the
+    // resolved generics)
+    int i = 0;
+    for (TypeMirror parameterType : xType.getParameterTypes()) {
+      if (!state.isTransportableType(parameterType)) {
+        // see comments in ProxyScanner#visitExecutable
+        state.poison(x.getParameters().get(i), Messages.untransportableType(parameterType));
+      }
+      i++;
+    }
+    return null;
   }
 
   @Override
   public Void visitType(TypeElement x, State state) {
+    checkedElement = x;
     Service service = x.getAnnotation(Service.class);
     ServiceName serviceName = x.getAnnotation(ServiceName.class);
     JsonRpcService jsonRpcService = x.getAnnotation(JsonRpcService.class);
@@ -115,12 +129,4 @@
     }
     return super.visitTypeParameter(x, state);
   }
-
-  @Override
-  public Void visitVariable(VariableElement x, State state) {
-    if (!state.isTransportableType(x.asType())) {
-      state.poison(x, Messages.untransportableType(x.asType()));
-    }
-    return super.visitVariable(x, state);
-  }
 }
\ No newline at end of file
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/RequestFactoryScanner.java b/user/src/com/google/web/bindery/requestfactory/apt/RequestFactoryScanner.java
index 3f881f7..e6448f9 100644
--- a/user/src/com/google/web/bindery/requestfactory/apt/RequestFactoryScanner.java
+++ b/user/src/com/google/web/bindery/requestfactory/apt/RequestFactoryScanner.java
@@ -19,6 +19,7 @@
 import javax.lang.model.element.ElementKind;
 import javax.lang.model.element.ExecutableElement;
 import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.ExecutableType;
 import javax.lang.model.type.TypeMirror;
 
 /**
@@ -26,6 +27,8 @@
  * the State object to validate the types that it encounters.
  */
 class RequestFactoryScanner extends ScannerBase<Void> {
+  private TypeElement checkedElement;
+
   @Override
   public Void visitExecutable(ExecutableElement x, State state) {
     if (shouldIgnore(x, state)) {
@@ -35,7 +38,9 @@
     if (!x.getParameters().isEmpty()) {
       state.poison(x, Messages.factoryNoMethodParameters());
     }
-    TypeMirror returnType = x.getReturnType();
+    // resolve type parameters, if any
+    ExecutableType xType = viewIn(checkedElement, x, state);
+    TypeMirror returnType = xType.getReturnType();
     if (state.types.isAssignable(returnType, state.requestContextType)) {
       Element returnTypeElement = state.types.asElement(returnType);
       if (!returnTypeElement.getKind().equals(ElementKind.INTERFACE)) {
@@ -59,6 +64,8 @@
       return null;
     }
 
+    checkedElement = x;
+
     scanAllInheritedMethods(x, state);
     state.checkExtraTypes(x);
     return null;
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/ScannerBase.java b/user/src/com/google/web/bindery/requestfactory/apt/ScannerBase.java
index 73efdb6..e88d8d1 100644
--- a/user/src/com/google/web/bindery/requestfactory/apt/ScannerBase.java
+++ b/user/src/com/google/web/bindery/requestfactory/apt/ScannerBase.java
@@ -24,6 +24,7 @@
 import javax.lang.model.element.ElementKind;
 import javax.lang.model.element.ExecutableElement;
 import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.DeclaredType;
 import javax.lang.model.type.ExecutableType;
 import javax.lang.model.type.TypeKind;
 import javax.lang.model.type.TypeMirror;
@@ -48,10 +49,22 @@
     }
   }
 
-  protected static ExecutableType viewIn(TypeElement lookIn, ExecutableElement methodElement, State state) {
+  protected static ExecutableType viewIn(TypeElement lookIn, ExecutableElement methodElement,
+      State state) {
+    // Do not use state.types.getDeclaredType, as we really want a
+    // "prototypical" type, and not a raw type.
+    // This is important when a proxy maps to a generic domain type:
+    // state.types.getDeclaredType without typeArgs would return the raw type,
+    // and asMemberOf would then return an ExecutableType using raw types too.
+    // For instance, if a class Foo<T> contains a method whose return type is
+    // List<String> (note it doesn't even make use of the T type parameter),
+    // then if we were to use raw types, the returned type of the ExecutableType
+    // would be the raw type java.util.List, and not List<String>. Using
+    // asType(), we'd get the expected List<String> though; and for a List<T>,
+    // we'd get a List<Object> (or whichever upper bound for the T type
+    // parameter).
     try {
-      return (ExecutableType) state.types.asMemberOf(state.types.getDeclaredType(lookIn),
-          methodElement);
+      return (ExecutableType) state.types.asMemberOf((DeclaredType) lookIn.asType(), methodElement);
     } catch (IllegalArgumentException e) {
       return (ExecutableType) methodElement.asType();
     }
diff --git a/user/test/com/google/web/bindery/requestfactory/gwt/RequestFactorySuite.java b/user/test/com/google/web/bindery/requestfactory/gwt/RequestFactorySuite.java
index 7d97052..09d259f 100644
--- a/user/test/com/google/web/bindery/requestfactory/gwt/RequestFactorySuite.java
+++ b/user/test/com/google/web/bindery/requestfactory/gwt/RequestFactorySuite.java
@@ -22,6 +22,7 @@
 import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryChainedContextTest;
 import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryExceptionHandlerTest;
 import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryExceptionPropagationTest;
+import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryGenericsTest;
 import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryPolymorphicTest;
 import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryTest;
 import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryUnicodeEscapingTest;
@@ -58,6 +59,7 @@
     suite.addTestSuite(RequestFactoryChainedContextTest.class);
     suite.addTestSuite(RequestFactoryExceptionHandlerTest.class);
     suite.addTestSuite(RequestFactoryExceptionPropagationTest.class);
+    suite.addTestSuite(RequestFactoryGenericsTest.class);
     suite.addTestSuite(RequestFactoryPolymorphicTest.class);
     suite.addTestSuite(RequestFactoryUnicodeEscapingTest.class);
     suite.addTestSuite(RequestPayloadTest.class);
diff --git a/user/test/com/google/web/bindery/requestfactory/gwt/client/RequestFactoryGenericsTest.java b/user/test/com/google/web/bindery/requestfactory/gwt/client/RequestFactoryGenericsTest.java
new file mode 100644
index 0000000..672e600
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/gwt/client/RequestFactoryGenericsTest.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.web.bindery.requestfactory.gwt.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.junit.client.GWTTestCase;
+import com.google.web.bindery.event.shared.SimpleEventBus;
+import com.google.web.bindery.requestfactory.shared.ProxyFor;
+import com.google.web.bindery.requestfactory.shared.Receiver;
+import com.google.web.bindery.requestfactory.shared.Request;
+import com.google.web.bindery.requestfactory.shared.RequestContext;
+import com.google.web.bindery.requestfactory.shared.RequestFactory;
+import com.google.web.bindery.requestfactory.shared.Service;
+import com.google.web.bindery.requestfactory.shared.ValueProxy;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Tests usage of generics in RequestFactory.
+ */
+public class RequestFactoryGenericsTest extends GWTTestCase {
+
+  /** Service under test. */
+  public static class Impl {
+    public static Domain echoDomain(Domain domainObject) {
+      return domainObject;
+    }
+
+    public static BaseDomain<?, ?> getBase() {
+      return new BaseDomain<Integer, Float>() {
+        {
+          setListOfStrings(Collections.singletonList("foo"));
+        }
+
+        @Override
+        public void setA(Integer a) {
+          throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Integer getA() {
+          throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public List<Integer> getListOfA() {
+          throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void setListOfA(List<Integer> list) {
+          throw new UnsupportedOperationException();
+        }
+      };
+    }
+
+    public static Container echoContainer(Container container) {
+      return container;
+    }
+  }
+
+  /** Generic interface implemented by both proxy and domain object. */
+  public interface HasA<A> {
+    A getA();
+
+    void setA(A a);
+
+    List<A> getListOfA();
+
+    void setListOfA(List<A> list);
+  }
+
+  /** Base generic domain object. */
+  public static abstract class BaseDomain<A, B> implements HasA<A> {
+    private B b;
+    private List<String> listOfStrings;
+
+    public B getB() {
+      return b;
+    }
+
+    public void setB(B b) {
+      this.b = b;
+    }
+
+    public List<String> getListOfStrings() {
+      return this.listOfStrings;
+    }
+
+    public void setListOfStrings(List<String> list) {
+      this.listOfStrings = list;
+    }
+  }
+
+  /** Domain object that "locks" generic type params from base class. */
+  public static class Domain extends BaseDomain<String, Boolean> {
+    private String str;
+    private List<String> list;
+
+    @Override
+    public String getA() {
+      return str;
+    }
+
+    @Override
+    public void setA(String a) {
+      str = a;
+    }
+
+    @Override
+    public List<String> getListOfA() {
+      return list;
+    }
+
+    @Override
+    public void setListOfA(List<String> list) {
+      this.list = list;
+    }
+  }
+
+  /** Domain object containing another object with inherited generics. */
+  public static class Container {
+    private Domain domain;
+
+    public Domain getDomain() {
+      return domain;
+    }
+
+    public void setDomain(Domain domain) {
+      this.domain = domain;
+    }
+  }
+
+  /** Proxy testing that the domain type can be parameterized. */
+  @ProxyFor(BaseDomain.class)
+  public interface BaseDomainProxy extends ValueProxy {
+    List<String> getListOfStrings();
+
+    void setListOfStrings(List<String> listOfStrings);
+  }
+
+  /** Proxy implementing generic interface (locking the type param). */
+  @ProxyFor(Domain.class)
+  public interface DomainProxy extends BaseDomainProxy, HasA<String> {
+
+    Boolean getB();
+
+    void setB(Boolean b);
+  }
+
+  /** Base interface references proxy "with generics". */
+  public interface BaseContainerProxy {
+    BaseDomainProxy getDomain();
+  }
+
+  /**
+   * Child-interface overrides method with co-variant return type (with
+   * "locked" type params).
+   */
+  @ProxyFor(Container.class)
+  public interface ContainerProxy extends ValueProxy, BaseContainerProxy {
+
+    @Override
+    DomainProxy getDomain();
+
+    void setDomain(DomainProxy domain);
+  }
+
+  /** Request context under test. */
+  @Service(Impl.class)
+  public interface Context extends RequestContext {
+    Request<DomainProxy> echoDomain(DomainProxy proxy);
+
+    Request<BaseDomainProxy> getBase();
+
+    Request<ContainerProxy> echoContainer(ContainerProxy container);
+  }
+
+  /** Factory under test. */
+  public interface Factory extends RequestFactory {
+    Context ctx();
+  }
+
+  @Override
+  public String getModuleName() {
+    return "com.google.web.bindery.requestfactory.gwt.RequestFactorySuite";
+  }
+
+  private static final int TEST_DELAY = 5000;
+
+  protected Factory factory;
+
+  public void testEchoDomain() throws Exception {
+    delayTestFinish(TEST_DELAY);
+    Context ctx = factory.ctx();
+    DomainProxy proxy = ctx.create(DomainProxy.class);
+    proxy.setA("foo");
+    proxy.setB(true);
+    proxy.setListOfA(Collections.singletonList("bar"));
+    proxy.setListOfStrings(Collections.singletonList("baz"));
+    ctx.echoDomain(proxy).fire(new Receiver<DomainProxy>() {
+      @Override
+      public void onSuccess(DomainProxy response) {
+        assertEquals("foo", response.getA());
+        assertEquals(Boolean.TRUE, response.getB());
+        assertEquals(Collections.singletonList("bar"), response.getListOfA());
+        assertEquals(Collections.singletonList("baz"), response.getListOfStrings());
+        finishTest();
+      }
+    });
+  }
+
+  public void testGetBase() throws Exception {
+    delayTestFinish(TEST_DELAY);
+    Context ctx = factory.ctx();
+    ctx.getBase().fire(new Receiver<BaseDomainProxy>() {
+      @Override
+      public void onSuccess(BaseDomainProxy response) {
+        assertEquals(Collections.singletonList("foo"), response.getListOfStrings());
+        finishTest();
+      }
+    });
+  }
+
+  public void testEchoContainer() throws Exception {
+    delayTestFinish(TEST_DELAY);
+    Context ctx = factory.ctx();
+    DomainProxy proxy = ctx.create(DomainProxy.class);
+    proxy.setA("42");
+    ContainerProxy container = ctx.create(ContainerProxy.class);
+    container.setDomain(proxy);
+    ctx.echoContainer(container).fire(new Receiver<ContainerProxy>() {
+      @Override
+      public void onSuccess(ContainerProxy response) {
+        assertEquals("42", response.getDomain().getA());
+        finishTest();
+      }
+    });
+  }
+
+  @Override
+  protected void gwtSetUp() throws Exception {
+    factory = createFactory();
+  }
+
+  protected Factory createFactory() {
+    Factory factory = GWT.create(Factory.class);
+    factory.initialize(new SimpleEventBus());
+    return factory;
+  }
+}
diff --git a/user/test/com/google/web/bindery/requestfactory/server/RequestFactoryGenericsJreTest.java b/user/test/com/google/web/bindery/requestfactory/server/RequestFactoryGenericsJreTest.java
new file mode 100644
index 0000000..18be510
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/server/RequestFactoryGenericsJreTest.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.web.bindery.requestfactory.server;
+
+import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryGenericsTest;
+
+/**
+ * A JRE version of {@link RequestFactoryGenericsTest}.
+ */
+public class RequestFactoryGenericsJreTest extends RequestFactoryGenericsTest {
+
+  @Override
+  public String getModuleName() {
+    return null;
+  }
+
+  @Override
+  protected Factory createFactory() {
+    return RequestFactoryJreTest.createInProcess(Factory.class);
+  }
+}
diff --git a/user/test/com/google/web/bindery/requestfactory/vm/RequestFactoryJreSuite.java b/user/test/com/google/web/bindery/requestfactory/vm/RequestFactoryJreSuite.java
index c5f80a7..5fa499d 100644
--- a/user/test/com/google/web/bindery/requestfactory/vm/RequestFactoryJreSuite.java
+++ b/user/test/com/google/web/bindery/requestfactory/vm/RequestFactoryJreSuite.java
@@ -25,6 +25,7 @@
 import com.google.web.bindery.requestfactory.server.MultipleFactoriesJreTest;
 import com.google.web.bindery.requestfactory.server.RequestFactoryChainedContextJreTest;
 import com.google.web.bindery.requestfactory.server.RequestFactoryExceptionPropagationJreTest;
+import com.google.web.bindery.requestfactory.server.RequestFactoryGenericsJreTest;
 import com.google.web.bindery.requestfactory.server.RequestFactoryJreTest;
 import com.google.web.bindery.requestfactory.server.RequestFactoryPolymorphicJreTest;
 import com.google.web.bindery.requestfactory.server.RequestFactoryUnicodeEscapingJreTest;
@@ -55,6 +56,7 @@
     suite.addTestSuite(MultipleFactoriesJreTest.class);
     suite.addTestSuite(RequestFactoryChainedContextJreTest.class);
     suite.addTestSuite(RequestFactoryExceptionPropagationJreTest.class);
+    suite.addTestSuite(RequestFactoryGenericsJreTest.class);
     suite.addTestSuite(RequestFactoryJreTest.class);
     suite.addTestSuite(RequestFactoryPolymorphicJreTest.class);
     suite.addTestSuite(RequestFactoryUnicodeEscapingJreTest.class);