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

ox.plugin.RequireIO.scala Maven / Gradle / Ivy

package ox.plugin

import dotty.tools.dotc.ast.Trees.*
import dotty.tools.dotc.ast.tpd
import dotty.tools.dotc.core.Annotations.Annotation
import dotty.tools.dotc.core.Contexts.Context
import dotty.tools.dotc.core.{Flags, Symbols}
import dotty.tools.dotc.core.Symbols.*
import dotty.tools.dotc.plugins.{PluginPhase, StandardPlugin}
import dotty.tools.dotc.report
import dotty.tools.dotc.transform.{Pickler, PostTyper}
import dotty.tools.dotc.util.Property

class RequireIO extends StandardPlugin:
  val name = "requireIO"
  override val description = "Require the IO capability when a Java method throws an IOException."

  def init(options: List[String]): List[PluginPhase] = new RequireIOPhase(options) :: Nil

class RequireIOPhase(ioLikeExceptionClasses: List[String]) extends PluginPhase:
  import tpd.*

  val phaseName = "requireIO"

  override val runsAfter: Set[String] = Set(PostTyper.name)
  override val runsBefore: Set[String] = Set(Pickler.name)

  override def allowsImplicitSearch: Boolean = true

  // exceptions, which signal that a method performs I/O
  private var ioLikeExceptions: List[Symbol] = _
  private var io: Symbol = _

  override def run(using Context): Unit = {
    ioLikeExceptions = ("java.io.IOException" :: "java.sql.SQLException" :: ioLikeExceptionClasses).map(requiredClass)
    io = requiredClass("ox.IO")

    super.run
  }

  private object ioAvailableProperty extends Property.Key[Boolean]

  override def prepareForDefDef(tree: tpd.DefDef)(using ctx: Context): Context =
    val hasGivenIOParameter = tree.paramss.exists { params =>
      params.exists { param =>
        param.tpe <:< io.namedType && param.mods.is(Flags.Given)
      }
    }

    if hasGivenIOParameter then ctx.withProperty(ioAvailableProperty, Some(true)) else ctx

  private def throwsIOLikeException(a: Annotation)(using ctx: Context): Option[Symbol] =
    if a.symbol == ctx.definitions.ThrowsAnnot then
      a.argument(0).flatMap(thrownException => ioLikeExceptions.find(ioLikeException => thrownException.tpe <:< ioLikeException.namedType))
    else None

  override def transformApply(tree: Apply)(implicit ctx: Context): Tree =
    tree.fun.symbol.annotations
      .flatMap(throwsIOLikeException)
      .headOption // we only want to check once per method
      .foreach: ex =>
        val ctxAtPhase = ctx.withPhase(this) // needed so that inferImplicit works, which checks if the phase allowsImplicitSearch
        val ioAvailableAsImplicit = ctxAtPhase.typer.inferImplicit(io.namedType, EmptyTree, tree.span)(using ctxAtPhase).isSuccess

        if !ioAvailableAsImplicit && !ctxAtPhase.property(ioAvailableProperty).getOrElse(false) then
          report.error(
            s"""The `${tree.fun.symbol.showFullName}` method throws an `${ex.showFullName}`,
               |but the `ox.IO` capability is not available in the implicit scope.
               |
               |Try adding a `using IO` clause to the enclosing method.
               |
               |In tests, you might `import ox.IO.globalForTesting.given`. Alternatively,
               |you can wrap your code with `IO.unsafe`, however this should only be used
               |in special circumstances, as it bypasses Ox's tracking of I/O.""".stripMargin,
            tree.sourcePos
          )

    tree




© 2015 - 2024 Weber Informatics LLC | Privacy Policy