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

com.softwaremill.clippy.ClippyPlugin.scala Maven / Gradle / Ivy

package com.softwaremill.clippy

import java.io.File
import java.net.{URL, URLClassLoader}
import java.util.concurrent.TimeoutException

import scala.collection.JavaConverters._
import scala.concurrent.Await
import scala.concurrent.duration._
import scala.reflect.internal.util.Position
import scala.tools.nsc.Global
import scala.tools.nsc.plugins.Plugin
import scala.tools.nsc.plugins.PluginComponent
import scala.tools.util.PathResolver

class ClippyPlugin(val global: Global) extends Plugin {

  override val name: String = "clippy"

  override val description: String = "gives good advice"

  var url: String = ""
  var colorsConfig: ColorsConfig = ColorsConfig.Disabled
  var testMode = false
  val DefaultStoreDir = new File(System.getProperty("user.home"), ".clippy")
  var localStoreDir = DefaultStoreDir
  var projectRoot: Option[File] = None

  lazy val localAdviceFiles = {
    val classPathURLs = new PathResolver(global.settings).result.asURLs
    val classLoader = new URLClassLoader(classPathURLs.toArray, getClass.getClassLoader)
    classLoader.getResources("clippy.json").asScala.toList
  }

  def handleError(pos: Position, msg: String): String = {
    val advices = loadAdvices(url, localStoreDir, projectRoot, localAdviceFiles)
    val parsedMsg = CompilationErrorParser.parse(msg)
    val matchers = advices.map(_.errMatching.lift)
    val matches = matchers.flatMap(pf => parsedMsg.flatMap(pf)).distinct

    matches.size match {
      case 0 =>
        (parsedMsg, colorsConfig) match {
          case (Some(tme: TypeMismatchError[ExactT]), cc: ColorsConfig.Enabled) => prettyPrintTypeMismatchError(tme, cc)
          case _ => msg
        }
      case 1 =>
        matches.mkString(s"$msg\n Clippy advises: ", "", "")
      case _ =>
        matches.mkString(s"$msg\n Clippy advises you to try one of these solutions: \n   ", "\n or\n   ", "")
    }
  }

  override def processOptions(options: List[String], error: (String) => Unit): Unit = {
    colorsConfig = colorsFromOptions(options)
    url = urlFromOptions(options)
    testMode = testModeFromOptions(options)
    localStoreDir = localStoreDirFromOptions(options)
    projectRoot = projectRootFromOptions(options)

    if (testMode) {
      val r = global.reporter
      global.reporter = new DelegatingReporter(r, handleError, colorsConfig)
    }
  }

  override val components: List[PluginComponent] = List(
    new InjectReporter(handleError, global) {
      override def colorsConfig = ClippyPlugin.this.colorsConfig
      override def isEnabled = !testMode
    }, new RestoreReporter(global) {
      override def isEnabled = !testMode
    }
  )

  private def prettyPrintTypeMismatchError(tme: TypeMismatchError[ExactT], colors: ColorsConfig.Enabled): String = {
    val colorDiff = (s: String) => colors.diff(s).toString
    val plain = new StringDiff(tme.found.toString, tme.required.toString, colorDiff)

    val expandsMsg = if (tme.hasExpands) {
      val reqExpandsTo = tme.requiredExpandsTo.getOrElse(tme.required)
      val foundExpandsTo = tme.foundExpandsTo.getOrElse(tme.found)
      val expands = new StringDiff(foundExpandsTo.toString, reqExpandsTo.toString, colorDiff)
      s"""${expands.diff("\nExpanded types:\nfound   : %s\nrequired: %s\"")}"""
    }
    else
      ""

    s""" type mismatch;
         | Clippy advises, pay attention to the marked parts:
         | ${plain.diff("found   : %s\n required: %s")}$expandsMsg${tme.notesAfterNewline}""".stripMargin
  }

  private def urlFromOptions(options: List[String]): String =
    options.find(_.startsWith("url=")).map(_.substring(4)).getOrElse("https://www.scala-clippy.org") + "/api/advices"

  private def colorsFromOptions(options: List[String]): ColorsConfig = {
    if (boolFromOptions(options, "colors")) {

      def colorToFansi(color: String): fansi.Attrs = color match {
        case "black" => fansi.Color.Black
        case "light-gray" => fansi.Color.LightGray
        case "dark-gray" => fansi.Color.DarkGray
        case "red" => fansi.Color.Red
        case "light-red" => fansi.Color.LightRed
        case "green" => fansi.Color.Green
        case "light-green" => fansi.Color.LightGreen
        case "yellow" => fansi.Color.Yellow
        case "light-yellow" => fansi.Color.LightYellow
        case "blue" => fansi.Color.Blue
        case "light-blue" => fansi.Color.LightBlue
        case "magenta" => fansi.Color.Magenta
        case "light-magenta" => fansi.Color.LightMagenta
        case "cyan" => fansi.Color.Cyan
        case "light-cyan" => fansi.Color.LightCyan
        case "white" => fansi.Color.White
        case "none" => fansi.Attrs.Empty
        case x =>
          global.warning("Unknown color: " + x)
          fansi.Attrs.Empty
      }

      val partColorPattern = "colors-(.*)=(.*)".r
      options.filter(_.startsWith("colors-")).foldLeft(ColorsConfig.defaultEnabled) {
        case (current, partAndColor) =>
          val partColorPattern(part, colorStr) = partAndColor
          val color = colorToFansi(colorStr.trim.toLowerCase())
          part.trim.toLowerCase match {
            case "diff" => current.copy(diff = color)
            case "comment" => current.copy(comment = color)
            case "type" => current.copy(`type` = color)
            case "literal" => current.copy(literal = color)
            case "keyword" => current.copy(keyword = color)
            case "reset" => current.copy(reset = color)
            case x =>
              global.warning("Unknown colored part: " + x)
              current
          }
      }
    }
    else ColorsConfig.Disabled
  }

  private def testModeFromOptions(options: List[String]): Boolean = boolFromOptions(options, "testmode")

  private def boolFromOptions(options: List[String], option: String): Boolean =
    options.find(_.startsWith(s"$option=")).map(_.substring(option.length + 1))
      .getOrElse("false")
      .toBoolean

  private def projectRootFromOptions(options: List[String]): Option[File] =
    options.find(_.startsWith("projectRoot=")).map(_.substring(12))
      .map(new File(_, ".clippy.json"))
      .filter(_.exists())

  private def localStoreDirFromOptions(options: List[String]): File =
    options.find(_.startsWith("store=")).map(_.substring(6)).map(new File(_)).getOrElse(DefaultStoreDir)

  private def loadAdvices(url: String, localStoreDir: File, projectAdviceFile: Option[File], localAdviceFiles: List[URL]): List[Advice] = {
    implicit val ec = scala.concurrent.ExecutionContext.Implicits.global

    try {
      Await
        .result(
          new AdviceLoader(global, url, localStoreDir, projectAdviceFile, localAdviceFiles).load(),
          10.seconds
        )
        .advices
    }
    catch {
      case e: TimeoutException =>
        global.warning(s"Unable to read advices from $url and store to $localStoreDir within 10 seconds.")
        Nil
      case e: Exception =>
        global.warning(s"Exception when reading advices from $url and storing to $localStoreDir: $e")
        Nil
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy