Add compile-time tests for APT-based RequestFactory interface validator.
Move all validator message formatting to a single class.
Patch by: bobv
Review by: keertip,pquitslund, t.broyer
Suggested by: t.broyer

Review at http://gwt-code-reviews.appspot.com/1473801


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10426 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/ClientToDomainMapper.java b/user/src/com/google/web/bindery/requestfactory/apt/ClientToDomainMapper.java
index f4cf816..c7332dc 100644
--- a/user/src/com/google/web/bindery/requestfactory/apt/ClientToDomainMapper.java
+++ b/user/src/com/google/web/bindery/requestfactory/apt/ClientToDomainMapper.java
@@ -15,8 +15,6 @@
  */
 package com.google.web.bindery.requestfactory.apt;
 
-import com.google.web.bindery.autobean.shared.ValueCodex;
-
 import java.util.Collection;
 import java.util.List;
 import java.util.Set;
@@ -30,14 +28,13 @@
 import javax.lang.model.type.TypeMirror;
 import javax.lang.model.type.TypeVariable;
 import javax.lang.model.type.WildcardType;
-import javax.lang.model.util.SimpleTypeVisitor6;
 
 /**
  * Uses information in a State object to convert client types to their domain
  * equivalents. This types assumes that any incoming type has already been
  * determined to be a transportable type.
  */
