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

org.scalactic.anyvals.CompileTimeAssertions.scala Maven / Gradle / Ivy

The newest version!
/*
* Copyright 2001-2014 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.anyvals

import org.scalactic.Resources

import scala.quoted._

/**
 * Trait providing assertion methods that can be called at compile time from macros
 * to validate literals in source code.
 *
 * 

* The intent of CompileTimeAssertions is to make it easier to create * AnyVals that restrict the values of types for which Scala supports * literals: Int, Long, Float, Double, Char, * and String. For example, if you are using odd integers in many places * in your code, you might have validity checks scattered throughout your code. Here's * an example of a method that both requires an odd Int is passed (as a * precondition, and ensures an odd * Int is returned (as * a postcondition): *

* *
 * def nextOdd(i: Int): Int = {
 *   def isOdd(x: Int): Boolean = x.abs % 2 == 1
 *   require(isOdd(i))
 *   (i + 2) ensuring (isOdd(_))
 * }
 * 
* *

* In either the precondition or postcondition check fails, an exception will * be thrown at runtime. If you have many methods like this you may want to * create a type to represent an odd Int, so that the checking * for validity errors is isolated in just one place. By using an AnyVal * you can avoid boxing the Int, which may be more efficient. * This might look like: *

* *
 * final class OddInt private (val value: Int) extends AnyVal {
 *   override def toString: String = s"OddInt($value)"
 * }
 *
 * object OddInt {
 *   def apply(value: Int): OddInt = {
 *     require(value.abs % 2 == 1)
 *     new OddInt(value)
 *   }
 * }
 * 
* *

* An AnyVal cannot have any constructor code, so to ensure that * any Int passed to the OddInt constructor is actually * odd, the constructor must be private. That way the only way to construct a * new OddInt is via the apply factory method in the * OddInt companion object, which can require that the value be * odd. This design eliminates the need for placing require and * ensuring clauses anywhere else that odd Ints are * needed, because the type promises the constraint. The nextOdd * method could, therefore, be rewritten as: *

* *
 * def nextOdd(oi: OddInt): OddInt = OddInt(oi.value + 2)
 * 
* *

* Using the compile-time assertions provided by this trait, you can construct * a factory method implemented via a macro that causes a compile failure * if OddInt.apply is passed anything besides an odd * Int literal. Class OddInt would look exactly the * same as before: *

* *
 * final class OddInt private (val value: Int) extends AnyVal {
 *   override def toString: String = s"OddInt($value)"
 * }
 * 
* *

* In the companion object, however, the apply method would * be implemented in terms of a macro. Because the apply method * will only work with literals, you'll need a second method that can work * an any expression of type Int. We recommend a from method * that returns an Option[OddInt] that returns Some[OddInt} if the passed Int is odd, * else returns None, and an ensuringValid method that returns an OddInt * if the passed Int is valid, else throws AssertionError. *

* *
 * object OddInt {
 *
 *   // The from factory method validates at run time
 *   def from(value: Int): Option[OddInt] =
 *     if (OddIntMacro.isValid(value)) Some(new OddInt(value)) else None
 *
 *   // The ensuringValid factory method validates at run time, but throws
 *   // an AssertionError if invalid
 *   def ensuringValid(value: Int): OddInt =
 *     if (OddIntMacro.isValid(value)) new OddInt(value) else {
 *       throw new AssertionError(s"$value was not a valid OddInt")
 *     }
 *
 *   // The apply factory method validates at compile time
 *   import scala.language.experimental.macros
 *   def apply(value: Int): OddInt = macro OddIntMacro.apply
 * }
 * 
* *

* The apply method refers to a macro implementation method in class * PosIntMacro. The macro implementation of any such method can look * very similar to this one. The only changes you'd need to make is the * isValid method implementation and the text of the error messages. *

* *
 * import org.scalactic.anyvals.CompileTimeAssertions
 * import reflect.macros.Context
 *
 * object OddIntMacro extends CompileTimeAssertions {
 *
 *   // Validation method used at both compile- and run-time
 *   def isValid(i: Int): Boolean = i.abs % 2 == 1
 *
 *   // Apply macro that performs a compile-time assertion
 *   def apply(c: Context)(value: c.Expr[Int]): c.Expr[OddInt] = {
 *
 *     // Prepare potential compiler error messages
 *     val notValidMsg = "OddInt.apply can only be invoked on odd Int literals, like OddInt(3)."
 *     val notLiteralMsg = "OddInt.apply can only be invoked on Int literals, like " +
 *           "OddInt(3). Please use OddInt.from instead."
 *
 *     // Validate via a compile-time assertion
 *     ensureValidIntLiteral(c)(value, notValidMsg, notLiteralMsg)(isValid)
 *
 *     // Validated, so rewrite the apply call to a from call
 *     c.universe.reify { OddInt.ensuringValid(value.splice) }
 *   }
 * }
 * 
* *

* The isValid method just takes the underlying type and returns true if it is valid, * else false. This method is placed here so the same valiation code can be used both in * the from method at runtime and the apply macro at compile time. The apply * actually does just two things. It calls a ensureValidIntLiteral, performing a compile-time assertion * that value passed to apply is an Int literal that is valid (in this case, odd). * If the assertion fails, ensureValidIntLiteral will complete abruptly with an exception that will * contain an appropriate error message (one of the two you passed in) and cause a compiler error with that message. * If the assertion succeeds, ensureValidIntLiteral will just return normally. The next line of code * will then execute. This line of code must construct an AST (abstract syntax tree) of code that will replace * the OddInt.apply invocation. We invoke the other factory method that either returns an OddInt * or throws an AssertionError, since we've proven at compile time that the call will succeed. *

* *

* You may wish to use quasi-quotes instead of reify. The reason we use reify is that this also works on 2.10 without * any additional plugin (i.e., you don't need macro paradise), and Scalactic supports 2.10. *

*/ trait CompileTimeAssertions { /** * Ensures a given expression of type Int is a literal with a valid value according to a given validation function. * *

* If the given Int expression is a literal whose value satisfies the given validation function, this method will * return normally. Otherwise, if the given Int expression is not a literal, this method will complete abruptly with * an exception whose detail message includes the String passed as notLiteralMsg. Otherwise, the * given Int expression is a literal that does not satisfy the given validation function, so this method will * complete abruptly with an exception whose detail message includes the String passed as notValidMsg. *

* *

* This method is intended to be invoked at compile time from macros. When called from a macro, exceptions thrown by this method * will result in compiler errors. The detail message of the thrown exception will appear as the compiler error message. *

* * @param c the compiler context for this assertion * @param value the Int expression to validate * @param notValidMsg a String message to include in the exception thrown if the expression is a literal, but not valid * @param notLiteralMsg a String message to include in the exception thrown if the expression is not a literal * @param isValid a function used to validate a literal value parsed from the given expression */ def ensureValidIntLiteral(value: Expr[Int], notValidMsg: String, notLiteralMsg: String)(isValid: Int => Boolean)(using Quotes): Unit = { import quotes.reflect._ value.asTerm.underlyingArgument match { case Literal(intConst) => val literalValue = intConst.value.toString.toInt if (!isValid(literalValue)) report.error(notValidMsg, value.asTerm.pos) case _ => report.error(notLiteralMsg, value.asTerm.pos) } } /** * Ensures a given expression of type Long is a literal with a valid value according to a given validation function. * *

* If the given Long expression is a literal whose value satisfies the given validation function, this method will * return normally. Otherwise, if the given Long expression is not a literal, this method will complete abruptly with * an exception whose detail message includes the String passed as notLiteralMsg. Otherwise, the * given Long expression is a literal that does not satisfy the given validation function, so this method will * complete abruptly with an exception whose detail message includes the String passed as notValidMsg. *

* *

* This method is intended to be invoked at compile time from macros. When called from a macro, exceptions thrown by this method * will result in compiler errors. The detail message of the thrown exception will appear as the compiler error message. *

* * @param c the compiler context for this assertion * @param value the Long expression to validate * @param notValidMsg a String message to include in the exception thrown if the expression is a literal, but not valid * @param notLiteralMsg a String message to include in the exception thrown if the expression is not a literal * @param isValid a function used to validate a literal value parsed from the given expression */ def ensureValidLongLiteral(value: Expr[Long], notValidMsg: String, notLiteralMsg: String)(isValid: Long => Boolean)(using Quotes): Unit = { import quotes.reflect._ value.asTerm.underlyingArgument match { case Literal(longConst) => val literalValue = longConst.value.toString.toLong if (!isValid(literalValue)) report.error(notValidMsg, value.asTerm.pos) case _ => report.error(notLiteralMsg, value.asTerm.pos) } } /** * Ensures a given expression of type Float is a literal with a valid value according to a given validation function. * *

* If the given Float expression is a literal whose value satisfies the given validation function, this method will * return normally. Otherwise, if the given Float expression is not a literal, this method will complete abruptly with * an exception whose detail message includes the String passed as notLiteralMsg. Otherwise, the * given Float expression is a literal that does not satisfy the given validation function, so this method will * complete abruptly with an exception whose detail message includes the String passed as notValidMsg. *

* *

* This method is intended to be invoked at compile time from macros. When called from a macro, exceptions thrown by this method * will result in compiler errors. The detail message of the thrown exception will appear as the compiler error message. *

* * @param c the compiler context for this assertion * @param value the Float expression to validate * @param notValidMsg a String message to include in the exception thrown if the expression is a literal, but not valid * @param notLiteralMsg a String message to include in the exception thrown if the expression is not a literal * @param isValid a function used to validate a literal value parsed from the given expression */ def ensureValidFloatLiteral(value: Expr[Float], notValidMsg: String, notLiteralMsg: String)(isValid: Float => Boolean)(using Quotes): Unit = { import quotes.reflect._ value.asTerm.underlyingArgument match { case Literal(floatConst) => val literalValue = floatConst.value.toString.toFloat if (!isValid(literalValue)) report.error(notValidMsg, value.asTerm.pos) case _ => report.error(notLiteralMsg, value.asTerm.pos) } } /** * Ensures a given expression of type Double is a literal with a valid value according to a given validation function. * *

* If the given Double expression is a literal whose value satisfies the given validation function, this method will * return normally. Otherwise, if the given Double expression is not a literal, this method will complete abruptly with * an exception whose detail message includes the String passed as notLiteralMsg. Otherwise, the * given Double expression is a literal that does not satisfy the given validation function, so this method will * complete abruptly with an exception whose detail message includes the String passed as notValidMsg. *

* *

* This method is intended to be invoked at compile time from macros. When called from a macro, exceptions thrown by this method * will result in compiler errors. The detail message of the thrown exception will appear as the compiler error message. *

* * @param c the compiler context for this assertion * @param value the Double expression to validate * @param notValidMsg a String message to include in the exception thrown if the expression is a literal, but not valid * @param notLiteralMsg a String message to include in the exception thrown if the expression is not a literal * @param isValid a function used to validate a literal value parsed from the given expression */ def ensureValidDoubleLiteral(value: Expr[Double], notValidMsg: String, notLiteralMsg: String)(isValid: Double => Boolean)(using Quotes): Unit = { import quotes.reflect._ value.asTerm.underlyingArgument match { case Literal(doubleConst) => val literalValue = doubleConst.value.toString.toDouble if (!isValid(literalValue)) report.error(notValidMsg, value.asTerm.pos) case _ => report.error(notLiteralMsg, value.asTerm.pos) } } /** * Ensures a given expression of type String is a literal with a valid value according to a given validation function. * *

* If the given String expression is a literal whose value satisfies the given validation function, this method will * return normally. Otherwise, if the given String expression is not a literal, this method will complete abruptly with * an exception whose detail message includes the String passed as notLiteralMsg. Otherwise, the * given String expression is a literal that does not satisfy the given validation function, so this method will * complete abruptly with an exception whose detail message includes the String passed as notValidMsg. *

* *

* This method is intended to be invoked at compile time from macros. When called from a macro, exceptions thrown by this method * will result in compiler errors. The detail message of the thrown exception will appear as the compiler error message. *

* * @param c the compiler context for this assertion * @param value the String expression to validate * @param notValidMsg a String message to include in the exception thrown if the expression is a literal, but not valid * @param notLiteralMsg a String message to include in the exception thrown if the expression is not a literal * @param isValid a function used to validate a literal value parsed from the given expression */ def ensureValidStringLiteral(value: Expr[String], notValidMsg: String, notLiteralMsg: String)(isValid: String => Boolean)(using Quotes): Unit = { import quotes.reflect._ value.asTerm.underlyingArgument match { case Literal(stringConst) => val literalValue = stringConst.value.toString if (!isValid(literalValue)) report.error(notValidMsg, value.asTerm.pos) case _ => report.error(notLiteralMsg, value.asTerm.pos) } } /** * Ensures a given expression of type Char is a literal with a valid value according to a given validation function. * *

* If the given Char expression is a literal whose value satisfies the given validation function, this method will * return normally. Otherwise, if the given Char expression is not a literal, this method will complete abruptly with * an exception whose detail message includes the String passed as notLiteralMsg. Otherwise, the * given Char expression is a literal that does not satisfy the given validation function, so this method will * complete abruptly with an exception whose detail message includes the String passed as notValidMsg. *

* *

* This method is intended to be invoked at compile time from macros. When called from a macro, exceptions thrown by this method * will result in compiler errors. The detail message of the thrown exception will appear as the compiler error message. *

* * @param c the compiler context for this assertion * @param value the Char expression to validate * @param notValidMsg a String message to include in the exception thrown if the expression is a literal, but not valid * @param notLiteralMsg a String message to include in the exception thrown if the expression is not a literal * @param isValid a function used to validate a literal value parsed from the given expression */ def ensureValidCharLiteral(value: Expr[Char], notValidMsg: String, notLiteralMsg: String)(isValid: Char => Boolean)(using Quotes): Unit = { import quotes.reflect._ value.asTerm.underlyingArgument match { case Literal(charConst) => val literalValue = charConst.value.toString.head if (!isValid(literalValue)) report.error(notValidMsg, value.asTerm.pos) case _ => report.error(notLiteralMsg, value.asTerm.pos) } } } /** * Companion object that facilitates the importing of CompileTimeAssertions members as * an alternative to mixing in the trait. */ object CompileTimeAssertions extends CompileTimeAssertions




© 2015 - 2024 Weber Informatics LLC | Privacy Policy