update ThrowingProvider to support an @ThrowingProvides annotation, much like @Provides.  update the internals of the class to give more explicit error messaging & use bind.addError instead of throwing exceptions.  added lots & lots of tests.

git-svn-id: https://google-guice.googlecode.com/svn/trunk@1312 d779f126-a31b-0410-b53b-1d3aecad763e
diff --git a/extensions/throwingproviders/src/com/google/inject/throwingproviders/ThrowingProviderBinder.java b/extensions/throwingproviders/src/com/google/inject/throwingproviders/ThrowingProviderBinder.java
index 90255b9..e171db2 100644
--- a/extensions/throwingproviders/src/com/google/inject/throwingproviders/ThrowingProviderBinder.java
+++ b/extensions/throwingproviders/src/com/google/inject/throwingproviders/ThrowingProviderBinder.java
@@ -18,6 +18,7 @@
 
 import com.google.inject.Binder;
 import com.google.inject.Key;
+import com.google.inject.Module;
 import com.google.inject.Provider;
 import com.google.inject.TypeLiteral;
 import com.google.inject.binder.ScopedBindingBuilder;
@@ -34,15 +35,31 @@
 import java.lang.reflect.Proxy;
 import java.lang.reflect.Type;
 import java.lang.reflect.TypeVariable;
+import java.util.Arrays;
 import java.util.Set;
 
 /**
- * <p>Builds a binding for a {@link ThrowingProvider} using a fluent API:
+ * <p>Builds a binding for a {@link ThrowingProvider}.
+ * 
+ * <p>You can use a fluent API and custom providers:
  * <pre><code>ThrowingProviderBinder.create(binder())
  *    .bind(RemoteProvider.class, Customer.class)
  *    .to(RemoteCustomerProvider.class)
  *    .in(RequestScope.class);
  * </code></pre>
+ * or, you can use throwing provider methods:
+ * <pre><code>class MyModule extends AbstractModule {
+ *   configure() {
+ *     ThrowingProviderBinder.install(this, binder());
+ *   }
+ *   
+ *   {@literal @}ThrowingProvides(RemoteProvider.class)
+ *   {@literal @}RequestScope
+ *   Customer provideCustomer(FlakyCustomerCreator creator) throws RemoteException {
+ *     return creator.getCustomerOrThrow();
+ *   }
+ * }
+ * </code></pre>
  * 
  * @author jmourits@google.com (Jerome Mourits)
  * @author jessewilson@google.com (Jesse Wilson)
@@ -55,8 +72,14 @@
     this.binder = binder;
   }
 
-  public static ThrowingProviderBinder create(Binder binder) { 
-    return new ThrowingProviderBinder(binder);
+  public static ThrowingProviderBinder create(Binder binder) {
+    return new ThrowingProviderBinder(binder.skipSources(
+        ThrowingProviderBinder.class,
+        ThrowingProviderBinder.SecondaryBinder.class));
+  }
+  
+  public static void install(Module module, Binder binder) {
+    binder.install(ThrowingProviderMethodsModule.forModule(module));
   }
 
   public <P extends ThrowingProvider> SecondaryBinder<P> 
@@ -69,13 +92,23 @@
     private final Type valueType;
     private Class<? extends Annotation> annotationType;
     private Annotation annotation;
-    private final Class<?> exceptionType;
+    private final Class<? extends Exception> exceptionType;
+    private final boolean valid;
 
     public SecondaryBinder(Class<P> interfaceType, Type valueType) {
       this.interfaceType = checkNotNull(interfaceType, "interfaceType");
       this.valueType = checkNotNull(valueType, "valueType");
-      checkInterface();
-      this.exceptionType = getExceptionType(interfaceType);
+      if(checkInterface()) {
+        this.exceptionType = getExceptionType(interfaceType);
+        valid = true;
+      } else {
+        valid = false;
+        this.exceptionType = null;
+      }
+    }
+    
+    Class<? extends Exception> getExceptionType() {
+      return exceptionType;
     }
 
     public SecondaryBinder<P> annotatedWith(Class<? extends Annotation> annotationType) {
@@ -103,32 +136,47 @@
     public ScopedBindingBuilder to(Class<? extends P> targetType) {
       return to(Key.get(targetType));
     }
+    
+    ScopedBindingBuilder toProviderMethod(ThrowingProviderMethod<?> target) {
+      Key<ThrowingProviderMethod> targetKey =
+        Key.get(ThrowingProviderMethod.class, UniqueAnnotations.create());
+      binder.bind(targetKey).toInstance(target);
+      
+      return toInternal(targetKey);
+    }
 
-    public ScopedBindingBuilder to(final Key<? extends P> targetKey) {
+    public ScopedBindingBuilder to(Key<? extends P> targetKey) {
       checkNotNull(targetKey, "targetKey");
+      return toInternal(targetKey);
+    }
+    
+    private ScopedBindingBuilder toInternal(final Key<? extends ThrowingProvider> targetKey) {
       final Key<Result> resultKey = Key.get(Result.class, UniqueAnnotations.create());
-      final Key<P> key = createKey();
+      final Key<P> key = createKey();      
       final Provider<Result> resultProvider = binder.getProvider(resultKey);
-      final Provider<? extends P> targetProvider = binder.getProvider(targetKey);
+      final Provider<? extends ThrowingProvider> targetProvider = binder.getProvider(targetKey);
 
-      binder.bind(key).toProvider(new ProviderWithDependencies<P>() {
-        private final P instance = interfaceType.cast(Proxy.newProxyInstance(
-            interfaceType.getClassLoader(), new Class<?>[] { interfaceType },
-            new InvocationHandler() {
-              public Object invoke(Object proxy, Method method, Object[] args)
-                  throws Throwable {
-                return resultProvider.get().getOrThrow();
-              }
-            }));
-          
-          public P get() {
-            return instance;
-          }
-          
-          public Set<Dependency<?>> getDependencies() {
-            return ImmutableSet.<Dependency<?>>of(Dependency.get(resultKey));
-          }
-        });
+      // don't bother binding the proxy type if this is in an invalid state.
+      if(valid) {
+        binder.bind(key).toProvider(new ProviderWithDependencies<P>() {
+          private final P instance = interfaceType.cast(Proxy.newProxyInstance(
+              interfaceType.getClassLoader(), new Class<?>[] { interfaceType },
+              new InvocationHandler() {
+                public Object invoke(Object proxy, Method method, Object[] args)
+                    throws Throwable {
+                  return resultProvider.get().getOrThrow();
+                }
+              }));
+            
+            public P get() {
+              return instance;
+            }
+            
+            public Set<Dependency<?>> getDependencies() {
+              return ImmutableSet.<Dependency<?>>of(Dependency.get(resultKey));
+            }
+          });
+      }
 
       return binder.bind(resultKey).toProvider(new ProviderWithDependencies<Result>() {
         public Result get() {
@@ -157,58 +205,94 @@
      * {@code interfaceType}.
      */
     @SuppressWarnings({"unchecked"})
