Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Improve error message when "No valid constructors" #9501

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions core/play-guice/src/test/scala/play/api/inject/ModulesSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import org.specs2.matcher.BeEqualTypedValueCheck
import org.specs2.mutable.Specification
import play.api.Configuration
import play.api.Environment
import play.api.PlayException
import play.{ Environment => JavaEnvironment }

class ModulesSpec extends Specification {
Expand Down Expand Up @@ -63,6 +64,21 @@ class ModulesSpec extends Specification {
}
}

"list current constructor parameters when none match" in {
val env = Environment.simple()
val conf = Configuration(
"play.modules.enabled" -> Seq(
classOf[InvalidConfigModule].getName
)
)

Modules.locate(env, conf) must throwA[PlayException].like {
case e =>
e.getMessage must contain("(com.typesafe.config.Config, java.lang.String, long)")
e.getMessage must contain("(com.typesafe.config.Config, java.lang.String)")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you got it the other way around.

If we throw such an exception to the users, we are just telling them which constructors we have found on the module they registered.

The actual suggestion was to inform the users that the module has the wrong constructor signature and therefore they need to adapt.

See @kubukoz comment here:
#8474 (comment)

And @gmethvin comment here:
#8474 (comment)

We should list the valid constructors, not those the user has defined.

Copy link
Contributor

@octonato octonato Aug 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a quick test and this is what I got:

class OnlyConfigModule(val config: Config) extends AbstractModule {
  override def configure(): Unit = ()
}

val env = Environment.simple()
val conf = Configuration(
  "play.modules.enabled" -> Seq(
    classOf[OnlyConfigModule].getName
  )
)

val located: Seq[Any] = Modules.locate(env, conf)

and it throws..

play.api.PlayException: No valid public constructors for play.api.inject.OnlyConfigModule. 
Expected one of: (com.typesafe.config.Config)

As a user, I would think: that's exactly the constructor that I defined, why it saying that it is expecting this constructor and still not using it.

}

}

}
Expand All @@ -78,3 +94,8 @@ class ScalaGuiceModule(val environment: Environment, val configuration: Configur
class JavaGuiceConfigModule(val environment: JavaEnvironment, val config: Config) extends AbstractModule {
override def configure(): Unit = ()
}

class InvalidConfigModule(val config: Config, val ignored: String) extends AbstractModule {
def this(config: Config, ignored: String, useless: Long) = this(config, ignored)
override def configure(): Unit = ()
}
18 changes: 18 additions & 0 deletions core/play/src/main/java/play/libs/reflect/ConstructorUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.Arrays;

/** Imported from apache.commons.lang3 3.6 */
public class ConstructorUtils {
Expand Down Expand Up @@ -115,6 +116,23 @@ public static <T> Constructor<T> getMatchingAccessibleConstructor(
return result;
}

/**
* Finds the class names of all accessible constructors.
*
* @param <T> the constructor type
* @return the constructors parameters, empty array if no matching accessible constructor found
*/
public static <T> String[][] getConstructorParameters(final Class<T> type) {
Constructor[] allConstructors = type.getDeclaredConstructors();
return Arrays.stream(allConstructors)
.map(
cons ->
Arrays.stream(cons.getParameterTypes())
.map(Class::getCanonicalName)
.toArray(String[]::new))
.toArray(String[][]::new);
}

/**
* Learn whether the specified class is generally accessible, i.e. is declared in an entirely
* {@code public} manner.
Expand Down
5 changes: 4 additions & 1 deletion core/play/src/main/scala/play/api/inject/Module.scala
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,10 @@ object Modules {
tryConstruct()
}
.getOrElse {
throw new PlayException("No valid constructors", "Module [" + className + "] cannot be instantiated.")
val parameters: Array[Array[String]] = ConstructorUtils.getConstructorParameters(moduleClass)
throw new PlayException(s"""No valid public constructors for ${moduleClass.getCanonicalName}. Expected one of:
${parameters.map(_.mkString("(", ", ", ")")).mkString(", ")}
""".stripMargin, "Module [" + className + "] cannot be instantiated.")
}
} catch {
case e: PlayException => throw e
Expand Down