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

com.tkroman.puree.Puree.scala Maven / Gradle / Ivy

package com.tkroman.puree

import java.util.concurrent.ConcurrentHashMap
import java.util.function
import scala.annotation.tailrec
import scala.tools.nsc.plugins.{Plugin, PluginComponent}
import scala.tools.nsc.{Global, Phase}
import com.tkroman.puree.Puree.Levels
import com.tkroman.puree.annotation.intended

object Puree {
  object Levels {
    val Off: Int = 0
    val Effects: Int = 1
    val Strict: Int = 2
  }
}

class Puree(val global: Global) extends Plugin {
  override val name: String = "puree"
  override val description: String = "Warn about unused effects"

  private val DefaultLevel = "effects"
  private val LevelKey: String = "level"
  private val AllLevels: Map[String, Int] = Map(
    "off" -> Levels.Off,
    DefaultLevel -> Levels.Effects,
    "strict" -> Levels.Strict
  )
  private val Usage: String =
    s"Available choices: ${AllLevels.keySet.mkString("|")}. Usage: -P:$name:$LevelKey:$$LEVEL"

  private var level: Int = Levels.Effects

  def getLevel: Int = level

  override def init(options: List[String], error: String => Unit): Boolean = {
    val suggestedLevel: Option[String] = options
      .find(_.startsWith(LevelKey))
      .map(_.stripPrefix(s"$LevelKey:"))
    suggestedLevel match {
      case Some(s) if AllLevels.isDefinedAt(s) =>
        level = AllLevels(s)
      case Some(s) =>
        error(
          s"Puree: invalid strictness level [$s]. $Usage"
        )
      case None => // default
    }

    level != Levels.Off
  }

  override lazy val components: List[UnusedEffectDetector] = List(
    new UnusedEffectDetector(this, global)
  )
}

class UnusedEffectDetector(puree: Puree, val global: Global)
    extends PluginComponent {
  import global._

  override val runsAfter: List[String] = List("typer")
  override val runsBefore: List[String] = List("patmat")
  override val phaseName: String = "puree-checker"

  private val UnitType: global.Type = typeOf[Unit]

  override def newPhase(prev: Phase): Phase = new StdPhase(prev) {
    override def apply(unit: CompilationUnit): Unit = {
      val tt: Traverser = new Traverser {
        override def traverse(tree: Tree): Unit = {
          val descend: Boolean = tree match {
            case t if intended(t) =>
              false
            case Template(_, _, body) =>
              noSuspiciousEffects(body)
            case Block(stats, _) =>
              noSuspiciousEffects(stats)
            case _ =>
              true
          }
          if (descend) {
            super.traverse(tree)
          }
        }
      }
      tt.traverse(unit.body)
    }
  }

  private def getEffect(a: Tree): Option[Type] = {
    a match {
      case _ if isSuperConstructorCall(a) =>
        // in constructors, calling super.
        // when super is an F[_, _*] is seen as an
        // unassigned effectful value :(
        None

      case _ =>
        Option(a.tpe).flatMap { tpe =>
          if (strict() && !(tpe =:= UnitType)) {
            // under strict settings we abort on any non-unit method
            // (assuming unit is always side-effecting)
            Some(tpe)
          } else if (tpe.typeSymbol.typeParams.nonEmpty) {
            Some(tpe)
          } else {
            val bts: List[Type] = tpe.baseTypeSeq.toList
            // looking at basetypeseq b/c None is an Option[A]
            if (bts.exists(bt => bt.typeSymbol.isSealed)) {
              bts.find(bt => bt.typeSymbol.typeParams.nonEmpty)
            } else {
              // Only F-bounded because if we just look for
              // ANY non-empty F[_] in baseTypeSeq b/c
              // e.g. String is Comparable[String] :/
              // FIXME: is it better to list (and allow for configuration)
              // FIXME: the set of "ok" F[_]s? Comparable etc
              bts.find(_.typeSymbol.typeParams.exists(_.isFBounded))
            }
          }
        }
    }
  }

  private def noSuspiciousEffects(stats: List[Tree]): Boolean = {
    def isOkUsage(e: Option[Type], warning: Type => Unit): Boolean = {
      e match {
        case Some(a) =>
          warning(a)
          false
        case None =>
          true
      }
    }

    stats.forall {
      case s if intended(s) =>
        true
      case a: Apply =>
        isOkUsage(
          getEffect(a),
          eff =>
            reporter.warning(
              a.pos,
              s"Unused effectful function call of type $eff"
            )
        )
      case t if t.isTerm =>
        isOkUsage(
          getEffect(t),
          eff =>
            reporter.warning(
              t.pos,
              s"Unused effectful member reference of type $eff"
            )
        )

      case _ =>
        true
    }
  }

  @tailrec
  private def isSuperConstructorCall(t: Tree): Boolean = {
    t match {
      case Apply(Select(Super(This(_), _), _), _) => true
      case Apply(nt, _)                           => isSuperConstructorCall(nt)
      case _                                      => false
    }
  }

  private def strict(): Boolean = {
    puree.getLevel == Levels.Strict
  }

  private def intended(a: global.Tree): Boolean = {
    def check(scrutinee: Annotatable[_]): Boolean =
      scrutinee.annotations.exists(_.tpe == typeOf[intended])

    if (a.isType && a.hasSymbolField) {
      check(a.symbol)
    } else if (a.isTerm) {
      check(a.tpe)
    } else if (a.isDef && a.hasSymbolField) {
      check(a.symbol)
    } else {
      false
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy