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

coop.plausible.nx.internal.Validator.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2014 Plausible Labs Cooperative, Inc.
 * All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package coop.plausible.nx.internal

import scala.annotation.tailrec
import coop.plausible.nx.{UncheckedExceptions, internal}

/**
 * No Exceptions Implementation.
 *
 * This trait may be mixed in with any valid reflection global, including:
 * - As a compiler plugin (see [[CompilerPlugin]])
 * - As a compile-time macro (see [[Macros]])
 */
private trait Validator extends Core with Errors with CheckedExceptionStrategies {
  import universe._

  private val compat = new MacroCompat with MacroTypes { override val universe: Validator.this.universe.type = Validator.this.universe }
  import compat._

  /**
   * Finds all unhandled throwables at a given tree node.
   *
   * @param checkedStrategy The checked exception strategy to be used when validating exceptions.
   */
  class ThrowableValidator (checkedStrategy: CheckedExceptionStrategy) {
    /**
     * Traverse `tree` and return all validation errors. Note that this will contain '''all''' unhandled `Throwable`
     * types, not just subclasses of `Exception`.
     *
     * The top-level node is treated as an exception propagation point; any exceptions that could be thrown
     * at the top of the tree are treated as unhandled exceptions.
     *
     * For example, given the following input, a value of Seq[UnhandledThrowable(... IOException)] will be provided.
     *
     * {{{
     *   @throws[IOException]("Bad data triggers failure") read () = ???
     *   read()
     * }}}
     *
     * @param tree The top-level node to be traversed.
     * @return An ordered sequence of validation errors.
     */
    def check (tree: Tree): Seq[ValidationError] = {
      /* Instantiate our traverse handler */
      val traverse = new ThrowableTraversal(checkedStrategy)

      /* Perform the traversal */
      traverse.validationErrors(tree)
    }
  }

  /**
   * Handles the actual (mutable) traversal of the tree.
   *
   * @param checkedStrategy The checked exception strategy to be used when validating exceptions.
   */
  private class ThrowableTraversal (checkedStrategy: CheckedExceptionStrategy) extends Traverser {
    import scala.collection.mutable

    /**
     * Traverse `tree` and return all validation errors. The top-level node is treated
     * as an exception propagation point; any exceptions that could be thrown at the top of the tree
     * are treated as unhandled exceptions.

     * @param tree The top-level node to be traversed.
     * @return An ordered sequence of validation errors.
     */
    def validationErrors (tree: Tree): Seq[ValidationError] = {
      /* The top of the tree is considered a propagation point */
      mutableState.propagationPoint(tree) {
        /* Perform traversal */
        traverse(tree)

        /* No exceptions are handled at the top of the tree */
        Set()
      }

      /* Provide the result */
      mutableState.validationErrors
    }

    /**
     * Represents a candidate throwing entity that may be caught or declared.
     *
     * @param pos The position at which the throwable may be raised.
     * @param tpe The throwable's type.
     */
    private case class Throwie (pos: Position, tpe: Type)

    /**
     * Mutable state required by the Traverser API. We vend a set of high-level APIs for operating on this state,
     * as to minimize the mutability headache.
     */
    private object mutableState {
      /**
       * The set of throwables types that are currently known to be throwable from the current position in the tree,
       * but may still be caught or declared.
       *
       * A new candidate list is pushed onto the stack upon entry into a propagation point, and this list is
       * popped at exit. An initial list is created for the root tree node.
       */
      private val candidateThrowieStack = mutable.Stack[mutable.MutableList[Throwie]]()

      /**
       * The set of validation errors that are found across the entire tree. This will be populated explicitly
       * during walking, as well as from the `candidateThrowies` at throwable propagation points.
       */
      private val validationErrorList = mutable.MutableList[ValidationError]()

      /**
       * Discard all candidate throwables that are a valid (sub)type of one of `throwTypes`
       *
       * @param throwTypes Types (and transitively, subtypes) to be removed from the set of candidate throwables.
       */
      private def filterCandidateThrowies (throwTypes: Set[Type]): Unit = throwTypes.foreach { throwType =>
        val filtered = candidateThrowieStack.pop.filterNot(_.tpe <:< throwType)
        candidateThrowieStack.push(filtered)
      }

      /**
       * Declare an explicit validation error. This will be added to the set of validation errors
       * available upon completion of a traversal.
       */
      def declareValidationError (error: ValidationError): Unit = {
        validationErrorList += error
      }

      /**
       * Declare one or more candidate throwables at the given point in the tree. The provided throwies will
       * be validated against the active checked exception strategy; any non-checked throwable types will be
       * discarded.
       *
       * @param throwies The throwies that may be thrown at this point.
       */
      def declareCandidateThrowies (throwies: Seq[Throwie]): Unit = {
        candidateThrowieStack.head ++= throwies.filterNot(t => checkedStrategy.isUnchecked(t.tpe))
      }

      /**
       * This methodSymbol should be called at catch pints to declare any caught throwable types (eg, from try-catch blocks).
       *
       * Any throwable types declared in `throwTypes` will be removed from the set of ''candidate'' uncaught throwables.
       *
       * @param throwTypes The throwable types declared as caught at this point.
       */
      def declareCatchPoint (throwTypes: Set[Type]): Unit = filterCandidateThrowies(throwTypes)

      /**
       * This should be called to define a propigation point; a new entry in the candidate throwies
       * stack will be created, and the provided function executed.
       *
       * Any throwable types returned by the expression will be removed from the set of ''candidate'' uncaught
       * throwables, and any undeclared types will be moved to the list of ''known'' unhandled throwables.
       *
       * @param tree The propigation point's node. If marked with an @unchecked annotation, `expr` will not be
       *             executed.
       * @param expr Expression to execute to generate the set of handled/declared throwable types.
       */
      def propagationPoint (tree: Tree) (expr: => Set[Type]): Unit = {
        /* We skip this entire propagation point if it's marked @unchecked */
        if (tree.symbol == null || !uncheckedAnnotated(tree.symbol.annotations)) {
          /* Create a new entry in the candidate stack. */
          candidateThrowieStack.push(mutable.MutableList())

          /* Gather declared throwables. */
          val declared = expr

          /* Filter declared types from the candidates */
          filterCandidateThrowies(declared)

          /* Move all remaining candidates to the list of unhandled throwables */
          validationErrorList ++= candidateThrowieStack.head.map { t => UnhandledThrowable(t.pos, t.tpe) }
          candidateThrowieStack.pop()
        }
      }

      /**
       * Return a snapshot of the currently collected set of validation errors. The errors will be ordered
       * according to the original point in the tree in which they occurred.
       */
      def validationErrors: Seq[ValidationError] = {
        validationErrorList.toSeq
      }
    }

    /** @inheritdoc */
    override def traverse (tree: Tree): Unit = {
      /* Traverse children; we work from the bottom up. */
      def defaultTraverse (): Unit = {
        super.traverseTrees(tree.children)
      }

      /* Look for exception-related constructs */
      tree match {
        /* Class body and constructors. This is a propagation point. */
        case cls:ClassDef => mutableState.propagationPoint(tree) {
          /*
           * Find the primary constructor declaration.
           *
           * Primary constructor annotations are attached to the constructor methodSymbol, but the constructor's code is
           * actually found in the class' body. We have to collect exception types from this constructor, and *then* descend
           * into the class body.
           */
          val primaryAnnotations = cls.impl.tpe.decls.collectFirst {
            case m: MethodSymbol if m.isPrimaryConstructor => m.annotations
          }.getOrElse(Seq())



          /* Find @throws annotations; we can't yet declare the propagation point, as the primary constructor's body is not
           * actually within the methodSymbol; we have to traverse into the class first. */
          val throws: Seq[Type] = extractAnnotatedThrows(cls, primaryAnnotations) match {
            case Right(exceptions) => exceptions
            case Left(err) =>
              /* Report the error, return an empty set */
              mutableState.declareValidationError(err)
              Seq()
          }

          /* Traverse into the class to populate the set of candidate throwables -- unless the primary constructor is
           * marked as unchecked. */
          if (!uncheckedAnnotated(primaryAnnotations)) {
            defaultTraverse()
          }

          /* Return the set of handled throwable types; this uses the annotations found on the primary constructor, and the throwables
           * found within the class body itself. */
          throws.toSet
        }


        /* try statement. This is a catch point. */
        case Try(block, catches, finalizer) =>
          /* Traverse into the try body to find all throwables */
          traverse(block)

          /*
           * Extract the exception types of all viable catches. We must exclude the following (usually valid) matches
           * that rely on runtime pattern matching:
           *
           * - Case statements that define a guard.
           * - Unapply-based statements (eg, Fatal(e))
           *
           * Since the matching is dynamic in those cases, we have no way to assert that the guard will match all
           * possible values at runtime.
           *
           * Medieval, isn't it? This perfectly illustrated how unchecked exceptions destroy type safety.
           */
          val caught = catches.filter(_.guard.isEmpty).map(_.pat).filter { pattern =>
            /* Recursively search for any function calls within the catch pattern. */
            val fcalls = for (fcall @ Apply(_, _) <- pattern) yield fcall

            /* Disallow patterns that contain unapply calls. */
            if (fcalls.size > 0) {
              false
            } else {
              true
            }
          }.map(_.tpe)

          /* Declare the catch point */
          mutableState.declareCatchPoint(caught.toSet)

          /*
           * Now extract any throwables from subtrees that should *not* be covered by the try's catch() block. This
           * must be done in the same order as they're declared in code so that we report issues in the correct
           * order:
           *
           * - Guard blocks.
           * - Case statement bodies.
           * - Finalizer block
           */
          catches.foreach { c =>
            traverse(c.pat)
            traverse(c.guard)
            traverse(c.body)
          }
          traverse(finalizer)

        /* Method or constructor definition. This is a propagation point. */
        case defdef:DefDef => mutableState.propagationPoint(tree) {
          /* Traverse all children */
          defaultTraverse()

          /*
           * Find exception types declared to be thrown by either the definition, or any of the symbols it overrides,
           * and verify that the subtype does not widen the declared superclass @throws.
           */
          val throws = for (
          /* Find directly attached @throws-annotated throwable types. */
            childThrows <- extractAnnotatedThrows(defdef, defdef.symbol.annotations).left.map(Seq(_)).right;
            /* Fetch the list of overridden symbols (may be none). */
            overriddenSymbols <- Right(defdef.symbol.overrides).right;

            /* Fetch all @throws-annotated throwable types from overridden symbols. */
            parentThrows <- extractAnnotatedThrows(defdef, overriddenSymbols.map(_.annotations).flatten).left.map(Seq(_)).right;

            /* Unify the parent and child throw sets, after first validating that the overriding method does not widen the
             * types declared by the parent(s). */
            result <- {
              /* Generate errors for any incorrect @throws declarations; if the parent-declared @throws don't specify a
               * superclass, the child is widening the throw set. */
              val invalidThrows = if (overriddenSymbols.size > 0) {
                for (throws <- childThrows if !parentThrows.exists(throws <:< _)) yield throws
              } else {
                /* If there are no parents, there can be no errors */
                Set()
              }

              /* Declare errors for incorrect @throw declarations */
              invalidThrows.foreach { throws =>
                mutableState.declareValidationError(CannotOverride(defdef.pos, throws, defdef.symbol))
              }

              /* We intentionally provide the unmodified set of declared exceptions; this avoids triggering spurious
               * UnhandledThrowable errors caused by a CannotOverride error stripping the @throws declaration. */
              Right(childThrows ++ parentThrows)
            }.right
          ) yield result

          /* Return the handled exceptions, or any errors. */
          throws match {
            case Right(types) =>
              types.toSet
            case Left(errs) =>
              for (err <- errs) mutableState.declareValidationError(err)
              Set()
          }
        }

        /* Explicit throw */
        case thr:Throw =>
          /* Traverse all children */
          defaultTraverse()

          /* Add the type to the set of candidate throwables. */
          mutableState.declareCandidateThrowies(Seq(Throwie(thr.pos, thr.expr.tpe)))

        /*
         * Method/function call
         */
        case apply:Apply =>
          /* Traverse all children */
          defaultTraverse()

          /* Extract both the target's @throws, as well as any symbols overridden by target. */
          val throws = for (
            applyThrows <- extractAnnotatedThrows(apply, apply.symbol.annotations).right;
            parentThrows <- extractAnnotatedThrows(apply, apply.symbol.overrides.map(_.annotations).flatten).right
          ) yield applyThrows ++ parentThrows

          /* Find exception types declared to be thrown by the target; declare them as candidate throwables */
          throws match {
            case Right(exceptions) =>
              mutableState.declareCandidateThrowies(exceptions.map(tpe => Throwie(apply.fun.pos, tpe)))
            case Left(err) =>
              mutableState.declareValidationError(err)
          }

        case _ =>
          /* Hand off to default traversal methodSymbol */
          super.traverse(tree)
      }
    }

