All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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 reflect.macros.Context
import exceptions.NullArgumentException

/**
 * 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 language.experimental.macros /** * 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) { 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) { 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]) { 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(UnquotedString(nullVariableNames(0))) else if (nullCount == 2) { val combinedVariableNames = Resources.and(nullVariableNames.head, nullVariableNames.last) FailureMessages.wereNull(UnquotedString(combinedVariableNames)) } else { val combinedVariableNames = Resources.commaAnd(nullVariableNames.dropRight(1).mkString(Resources.comma), nullVariableNames.last) FailureMessages.wereNull(UnquotedString(combinedVariableNames)) } throw new NullArgumentException(errorMessage) } } } /** * Helper instance used by code generated by macro assertion. */ val requirementsHelper = new 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. */ def require(condition: Boolean): Unit = macro RequirementsMacro.require /** * 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. */ def require(condition: Boolean, clue: Any): Unit = macro RequirementsMacro.requireWithClue /** * 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. */ def requireState(condition: Boolean): Unit = macro RequirementsMacro.requireState /** * 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. */ def requireState(condition: Boolean, clue: Any): Unit = macro RequirementsMacro.requireStateWithClue /** * 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. */ def requireNonNull(arguments: Any*): Unit = macro RequirementsMacro.requireNonNull } /** * Macro implementation that provides rich error message for boolean expression requirements. */ private[scalactic] 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(context: Context)(condition: context.Expr[Boolean]): context.Expr[Unit] = new BooleanMacro[context.type](context, "requirementsHelper").genMacro(condition, "macroRequire", context.literal("")) /** * Provides requirement implementation for Requirements.require(booleanExpr: Boolean, clue: Any), with rich error message. * * @param context macro context * @param condition original condition expression * @param clue original clue expression * @return transformed expression that performs the requirement check and throw IllegalArgumentException with rich error message (clue included) if requirement failed */ def requireWithClue(context: Context)(condition: context.Expr[Boolean], clue: context.Expr[Any]): context.Expr[Unit] = new BooleanMacro[context.type](context, "requirementsHelper").genMacro(condition, "macroRequire", 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(context: Context)(condition: context.Expr[Boolean]): context.Expr[Unit] = new BooleanMacro[context.type](context, "requirementsHelper").genMacro(condition, "macroRequireState", context.literal("")) /** * Provides requirement implementation for Requirements.requireState(booleanExpr: Boolean, clue: Any), with rich error message. * * @param context macro context * @param condition original condition expression * @param clue original clue expression * @return transformed expression that performs the requirement check and throw IllegalStateException with rich error message (clue included) if requirement failed */ def requireStateWithClue(context: Context)(condition: context.Expr[Boolean], clue: context.Expr[Any]): context.Expr[Unit] = new BooleanMacro[context.type](context, "requirementsHelper").genMacro(condition, "macroRequireState", clue) /** * Provides requirement implementation for Requirements.requireNonNull(arguments: Any*), with rich error message. * * @param context macro context * @param arguments original arguments expression(s) * @return transformed expression that performs the requirement check and throw NullArgumentException with rich error message if requirement failed */ def requireNonNull(context: Context)(arguments: context.Expr[Any]*): context.Expr[Unit] = { import context.universe._ // 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 variablesNamesArray = Apply( Select( Ident("Array"), newTermName("apply") ), List(arguments.map(e => context.literal(show(e.tree)).tree): _*) ) // 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 = Apply( Select( Ident("Array"), newTermName("apply") ), List(arguments.map(e => e.tree): _*) ) // Generate AST to call requirementsHelper.macroRequireNonNull and pass in both variable names and values array: // // requirementsHelper.macroRequireNonNull(variableNamesArray, valuesArray) context.Expr( Apply( Select( Ident("requirementsHelper"), newTermName("macroRequireNonNull") ), List(variablesNamesArray, argumentsArray) ) ) } } /** * 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





© 2015 - 2025 Weber Informatics LLC | Privacy Policy