-    private <P extends ThrowingProvider> Class<?> getExceptionType(Class<P> interfaceType) {
+    private Class<? extends Exception> getExceptionType(Class<P> interfaceType) {
       ParameterizedType genericUnreliableProvider
           = (ParameterizedType) interfaceType.getGenericInterfaces()[0];
       return (Class<? extends Exception>) genericUnreliableProvider.getActualTypeArguments()[1];
     }
 
-    private void checkInterface() {
-      String errorMessage = "%s is not a compliant interface "
-          + "- see the Javadoc for ThrowingProvider";
-
-      checkArgument(interfaceType.isInterface(), errorMessage, interfaceType.getName());
-      checkArgument(interfaceType.getGenericInterfaces().length == 1, errorMessage,
-          interfaceType.getName());
-      checkArgument(interfaceType.getInterfaces()[0] == ThrowingProvider.class,
-          errorMessage, interfaceType.getName());
+    private boolean checkInterface() {
+      if(!checkArgument(interfaceType.isInterface(),
+         "%s must be an interface", interfaceType.getName())) {
+        return false;
+      }
+      if(!checkArgument(interfaceType.getGenericInterfaces().length == 1,
+          "%s must extend ThrowingProvider (and only ThrowingProvider)",
+          interfaceType)) {
+        return false;
+      }
+      if(!checkArgument(interfaceType.getInterfaces()[0] == ThrowingProvider.class,
+          "%s must extend ThrowingProvider (and only ThrowingProvider)",
+          interfaceType)) {
+        return false;
+      }
 
       // Ensure that T is parameterized and unconstrained.
       ParameterizedType genericThrowingProvider
           = (ParameterizedType) interfaceType.getGenericInterfaces()[0];
       if (interfaceType.getTypeParameters().length == 1) {
-        checkArgument(interfaceType.getTypeParameters().length == 1, errorMessage,
-            interfaceType.getName());
         String returnTypeName = interfaceType.getTypeParameters()[0].getName();
         Type returnType = genericThrowingProvider.getActualTypeArguments()[0];
-        checkArgument(returnType instanceof TypeVariable, errorMessage, interfaceType.getName());
-        checkArgument(returnTypeName.equals(((TypeVariable) returnType).getName()),
-            errorMessage, interfaceType.getName());
+        if(!checkArgument(returnType instanceof TypeVariable,
+            "%s does not properly extend ThrowingProvider, the first type parameter of ThrowingProvider (%s) is not a generic type",
+            interfaceType, returnType)) {
+          return false;
+        }
+        if(!checkArgument(returnTypeName.equals(((TypeVariable) returnType).getName()),
+            "The generic type (%s) of %s does not match the generic type of ThrowingProvider (%s)",
+            returnTypeName, interfaceType, ((TypeVariable)returnType).getName())) {
+          return false;
+        }
       } else {
-        checkArgument(interfaceType.getTypeParameters().length == 0,
-            errorMessage, interfaceType.getName());
-        checkArgument(genericThrowingProvider.getActualTypeArguments()[0].equals(valueType),
-            errorMessage, interfaceType.getName());
+        if(!checkArgument(interfaceType.getTypeParameters().length == 0,
+            "%s has more than one generic type parameter: %s",
+            interfaceType, Arrays.asList(interfaceType.getTypeParameters()))) {
+          return false;
+        }
+        if(!checkArgument(genericThrowingProvider.getActualTypeArguments()[0].equals(valueType),
+            "%s expects the value type to be %s, but it was %s",
+            interfaceType, genericThrowingProvider.getActualTypeArguments()[0], valueType)) {
+          return false;
+        }
       }
 
       Type exceptionType = genericThrowingProvider.getActualTypeArguments()[1];
-      checkArgument(exceptionType instanceof Class, errorMessage, interfaceType.getName());
+      if(!checkArgument(exceptionType instanceof Class,
+          "%s has the wrong Exception generic type (%s) when extending ThrowingProvider",
+          interfaceType, exceptionType)) {
+        return false;
+      }
       
       if (interfaceType.getDeclaredMethods().length == 1) {
         Method method = interfaceType.getDeclaredMethods()[0];
-        checkArgument(method.getName().equals("get"), errorMessage, interfaceType.getName());
-        checkArgument(method.getParameterTypes().length == 0,
-            errorMessage, interfaceType.getName());
+        if(!checkArgument(method.getName().equals("get"),
+            "%s may not declare any new methods, but declared %s",
+            interfaceType, method)) {
+          return false;
+        }
+        if(!checkArgument(method.getParameterTypes().length == 0,
+            "%s may not declare any new methods, but declared %s",
+            interfaceType, method.toGenericString())) {
+          return false;
+        }
       } else {
-        checkArgument(interfaceType.getDeclaredMethods().length == 0,
-            errorMessage, interfaceType.getName());
+        if(!checkArgument(interfaceType.getDeclaredMethods().length == 0,
+            "%s may not declare any new methods, but declared %s",
+            interfaceType, Arrays.asList(interfaceType.getDeclaredMethods()))) {
+          return false;
+        }
       }
+      
+      return true;
     }
 
-    private void checkArgument(boolean condition,
+    private boolean checkArgument(boolean condition,
         String messageFormat, Object... args) {
       if (!condition) {
-        throw new IllegalArgumentException(String.format(messageFormat, args));
+        binder.addError(messageFormat, args);
+        return false;
+      } else {
+        return true;
       }
     }
 
diff --git a/extensions/throwingproviders/src/com/google/inject/throwingproviders/ThrowingProviderMethod.java b/extensions/throwingproviders/src/com/google/inject/throwingproviders/ThrowingProviderMethod.java
new file mode 100644
index 0000000..0f20949
--- /dev/null
+++ b/extensions/throwingproviders/src/com/google/inject/throwingproviders/ThrowingProviderMethod.java
@@ -0,0 +1,156 @@
+/**
+ * Copyright (C) 2008 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.inject.throwingproviders;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Set;
+
+import com.google.inject.Binder;
+import com.google.inject.Exposed;
+import com.google.inject.Key;
+import com.google.inject.PrivateBinder;
+import com.google.inject.Provider;
+import com.google.inject.TypeLiteral;
+import com.google.inject.binder.ScopedBindingBuilder;
+import com.google.inject.internal.util.ImmutableSet;
+import com.google.inject.internal.util.StackTraceElements;
+import com.google.inject.spi.Dependency;
+import com.google.inject.spi.HasDependencies;
+import com.google.inject.throwingproviders.ThrowingProviderBinder.SecondaryBinder;
+
+/**
+ * A provider that invokes a method and returns its result.
+ *
+ * @author sameb@google.com (Sam Berlin)
+ */
+class ThrowingProviderMethod<T> implements ThrowingProvider<T, Exception>, HasDependencies {
+  private final Key<T> key;
+  private final Class<? extends Annotation> scopeAnnotation;
+  private final Object instance;
+  private final Method method;
+  private final ImmutableSet<Dependency<?>> dependencies;
+  private final List<Provider<?>> parameterProviders;
+  private final boolean exposed;
+  private final Class<? extends ThrowingProvider> throwingProvider;
+  private final List<TypeLiteral<?>> exceptionTypes;
+
+  ThrowingProviderMethod(
+      Key<T> key,
+      Method method,
+      Object instance,
+      ImmutableSet<Dependency<?>> dependencies,
+      List<Provider<?>> parameterProviders,
+      Class<? extends Annotation> scopeAnnotation,
+      Class<? extends ThrowingProvider> throwingProvider,
+      List<TypeLiteral<?>> exceptionTypes) {
+    this.key = key;
+    this.scopeAnnotation = scopeAnnotation;
+    this.instance = instance;
+    this.dependencies = dependencies;
+    this.method = method;
+    this.parameterProviders = parameterProviders;
+    this.exposed = method.isAnnotationPresent(Exposed.class);
+    this.throwingProvider = throwingProvider;
+    this.exceptionTypes = exceptionTypes;
+
+    method.setAccessible(true);
+  }
+
+  public Key<T> getKey() {
+    return key;
+  }
+
+  public Method getMethod() {
+    return method;
+  }
+
+  public void configure(Binder binder) {
+    binder = binder.withSource(method);
+
+    SecondaryBinder<?> sbinder = 
+      ThrowingProviderBinder.create(binder)
+        .bind(throwingProvider, key.getTypeLiteral().getType());
+    if(key.getAnnotation() != null) {
+      sbinder = sbinder.annotatedWith(key.getAnnotation());
+    } else if(key.getAnnotationType() != null) {
+      sbinder = sbinder.annotatedWith(key.getAnnotationType());
+    } 
+    ScopedBindingBuilder sbbuilder = sbinder.toProviderMethod(this);
+    if(scopeAnnotation != null) {
+      sbbuilder.in(scopeAnnotation);
+    }
+
+    if (exposed) {
+      // the cast is safe 'cause the only binder we have implements PrivateBinder. If there's a
+      // misplaced @Exposed, calling this will add an error to the binder's error queue
+      ((PrivateBinder) binder).expose(key);
+    }
+
+    // Validate the exceptions in the method match the exceptions
+    // in the ThrowingProvider.
+    for(TypeLiteral<?> exType : exceptionTypes) {
+      // Ignore runtime exceptions.
+      if(RuntimeException.class.isAssignableFrom(exType.getRawType())) {
+        continue;
+      }
+      
+      if(sbinder.getExceptionType() != null) {
+        if (!sbinder.getExceptionType().isAssignableFrom(exType.getRawType())) {
+          binder.addError(
+              "%s is not compatible with the exception (%s) declared in the ThrowingProvider interface (%s)",
+              exType.getRawType(), sbinder.getExceptionType(), throwingProvider);
+        }
+      }
+    }
+  }
+
+  public T get() throws Exception {
+    Object[] parameters = new Object[parameterProviders.size()];
+    for (int i = 0; i < parameters.length; i++) {
+      parameters[i] = parameterProviders.get(i).get();
+    }
+
+    try {
+      // We know this cast is safe becase T is the method's return type.
+      @SuppressWarnings({ "unchecked", "UnnecessaryLocalVariable" })
+      T result = (T) method.invoke(instance, parameters);
+      return result;
+    } catch (IllegalAccessException e) {
+      throw new AssertionError(e);
+    } catch (InvocationTargetException e) {
+      Throwable t = e.getCause();
+      if(t instanceof Exception) {
+        throw (Exception)t;
+      } else if(t instanceof Error) {
+        throw (Error)t;
+      } else {
+        throw new IllegalStateException(t);
+      }
+    }
+  }
+
+  public Set<Dependency<?>> getDependencies() {
+    return dependencies;
+  }
+
+  @Override public String toString() {
+    return "@ThrowingProvides " + StackTraceElements.forMember(method).toString();
+  }
+}
diff --git a/extensions/throwingproviders/src/com/google/inject/throwingproviders/ThrowingProviderMethodsModule.java b/extensions/throwingproviders/src/com/google/inject/throwingproviders/ThrowingProviderMethodsModule.java
new file mode 100644
index 0000000..528782a
--- /dev/null
+++ b/extensions/throwingproviders/src/com/google/inject/throwingproviders/ThrowingProviderMethodsModule.java
@@ -0,0 +1,152 @@
+/**
+ * Copyright (C) 2010 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.inject.throwingproviders;
+
+import com.google.inject.Binder;
+import com.google.inject.Key;
+import com.google.inject.Module;
+import com.google.inject.Provider;
+import com.google.inject.TypeLiteral;
+import com.google.inject.internal.Annotations;
+import com.google.inject.internal.Errors;
+import com.google.inject.internal.UniqueAnnotations;
+import com.google.inject.internal.util.ImmutableSet;
+import com.google.inject.internal.util.Lists;
+import static com.google.inject.internal.util.Preconditions.checkNotNull;
+import com.google.inject.spi.Dependency;
+import com.google.inject.spi.Message;
+import com.google.inject.util.Modules;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Member;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.logging.Logger;
+
+/**
+ * Creates bindings to methods annotated with {@literal @}{@link ThrowingProvides}. Use the scope
+ * and binding annotations on the provider method to configure the binding.
+ * 
+ * @author sameb@google.com (Sam Berlin)
+ */
+final class ThrowingProviderMethodsModule implements Module {
+  private final Object delegate;
+  private final TypeLiteral<?> typeLiteral;
+
+  private ThrowingProviderMethodsModule(Object delegate) {
+    this.delegate = checkNotNull(delegate, "delegate");
+    this.typeLiteral = TypeLiteral.get(this.delegate.getClass());
+  }
+
+  /**
+   * Returns a module which creates bindings for provider methods from the given module.
+   */
+  static Module forModule(Module module) {
+    // avoid infinite recursion, since installing a module always installs itself
+    if (module instanceof ThrowingProviderMethodsModule) {
+      return Modules.EMPTY_MODULE;
+    }
+
+    return new ThrowingProviderMethodsModule(module);
+  }
+  
+  public synchronized void configure(Binder binder) {
+    for (ThrowingProviderMethod<?> throwingProviderMethod : getProviderMethods(binder)) {
+      throwingProviderMethod.configure(binder);
+    }
+  }
+
+  List<ThrowingProviderMethod<?>> getProviderMethods(Binder binder) {
+    List<ThrowingProviderMethod<?>> result = Lists.newArrayList();
+    for (Class<?> c = delegate.getClass(); c != Object.class; c = c.getSuperclass()) {
+      for (Method method : c.getDeclaredMethods()) {
+        ThrowingProvides throwingProvides =
+          (ThrowingProvides)method.getAnnotation(ThrowingProvides.class);
+        if(throwingProvides != null) {
+          result.add(createProviderMethod(binder, method, throwingProvides.value()));
+        }
+      }
+    }
+    return result;
+  }
+
+  <T> ThrowingProviderMethod<T> createProviderMethod(Binder binder, final Method method,
+      Class<? extends ThrowingProvider> throwingProvider) {
+    binder = binder.withSource(method);
+    Errors errors = new Errors(method);
+
+    // prepare the parameter providers
+    List<Dependency<?>> dependencies = Lists.newArrayList();
+    List<Provider<?>> parameterProviders = Lists.newArrayList();
+    List<TypeLiteral<?>> parameterTypes = typeLiteral.getParameterTypes(method);
+    Annotation[][] parameterAnnotations = method.getParameterAnnotations();
+    for (int i = 0; i < parameterTypes.size(); i++) {
+      Key<?> key = getKey(errors, parameterTypes.get(i), method, parameterAnnotations[i]);
+      if(key.equals(Key.get(Logger.class))) {
+        // If it was a Logger, change the key to be unique & bind it to a
+        // provider that provides a logger with a proper name.
+        // This solves issue 482 (returning a new anonymous logger on every call exhausts memory)
+        Key<Logger> loggerKey = Key.get(Logger.class, UniqueAnnotations.create());
+        binder.bind(loggerKey).toProvider(new LogProvider(method));
+        key = loggerKey;
+      }
+      dependencies.add(Dependency.get(key));
+      parameterProviders.add(binder.getProvider(key));        
+    }
+
+    @SuppressWarnings("unchecked") // Define T as the method's return type.
+    TypeLiteral<T> returnType = (TypeLiteral<T>) typeLiteral.getReturnType(method);
+    List<TypeLiteral<?>> exceptionTypes = typeLiteral.getExceptionTypes(method);
+
+    Key<T> key = getKey(errors, returnType, method, method.getAnnotations());
+    Class<? extends Annotation> scopeAnnotation
+        = Annotations.findScopeAnnotation(errors, method.getAnnotations());
+
+    for (Message message : errors.getMessages()) {
+      binder.addError(message);
+    }
+
+    return new ThrowingProviderMethod<T>(key, method, delegate, ImmutableSet.copyOf(dependencies),
+        parameterProviders, scopeAnnotation, throwingProvider, exceptionTypes);
+  }
+
+  <T> Key<T> getKey(Errors errors, TypeLiteral<T> type, Member member, Annotation[] annotations) {
+    Annotation bindingAnnotation = Annotations.findBindingAnnotation(errors, member, annotations);
+    return bindingAnnotation == null ? Key.get(type) : Key.get(type, bindingAnnotation);
+  }
+
+  @Override public boolean equals(Object o) {
+    return o instanceof ThrowingProviderMethodsModule
+        && ((ThrowingProviderMethodsModule) o).delegate == delegate;
+  }
+
+  @Override public int hashCode() {
+    return delegate.hashCode();
+  }
+  
+  /** A provider that returns a logger based on the method name. */
+  private static final class LogProvider implements Provider<Logger> {
+    private final String name;
+    
+    public LogProvider(Method method) {
+      this.name = method.getDeclaringClass().getName() + "." + method.getName();
+    }
+    
+    public Logger get() {
+      return Logger.getLogger(name);
+    }
+  }
+}
diff --git a/extensions/throwingproviders/src/com/google/inject/throwingproviders/ThrowingProvides.java b/extensions/throwingproviders/src/com/google/inject/throwingproviders/ThrowingProvides.java
new file mode 100644
index 0000000..d680202
--- /dev/null
+++ b/extensions/throwingproviders/src/com/google/inject/throwingproviders/ThrowingProvides.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (C) 2010 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.inject.throwingproviders;
+
+import java.lang.annotation.Documented;
+import static java.lang.annotation.ElementType.METHOD;
+import java.lang.annotation.Retention;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import java.lang.annotation.Target;
+
+/**
+ * Annotates methods of a {@link Module} to create a provider method binding that can throw
+ * exceptions. The method's return type is bound to it's returned value. Guice will pass
+ * dependencies to the method as parameters.
+ * 
+ * @author sameb@google.com (Sam Berlin)
+ * @since 3.0
+ */
+@Documented @Target(METHOD) @Retention(RUNTIME)
+public @interface ThrowingProvides {
+  
+  /**
+   * The interface that provides this value, a subinterface of ThrowingProvider.
+   */
+  Class<? extends ThrowingProvider> value();
+  
+}
diff --git a/extensions/throwingproviders/test/com/google/inject/throwingproviders/ThrowingProviderBinderTest.java b/extensions/throwingproviders/test/com/google/inject/throwingproviders/ThrowingProviderBinderTest.java
index a882ab0..6bb1f88 100644
--- a/extensions/throwingproviders/test/com/google/inject/throwingproviders/ThrowingProviderBinderTest.java
+++ b/extensions/throwingproviders/test/com/google/inject/throwingproviders/ThrowingProviderBinderTest.java
@@ -17,7 +17,6 @@
 package com.google.inject.throwingproviders;
 
 import com.google.inject.AbstractModule;
-import static com.google.inject.Asserts.assertContains;
 
 import com.google.inject.CreationException;
 import com.google.inject.Guice;
@@ -25,17 +24,24 @@
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import com.google.inject.TypeLiteral;
+import com.google.inject.internal.MoreTypes;
 import com.google.inject.internal.util.Function;
+import com.google.inject.internal.util.ImmutableList;
 import com.google.inject.internal.util.ImmutableSet;
 import com.google.inject.internal.util.Iterables;
+import com.google.inject.name.Named;
 import com.google.inject.name.Names;
 import com.google.inject.spi.Dependency;
 import com.google.inject.spi.HasDependencies;
+import com.google.inject.spi.Message;
 
+import java.io.IOException;
+import java.rmi.AccessException;
 import java.rmi.RemoteException;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Set;
+import java.util.TooManyListenersException;
 
 import junit.framework.TestCase;
 
@@ -49,7 +55,7 @@
       = new TypeLiteral<RemoteProvider<String>>() { };
   private final MockRemoteProvider<String> mockRemoteProvider = new MockRemoteProvider<String>();
   private final TestScope testScope = new TestScope();
-  private Injector injector = Guice.createInjector(new AbstractModule() {
+  private Injector bindInjector = Guice.createInjector(new AbstractModule() {
     protected void configure() {
       ThrowingProviderBinder.create(binder())
           .bind(RemoteProvider.class, String.class)
@@ -57,10 +63,31 @@
           .in(testScope);
     }
   });
+  private Injector providesInjector = Guice.createInjector(new AbstractModule() {
+    protected void configure() {
+     ThrowingProviderBinder.install(this, binder());
+     bindScope(TestScope.Scoped.class, testScope);
+    }
+    
+    @SuppressWarnings("unused")
+    @ThrowingProvides(RemoteProvider.class)
+    @TestScope.Scoped
+    String throwOrGet() throws RemoteException {
+      return mockRemoteProvider.get();
+    }
+  });
 
-  public void testExceptionsThrown() {
+  public void testExceptionsThrown_Bind() {
+    tExceptionsThrown(bindInjector);
+  }
+  
+  public void testExceptionsThrown_Provides() {
+    tExceptionsThrown(providesInjector);
+  }
+  
+  private void tExceptionsThrown(Injector injector) {
     RemoteProvider<String> remoteProvider = 
-        injector.getInstance(Key.get(remoteProviderOfString));
+      injector.getInstance(Key.get(remoteProviderOfString));
 
     mockRemoteProvider.throwOnNextGet("kaboom!");
     try {
@@ -71,9 +98,17 @@
     }
   }
 
-  public void testValuesScoped() throws RemoteException {
+  public void testValuesScoped_Bind() throws RemoteException {
+    tValuesScoped(bindInjector);
+  }
+  
+  public void testValuesScoped_Provides() throws RemoteException {
+    tValuesScoped(providesInjector);
+  }
+  
+  private void tValuesScoped(Injector injector) throws RemoteException {
     RemoteProvider<String> remoteProvider = 
-        injector.getInstance(Key.get(remoteProviderOfString));
+      injector.getInstance(Key.get(remoteProviderOfString));
 
     mockRemoteProvider.setNextToReturn("A");
     assertEquals("A", remoteProvider.get());
@@ -85,7 +120,15 @@
     assertEquals("B", remoteProvider.get());
   }
 
-  public void testExceptionsScoped() {
+  public void testExceptionsScoped_Bind() {
+    tExceptionsScoped(bindInjector);
+  }
+  
+  public void testExceptionsScoped_Provides() {
+    tExceptionsScoped(providesInjector);
+  }
+  
+  private void tExceptionsScoped(Injector injector) {
     RemoteProvider<String> remoteProvider = 
         injector.getInstance(Key.get(remoteProviderOfString));
 
@@ -106,13 +149,10 @@
     }
   }
   
-  public void testAnnotations() throws RemoteException {
+  public void testAnnotations_Bind() throws RemoteException {
     final MockRemoteProvider<String> mockRemoteProviderA = new MockRemoteProvider<String>();
-    mockRemoteProviderA.setNextToReturn("A");
     final MockRemoteProvider<String> mockRemoteProviderB = new MockRemoteProvider<String>();
-    mockRemoteProviderB.setNextToReturn("B");
-
-    injector = Guice.createInjector(new AbstractModule() {
+    bindInjector = Guice.createInjector(new AbstractModule() {
       protected void configure() {
         ThrowingProviderBinder.create(binder())
             .bind(RemoteProvider.class, String.class)
@@ -124,19 +164,55 @@
             .to(mockRemoteProviderB);
       }
     });
-
+    tAnnotations(bindInjector, mockRemoteProviderA, mockRemoteProviderB);
+  }
+  
+  public void testAnnotations_Provides() throws RemoteException {
+    final MockRemoteProvider<String> mockRemoteProviderA = new MockRemoteProvider<String>();
+    final MockRemoteProvider<String> mockRemoteProviderB = new MockRemoteProvider<String>();
+    providesInjector = Guice.createInjector(new AbstractModule() {
+      protected void configure() {
+        ThrowingProviderBinder.install(this, binder());
+       }
+       
+       @SuppressWarnings("unused")
+       @ThrowingProvides(RemoteProvider.class)
+       @Named("a")
+       String throwOrGet() throws RemoteException {
+         return mockRemoteProviderA.get();
+       }
+       
+       @SuppressWarnings("unused")
+       @ThrowingProvides(RemoteProvider.class)
+       String throwOrGet2() throws RemoteException {
+         return mockRemoteProviderB.get();
+       }
+    });
+    tAnnotations(providesInjector, mockRemoteProviderA, mockRemoteProviderB);
+  }
+  
+  private void tAnnotations(Injector injector, MockRemoteProvider<String> mockA,
+      MockRemoteProvider<String> mockB) throws RemoteException {
+    mockA.setNextToReturn("A");
+    mockB.setNextToReturn("B");
     assertEquals("A", 
         injector.getInstance(Key.get(remoteProviderOfString, Names.named("a"))).get());
 
     assertEquals("B", 
         injector.getInstance(Key.get(remoteProviderOfString)).get());
-
   }
   
-  public void testUndeclaredExceptions() throws RemoteException {
+  public void testUndeclaredExceptions_Bind() throws RemoteException {
+    tUndeclaredExceptions(bindInjector);
+  }
+  
+  public void testUndeclaredExceptions_Provides() throws RemoteException {
+    tUndeclaredExceptions(providesInjector);
+  }
+
+  private void tUndeclaredExceptions(Injector injector) throws RemoteException { 
     RemoteProvider<String> remoteProvider = 
         injector.getInstance(Key.get(remoteProviderOfString));
-
     mockRemoteProvider.throwOnNextGet(new IndexOutOfBoundsException("A"));
     try {
       remoteProvider.get();
@@ -159,7 +235,7 @@
     final SubMockRemoteProvider aProvider = new SubMockRemoteProvider();
     aProvider.setNextToReturn("A");
 
-    injector = Guice.createInjector(new AbstractModule() {
+    bindInjector = Guice.createInjector(new AbstractModule() {
       protected void configure() {
         ThrowingProviderBinder.create(binder())
             .bind(RemoteProvider.class, String.class)
@@ -168,14 +244,14 @@
     });
 
     assertEquals("A",
-        injector.getInstance(Key.get(remoteProviderOfString)).get());
+        bindInjector.getInstance(Key.get(remoteProviderOfString)).get());
   }
 
   static class SubMockRemoteProvider extends MockRemoteProvider<String> { }
 
-  public void testBindingToNonInterfaceType() throws RemoteException {
+  public void testBindingToNonInterfaceType_Bind() throws RemoteException {
     try {
-      injector = Guice.createInjector(new AbstractModule() {
+      Guice.createInjector(new AbstractModule() {
         protected void configure() {
           ThrowingProviderBinder.create(binder())
               .bind(MockRemoteProvider.class, String.class)
@@ -184,13 +260,34 @@
       });
       fail();
     } catch (CreationException expected) {
-      assertContains(expected.getMessage(), "is not a compliant interface");
+      assertEquals(MockRemoteProvider.class.getName() + " must be an interface",
+          Iterables.getOnlyElement(expected.getErrorMessages()).getMessage());
     }
   }
   
-  public void testBindingToSubSubInterface() throws RemoteException {
+  public void testBindingToNonInterfaceType_Provides() throws RemoteException {
     try {
-      injector = Guice.createInjector(new AbstractModule() {
+      Guice.createInjector(new AbstractModule() {
+        protected void configure() {
+          ThrowingProviderBinder.install(this, binder());
+        }
+          
+        @SuppressWarnings("unused")
+        @ThrowingProvides(MockRemoteProvider.class)
+        String foo() {
+          return null;
+        }
+      });
+      fail();
+    } catch (CreationException expected) {
+      assertEquals(MockRemoteProvider.class.getName() + " must be an interface",
+          Iterables.getOnlyElement(expected.getErrorMessages()).getMessage());
+    }
+  }  
+  
+  public void testBindingToSubSubInterface_Bind() throws RemoteException {
+    try {
+      bindInjector = Guice.createInjector(new AbstractModule() {
         protected void configure() {
           ThrowingProviderBinder.create(binder())
               .bind(SubRemoteProvider.class, String.class);
@@ -198,15 +295,36 @@
       });
       fail();
     } catch (CreationException expected) {
-      assertContains(expected.getMessage(), "is not a compliant interface");
+      assertEquals(SubRemoteProvider.class.getName() + " must extend ThrowingProvider (and only ThrowingProvider)",
+          Iterables.getOnlyElement(expected.getErrorMessages()).getMessage());
     }
   }
+  
+  public void testBindingToSubSubInterface_Provides() throws RemoteException {
+    try {
+      Guice.createInjector(new AbstractModule() {
+        protected void configure() {
+          ThrowingProviderBinder.install(this, binder());
+        }
+          
+        @SuppressWarnings("unused")
+        @ThrowingProvides(SubRemoteProvider.class)
+        String foo() {
+          return null;
+        }
+      });
+      fail();
+    } catch (CreationException expected) {
+      assertEquals(SubRemoteProvider.class.getName() + " must extend ThrowingProvider (and only ThrowingProvider)",
+          Iterables.getOnlyElement(expected.getErrorMessages()).getMessage());
+    }
+  }    
 
   interface SubRemoteProvider extends RemoteProvider<String> { }
 
-  public void testBindingToInterfaceWithExtraMethod() throws RemoteException {
+  public void testBindingToInterfaceWithExtraMethod_Bind() throws RemoteException {
     try {
-      injector = Guice.createInjector(new AbstractModule() {
+      bindInjector = Guice.createInjector(new AbstractModule() {
         protected void configure() {
           ThrowingProviderBinder.create(binder())
               .bind(RemoteProviderWithExtraMethod.class, String.class);
@@ -214,12 +332,35 @@
       });
       fail();
     } catch (CreationException expected) {
-      assertContains(expected.getMessage(), "is not a compliant interface");
+      assertEquals(RemoteProviderWithExtraMethod.class.getName() + " may not declare any new methods, but declared " 
+          + RemoteProviderWithExtraMethod.class.getDeclaredMethods()[0].toGenericString(),
+          Iterables.getOnlyElement(expected.getErrorMessages()).getMessage());
     }
   }
   
-  public void testDependencies() {
-    injector = Guice.createInjector(new AbstractModule() {
+  public void testBindingToInterfaceWithExtraMethod_Provides() throws RemoteException {
+    try {
+      Guice.createInjector(new AbstractModule() {
+        protected void configure() {
+          ThrowingProviderBinder.install(this, binder());
+        }
+          
+        @SuppressWarnings("unused")
+        @ThrowingProvides(RemoteProviderWithExtraMethod.class)
+        String foo() {
+          return null;
+        }
+      });
+      fail();
+    } catch (CreationException expected) {
+      assertEquals(RemoteProviderWithExtraMethod.class.getName() + " may not declare any new methods, but declared " 
+          + RemoteProviderWithExtraMethod.class.getDeclaredMethods()[0].toGenericString(),
+          Iterables.getOnlyElement(expected.getErrorMessages()).getMessage());
+    }
+  }
+  
+  public void testDependencies_Bind() {
+    bindInjector = Guice.createInjector(new AbstractModule() {
       protected void configure() {
         bind(String.class).toInstance("Foo");
         bind(Integer.class).toInstance(5);
@@ -232,15 +373,15 @@
     });
     
     HasDependencies hasDependencies =
-        (HasDependencies)injector.getBinding(Key.get(remoteProviderOfString));
+        (HasDependencies)bindInjector.getBinding(Key.get(remoteProviderOfString));
     hasDependencies = 
-        (HasDependencies)injector.getBinding(
+        (HasDependencies)bindInjector.getBinding(
             Iterables.getOnlyElement(hasDependencies.getDependencies()).getKey());
     // Make sure that that is dependent on DependentRemoteProvider.
     assertEquals(Dependency.get(Key.get(DependentRemoteProvider.class)), 
         Iterables.getOnlyElement(hasDependencies.getDependencies()));
     // And make sure DependentRemoteProvider has the proper dependencies.
-    hasDependencies = (HasDependencies)injector.getBinding(DependentRemoteProvider.class);
+    hasDependencies = (HasDependencies)bindInjector.getBinding(DependentRemoteProvider.class);
     Set<Key<?>> dependencyKeys = ImmutableSet.copyOf(
         Iterables.transform(hasDependencies.getDependencies(),
           new Function<Dependency<?>, Key<?>>() {
@@ -251,6 +392,43 @@
     assertEquals(ImmutableSet.<Key<?>>of(Key.get(String.class), Key.get(Integer.class),
         Key.get(Long.class), Key.get(Double.class)), dependencyKeys);
   }
+  
+  public void testDependencies_Provides() {
+    providesInjector = Guice.createInjector(new AbstractModule() {
+      protected void configure() {
+        bind(String.class).toInstance("Foo");
+        bind(Integer.class).toInstance(5);
+        bind(Double.class).toInstance(5d);
+        bind(Long.class).toInstance(5L);
+        ThrowingProviderBinder.install(this, binder());
+      }
+      
+      @SuppressWarnings("unused")
+      @ThrowingProvides(RemoteProvider.class)
+      String foo(String s, Integer i, Double d, Long l) {
+        return null;
+      }
+    });
+    
+    HasDependencies hasDependencies =
+        (HasDependencies)providesInjector.getBinding(Key.get(remoteProviderOfString));
+    // RemoteProvider<String> is dependent on the provider method..
+    hasDependencies = 
+        (HasDependencies)providesInjector.getBinding(
+            Iterables.getOnlyElement(hasDependencies.getDependencies()).getKey());
+    // And the provider method has our real dependencies..
+    hasDependencies = (HasDependencies)providesInjector.getBinding(
+        Iterables.getOnlyElement(hasDependencies.getDependencies()).getKey());
+    Set<Key<?>> dependencyKeys = ImmutableSet.copyOf(
+        Iterables.transform(hasDependencies.getDependencies(),
+          new Function<Dependency<?>, Key<?>>() {
+            public Key<?> apply(Dependency<?> from) {
+              return from.getKey();
+            }
+          }));
+    assertEquals(ImmutableSet.<Key<?>>of(Key.get(String.class), Key.get(Integer.class),
+        Key.get(Long.class), Key.get(Double.class)), dependencyKeys);
+  }  
 
   interface RemoteProviderWithExtraMethod<T> extends ThrowingProvider<T, RemoteException> {
     T get(T defaultValue) throws RemoteException;
@@ -300,8 +478,8 @@
     }
   }
 
-  public void testBindingToInterfaceWithBoundValueType() throws RemoteException {
-    injector = Guice.createInjector(new AbstractModule() {
+  public void testBindingToInterfaceWithBoundValueType_Bind() throws RemoteException {
+    bindInjector = Guice.createInjector(new AbstractModule() {
       protected void configure() {
         ThrowingProviderBinder.create(binder())
             .bind(StringRemoteProvider.class, String.class)
@@ -313,13 +491,29 @@
       }
     });
     
-    assertEquals("A", injector.getInstance(StringRemoteProvider.class).get());
+    assertEquals("A", bindInjector.getInstance(StringRemoteProvider.class).get());
+  }
+  
+  public void testBindingToInterfaceWithBoundValueType_Provides() throws RemoteException {
+    providesInjector = Guice.createInjector(new AbstractModule() {
+      protected void configure() {
+        ThrowingProviderBinder.install(this, binder());
+      }
+      
+      @SuppressWarnings("unused")
+      @ThrowingProvides(StringRemoteProvider.class)
+      String foo() throws RemoteException {
+          return "A";
+      }
+    });
+    
+    assertEquals("A", providesInjector.getInstance(StringRemoteProvider.class).get());
   }
 
   interface StringRemoteProvider extends ThrowingProvider<String, RemoteException> { }
 
-  public void testBindingToInterfaceWithGeneric() throws RemoteException {
-    injector = Guice.createInjector(new AbstractModule() {
+  public void testBindingToInterfaceWithGeneric_Bind() throws RemoteException {
+    bindInjector = Guice.createInjector(new AbstractModule() {
       protected void configure() {
         ThrowingProviderBinder.create(binder())
             .bind(RemoteProvider.class, new TypeLiteral<List<String>>() { }.getType())
@@ -333,6 +527,288 @@
 
     Key<RemoteProvider<List<String>>> key
         = Key.get(new TypeLiteral<RemoteProvider<List<String>>>() { });
-    assertEquals(Arrays.asList("A", "B"), injector.getInstance(key).get());
+    assertEquals(Arrays.asList("A", "B"), bindInjector.getInstance(key).get());
+  }
+  
+  public void testBindingToInterfaceWithGeneric_Provides() throws RemoteException {
+    providesInjector = Guice.createInjector(new AbstractModule() {
+      protected void configure() {
+        ThrowingProviderBinder.install(this, binder());
+      }
+      
+      @SuppressWarnings("unused")
+      @ThrowingProvides(RemoteProvider.class)
+      List<String> foo() throws RemoteException {
+          return Arrays.asList("A", "B");
+      }
+    });
+
+    Key<RemoteProvider<List<String>>> key
+        = Key.get(new TypeLiteral<RemoteProvider<List<String>>>() { });
+    assertEquals(Arrays.asList("A", "B"), providesInjector.getInstance(key).get());
+  }
+  
+  public void testProviderMethodWithWrongException() {
+    try {
+      Guice.createInjector(new AbstractModule() {
+        protected void configure() {
+          ThrowingProviderBinder.install(this, binder());
+        }
+        
+        @SuppressWarnings("unused")
+        @ThrowingProvides(RemoteProvider.class)
+        String foo() throws InterruptedException {
+            return null;
+        }
+      });
+      fail();
+    } catch(CreationException ce) {
+      assertEquals(InterruptedException.class.getName() + " is not compatible with the exception ("
+          + RemoteException.class.getName() + ") declared in the ThrowingProvider interface ("
+          + RemoteProvider.class.getName() + ")",
+          Iterables.getOnlyElement(ce.getErrorMessages()).getMessage());
+    }
+  }
+  
+  public void testProviderMethodWithSubclassOfExceptionIsOk() {
+    providesInjector = Guice.createInjector(new AbstractModule() {
+      protected void configure() {
+        ThrowingProviderBinder.install(this, binder());
+      }
+      
+      @SuppressWarnings("unused")
+      @ThrowingProvides(RemoteProvider.class)
+      String foo() throws AccessException {
+        throw new AccessException("boo!");
+      }
+    });
+    
+    RemoteProvider<String> remoteProvider = 
+      providesInjector.getInstance(Key.get(remoteProviderOfString));
+
+    try {
+      remoteProvider.get();
+      fail();
+    } catch (RemoteException expected) {
+      assertTrue(expected instanceof AccessException);
+      assertEquals("boo!", expected.getMessage());
+    }
+  }
+  
+  public void testProviderMethodWithSuperclassFails() {
+    try {
+      Guice.createInjector(new AbstractModule() {
+        protected void configure() {
+          ThrowingProviderBinder.install(this, binder());
+        }
+        
+        @SuppressWarnings("unused")
+        @ThrowingProvides(RemoteProvider.class)
+        String foo() throws IOException {
+            return null;
+        }
+      });
+      fail();
+    } catch(CreationException ce) {
+      assertEquals(IOException.class.getName() + " is not compatible with the exception ("
+          + RemoteException.class.getName() + ") declared in the ThrowingProvider interface ("
+          + RemoteProvider.class.getName() + ")",
+          Iterables.getOnlyElement(ce.getErrorMessages()).getMessage());
+    }
+  }
+  
+  public void testProviderMethodWithRuntimeExceptionsIsOk() throws RemoteException {
+    providesInjector = Guice.createInjector(new AbstractModule() {
+      protected void configure() {
+        ThrowingProviderBinder.install(this, binder());
+      }
+      
+      @SuppressWarnings("unused")
+      @ThrowingProvides(RemoteProvider.class)
+      String foo() throws RuntimeException {
+        throw new RuntimeException("boo!");
+      }
+    });
+    
+    RemoteProvider<String> remoteProvider = 
+      providesInjector.getInstance(Key.get(remoteProviderOfString));
+
+    try {
+      remoteProvider.get();
+      fail();
+    } catch (RuntimeException expected) {
+      assertEquals("boo!", expected.getCause().getMessage());
+    }
+  }
+  
+  public void testProviderMethodWithManyExceptions() {
+    try {
+      Guice.createInjector(new AbstractModule() {
+        protected void configure() {
+          ThrowingProviderBinder.install(this, binder());
+        }
+        
+        @SuppressWarnings("unused")
+        @ThrowingProvides(RemoteProvider.class)
+        String foo() throws InterruptedException, RuntimeException, RemoteException, 
+                            AccessException, TooManyListenersException {
+            return null;
+        }
+      });
+      fail();
+    } catch(CreationException ce) {
+      // The only two that should fail are Interrupted & TooManyListeners.. the rest are OK.
+      List<Message> errors = ImmutableList.copyOf(ce.getErrorMessages());
+      assertEquals(InterruptedException.class.getName() + " is not compatible with the exception ("
+          + RemoteException.class.getName() + ") declared in the ThrowingProvider interface ("
+          + RemoteProvider.class.getName() + ")",
+          errors.get(0).getMessage());
+      assertEquals(TooManyListenersException.class.getName() + " is not compatible with the exception ("
+          + RemoteException.class.getName() + ") declared in the ThrowingProvider interface ("
+          + RemoteProvider.class.getName() + ")",
+          errors.get(1).getMessage());
+      assertEquals(2, errors.size());
+    }
+  }
+  
+  public void testMoreTypeParameters() {
+    try {
+      Guice.createInjector(new AbstractModule() {
+        protected void configure() {
+          ThrowingProviderBinder.install(this, binder());
+        }
+        
+        @SuppressWarnings("unused")
+        @ThrowingProvides(TooManyTypeParameters.class)
+        String foo() {
+            return null;
+        }
+      });
+      fail();
+    } catch(CreationException ce) {
+      assertEquals(TooManyTypeParameters.class.getName() + " has more than one generic type parameter: [T, P]",
+          Iterables.getOnlyElement(ce.getErrorMessages()).getMessage());
+    }    
+  }
+  
+  public void testWrongThrowingProviderType() {
+    try {
+      Guice.createInjector(new AbstractModule() {
+        protected void configure() {
+          ThrowingProviderBinder.install(this, binder());
+        }
+        
+        @SuppressWarnings("unused")
+        @ThrowingProvides(WrongThrowingProviderType.class)
+        String foo() {
+            return null;
+        }
+      });
+      fail();
+    } catch(CreationException ce) {
+      assertEquals(WrongThrowingProviderType.class.getName() 
+          + " does not properly extend ThrowingProvider, the first type parameter of ThrowingProvider "
+          + "(java.lang.String) is not a generic type",
+          Iterables.getOnlyElement(ce.getErrorMessages()).getMessage());
+    }    
+  }
+  
+  public void testOneMethodThatIsntGet() {
+    try {
+      Guice.createInjector(new AbstractModule() {
+        protected void configure() {
+          ThrowingProviderBinder.install(this, binder());
+        }
+        
+        @SuppressWarnings("unused")
+        @ThrowingProvides(OneNoneGetMethod.class)
+        String foo() {
+            return null;
+        }
+      });
+      fail();
+    } catch(CreationException ce) {
+      assertEquals(OneNoneGetMethod.class.getName() 
+          + " may not declare any new methods, but declared " + MoreTypes.toString(OneNoneGetMethod.class.getDeclaredMethods()[0]),
+          Iterables.getOnlyElement(ce.getErrorMessages()).getMessage());
+    }
+  }
+  
+  public void testManyMethods() {
+    try {
+      Guice.createInjector(new AbstractModule() {
+        protected void configure() {
+          ThrowingProviderBinder.install(this, binder());
+        }
+        
+        @SuppressWarnings("unused")
+        @ThrowingProvides(ManyMethods.class)
+        String foo() {
+            return null;
+        }
+      });
+      fail();
+    } catch(CreationException ce) {
+      assertEquals(ManyMethods.class.getName() 
+          + " may not declare any new methods, but declared " + Arrays.asList(ManyMethods.class.getDeclaredMethods()),
+          Iterables.getOnlyElement(ce.getErrorMessages()).getMessage());
+    }
+  }
+  
+  public void testIncorrectPredefinedType_Bind() {
+    try {
+      Guice.createInjector(new AbstractModule() {
+        protected void configure() {
+          ThrowingProviderBinder.create(binder())
+              .bind(StringRemoteProvider.class, Integer.class)
+              .to(new StringRemoteProvider() {
+                public String get() throws RemoteException {
+                  return "A";
+                }
+              });
+        }
+      });
+      fail();
+    } catch(CreationException ce) {
+      assertEquals(StringRemoteProvider.class.getName() 
+          + " expects the value type to be java.lang.String, but it was java.lang.Integer",
+          Iterables.getOnlyElement(ce.getErrorMessages()).getMessage());
+    }
+  }
+  
+  public void testIncorrectPredefinedType_Provides() {
+    try {
+      Guice.createInjector(new AbstractModule() {
+        protected void configure() {
+          ThrowingProviderBinder.install(this, binder());
+        }
+        
+        @SuppressWarnings("unused")
+        @ThrowingProvides(StringRemoteProvider.class)
+        Integer foo() {
+            return null;
+        }
+      });
+      fail();
+    } catch(CreationException ce) {
+      assertEquals(StringRemoteProvider.class.getName() 
+          + " expects the value type to be java.lang.String, but it was java.lang.Integer",
+          Iterables.getOnlyElement(ce.getErrorMessages()).getMessage());
+    }
+  }
+  
+  private static interface TooManyTypeParameters<T, P> extends ThrowingProvider<T, Exception> {    
+  }
+  
+  private static interface WrongThrowingProviderType<T> extends ThrowingProvider<String, Exception> {    
+  }
+  
+  private static interface OneNoneGetMethod<T> extends ThrowingProvider<T, Exception> {
+    T bar();
+  }
+  
+  private static interface ManyMethods<T> extends ThrowingProvider<T, Exception> {
+    T bar();
+    String baz();
   }
 }