org.scalactic.Requirements.scala Maven / Gradle / Ivy
/*
* Copyright 2001-2012 Artima, 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 org.scalactic
import exceptions.NullArgumentException
import scala.quoted._
/**
* Trait that contains require
, and requireState
, and requireNonNull
methods for checking pre-conditions
* that give descriptive error messages extracted via a macro.
*
* These methods of trait Requirements
aim to improve error messages provided when a pre-condition check fails at runtime in
* production code. Although it is recommended practice to supply helpful error messages when doing pre-condition checks, often people
* don't. Instead of this:
*
*
* scala> val length = 5
* length: Int = 5
*
* scala> val idx = 6
* idx: Int = 6
*
* scala> require(idx >= 0 && idx <= length, "index, " + idx + ", was less than zero or greater than or equal to length, " + length)
* java.lang.IllegalArgumentException: requirement failed: index, 6, was less than zero or greater than or equal to length, 5
* at scala.Predef$.require(Predef.scala:233)
* ...
*
*
*
* People write simply:
*
*
*
* scala> require(idx >= 0 && idx <= length)
* java.lang.IllegalArgumentException: requirement failed
* at scala.Predef$.require(Predef.scala:221)
* ...
*
*
*
* Note that the detail message of the IllegalArgumentException
thrown by the previous line of code is simply, "requirement failed"
.
* Such messages often end up in a log file or bug report, where a better error message can save time in debugging the problem.
* By importing the members of Requirements
(or mixing in its companion trait), you'll get a more helpful error message
* extracted by a macro, whether or not a clue message is provided:
*
*
*
* scala> import org.scalactic._
* import org.scalactic._
*
* scala> import Requirements._
* import Requirements._
*
* scala> require(idx >= 0 && idx <= length)
* java.lang.IllegalArgumentException: 6 was greater than or equal to 0, but 6 was not less than or equal to 5
* at org.scalactic.Requirements$RequirementsHelper.macroRequire(Requirements.scala:56)
* ...
*
* scala> require(idx >= 0 && idx <= length, "(hopefully that helps)")
* java.lang.IllegalArgumentException: 6 was greater than or equal to 0, but 6 was not less than or equal to 5 (hopefully that helps)
* at org.scalactic.Requirements$RequirementsHelper.macroRequire(Requirements.scala:56)
* ...
*
*
*
* The requireState
method provides identical error messages to require
, but throws
* IllegalStateException
instead of IllegalArgumentException
:
*
*
*
* scala> val connectionOpen = false
* connectionOpen: Boolean = false
*
* scala> requireState(connectionOpen)
* java.lang.IllegalStateException: connectionOpen was false
* at org.scalactic.Requirements$RequirementsHelper.macroRequireState(Requirements.scala:71)
* ...
*
*
*
* Thus, whereas the require
methods throw the Java platform's standard exception indicating a passed argument
* violated a precondition, IllegalArgumentException
, the requireState
methods throw the standard
* exception indicating an object's method was invoked when the object was in an inappropriate state for that method,
* IllegalStateException
.
*
*
*
* The requireNonNull
method takes one or more variables as arguments and throws NullArgumentException
* with an error messages that includes the variable names if any are null
. Here's an example:
*
*
*
* scala> val e: String = null
* e: String = null
*
* scala> val f: java.util.Date = null
* f: java.util.Date = null
*
* scala> requireNonNull(a, b, c, d, e, f)
* org.scalactic.exceptions.NullArgumentException: e and f were null
* at org.scalactic.Requirements$RequirementsHelper.macroRequireNonNull(Requirements.scala:101)
* ...
*
*
*
* Although trait Requirements
can help you debug problems that occur in production, bear in mind that a much
* better alternative is to make it impossible for such events to occur at all. Use the type system to ensure that all
* pre-conditions are met so that the compiler can find broken pre-conditions and point them out with compiler error messages.
* When this is not possible or practical, however, trait Requirements
is helpful.
*
*/
trait Requirements {
import Requirements.requirementsHelper
/**
* Require that a boolean condition is true about an argument passed to a method, function, or constructor.
*
*
* If the condition is true
, this method returns normally.
* Else, it throws IllegalArgumentException
.
*
*
*
* This method is implemented in terms of a Scala macro that will generate an error message.
* See the main documentation for this trait for examples.
*
*
* @param condition the boolean condition to check as requirement
* @throws IllegalArgumentException if the condition is false
.
*/
inline def require(inline condition: Boolean)(implicit prettifier: Prettifier): Unit =
${ RequirementsMacro.require('{condition}, '{prettifier}, '{""}) }
/**
* Require that a boolean condition about an argument passed to a method, function, or constructor,
* and described in the given clue
, is true.
*
* If the condition is true
, this method returns normally.
* Else, it throws IllegalArgumentException
with the
* String
obtained by invoking toString
on the
* specified clue
and appending that to the macro-generated
* error message as the exception's detail message.
*
* @param condition the boolean condition to check as requirement
* @param clue an objects whose toString
method returns a message to include in a failure report.
* @throws IllegalArgumentException if the condition is false
.
* @throws NullPointerException if message
is null
.
*/
inline def require(inline condition: Boolean, clue: Any)(implicit prettifier: Prettifier): Unit =
${ RequirementsMacro.require('{condition}, '{prettifier}, '{clue}) }
/**
* Require that a boolean condition is true about the state of an object on which a method has been invoked.
*
*
* If the condition is true
, this method returns normally.
* Else, it throws IllegalStateException
.
*
*
*
* This method is implemented in terms of a Scala macro that will generate an error message.
*
*
* @param condition the boolean condition to check as requirement
* @throws IllegalStateException if the condition is false
.
*/
inline def requireState(inline condition: Boolean)(implicit prettifier: Prettifier): Unit =
${ RequirementsMacro.requireState('{condition}, '{prettifier}, '{""}) }
/**
* Require that a boolean condition about the state of an object on which a method has been
* invoked, and described in the given clue
, is true.
*
*
* If the condition is true
, this method returns normally.
* Else, it throws IllegalStateException
with the
* String
obtained by invoking toString
on the
* specified clue
appended to the macro-generated error message
* as the exception's detail message.
*
*
* @param condition the boolean condition to check as a requirement
* @param clue an object whose toString
method returns a message to include in a failure report.
* @throws IllegalStateException if the condition is false
.
* @throws NullPointerException if message
is null
.
*/
inline def requireState(inline condition: Boolean, clue: Any)(implicit prettifier: Prettifier): Unit =
${ RequirementsMacro.requireState('{condition}, '{prettifier}, '{clue}) }
/**
* Require that all passed arguments are non-null.
*
*
* If none of the passed arguments are null
, this method returns normally.
* Else, it throws NullArgumentException
with an error message that includes the name
* (as it appeared in the source) of each argument that was null
.
*
*
* @param arguments arguments to check for null
value
* @throws NullArgumentException if any of the arguments are null
.
*/
inline def requireNonNull(arguments: Any*)(implicit prettifier: Prettifier, pos: source.Position): Unit =
${ RequirementsMacro.requireNonNull('{arguments}, '{prettifier}, '{pos}) }
}
// /**
// * Macro implementation that provides rich error message for boolean expression requirements.
// */
object RequirementsMacro {
/**
* Provides requirement implementation for Requirements.require(booleanExpr: Boolean)
, with rich error message.
*
* @param context macro context
* @param condition original condition expression
* @return transformed expression that performs the requirement check and throw IllegalArgumentException
with rich error message if requirement failed
*/
def require(condition: Expr[Boolean], prettifier: Expr[Prettifier], clue: Expr[Any])(using Quotes): Expr[Unit] = {
val bool = BooleanMacro.parse(condition, prettifier)
'{ Requirements.requirementsHelper.macroRequire($bool, $clue) }
}
/**
* Provides requirement implementation for Requirements.requireState(booleanExpr: Boolean)
, with rich error message.
*
* @param context macro context
* @param condition original condition expression
* @return transformed expression that performs the requirement check and throw IllegalStateException
with rich error message if requirement failed
*/
def requireState(condition: Expr[Boolean], prettifier: Expr[Prettifier], clue: Expr[Any])(using Quotes): Expr[Unit] = {
val bool = BooleanMacro.parse(condition, prettifier)
'{ Requirements.requirementsHelper.macroRequireState($bool, $clue) }
}
/**
* Provides requirement implementation for Requirements.requireNonNull(arguments: Any*)
, with rich error message.
*
* @param arguments original arguments expression(s)
* @param prettifier Prettifier
to be used for error message
* @return transformed expression that performs the requirement check and throw NullArgumentException
with rich error message if requirement failed
*/
def requireNonNull(arguments: Expr[Seq[Any]], prettifier: Expr[Prettifier], pos: Expr[source.Position])(using Quotes): Expr[Unit] = {
import quotes.reflect._
def liftSeq(args: Seq[Expr[String]]): Expr[Seq[String]] = args match {
case x :: xs => '{ ($x) +: ${ liftSeq(xs) } }
case Nil => '{ Seq(): Seq[String] }
}
val argStr: List[Expr[String]] = arguments.asTerm.underlyingArgument match {
case Typed(Repeated(args, _), _) => // only sequence literal
args.map(arg => Expr(arg.asExprOf[Any].show))
case _ =>
report.throwError("requireNonNull can only be used with sequence literal, not `seq : _*`")
}
// generate AST that create array containing the argument name in source (get from calling 'show')
// for example, if you have:
// val a = "1"
// val b = null
// val c = "3"
// requireNonNull(a, b, c)
// it will generate the following code:
//
// Array("a", "b", "c")
val argumentsS: Expr[Seq[String]] = liftSeq(argStr)
// generate AST that create array containing the argument values
// for example, if you have:
// val a = "1"
// val b = null
// val c = "3"
// requireNonNull(a, b, c)
// it will generate the following code:
//
// Array(a, b, c)
// val argumentsArray = '{ $arguments.toArray }
// Generate AST to call requirementsHelper.macroRequireNonNull and pass in both variable names and values array:
//
// requirementsHelper.macroRequireNonNull(variableNamesArray, valuesArray)
'{ Requirements.requirementsHelper.macroRequireNonNull(($argumentsS).toArray, ($arguments).toArray, $prettifier, $pos) }
}
}
/**
* Companion object that facilitates the importing of Requirements
members as
* an alternative to mixing it in. One use case is to import Requirements
members so you can use
* them in the Scala interpreter:
*
*
* $scala -classpath scalatest.jar
* Welcome to Scala version 2.10.3.final (Java HotSpot(TM) Client VM, Java xxxxxx).
* Type in expressions to have them evaluated.
* Type :help for more information.
*
* scala> import org.scalactic.Requirements._
* import org.scalactic.Requirements._
*
* scala> val a = 1
* a: Int = 1
*
* scala> require(a == 2)
* java.lang.IllegalArgumentException: 1 did not equal 2
* at org.scalactic.Requirements$RequirementsHelper.macroRequire(Requirements.scala:56)
* at .<init>(<console>:20)
* at .<clinit>(<console>)
* at .<init>(<console>:7)
* at .<clinit>(<console>)
* at $print(<console>)
* at sun.reflect.NativeMethodAccessorImpl.invoke...
*/
object Requirements extends Requirements {
/**
* Helper class used by code generated by the require
macro.
*/
class RequirementsHelper extends Serializable {
private def append(currentMessage: String, clue: Any): String = {
val clueStr = clue.toString
if (clueStr.isEmpty)
currentMessage
else {
val firstChar = clueStr.head
if (firstChar.isWhitespace || firstChar == '.' || firstChar == ',' || firstChar == ';' || currentMessage.isEmpty)
currentMessage + clueStr
else
currentMessage + " " + clueStr
}
}
/**
* Require that the passed in Bool
is true
, else fail with IllegalArgumentException
.
*
* @param bool the Bool
to check as requirement
* @param clue optional clue to be included in IllegalArgumentException
's error message when the requirement failed
*/
def macroRequire(bool: Bool, clue: Any): Unit = {
if (clue == null)
throw new NullPointerException("clue was null")
if (!bool.value) {
val failureMessage = if (Bool.isSimpleWithoutExpressionText(bool)) append("", clue) else append(bool.failureMessage, clue)
throw new IllegalArgumentException(if (failureMessage.isEmpty) FailureMessages.expressionWasFalse else failureMessage)
}
}
/**
* Require that the passed in Bool
is true
, else fail with IllegalStateException
.
*
* @param bool the Bool
to check as requirement
* @param clue optional clue to be included in IllegalStateException
's error message when the requirement failed
*/
def macroRequireState(bool: Bool, clue: Any): Unit = {
if (clue == null)
throw new NullPointerException("clue was null")
if (!bool.value) {
val failureMessage = if (Bool.isSimpleWithoutExpressionText(bool)) append("", clue) else append(bool.failureMessage, clue)
throw new IllegalStateException(if (failureMessage.isEmpty) FailureMessages.expressionWasFalse else failureMessage)
}
}
/**
* Require that all of the passed in arguments are not null
, else fail with NullArgumentException
.
*
* @param variableNames names of variable passed as appear in source
* @param arguments arguments to check for null
value
*/
def macroRequireNonNull(variableNames: Array[String], arguments: Array[Any], prettifier: Prettifier, pos: source.Position): Unit = {
val nullList = arguments.zipWithIndex.filter { case (e, idx) =>
e == null
}
val nullCount = nullList.size
if (nullCount > 0) {
val nullVariableNames = nullList.map { case (e, idx) =>
variableNames(idx)
}
val errorMessage =
if (nullCount == 1)
FailureMessages.wasNull(prettifier, UnquotedString(nullVariableNames(0)))
else if (nullCount == 2) {
val combinedVariableNames = Resources.and(nullVariableNames.head, nullVariableNames.last)
FailureMessages.wereNull(prettifier, UnquotedString(combinedVariableNames))
}
else {
val combinedVariableNames = Resources.commaAnd(nullVariableNames.dropRight(1).mkString(Resources.comma), nullVariableNames.last)
FailureMessages.wereNull(prettifier, UnquotedString(combinedVariableNames))
}
throw new NullArgumentException(errorMessage)
}
}
}
/**
* Helper instance used by code generated by macro assertion.
*/
val requirementsHelper = new RequirementsHelper
}