-class ClientToDomainMapper extends SimpleTypeVisitor6<TypeMirror, State> {
+class ClientToDomainMapper extends TypeVisitorBase<TypeMirror> {
   public static class UnmappedTypeException extends RuntimeException {
     private final TypeMirror clientType;
 
@@ -83,12 +80,8 @@
       // InstanceRequest<FooProxy, X> -> FooDomain
       return convertSingleParamType(x, state.instanceRequestType, 1, state);
     }
-    for (Class<?> clazz : ValueCodex.getAllValueTypes()) {
-      if (clazz.isPrimitive()) {
-        continue;
-      }
-
-      if (state.types.isAssignable(x, state.findType(clazz))) {
+    for (DeclaredType valueType : getValueTypes(state)) {
+      if (state.types.isAssignable(x, valueType)) {
         // Value types map straight through
         return x;
       }
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/DomainChecker.java b/user/src/com/google/web/bindery/requestfactory/apt/DomainChecker.java
index 7c53177..dba4293 100644
--- a/user/src/com/google/web/bindery/requestfactory/apt/DomainChecker.java
+++ b/user/src/com/google/web/bindery/requestfactory/apt/DomainChecker.java
@@ -48,14 +48,16 @@
   static class MethodFinder extends ScannerBase<ExecutableElement> {
     private TypeElement domainType;
     private ExecutableElement found;
+    private final boolean boxReturnType;
     private final CharSequence name;
     private final TypeMirror returnType;
     private final List<TypeMirror> params;
 
     public MethodFinder(CharSequence name, TypeMirror returnType, List<TypeMirror> params,
-        State state) {
+        boolean boxReturnType, State state) {
+      this.boxReturnType = boxReturnType;
       this.name = name;
-      this.returnType = TypeSimplifier.simplify(returnType, true, state);
+      this.returnType = TypeSimplifier.simplify(returnType, boxReturnType, state);
       List<TypeMirror> temp = new ArrayList<TypeMirror>(params.size());
       for (TypeMirror param : params) {
         temp.add(TypeSimplifier.simplify(param, false, state));
@@ -80,11 +82,9 @@
           returnTypeMatches = true;
         } else {
           TypeMirror domainReturn =
-              TypeSimplifier.simplify(domainMethod.getReturnType(), true, state);
+              TypeSimplifier.simplify(domainMethod.getReturnType(), boxReturnType, state);
           // The isSameType handles the NONE case.
-          returnTypeMatches =
-              state.types.isSameType(domainReturn, returnType)
-                  || state.types.isAssignable(domainReturn, returnType);
+          returnTypeMatches = state.types.isSubtype(domainReturn, returnType);
         }
         if (returnTypeMatches) {
           boolean paramsMatch = true;
@@ -94,8 +94,7 @@
             assert domainParam.hasNext();
             TypeMirror requestedType = lookFor.next();
             TypeMirror paramType = TypeSimplifier.simplify(domainParam.next(), false, state);
-            if (!state.types.isSameType(requestedType, paramType)
-                && !state.types.isAssignable(requestedType, paramType)) {
+            if (!state.types.isSubtype(requestedType, paramType)) {
               paramsMatch = false;
             }
           }
@@ -134,11 +133,11 @@
    * This is used as the target for errors since generic methods show up as
    * synthetic elements that don't correspond to any source.
    */
-  private TypeElement checkedType;
+  private TypeElement checkedElement;
   private boolean currentTypeIsProxy;
+  private TypeElement domainElement;
   private boolean requireInstanceDomainMethods;
   private boolean requireStaticDomainMethods;
-  private TypeElement domainType;
 
   @Override
   public Void visitExecutable(ExecutableElement clientMethodElement, State state) {
@@ -152,7 +151,7 @@
       return null;
     }
 
-    ExecutableType clientMethod = viewIn(checkedType, clientMethodElement, state);
+    ExecutableType clientMethod = viewIn(checkedElement, clientMethodElement, state);
     List<TypeMirror> lookFor = new ArrayList<TypeMirror>();
     // Convert client method signature to domain types
     TypeMirror returnType;
@@ -173,16 +172,23 @@
     if (currentTypeIsProxy && isSetter(clientMethodElement, state)) {
       // Look for void setFoo(...)
       domainMethod =
-          new MethodFinder(name, state.types.getNoType(TypeKind.VOID), lookFor, state).scan(
-              domainType, state);
+          new MethodFinder(name, state.types.getNoType(TypeKind.VOID), lookFor, false, state).scan(
+              domainElement, state);
       if (domainMethod == null) {
         // Try a builder style
         domainMethod =
-            new MethodFinder(name, domainType.asType(), lookFor, state).scan(domainType, state);
+            new MethodFinder(name, domainElement.asType(), lookFor, false, state).scan(
+                domainElement, state);
       }
     } else {
-      // The usual case for getters and all service methods
-      domainMethod = new MethodFinder(name, returnType, lookFor, state).scan(domainType, state);
+      /*
+       * The usual case for getters and all service methods. Only box return
+       * types when matching context methods since there's a significant
+       * semantic difference between a null Integer and 0.
+       */
+      domainMethod =
+          new MethodFinder(name, returnType, lookFor, !currentTypeIsProxy, state).scan(
+              domainElement, state);
     }
 
     if (domainMethod == null) {
@@ -194,7 +200,7 @@
       }
       sb.append(")");
 
-      state.poison(clientMethodElement, "Could not find domain method similar to %s", sb);
+      state.poison(clientMethodElement, Messages.domainMissingMethod(sb));
       return null;
     }
 
@@ -203,21 +209,19 @@
      * InstanceRequests assume instance methods on the domain type.
      */
     boolean isInstanceRequest =
-        state.types.isAssignable(clientMethod.getReturnType(), state.instanceRequestType);
+        state.types.isSubtype(clientMethod.getReturnType(), state.instanceRequestType);
 
     if ((isInstanceRequest || requireInstanceDomainMethods)
         && domainMethod.getModifiers().contains(Modifier.STATIC)) {
-      state.poison(checkedType, "Found static domain method %s when instance method required",
-          domainMethod.getSimpleName());
+      state.poison(checkedElement, Messages.domainMethodWrongModifier(false, domainMethod
+          .getSimpleName()));
     }
     if (!isInstanceRequest && requireStaticDomainMethods
         && !domainMethod.getModifiers().contains(Modifier.STATIC)) {
-      state.poison(checkedType, "Found instance domain method %s when static method required",
-          domainMethod.getSimpleName());
+      state.poison(checkedElement, Messages.domainMethodWrongModifier(true, domainMethod
+          .getSimpleName()));
     }
-    if (state.verbose) {
-      state.warn(clientMethodElement, "Found domain method %s", domainMethod.toString());
-    }
+    state.debug(clientMethodElement, "Found domain method %s", domainMethod.toString());
 
     return null;
   }
@@ -225,12 +229,11 @@
   @Override
   public Void visitType(TypeElement clientTypeElement, State state) {
     TypeMirror clientType = clientTypeElement.asType();
-    checkedType = clientTypeElement;
-    boolean isEntityProxy = state.types.isAssignable(clientType, state.entityProxyType);
-    currentTypeIsProxy =
-        isEntityProxy || state.types.isAssignable(clientType, state.valueProxyType);
-    domainType = state.getClientToDomainMap().get(clientTypeElement);
-    if (domainType == null) {
+    checkedElement = clientTypeElement;
+    boolean isEntityProxy = state.types.isSubtype(clientType, state.entityProxyType);
+    currentTypeIsProxy = isEntityProxy || state.types.isSubtype(clientType, state.valueProxyType);
+    domainElement = state.getClientToDomainMap().get(clientTypeElement);
+    if (domainElement == null) {
       // A proxy with an unresolved domain type (e.g. ProxyForName(""))
       return null;
     }
@@ -243,11 +246,10 @@
       requireInstanceDomainMethods = true;
       if (!hasProxyLocator(clientTypeElement, state)) {
         // Domain types without a Locator should have a no-arg constructor
-        if (!hasNoArgConstructor(domainType)) {
-          state.warn(clientTypeElement, "The domain type %s has no default constructor."
-              + " Calling %s.create(%s.class) will cause a server error.", domainType,
-              state.requestContextType.asElement().getSimpleName(), clientTypeElement
-                  .getSimpleName());
+        if (!hasNoArgConstructor(domainElement)) {
+          state.warn(clientTypeElement, Messages.domainNoDefaultConstructor(domainElement
+              .getSimpleName(), clientTypeElement.getSimpleName(), state.requestContextType
+              .asElement().getSimpleName()));
         }
 
         /*
@@ -279,40 +281,35 @@
    */
   private void checkDomainEntityMethods(State state) {
     ExecutableElement getId =
-        new MethodFinder("getId", null, Collections.<TypeMirror> emptyList(), state).scan(
-            domainType, state);
+        new MethodFinder("getId", null, Collections.<TypeMirror> emptyList(), false, state).scan(
+            domainElement, state);
     if (getId == null) {
-      state.poison(checkedType, "Domain type %s does not have a getId() method", domainType
-          .asType());
+      state.poison(checkedElement, Messages.domainNoGetId(domainElement.asType()));
     } else {
       if (getId.getModifiers().contains(Modifier.STATIC)) {
-
-        state.poison(checkedType, "The domain type's getId() method must not be static");
+        state.poison(checkedElement, Messages.domainGetIdStatic());
       }
 
       // Can only check findFoo() if we have a getId
       ExecutableElement find =
-          new MethodFinder("find" + domainType.getSimpleName(), domainType.asType(), Collections
-              .singletonList(getId.getReturnType()), state).scan(domainType, state);
+          new MethodFinder("find" + domainElement.getSimpleName(), domainElement.asType(),
+              Collections.singletonList(getId.getReturnType()), false, state).scan(domainElement,
+              state);
       if (find == null) {
-        state.warn(checkedType, "The domain type %s has no %s find%s(%s) method. "
-            + "Attempting to send a %s to the server will result in a server error.", domainType
-            .asType(), domainType.getSimpleName(), domainType.getSimpleName(), getId
-            .getReturnType(), checkedType.getSimpleName());
+        state.warn(checkedElement, Messages.domainMissingFind(domainElement.asType(), domainElement
+            .getSimpleName(), getId.getReturnType(), checkedElement.getSimpleName()));
       } else if (!find.getModifiers().contains(Modifier.STATIC)) {
-        state.poison(checkedType, "The domain object's find%s() method is not static", domainType
-            .getSimpleName());
+        state.poison(checkedElement, Messages.domainFindNotStatic(domainElement.getSimpleName()));
       }
     }
 
     ExecutableElement getVersion =
-        new MethodFinder("getVersion", null, Collections.<TypeMirror> emptyList(), state).scan(
-            domainType, state);
+        new MethodFinder("getVersion", null, Collections.<TypeMirror> emptyList(), false, state)
+            .scan(domainElement, state);
     if (getVersion == null) {
-      state.poison(checkedType, "Domain type %s does not have a getVersion() method", domainType
-          .asType());
+      state.poison(checkedElement, Messages.domainNoGetVersion(domainElement.asType()));
     } else if (getVersion.getModifiers().contains(Modifier.STATIC)) {
-      state.poison(checkedType, "The domain type's getVersion() method must not be static");
+      state.poison(checkedElement, Messages.domainGetVersionStatic());
     }
   }
 
@@ -338,8 +335,7 @@
     } catch (UnmappedTypeException e) {
       error = true;
       returnType = null;
-      state.warn(warnTo, "Cannot validate this method because the domain mapping for the"
-          + " return type (%s) could not be resolved to a domain type", e.getClientType());
+      state.warn(warnTo, Messages.methodNoDomainPeer(e.getClientType(), false));
     }
     for (TypeMirror param : clientMethod.getParameterTypes()) {
       try {
@@ -347,8 +343,7 @@
       } catch (UnmappedTypeException e) {
         parameterAccumulator.add(null);
         error = true;
-        state.warn(warnTo, "Cannot validate this method because the domain mapping for a"
-            + " parameter of type (%s) could not be resolved to a domain type", e.getClientType());
+        state.warn(warnTo, Messages.methodNoDomainPeer(e.getClientType(), true));
       }
     }
     if (error) {
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/Messages.java b/user/src/com/google/web/bindery/requestfactory/apt/Messages.java
new file mode 100644
index 0000000..4f5cbd9
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/Messages.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2011 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.apt;
+
+import com.google.web.bindery.requestfactory.shared.JsonRpcProxy;
+import com.google.web.bindery.requestfactory.shared.JsonRpcService;
+import com.google.web.bindery.requestfactory.shared.ProxyFor;
+import com.google.web.bindery.requestfactory.shared.ProxyForName;
+import com.google.web.bindery.requestfactory.shared.Service;
+import com.google.web.bindery.requestfactory.shared.ServiceName;
+
+/**
+ * Contains string-formatting methods to produce error messages. This class
+ * exists to avoid the need to duplicate error messages in test code. All method
+ * parameters in this class accept {@code Object} so that the production code
+ * can pass {@code javax.lang.model} types and the test code can pass Strings.
+ */
+class Messages {
+  /*
+   * Note to maintainers: When new messages are added to this class, the
+   * RfValidatorTest.testErrorsAndWarnings() method should be updated to test
+   * the new message.
+   */
+
+  public static String contextMissingDomainType(Object domainTypeName) {
+    return String.format("Cannot fully validate context since domain type %s is not available",
+        domainTypeName);
+  }
+
+  public static String contextMustBeAnnotated(Object requestContextName) {
+    return String.format("A %s must be annotated with %s, %s, or %s", requestContextName,
+        Service.class.getSimpleName(), ServiceName.class.getSimpleName(), JsonRpcService.class
+            .getSimpleName());
+  }
+
+  public static String contextRequiredReturnTypes(Object requestName, Object instanceRequestName) {
+    return String.format("The return type must be a %s or %s", requestName, instanceRequestName);
+  }
+
+  public static String domainFindNotStatic(Object domainTypeName) {
+    return String.format("The domain object's find%s() method is not static", domainTypeName);
+  }
+
+  public static String domainGetIdStatic() {
+    return "The domain type's getId() method must not be static";
+  }
+
+  public static String domainGetVersionStatic() {
+    return "The domain type's getVersion() method must not be static";
+  }
+
+  public static String domainMethodWrongModifier(boolean expectStatic, Object domainMethodName) {
+    return String.format("Found %s domain method %s when %s method required", expectStatic
+        ? "instance" : "static", domainMethodName, expectStatic ? "static" : "instance");
+  }
+
+  public static String domainMissingFind(Object domainType, Object simpleName,
+      Object getIdReturnType, Object checkedTypeName) {
+    return String.format("The domain type %s has no %s find%s(%s) method. "
+        + "Attempting to send a %s to the server will result in a server error.", domainType,
+        simpleName, simpleName, getIdReturnType, checkedTypeName);
+  }
+
+  public static String domainMissingMethod(Object description) {
+    return String.format("Could not find domain method similar to %s", description);
+  }
+
+  public static String domainNoDefaultConstructor(Object domainName, Object proxyName,
+      Object requestContextName) {
+    return String.format("The domain type %s has no default constructor."
+        + " Calling %s.create(%s.class) will cause a server error.", domainName,
+        requestContextName, proxyName);
+  }
+
+  public static String domainNoGetId(Object domainType) {
+    return String.format("Domain type %s does not have a getId() method", domainType);
+  }
+
+  public static String domainNoGetVersion(Object domainType) {
+    return String.format("Domain type %s does not have a getVersion() method", domainType);
+  }
+
+  public static String factoryMustBeAssignable(Object assignableTo) {
+    return String.format("The return type of this method must return a %s", assignableTo);
+  }
+
+  public static String factoryMustReturnInterface(Object returnType) {
+    return String.format("The return type %s must be an interface", returnType);
+  }
+
+  public static String factoryNoMethodParameters() {
+    return "This method must have no parameters";
+  }
+
+  public static String methodNoDomainPeer(Object proxyTypeName, boolean isParameter) {
+    return String.format("Cannot validate this method because the domain mapping for "
+        + " %s type (%s) could not be resolved to a domain type", isParameter ? "a parameter of"
+        : "the return", proxyTypeName);
+  }
+
+  public static String proxyMissingDomainType(Object missingDomainName) {
+    return String.format("Cannot fully validate proxy since type %s is not available",
+        missingDomainName);
+  }
+
+  public static String proxyMustBeAnnotated() {
+    return String.format("A proxy must be annotated with %s, %s, or %s", ProxyFor.class
+        .getSimpleName(), ProxyForName.class.getSimpleName(), JsonRpcProxy.class.getSimpleName());
+  }
+
+  public static String proxyOnlyGettersSetters() {
+    return "Only getters and setters allowed";
+  }
+
+  public static String rawType() {
+    return "A raw type may not be used here";
+  }
+
+  public static String redundantAnnotation(Object annotationName) {
+    return String.format("Redundant annotation: %s", annotationName);
+  }
+
+  public static String untransportableType(Object returnType) {
+    return String.format("The type %s cannot be used here", returnType);
+  }
+
+  public static String warnSuffix() {
+    return "\n\nAdd @SuppressWarnings(\"requestfactory\") to dismiss.";
+  }
+
+  private Messages() {
+  }
+}
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 f3c85c7..2b72edf 100644
--- a/user/src/com/google/web/bindery/requestfactory/apt/ProxyScanner.java
+++ b/user/src/com/google/web/bindery/requestfactory/apt/ProxyScanner.java
@@ -41,10 +41,10 @@
     if (isGetter(x, state)) {
       TypeMirror returnType = x.getReturnType();
       if (!state.isTransportableType(returnType)) {
-        state.poison(x, "The return type %s cannot be used here", returnType.toString());
+        state.poison(x, Messages.untransportableType(returnType));
       }
     } else if (!isSetter(x, state)) {
-      state.poison(x, "Only getters and setters allowed");
+      state.poison(x, Messages.proxyOnlyGettersSetters());
     }
     // Parameters checked by visitVariable
     return super.visitExecutable(x, state);
@@ -72,8 +72,7 @@
       TypeElement domain =
           state.elements.getTypeElement(BinaryName.toSourceName(proxyForName.value()));
       if (domain == null) {
-        state.warn(x, "Cannot fully validate proxy since type %s is not available", proxyForName
-            .value());
+        state.warn(x, Messages.proxyMissingDomainType(proxyForName.value()));
       }
       state.addMapping(x, domain);
     }
@@ -86,7 +85,7 @@
   @Override
   public Void visitVariable(VariableElement x, State state) {
     if (!state.isTransportableType(x.asType())) {
-      state.poison(x, "The type %s cannot be used here", x.asType().toString());
+      state.poison(x, Messages.untransportableType(x.asType()));
     }
     return super.visitVariable(x, state);
   }
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 3fe796a..2fecf20 100644
--- a/user/src/com/google/web/bindery/requestfactory/apt/RequestContextScanner.java
+++ b/user/src/com/google/web/bindery/requestfactory/apt/RequestContextScanner.java
@@ -44,11 +44,11 @@
       // Extract Request<Foo> type
       DeclaredType asRequest = (DeclaredType) State.viewAs(state.requestType, returnType, state);
       if (asRequest.getTypeArguments().isEmpty()) {
-        state.poison(x, "A raw return type may not be used here");
+        state.poison(x, Messages.rawType());
       } else {
         TypeMirror requestReturn = asRequest.getTypeArguments().get(0);
         if (!state.isTransportableType(requestReturn)) {
-          state.poison(x, "The type %s cannot be used as a return value", requestReturn.toString());
+          state.poison(x, Messages.untransportableType(requestReturn));
         }
       }
     } else if (state.types.isAssignable(returnType, state.instanceRequestType)) {
@@ -56,23 +56,20 @@
       DeclaredType asInstanceRequest =
           (DeclaredType) State.viewAs(state.instanceRequestType, returnType, state);
       if (asInstanceRequest.getTypeArguments().isEmpty()) {
-        state.poison(x, "A raw return type may not be used here");
+        state.poison(x, Messages.rawType());
       } else {
         TypeMirror instanceType = asInstanceRequest.getTypeArguments().get(0);
-        if (!state.isTransportableType(instanceType)) {
-          state.poison(x, "The type %s cannot be used as an invocation target", instanceType
-              .toString());
-        }
+        state.maybeScanProxy((TypeElement) state.types.asElement(instanceType));
         TypeMirror requestReturn = asInstanceRequest.getTypeArguments().get(1);
         if (!state.isTransportableType(requestReturn)) {
-          state.poison(x, "The type %s cannot be used as a return value", requestReturn.toString());
+          state.poison(x, Messages.untransportableType(requestReturn));
         }
       }
     } else if (isSetter(x, state)) {
       // Parameter checked in visitVariable
     } else {
-      state.poison(x, "The return type must be a %s or %s", state.requestType.asElement()
-          .getSimpleName(), state.instanceRequestType.asElement().getSimpleName());
+      state.poison(x, Messages.contextRequiredReturnTypes(state.requestType.asElement()
+          .getSimpleName(), state.instanceRequestType.asElement().getSimpleName()));
     }
     return super.visitExecutable(x, state);
   }
@@ -83,9 +80,8 @@
     ServiceName serviceName = x.getAnnotation(ServiceName.class);
     JsonRpcService jsonRpcService = x.getAnnotation(JsonRpcService.class);
     if (service == null && serviceName == null && jsonRpcService == null) {
-      state.poison(x, "A %s must be annotated with %s, %s, or %s", state.types.asElement(
-          state.requestContextType).getSimpleName(), Service.class.getSimpleName(),
-          ServiceName.class.getSimpleName(), JsonRpcService.class.getSimpleName());
+      state.poison(x, Messages.contextMustBeAnnotated(state.requestContextType.asElement()
+          .getSimpleName()));
     }
     if (service != null) {
       poisonIfAnnotationPresent(state, x, serviceName, jsonRpcService);
@@ -104,8 +100,7 @@
       TypeElement domain =
           state.elements.getTypeElement(BinaryName.toSourceName(serviceName.value()));
       if (domain == null) {
-        state.warn(x, "Cannot fully validate context since type %s is not available", serviceName
-            .value());
+        state.warn(x, Messages.contextMissingDomainType(serviceName.value()));
       } else {
         state.addMapping(x, domain);
       }
@@ -120,7 +115,7 @@
   public Void visitTypeParameter(TypeParameterElement x, State state) {
     for (TypeMirror bound : x.getBounds()) {
       if (!state.isTransportableType(bound)) {
-        state.poison(x, "The type %s cannot be used here", bound);
+        state.poison(x, Messages.untransportableType(bound));
       }
     }
     return super.visitTypeParameter(x, state);
@@ -129,7 +124,7 @@
   @Override
   public Void visitVariable(VariableElement x, State state) {
     if (!state.isTransportableType(x.asType())) {
-      state.poison(x, "The type %s cannot be used here", x.asType());
+      state.poison(x, Messages.untransportableType(x.asType()));
     }
     return super.visitVariable(x, state);
   }
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 e9cea1c..d36e38c 100644
--- a/user/src/com/google/web/bindery/requestfactory/apt/RequestFactoryScanner.java
+++ b/user/src/com/google/web/bindery/requestfactory/apt/RequestFactoryScanner.java
@@ -33,18 +33,19 @@
       return null;
     }
     if (!x.getParameters().isEmpty()) {
-      state.poison(x, "This method must have no parameters");
+      state.poison(x, Messages.factoryNoMethodParameters());
     }
     TypeMirror returnType = x.getReturnType();
     if (state.types.isAssignable(returnType, state.requestContextType)) {
       Element returnTypeElement = state.types.asElement(returnType);
       if (!returnTypeElement.getKind().equals(ElementKind.INTERFACE)) {
-        state.poison(x, "The return type %s must be an interface", returnType.toString());
+        state.poison(x, Messages.factoryMustReturnInterface(returnTypeElement.getSimpleName()));
       } else {
         state.maybeScanContext((TypeElement) returnTypeElement);
       }
     } else {
-      state.poison(x, "This method must return a %s", state.requestContextType);
+      state.poison(x, Messages.factoryMustBeAssignable(state.requestContextType.asElement()
+          .getSimpleName()));
     }
     return null;
   }
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/RfValidator.java b/user/src/com/google/web/bindery/requestfactory/apt/RfValidator.java
index 332c6d9..1d1eec8 100644
--- a/user/src/com/google/web/bindery/requestfactory/apt/RfValidator.java
+++ b/user/src/com/google/web/bindery/requestfactory/apt/RfValidator.java
@@ -34,12 +34,13 @@
 @SupportedOptions({"suppressErrors", "suppressWarnings", "verbose"})
 public class RfValidator extends AbstractProcessor {
 
+  private boolean forceErrors;
   private State state;
 
   @Override
   public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
     if (state == null) {
-      state = new State(processingEnv);
+      state = forceErrors ? new State.ForTesting(processingEnv) : new State(processingEnv);
     }
 
     try {
@@ -57,4 +58,8 @@
     }
     return false;
   }
+
+  void setForceErrors(boolean forceErrors) {
+    this.forceErrors = forceErrors;
+  }
 }
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 49268da..85ceea6 100644
--- a/user/src/com/google/web/bindery/requestfactory/apt/ScannerBase.java
+++ b/user/src/com/google/web/bindery/requestfactory/apt/ScannerBase.java
@@ -42,7 +42,7 @@
       Annotation... annotations) {
     for (Annotation a : annotations) {
       if (a != null) {
-        state.poison(x, "Redundant annotation: %s", a.annotationType().getSimpleName());
+        state.poison(x, Messages.redundantAnnotation(a.annotationType().getSimpleName()));
       }
     }
   }
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/State.java b/user/src/com/google/web/bindery/requestfactory/apt/State.java
index 52c6ef3..6285eef 100644
--- a/user/src/com/google/web/bindery/requestfactory/apt/State.java
+++ b/user/src/com/google/web/bindery/requestfactory/apt/State.java
@@ -15,9 +15,6 @@
  */
 package com.google.web.bindery.requestfactory.apt;
 
-import com.google.web.bindery.requestfactory.shared.JsonRpcProxy;
-import com.google.web.bindery.requestfactory.shared.ProxyFor;
-import com.google.web.bindery.requestfactory.shared.ProxyForName;
 import com.google.web.bindery.requestfactory.shared.SkipInterfaceValidation;
 
 import java.io.PrintWriter;
@@ -48,6 +45,20 @@
 import javax.tools.Diagnostic.Kind;
 
 class State {
+  /**
+   * Slightly tweaked implementation used when running tests.
+   */
+  static class ForTesting extends State {
+    public ForTesting(ProcessingEnvironment processingEnv) {
+      super(processingEnv);
+    }
+
+    @Override
+    boolean respectAnnotations() {
+      return false;
+    }
+  }
+
   private static class Job {
     public final TypeElement element;
     public final ScannerBase<?> scanner;
@@ -103,19 +114,20 @@
   final DeclaredType requestType;
   final DeclaredType serviceLocatorType;
   final Set<TypeElement> seen;
-  final boolean suppressErrors;
-  final boolean suppressWarnings;
   final Types types;
   final DeclaredType valueProxyType;
-  final boolean verbose;
   private final Map<TypeElement, TypeElement> clientToDomainMain;
   private final List<Job> jobs = new LinkedList<Job>();
   private final Messager messager;
   private boolean poisoned;
+  private final boolean suppressErrors;
+  private final boolean suppressWarnings;
+  private final boolean verbose;
   /**
    * Prevents duplicate messages from being emitted.
    */
   private final Map<Element, Set<String>> previousMessages = new HashMap<Element, Set<String>>();
+
   private final Set<TypeElement> proxiesRequiringMapping = new LinkedHashSet<TypeElement>();
 
   public State(ProcessingEnvironment processingEnv) {
@@ -123,9 +135,9 @@
     elements = processingEnv.getElementUtils();
     messager = processingEnv.getMessager();
     types = processingEnv.getTypeUtils();
-    verbose = Boolean.parseBoolean(processingEnv.getOptions().get("verbose"));
     suppressErrors = Boolean.parseBoolean(processingEnv.getOptions().get("suppressErrors"));
     suppressWarnings = Boolean.parseBoolean(processingEnv.getOptions().get("suppressWarnings"));
+    verbose = Boolean.parseBoolean(processingEnv.getOptions().get("verbose"));
 
     entityProxyType = findType("EntityProxy");
     entityProxyIdType = findType("EntityProxyId");
@@ -197,13 +209,21 @@
     }).scan(x, this);
   }
 
+  /**
+   * Print a warning message if verbose mode is enabled. A warning is used to
+   * ensure that the message shows up in Eclipse's editor (a note only makes it
+   * into the error console).
+   */
+  public void debug(Element elt, String message, Object... args) {
+    if (verbose) {
+      messager.printMessage(Kind.WARNING, String.format(message, args), elt);
+    }
+  }
+
   public void executeJobs() {
     while (!jobs.isEmpty()) {
       Job job = jobs.remove(0);
-      if (verbose) {
-        messager.printMessage(Kind.NOTE, String.format("Scanning %s", elements
-            .getBinaryName(job.element)));
-      }
+      debug(job.element, "Scanning");
       try {
         job.scanner.scan(job.element, this);
       } catch (HaltException ignored) {
@@ -216,9 +236,7 @@
     }
     for (TypeElement proxyElement : proxiesRequiringMapping) {
       if (!getClientToDomainMap().containsKey(proxyElement)) {
-        poison(proxyElement, "A proxy must be annotated with %s, %s, or %s", ProxyFor.class
-            .getSimpleName(), ProxyForName.class.getSimpleName(), JsonRpcProxy.class
-            .getSimpleName());
+        poison(proxyElement, Messages.proxyMustBeAnnotated());
       }
     }
   }
@@ -287,28 +305,29 @@
    * eclosing type is annotated with {@link SkipInterfaceValidation} the message
    * will be dropped.
    */
-  public void poison(Element elt, String message, Object... args) {
+  public void poison(Element elt, String message) {
     if (suppressErrors) {
       return;
     }
 
-    String formatted = String.format(message, args);
-    if (squelchMessage(elt, formatted)) {
+    if (squelchMessage(elt, message)) {
       return;
     }
 
-    Element check = elt;
-    while (check != null) {
-      if (check.getAnnotation(SkipInterfaceValidation.class) != null) {
-        return;
+    if (respectAnnotations()) {
+      Element check = elt;
+      while (check != null) {
+        if (check.getAnnotation(SkipInterfaceValidation.class) != null) {
+          return;
+        }
+        check = check.getEnclosingElement();
       }
-      check = check.getEnclosingElement();
     }
 
     if (elt == null) {
-      messager.printMessage(Kind.ERROR, formatted);
+      messager.printMessage(Kind.ERROR, message);
     } else {
-      messager.printMessage(Kind.ERROR, formatted, elt);
+      messager.printMessage(Kind.ERROR, message, elt);
     }
   }
 
@@ -320,30 +339,41 @@
    * Emits a warning message, unless the element or an enclosing element are
    * annotated with a {@code @SuppressWarnings("requestfactory")}.
    */
-  public void warn(Element elt, String message, Object... args) {
+  public void warn(Element elt, String message) {
     if (suppressWarnings) {
       return;
     }
 
-    String formatted = String.format(message, args);
-    if (squelchMessage(elt, formatted)) {
+    if (squelchMessage(elt, message)) {
       return;
     }
 
-    SuppressWarnings suppress;
-    Element check = elt;
-    while (check != null) {
-      suppress = check.getAnnotation(SuppressWarnings.class);
-      if (suppress != null) {
-        if (Arrays.asList(suppress.value()).contains("requestfactory")) {
+    if (respectAnnotations()) {
+      SuppressWarnings suppress;
+      Element check = elt;
+      while (check != null) {
+        if (check.getAnnotation(SkipInterfaceValidation.class) != null) {
           return;
         }
+        suppress = check.getAnnotation(SuppressWarnings.class);
+        if (suppress != null) {
+          if (Arrays.asList(suppress.value()).contains("requestfactory")) {
+            return;
+          }
+        }
+        check = check.getEnclosingElement();
       }
-      check = check.getEnclosingElement();
     }
 
-    messager.printMessage(Kind.WARNING, formatted
-        + "\n\nAdd @SuppressWarnings(\"requestfactory\") to dismiss.", elt);
+    messager.printMessage(Kind.WARNING, message + Messages.warnSuffix(), elt);
+  }
+
+  /**
+   * This switch allows the RfValidatorTest code to be worked on in the IDE
+   * without causing compilation failures.
+   */
+  boolean respectAnnotations() {
+    return true;
   }
 
   private boolean fastFail(TypeElement element) {
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/TransportableTypeVisitor.java b/user/src/com/google/web/bindery/requestfactory/apt/TransportableTypeVisitor.java
index e6d106d..e9e5d08 100644
--- a/user/src/com/google/web/bindery/requestfactory/apt/TransportableTypeVisitor.java
+++ b/user/src/com/google/web/bindery/requestfactory/apt/TransportableTypeVisitor.java
@@ -15,8 +15,6 @@
  */
 package com.google.web.bindery.requestfactory.apt;
 
-import com.google.web.bindery.autobean.shared.ValueCodex;
-
 import java.util.Collection;
 import java.util.List;
 import java.util.Set;
@@ -28,12 +26,11 @@
 import javax.lang.model.type.TypeMirror;
 import javax.lang.model.type.TypeVariable;
 import javax.lang.model.type.WildcardType;
-import javax.lang.model.util.SimpleTypeVisitor6;
 
 /**
  * Scans a TypeMirror to determine if it can be transported by RequestFactory.
  */
-class TransportableTypeVisitor extends SimpleTypeVisitor6<Boolean, State> {
+class TransportableTypeVisitor extends TypeVisitorBase<Boolean> {
 
   /**
    * AutoBeans supports arbitrary parameterizations, but there's work that needs
@@ -62,12 +59,8 @@
       }
       return asId.getTypeArguments().get(0).accept(this, state);
     }
-    for (Class<?> clazz : ValueCodex.getAllValueTypes()) {
-      if (clazz.isPrimitive()) {
-        continue;
-      }
-
-      if (state.types.isAssignable(t, state.findType(clazz))) {
+    for (DeclaredType valueType : getValueTypes(state)) {
+      if (state.types.isAssignable(t, valueType)) {
         return true;
       }
     }
diff --git a/user/src/com/google/web/bindery/requestfactory/apt/TypeVisitorBase.java b/user/src/com/google/web/bindery/requestfactory/apt/TypeVisitorBase.java
new file mode 100644
index 0000000..5468981
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/apt/TypeVisitorBase.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2011 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.apt;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.PrimitiveType;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.util.SimpleTypeVisitor6;
+
+/**
+ * Provides utility functions for type visitors.
+ * 
+ * @param <T> the return type for the visitor
+ */
+class TypeVisitorBase<T> extends SimpleTypeVisitor6<T, State> {
+  /**
+   * This method should be kept in sync with
+   * {@code ValueCodex.getAllValueTypes()}. It doesn't use
+   * {@code getAllValueTypes()} because a dependency on {@code ValueCodex} would
+   * pull in a large number of dependencies into the minimal
+   * {@code requestfactory-apt.jar}.
+   */
+  protected List<DeclaredType> getValueTypes(State state) {
+    List<DeclaredType> types = new ArrayList<DeclaredType>();
+    for (TypeKind kind : TypeKind.values()) {
+      if (kind.isPrimitive()) {
+        PrimitiveType primitiveType = state.types.getPrimitiveType(kind);
+        TypeElement boxedClass = state.types.boxedClass(primitiveType);
+        types.add((DeclaredType) boxedClass.asType());
+      }
+    }
+    types.add(state.findType(BigDecimal.class));
+    types.add(state.findType(BigInteger.class));
+    types.add(state.findType(Date.class));
+    types.add(state.findType(String.class));
+    types.add(state.findType(Void.class));
+    // Avoids compile-dependency bloat
+    types.add(state.types.getDeclaredType(state.elements
+        .getTypeElement("com.google.web.bindery.autobean.shared.Splittable")));
+    return Collections.unmodifiableList(types);
+  }
+}
diff --git a/user/src/com/google/web/bindery/requestfactory/shared/SkipInterfaceValidation.java b/user/src/com/google/web/bindery/requestfactory/shared/SkipInterfaceValidation.java
index c2b69c1..3bc051d 100644
--- a/user/src/com/google/web/bindery/requestfactory/shared/SkipInterfaceValidation.java
+++ b/user/src/com/google/web/bindery/requestfactory/shared/SkipInterfaceValidation.java
@@ -30,6 +30,6 @@
  */
 @Documented
 @Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.METHOD, ElementType.TYPE})
+@Target({ElementType.METHOD, ElementType.PACKAGE, ElementType.TYPE})
 public @interface SkipInterfaceValidation {
 }
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/DiagnosticComparator.java b/user/test/com/google/web/bindery/requestfactory/apt/DiagnosticComparator.java
new file mode 100644
index 0000000..80b1938
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/DiagnosticComparator.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2011 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.apt;
+
+import java.util.Comparator;
+
+import javax.tools.Diagnostic;
+import javax.tools.JavaFileObject;
+
+/**
+ * Orders Diagnostic objects by filename, position, and message.
+ */
+class DiagnosticComparator implements Comparator<Diagnostic<? extends JavaFileObject>> {
+  @Override
+  public int compare(Diagnostic<? extends JavaFileObject> o1,
+      Diagnostic<? extends JavaFileObject> o2) {
+    int c;
+    if (o1.getSource() != null && o2.getSource() != null) {
+      c = o1.getSource().toUri().toString().compareTo(o2.getSource().toUri().toString());
+      if (c != 0) {
+        return c;
+      }
+    }
+    long p = o1.getPosition() - o2.getPosition();
+    if (p != 0) {
+      return Long.signum(p);
+    }
+    return o1.getMessage(null).compareTo(o2.getMessage(null));
+  }
+}
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/EntityProxyCheckDomainMapping.java b/user/test/com/google/web/bindery/requestfactory/apt/EntityProxyCheckDomainMapping.java
new file mode 100644
index 0000000..6a1ee98
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/EntityProxyCheckDomainMapping.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2011 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.apt;
+
+import com.google.web.bindery.requestfactory.shared.EntityProxy;
+import com.google.web.bindery.requestfactory.shared.ProxyFor;
+
+@Expected({
+    @Expect(method = "domainGetIdStatic"),
+    @Expect(method = "domainGetVersionStatic"),
+    @Expect(method = "domainFindNotStatic", args = "Domain"),
+    @Expect(method = "domainMethodWrongModifier", args = {"false", "getFoo"}),
+    @Expect(method = "domainNoDefaultConstructor", args = {
+        "Domain", "EntityProxyCheckDomainMapping", "RequestContext"}, warning = true)})
+@ProxyFor(EntityProxyCheckDomainMapping.Domain.class)
+@SuppressWarnings("requestfactory")
+interface EntityProxyCheckDomainMapping extends EntityProxy {
+  public static class Domain {
+    public static String getFoo() {
+      return null;
+    }
+
+    public static String getId() {
+      return null;
+    }
+
+    public static String getVersion() {
+      return null;
+    }
+
+    public Domain(@SuppressWarnings("unused") boolean ignored) {
+    }
+
+    public Domain findDomain(@SuppressWarnings("unused") String id) {
+      return null;
+    }
+  }
+
+  String getFoo();
+
+  @Expect(method = "domainMissingMethod", args = "java.lang.String getMissingProperty()")
+  String getMissingProperty();
+
+  @Expected({
+      @Expect(method = "methodNoDomainPeer", args = {"java.lang.Object", "false"}, warning = true),
+      @Expect(method = "untransportableType", args = "java.lang.Object")})
+  Object getUntransportable();
+
+  @Expect(method = "methodNoDomainPeer", args = {"java.lang.Object", "true"}, warning = true)
+  void setUntransportable(
+      @Expect(method = "untransportableType", args = "java.lang.Object") Object obj);
+
+  @Expected({
+      @Expect(method = "proxyOnlyGettersSetters"),
+      @Expect(method = "domainMissingMethod", args = "java.lang.String notAProperty()")})
+  String notAProperty();
+}
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/EntityProxyMismatchedFind.java b/user/test/com/google/web/bindery/requestfactory/apt/EntityProxyMismatchedFind.java
new file mode 100644
index 0000000..ad943b8
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/EntityProxyMismatchedFind.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2011 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.apt;
+
+import com.google.web.bindery.requestfactory.shared.EntityProxy;
+import com.google.web.bindery.requestfactory.shared.ProxyFor;
+
+@Expect(method = "domainMissingFind", args = {
+    "com.google.web.bindery.requestfactory.apt.EntityProxyMismatchedFind.Domain", "Domain",
+    "java.lang.String", "EntityProxyMismatchedFind"}, warning = true)
+@ProxyFor(EntityProxyMismatchedFind.Domain.class)
+@SuppressWarnings("requestfactory")
+interface EntityProxyMismatchedFind extends EntityProxy {
+  public static class Domain {
+    public static Domain findDomain(@SuppressWarnings("unused") Integer id) {
+      return null;
+    }
+
+    public String getId() {
+      return null;
+    }
+
+    public String getVersion() {
+      return null;
+    }
+  }
+}
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/EntityProxyMissingDomainLocatorMethods.java b/user/test/com/google/web/bindery/requestfactory/apt/EntityProxyMissingDomainLocatorMethods.java
new file mode 100644
index 0000000..a222e01
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/EntityProxyMissingDomainLocatorMethods.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2011 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.apt;
+
+import com.google.web.bindery.requestfactory.shared.EntityProxy;
+import com.google.web.bindery.requestfactory.shared.ProxyFor;
+
+@Expected({
+    @Expect(method = "domainNoGetId", args = "com.google.web.bindery.requestfactory.apt.EntityProxyMissingDomainLocatorMethods.Domain"),
+    @Expect(method = "domainNoGetVersion", args = "com.google.web.bindery.requestfactory.apt.EntityProxyMissingDomainLocatorMethods.Domain")})
+@ProxyFor(EntityProxyMissingDomainLocatorMethods.Domain.class)
+interface EntityProxyMissingDomainLocatorMethods extends EntityProxy {
+  public static class Domain {
+  }
+}
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/EntityProxyMissingDomainType.java b/user/test/com/google/web/bindery/requestfactory/apt/EntityProxyMissingDomainType.java
new file mode 100644
index 0000000..2282bc8
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/EntityProxyMissingDomainType.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2011 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.apt;
+
+import com.google.web.bindery.requestfactory.shared.EntityProxy;
+import com.google.web.bindery.requestfactory.shared.ProxyForName;
+
+@Expect(method = "proxyMissingDomainType", args = "does.not.exist", warning = true)
+@ProxyForName("does.not.exist")
+interface EntityProxyMissingDomainType extends EntityProxy {
+}
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/Expect.java b/user/test/com/google/web/bindery/requestfactory/apt/Expect.java
new file mode 100644
index 0000000..953b2b5
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/Expect.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2011 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.apt;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This annotation is applied to any element that is expected to be the target
+ * of an error or a warning from the validator.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@interface Expect {
+  /**
+   * The arguments to be passed to {@code method}.
+   */
+  String[] args() default {};
+
+  /**
+   * The name of a method defined in {@link Messages}.
+   */
+  String method();
+
+  /**
+   * Specifies whether the diagnostic will be a warning or an error.
+   */
+  boolean warning() default false;
+}
\ No newline at end of file
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/ExpectCollector.java b/user/test/com/google/web/bindery/requestfactory/apt/ExpectCollector.java
new file mode 100644
index 0000000..31f7cde
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/ExpectCollector.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2011 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.apt;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Set;
+
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.Messager;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.annotation.processing.SupportedSourceVersion;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.util.ElementScanner6;
+import javax.tools.Diagnostic.Kind;
+
+/**
+ * This is a trivial scanner that collects {@code @Expect} declarations on a
+ * type. It does not perform any type-chasing or supertype traversal. The named
+ * method on the {@link Messages} class is invoked with the provided arguments
+ * and the resulting message is emitted to the {@link Messager}.
+ */
+@SupportedAnnotationTypes({
+    "com.google.web.bindery.requestfactory.apt.Expect",
+    "com.google.web.bindery.requestfactory.apt.Expected"})
+@SupportedSourceVersion(SourceVersion.RELEASE_6)
+class ExpectCollector extends AbstractProcessor {
+  class Scanner extends ElementScanner6<Void, Void> {
+
+    private final Messager messager;
+
+    public Scanner(Messager messager) {
+      this.messager = messager;
+    }
+
+    @Override
+    public Void scan(Element e, Void p) {
+      Expect expect = e.getAnnotation(Expect.class);
+      if (expect != null) {
+        addExpect(expect, e);
+      }
+      Expected expected = e.getAnnotation(Expected.class);
+      if (expected != null) {
+        for (Expect v : expected.value()) {
+          addExpect(v, e);
+        }
+      }
+      return super.scan(e, p);
+    }
+
+    private void addExpect(Expect expect, Element accumulator) {
+      Method toInvoke = null;
+      for (Method m : Messages.class.getDeclaredMethods()) {
+        if (m.getName().equals(expect.method())) {
+          toInvoke = m;
+          break;
+        }
+      }
+      if (toInvoke == null) {
+        throw new RuntimeException("No method named " + expect.method());
+      }
+      String[] originalArgs = expect.args();
+      Object[] args = new Object[originalArgs.length];
+      for (int i = 0, j = args.length; i < j; i++) {
+        // Special case for domainMethodWrongModifier
+        if (boolean.class.equals(toInvoke.getParameterTypes()[i])) {
+          args[i] = Boolean.valueOf(originalArgs[i]);
+        } else {
+          args[i] = originalArgs[i];
+        }
+      }
+      String message;
+      Throwable ex;
+      try {
+        message = (String) toInvoke.invoke(null, args);
+        if (expect.warning()) {
+          message += Messages.warnSuffix();
+        }
+        messager.printMessage(expect.warning() ? Kind.WARNING : Kind.ERROR, message, accumulator);
+        return;
+      } catch (IllegalArgumentException e) {
+        ex = e;
+      } catch (IllegalAccessException e) {
+        ex = e;
+      } catch (InvocationTargetException e) {
+        ex = e.getCause();
+      }
+      throw new RuntimeException("Could not get test message", ex);
+    }
+  }
+
+  @Override
+  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
+    Scanner scanner = new Scanner(processingEnv.getMessager());
+    scanner.scan(roundEnv.getElementsAnnotatedWith(Expect.class), null);
+    scanner.scan(roundEnv.getElementsAnnotatedWith(Expected.class), null);
+    return false;
+  }
+}
\ No newline at end of file
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/Expected.java b/user/test/com/google/web/bindery/requestfactory/apt/Expected.java
new file mode 100644
index 0000000..5e73483
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/Expected.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2011 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.apt;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Allows several {@link Expect} annotations to be applied to an element.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@interface Expected {
+  Expect[] value();
+}
\ No newline at end of file
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/MyRequestContext.java b/user/test/com/google/web/bindery/requestfactory/apt/MyRequestContext.java
new file mode 100644
index 0000000..9e118f9
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/MyRequestContext.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2011 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.apt;
+
+import com.google.web.bindery.requestfactory.shared.EntityProxy;
+import com.google.web.bindery.requestfactory.shared.InstanceRequest;
+import com.google.web.bindery.requestfactory.shared.JsonRpcProxy;
+import com.google.web.bindery.requestfactory.shared.ProxyFor;
+import com.google.web.bindery.requestfactory.shared.ProxyForName;
+import com.google.web.bindery.requestfactory.shared.Request;
+import com.google.web.bindery.requestfactory.shared.RequestContext;
+
+@Expect(method = "contextMustBeAnnotated", args = "RequestContext")
+interface MyRequestContext extends RequestContext {
+  @Expect(method = "proxyMustBeAnnotated")
+  interface ProxyMissingAnnotation extends EntityProxy {
+  }
+
+  @Expected({
+      @Expect(method = "proxyMissingDomainType", args = "bad", warning = true),
+      @Expect(method = "redundantAnnotation", args = "ProxyForName"),
+      @Expect(method = "redundantAnnotation", args = "JsonRpcProxy")})
+  @ProxyFor(ProxyWithRedundantAnnotations.Domain.class)
+  @ProxyForName("bad")
+  @JsonRpcProxy
+  interface ProxyWithRedundantAnnotations extends EntityProxy {
+    static class Domain {
+    }
+  }
+
+  /**
+   * Because this is not referenced from the context, it shouldn't generate an
+   * error.
+   */
+  interface UnusedProxyBase extends EntityProxy {
+  }
+
+  @Expect(method = "untransportableType", args = "java.lang.Object")
+  InstanceRequest<ProxyWithRedundantAnnotations, Object> badInstanceReturn();
+
+  @Expect(method = "contextRequiredReturnTypes", args = {"Request", "InstanceRequest"})
+  String badMethod();
+
+  Request<Void> badParam(@Expect(method = "untransportableType", args = "java.lang.Object") Object o);
+
+  @Expect(method = "untransportableType", args = "java.lang.Object")
+  Request<Object> badReturn();
+
+  Request<ProxyMissingAnnotation> forceAnnotation();
+
+  @Expect(method = "rawType")
+  @SuppressWarnings("rawtypes")
+  InstanceRequest rawInstance();
+
+  @Expect(method = "rawType")
+  @SuppressWarnings("rawtypes")
+  Request rawRequest();
+}
\ No newline at end of file
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/MyRequestFactory.java b/user/test/com/google/web/bindery/requestfactory/apt/MyRequestFactory.java
new file mode 100644
index 0000000..2abe076
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/MyRequestFactory.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2011 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.apt;
+
+import com.google.web.bindery.requestfactory.shared.RequestFactory;
+import com.google.web.bindery.requestfactory.shared.impl.AbstractRequestContext;
+
+interface MyRequestFactory extends RequestFactory {
+  @Expect(method = "factoryMustReturnInterface", args = "AbstractRequestContext")
+  AbstractRequestContext concreteContext();
+
+  @Expected({
+      @Expect(method = "factoryNoMethodParameters"),
+      @Expect(method = "factoryMustBeAssignable", args = "RequestContext")})
+  String stringMethod(String bad);
+}
\ No newline at end of file
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/RequestContextMissingDomainType.java b/user/test/com/google/web/bindery/requestfactory/apt/RequestContextMissingDomainType.java
new file mode 100644
index 0000000..57de96a
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/RequestContextMissingDomainType.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2011 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.apt;
+
+import com.google.web.bindery.requestfactory.shared.RequestContext;
+import com.google.web.bindery.requestfactory.shared.ServiceName;
+
+@Expect(method = "contextMissingDomainType", args = "does.not.exist", warning = true)
+@ServiceName("does.not.exist")
+interface RequestContextMissingDomainType extends RequestContext {
+}
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/RequestContextUsingUnmappedProxy.java b/user/test/com/google/web/bindery/requestfactory/apt/RequestContextUsingUnmappedProxy.java
new file mode 100644
index 0000000..ddb0efd
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/RequestContextUsingUnmappedProxy.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2011 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.apt;
+
+import com.google.web.bindery.requestfactory.shared.Request;
+import com.google.web.bindery.requestfactory.shared.RequestContext;
+import com.google.web.bindery.requestfactory.shared.Service;
+
+@Service(RequestContextUsingUnmappedProxy.Domain.class)
+interface RequestContextUsingUnmappedProxy extends RequestContext {
+  public static class Domain {
+  }
+
+  @Expected({
+      @Expect(method = "methodNoDomainPeer", args = {
+          "com.google.web.bindery.requestfactory.apt.EntityProxyMissingDomainType", "false"}, warning = true),
+      @Expect(method = "methodNoDomainPeer", args = {
+          "com.google.web.bindery.requestfactory.apt.EntityProxyMissingDomainType", "true"}, warning = true)})
+  Request<EntityProxyMissingDomainType> cannotVerify(EntityProxyMissingDomainType proxy);
+}
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/RequestContextWithMismatchedBoxes.java b/user/test/com/google/web/bindery/requestfactory/apt/RequestContextWithMismatchedBoxes.java
new file mode 100644
index 0000000..ee854aa
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/RequestContextWithMismatchedBoxes.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2011 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.apt;
+
+import com.google.web.bindery.requestfactory.shared.ProxyFor;
+import com.google.web.bindery.requestfactory.shared.Request;
+import com.google.web.bindery.requestfactory.shared.RequestContext;
+import com.google.web.bindery.requestfactory.shared.Service;
+import com.google.web.bindery.requestfactory.shared.ValueProxy;
+
+/**
+ * Checks that proxy property accessors match exactly and that context method
+ * arguments must match exactly.
+ */
+@Service(RequestContextWithMismatchedBoxes.Domain.class)
+interface RequestContextWithMismatchedBoxes extends RequestContext {
+  @Expect(method = "domainMissingMethod", args = "java.lang.Void checkBoxed(int)")
+  Request<Void> checkBoxed(int value);
+
+  @Expect(method = "domainMissingMethod", args = "java.lang.Void checkPrimitive(java.lang.Integer)")
+  Request<Void> checkPrimitive(Integer value);
+
+  @ProxyFor(Domain.class)
+  interface ProxyMismatchedGetterA extends ValueProxy {
+    @Expect(method = "domainMissingMethod", args = "int getBoxed()")
+    int getBoxed();
+
+    @Expect(method = "domainMissingMethod", args = "java.lang.Integer getPrimitive()")
+    Integer getPrimitive();
+  }
+
+  static class Domain {
+    static void checkBoxed(@SuppressWarnings("unused") Integer value) {
+    }
+
+    static void checkPrimitive(@SuppressWarnings("unused") int value) {
+    }
+
+    Integer getBoxed() {
+      return null;
+    }
+
+    int getPrimitive() {
+      return 0;
+    }
+  }
+}
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/RequestContextWithMismatchedInstance.java b/user/test/com/google/web/bindery/requestfactory/apt/RequestContextWithMismatchedInstance.java
new file mode 100644
index 0000000..26c428d
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/RequestContextWithMismatchedInstance.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2011 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.apt;
+
+import com.google.web.bindery.requestfactory.shared.EntityProxy;
+import com.google.web.bindery.requestfactory.shared.InstanceRequest;
+import com.google.web.bindery.requestfactory.shared.ProxyFor;
+import com.google.web.bindery.requestfactory.shared.Request;
+import com.google.web.bindery.requestfactory.shared.RequestContext;
+import com.google.web.bindery.requestfactory.shared.Service;
+
+/**
+ * Tests Request and InstanceRequest methods bound to methods with the wrong
+ * static modifier.
+ */
+@Expected({
+    @Expect(method = "domainMethodWrongModifier", args = {"true", "instanceMethod"}),
+    @Expect(method = "domainMethodWrongModifier", args = {"false", "staticMethod"})})
+@Service(RequestContextWithMismatchedInstance.Domain.class)
+interface RequestContextWithMismatchedInstance extends RequestContext {
+  static class Domain {
+    public static Domain findDomain(@SuppressWarnings("unused") String id) {
+      return null;
+    }
+
+    public static void staticMethod() {
+    }
+
+    public String getId() {
+      return null;
+    }
+
+    public String getVersion() {
+      return null;
+    }
+
+    public void instanceMethod() {
+    }
+  }
+
+  @ProxyFor(Domain.class)
+  interface Proxy extends EntityProxy {
+  }
+
+  Request<Void> instanceMethod();
+
+  InstanceRequest<Proxy, Void> staticMethod();
+}
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/RfValidatorTest.java b/user/test/com/google/web/bindery/requestfactory/apt/RfValidatorTest.java
new file mode 100644
index 0000000..c57ae33
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/RfValidatorTest.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2011 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.apt;
+
+import com.google.gwt.dev.util.Util;
+import com.google.web.bindery.requestfactory.shared.LoggingRequest;
+import com.google.web.bindery.requestfactory.shared.SimpleRequestFactory;
+import com.google.web.bindery.requestfactory.shared.TestRequestFactory;
+import com.google.web.bindery.requestfactory.shared.impl.FindRequest;
+
+import junit.framework.TestCase;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.TreeSet;
+
+import javax.tools.Diagnostic;
+import javax.tools.DiagnosticCollector;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaCompiler.CompilationTask;
+import javax.tools.JavaFileObject;
+import javax.tools.SimpleJavaFileObject;
+import javax.tools.ToolProvider;
+
+/**
+ * Integration test of {@link RfValidator} using the Java6 tools API to invoke
+ * the Java compiler. This test requires that the gwt-user source is available
+ * on the classpath.
+ */
+public class RfValidatorTest extends TestCase {
+  /**
+   * Provides access to a source file from the classpath.
+   */
+  private static class UriJavaFileObject extends SimpleJavaFileObject {
+    public static UriJavaFileObject create(Class<?> toLoad) {
+      try {
+        String path = toLoad.getName().replace('.', '/') + ".java";
+        /*
+         * SimpleJavaFileObject does not like the URI's created from jar URLs
+         * since their path does not start with a leading forward-slash. The
+         * choice of "classpath" as the scheme is arbitrary and meaningless.
+         */
+        URI fakeLocation = new URI("classpath:/" + path);
+        return new UriJavaFileObject(fakeLocation, Thread.currentThread().getContextClassLoader()
+            .getResource(path));
+      } catch (URISyntaxException e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    private final URL contents;
+
+    public UriJavaFileObject(URI fakeLocation, URL contents) throws URISyntaxException {
+      super(fakeLocation, JavaFileObject.Kind.SOURCE);
+      this.contents = contents;
+    }
+
+    @Override
+    public CharSequence getCharContent(boolean ignored) throws IOException {
+      return Util.readStreamAsString(contents.openStream());
+    }
+  }
+
+  /**
+   * Smoke test to ensure that appropriate errors and warnings are emitted.
+   */
+  public void testErrorsAndWarnings() {
+    testGeneratedMessages(EntityProxyCheckDomainMapping.class);
+    testGeneratedMessages(EntityProxyMismatchedFind.class);
+    testGeneratedMessages(EntityProxyMissingDomainLocatorMethods.class);
+    testGeneratedMessages(EntityProxyMissingDomainType.class);
+    testGeneratedMessages(MyRequestContext.class);
+    testGeneratedMessages(MyRequestFactory.class);
+    testGeneratedMessages(RequestContextMissingDomainType.class);
+    testGeneratedMessages(RequestContextUsingUnmappedProxy.class,
+        EntityProxyMissingDomainType.class);
+    testGeneratedMessages(RequestContextWithMismatchedBoxes.class);
+    testGeneratedMessages(RequestContextWithMismatchedInstance.class);
+  }
+
+  /**
+   * The target classes for this method don't contain any {@code @Expect}
+   * annotations, so this test will verify that they are error- and
+   * warning-free.
+   */
+  public void testTestClasses() {
+    testGeneratedMessages(FindRequest.class);
+    testGeneratedMessages(LoggingRequest.class);
+    testGeneratedMessages(SimpleRequestFactory.class);
+    testGeneratedMessages(TestRequestFactory.class);
+  }
+
+  private void assertEquals(TreeSet<Diagnostic<? extends JavaFileObject>> expected,
+      TreeSet<Diagnostic<? extends JavaFileObject>> actual) {
+    List<Diagnostic<?>> unmatched = new ArrayList<Diagnostic<?>>();
+
+    // Remove actual elements from expect, saving any unexpected elements
+    for (Diagnostic<? extends JavaFileObject> d : actual) {
+      if (!expected.remove(d)) {
+        unmatched.add(d);
+      }
+    }
+
+    assertTrue("Did not see expected errors: " + expected + "\n\nLeftovers :" + unmatched, expected
+        .isEmpty());
+    assertTrue("Unexpected errors: " + unmatched, unmatched.isEmpty());
+  }
+
+  /**
+   * Run the annotation processor over one or more classes and verify that the
+   * appropriate messages are generated.
+   */
+  private void testGeneratedMessages(Class<?>... classes) {
+    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+
+    List<JavaFileObject> files = new ArrayList<JavaFileObject>(classes.length);
+    for (Class<?> clazz : classes) {
+      JavaFileObject obj = UriJavaFileObject.create(clazz);
+      files.add(obj);
+    }
+    StringWriter errorWriter = new StringWriter();
+    RfValidator rfValidator = new RfValidator();
+    rfValidator.setForceErrors(true);
+
+    DiagnosticCollector<JavaFileObject> expectedCollector =
+        new DiagnosticCollector<JavaFileObject>();
+    CompilationTask expectedTask =
+        compiler.getTask(errorWriter, null, expectedCollector, Arrays.asList("-proc:only"), null,
+            files);
+    expectedTask.setProcessors(Arrays.asList(new ExpectCollector()));
+    expectedTask.call();
+
+    DiagnosticCollector<JavaFileObject> actualCollector = new DiagnosticCollector<JavaFileObject>();
+    CompilationTask actualTask =
+        compiler.getTask(errorWriter, null, actualCollector, Arrays.asList("-proc:only"), null,
+            files);
+    actualTask.setProcessors(Arrays.asList(rfValidator));
+    actualTask.call();
+
+    TreeSet<Diagnostic<? extends JavaFileObject>> expected =
+        new TreeSet<Diagnostic<? extends JavaFileObject>>(new DiagnosticComparator());
+    expected.addAll(expectedCollector.getDiagnostics());
+    TreeSet<Diagnostic<? extends JavaFileObject>> actual =
+        new TreeSet<Diagnostic<? extends JavaFileObject>>(new DiagnosticComparator());
+    actual.addAll(actualCollector.getDiagnostics());
+    assertEquals(expected, actual);
+  }
+}
diff --git a/user/test/com/google/web/bindery/requestfactory/apt/package-info.java b/user/test/com/google/web/bindery/requestfactory/apt/package-info.java
new file mode 100644
index 0000000..d2eab38
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/apt/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2011 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.
+ */
+
+/**
+ * By disabling the following annotation, a manual check can be made against the
+ * various {@link Expect} annotations and the IDE's behavior.
+ */
+@SkipInterfaceValidation
+package com.google.web.bindery.requestfactory.apt;
+
+import com.google.web.bindery.requestfactory.shared.SkipInterfaceValidation;
+
diff --git a/user/test/com/google/web/bindery/requestfactory/gwt/RequestFactoryGwtJreSuite.java b/user/test/com/google/web/bindery/requestfactory/gwt/RequestFactoryGwtJreSuite.java
index 5a2f008..2505961 100644
--- a/user/test/com/google/web/bindery/requestfactory/gwt/RequestFactoryGwtJreSuite.java
+++ b/user/test/com/google/web/bindery/requestfactory/gwt/RequestFactoryGwtJreSuite.java
@@ -15,6 +15,7 @@
  */
 package com.google.web.bindery.requestfactory.gwt;
 
+import com.google.web.bindery.requestfactory.apt.RfValidatorTest;
 import com.google.web.bindery.requestfactory.gwt.rebind.model.RequestFactoryModelTest;
 
 import junit.framework.Test;
@@ -33,6 +34,7 @@
     TestSuite suite = new TestSuite(
         "requestfactory package tests that require the JRE and gwt-user");
     suite.addTestSuite(RequestFactoryModelTest.class);
+    suite.addTestSuite(RfValidatorTest.class);
 
     return suite;
   }
diff --git a/user/test/com/google/web/bindery/requestfactory/server/BoxesAndPrimitivesJreTest.java b/user/test/com/google/web/bindery/requestfactory/server/BoxesAndPrimitivesJreTest.java
index c38d6c5..bddcdd0 100644
--- a/user/test/com/google/web/bindery/requestfactory/server/BoxesAndPrimitivesJreTest.java
+++ b/user/test/com/google/web/bindery/requestfactory/server/BoxesAndPrimitivesJreTest.java
@@ -24,6 +24,7 @@
 import com.google.web.bindery.requestfactory.shared.Request;
 import com.google.web.bindery.requestfactory.shared.RequestContext;
 import com.google.web.bindery.requestfactory.shared.Service;
+import com.google.web.bindery.requestfactory.shared.SkipInterfaceValidation;
 
 import java.util.Arrays;
 import java.util.logging.Logger;
@@ -32,7 +33,13 @@
  * A JRE version of {@link BoxesAndPrimitivesTest} with additional validation
  * tests.
  */
+@SkipInterfaceValidation
 public class BoxesAndPrimitivesJreTest extends BoxesAndPrimitivesTest {
+  /*
+   * The SkipInterfaceValidation annotation is to prevent RfValidator from
+   * blowing up. This annotation, the inner interfaces, and the tests here can
+   * be removed when RequestFactoryInterfaceValidator is retired.
+   */
 
   @Service(ServiceImpl.class)
   interface ContextMismatchedParameterA extends RequestContext {
diff --git a/user/test/com/google/web/bindery/requestfactory/shared/TestFooPolymorphicRequest.java b/user/test/com/google/web/bindery/requestfactory/shared/TestFooPolymorphicRequest.java
index 821f20a..350b8c9 100644
--- a/user/test/com/google/web/bindery/requestfactory/shared/TestFooPolymorphicRequest.java
+++ b/user/test/com/google/web/bindery/requestfactory/shared/TestFooPolymorphicRequest.java
@@ -22,5 +22,5 @@
  */
 @Service(com.google.web.bindery.requestfactory.server.SimpleFoo.class)
 public interface TestFooPolymorphicRequest extends RequestContext {
-  <P extends SimpleFooProxy> Request<P> echo(SimpleFooProxy proxy);
+  <P extends SimpleFooProxy> Request<P> echo(P proxy);
 }