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

org.scalactic.Requirements.scala Maven / Gradle / Ivy

The newest version!
/*
 * 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

}





© 2015 - 2024 Weber Informatics LLC | Privacy Policy