    /** Class symbol for our `unchecked` annotation, or None if the runtime library is not available */
    private lazy val UncheckedClass: Option[Symbol] = try {
      Some(rootMirror.staticClass(classOf[UncheckedExceptions].getCanonicalName))
    } catch {
      case mre:Exception => None
    }

    /**
     * Return true if `annotations` contains an @unchecked annotation, false otherwise.
     *
     * @param annotations The annotations to be checked.
     * @return true if the @unchecked is included in the annotations, false otherwise.
     */
    private def uncheckedAnnotated (annotations: Seq[Annotation]): Boolean = {
      UncheckedClass.exists(uncheckedClass => annotations.exists(_.tree.tpe.typeSymbol == uncheckedClass))
    }

    /** The class symbol for the 'scala.throws' annotation */
    private lazy val ThrowsClass = rootMirror.staticClass("scala.throws")

    /**
     * Given a sequence of annotations, extract the exception type from any @throws annotations.
     *
     * @param owner The tree element to which the annotations are attached.
     * @param annotations The annotations from which to fetch @throws exception declarations.
     * @return The sequence of throwable types declared using @throws annotations.
     */
    private def extractAnnotatedThrows (owner: Tree, annotations: Seq[Annotation]): Either[ValidationError, Seq[Type]] = {
      /* Filter non-@throws annotations */
      val throwsAnnotations = annotations.filter(_.tree.tpe.typeSymbol == ThrowsClass)

      /* Perform the actual extraction (recursively) */
      @tailrec def extractor (head: Annotation, tail: Seq[Annotation], accum: Seq[Type]): Either[ValidationError, Seq[Type]] = {
        /* Parse this annotation's argument. */
        val parsed = (head.tree.tpe, head.tree.children.tail) match {
          /* Scala 2.9 API: @throws(classOf[Exception]) (which is throws[T](classOf[Exception])) */
          case (_, List(Literal(Constant(tpe: Type)))) => Right(tpe)
            
          /* Scala 2.10 API: @throws[Exception], @throws[Exception]("cause") */
          case (TypeRef(_, _, args), _) if args.size > 0 => Right(args.head)

          /* Unsupported annotation arguments. */
          case _ => Left(InvalidThrowsAnnotation(owner.pos, s"Unsupported @throws annotation '$head' on `$owner`"))
        }
        
        /* On success, recursively parse the next annotation. On failure, return immediately */
        parsed match {
          /* Extraction succeeded */
          case Right(tpe) =>
            if (tail.size == 0) {
              Right(accum :+ tpe)
            } else {
              extractor(tail.head, tail.drop(1), accum :+ tpe)
            }
          case Left(error) => Left(error)
        }
      }

      /* If there are any annotations, extract them */
      if (throwsAnnotations.size > 0)
        extractor(throwsAnnotations.head, throwsAnnotations.drop(1), Vector())
      else
        Right(Vector())
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy