Skip to content

Commit

Permalink
Configurable types (#14)
Browse files Browse the repository at this point in the history
* Support for reading extra types from an optional list of types

* Adding readme
  • Loading branch information
hmemcpy authored Sep 22, 2023
1 parent e62762b commit 79f81ef
Show file tree
Hide file tree
Showing 9 changed files with 127 additions and 29 deletions.
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,39 @@ Running `sbt +install` builds the plugin jar for all compatible Scala versions a

The plugin supports Scala 2.12, 2.13 with Scala 3 support coming soon! The plugin supports both ZIO 1 and ZIO 2.

### Alternative installation method

If desired, add the following to your `build.sbt` to install the plugin in your project:

```scala
addCompilerPlugin("com.hmemcpy" %% "zio-clippy" % <latest version>)
```

(replace with the latest available version from Maven Central)


## Additional configuration

Note: The recommended way to specify additional configuration is via a global sbt configuration file, without directly modifying your project's `build.sbt`.

Create a file `clippy.sbt` in your global sbt directory, `~/.sbt/1.0`. You can specify the options in this file, and they will be loaded automatically by your project.

### Original type mismatch error

To render the original type mismatch error in addition to the plugin output, add the following flag to your `scalacOptions`:

```scala
"-P:clippy:show-original-error"
scalaOptions += "-P:clippy:show-original-error"
```

![](.github/img/full-error.png)

### Additional types for detection

ZIO Clippy support additional, *ZIO-like* types when parsing type mismatch errors. Any type that has 3 type parameters (e.g. `org.company.Saga[R, E, A]`) can be specified. To enable, provide a comma-separated *fully-qualified* list of names to the following option:

```scala
scalaOptions += "-P:clippy:additional-types:zio.flow.ZFlow,org.company.Saga"
```

## Technical information

Expand Down
8 changes: 4 additions & 4 deletions project/ZIOPlugin.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package clippy

import sbt.*
import sbt.Keys.*
import sbt._
import sbt.Keys._

object ZIOPlugin extends AutoPlugin {

Expand All @@ -13,8 +13,8 @@ object ZIOPlugin extends AutoPlugin {
def afterScala2_13_12(scalaVersion: VersionNumber): Boolean =
scalaVersion.matchesSemVer(SemanticSelector(">=2.13.12"))

import autoImport.*
override lazy val projectSettings: Seq[Setting[?]] = Seq(
import autoImport._
override lazy val projectSettings: Seq[Setting[_]] = Seq(
zioPluginJar := Def.taskDyn {
val Some((major, minor)) = CrossVersion.partialVersion(scalaVersion.value)
val versionNumber = VersionNumber(scalaVersion.value)
Expand Down
5 changes: 4 additions & 1 deletion src/main/scala-2.12/clippy/ZIOErrorReporter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import scala.reflect.internal.Reporter
import scala.reflect.internal.util.Position
import scala.tools.nsc.Settings
import scala.tools.nsc.reporters.FilteringReporter
final class ZIOErrorReporter(val settings: Settings, underlying: Reporter, showOriginalError: Boolean)

final class ZIOErrorReporter(val settings: Settings, underlying: Reporter, showOriginalError: Boolean, additionalTypes: List[String])
extends FilteringReporter {

val IsZIOTypeError = new IsZIOTypeErrorExtractor(additionalTypes)
override def doReport(pos: Position, msg: String, severity: Severity): Unit =
severity match {
case Reporter.ERROR =>
Expand Down
12 changes: 12 additions & 0 deletions src/main/scala-2.12/clippy/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,15 @@ package object clippy {
if (cond) Some(a) else None
}
}

package scala.jdk {
object CollectionConverters{

// copied from Scala 2.13 because I don't want to deal with imports
implicit class EnumerationHasAsScala[A](e: java.util.Enumeration[A]) {
def asScala: Iterator[A] = _root_.scala.collection.JavaConverters.enumerationAsScalaIterator(e)
}
}
}


4 changes: 3 additions & 1 deletion src/main/scala-2.13.12+/clippy/ZIOErrorReporter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import scala.tools.nsc.Settings
import scala.tools.nsc.reporters.FilteringReporter

// copied over to avoid dealing with Scala 2.13.12
final class ZIOErrorReporter(val settings: Settings, underlying: Reporter, showOriginalError: Boolean)
final class ZIOErrorReporter(val settings: Settings, underlying: Reporter, showOriginalError: Boolean, additionalTypes: List[String])
extends FilteringReporter {

val IsZIOTypeError = new IsZIOTypeErrorExtractor(additionalTypes)

override def doReport(pos: Position, msg: String, severity: Severity, actions: List[CodeAction]): Unit =
severity match {
case Reporter.ERROR =>
Expand Down
4 changes: 3 additions & 1 deletion src/main/scala-2.13.x/clippy/ZIOErrorReporter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import scala.reflect.internal.Reporter
import scala.reflect.internal.util.Position
import scala.tools.nsc.Settings
import scala.tools.nsc.reporters.FilteringReporter
final class ZIOErrorReporter(val settings: Settings, underlying: Reporter, showOriginalError: Boolean)
final class ZIOErrorReporter(val settings: Settings, underlying: Reporter, showOriginalError: Boolean, additionalTypes: List[String])
extends FilteringReporter {

val IsZIOTypeError = new IsZIOTypeErrorExtractor(additionalTypes)
override def doReport(pos: Position, msg: String, severity: Severity): Unit =
severity match {
case Reporter.ERROR =>
Expand Down
16 changes: 12 additions & 4 deletions src/main/scala-2/clippy/Plugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ final class Plugin(override val global: Global) extends plugins.Plugin {
override val name: String = "clippy"

private val knobs = Map(
"show-original-error" -> "Shows the original Scala type mismatch error"
"show-original-error" -> "Shows the original Scala type mismatch error",
"additional-types" -> "Additional types (fully-qualified) to consider when extracting type mismatches"
)

override val optionsHelp: Option[String] = Some(
Expand All @@ -20,16 +21,23 @@ final class Plugin(override val global: Global) extends plugins.Plugin {
override def init(options: List[String], error: String => Unit): Boolean = {
val (known, unknown) = options.partition(s => knobs.keys.exists(s.startsWith))
if (unknown.nonEmpty) {
error(s"Unknown options: ${unknown.mkString(", ")}")
return false
global.reporter.echo(s"ZIO Clippy - Unknown options: ${unknown.mkString(", ")}")
}

val showOriginalError = known.contains("show-original-error")
val additionalTypes = options.find(_.contains("additional-types")).map(extractAdditionalTypes).getOrElse(Nil)

global.reporter = new ZIOErrorReporter(global.settings, global.reporter, showOriginalError)
global.reporter = new ZIOErrorReporter(global.settings, global.reporter, showOriginalError, additionalTypes)

true
}

private def extractAdditionalTypes(s: String): List[String] =
s.split(":")
.tail
.flatMap(_.split(","))
.map(_.trim)
.toList

override val components: List[PluginComponent] = Nil
}
40 changes: 25 additions & 15 deletions src/main/scala/clippy/utils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ object utils {
val any = "Any"
val anySet = Set(any)

implicit final class AnySyntax[A](private val a: A) extends AnyVal {
def |>[B](f: A => B): B = f(a)
}

sealed trait ErrorKind extends Product with Serializable
object ErrorKind {
case object Overriding extends ErrorKind
Expand Down Expand Up @@ -67,29 +71,35 @@ object utils {

def from(r: String, e: String, a: String) =
Info(
R = r.split(" with ").map(normalize).toSet,
E = e,
A = a
R = r.split(" with ").map(_.trim |> normalize).toSet,
E = e.trim,
A = a.trim
)

def unapply(m: Regex.Match): Option[Info] =
(m.group(1), Option(m.group(2)), Option(m.group(3)), Option(m.group(4))) match {
case ("ZIO" | "ZLayer", Some(r), Some(e), Some(a)) => Some(from(r, e, a))
case ("UIO" | "ULayer", _, _, Some(a)) => Some(from("Any", "Nothing", a))
case ("URIO" | "URLayer", Some(r), _, Some(a)) => Some(from(r, "Nothing", a))
case ("Task", _, _, Some(a)) => Some(from("Any", "Throwable", a))
case ("RIO", Some(r), _, Some(a)) => Some(from(r, "Throwable", a))
case _ => None
(m.group(1), Option(m.group(2)), Option(m.group(3)), Option(m.group(4)), Option(m.group(5))) match {
case ("UIO" | "ULayer", _, _, _, Some(a)) => Some(from("Any", "Nothing", a))
case ("URIO" | "URLayer", _, Some(r), _, Some(a)) => Some(from(r, "Nothing", a))
case ("Task", _, _, _, Some(a)) => Some(from("Any", "Throwable", a))
case ("RIO", Some(r), _, _, Some(a)) => Some(from(r, "Throwable", a))
case (_, _, Some(r), Some(e), Some(a)) => Some(from(r, e, a))
case _ => None
}
}

object IsZIOTypeError {
val mismatch = raw"zio\.(ZIO|ZLayer)\[(.+),([^,\]]+),([^,\]]+)]".r
class IsZIOTypeErrorExtractor(additionalTypes: List[String]) {
val brackets = raw"\[(.+),([^,\]]+),([^,\]]+)]"
val default = raw"zio\.(ZIO|ZLayer)"
val matches = buildRegex
private def buildRegex: Regex = {
val allRegex = default :: additionalTypes.map(_.replace(".", "\\."))
(allRegex.mkString("(", "|", ")") + brackets).r
}

private def findMismatches(msg: String, kind: ErrorKind): Option[(ErrorKind, Info, Info)] =
mismatch.findAllMatchIn(msg).toList match {
case _ :: Info(found) :: _ :: Info(required) :: Nil => Some(kind, found, required)
case Info(found) :: Info(required) :: _ => Some(kind, found, required)
matches.findAllMatchIn(msg).toList match {
case _ :: Info(found) :: _ :: Info(required) :: Nil => Some((kind, found, required))
case Info(found) :: Info(required) :: _ => Some((kind, found, required))
case _ => None
}

Expand Down
40 changes: 39 additions & 1 deletion src/test/scala/clippy/RegexSpec.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package clippy

import clippy.utils.IsZIOTypeError
import clippy.utils.IsZIOTypeErrorExtractor
import zio.Scope
import zio.test._

object RegexSpec extends ZIOSpecDefault {
override def spec: Spec[TestEnvironment with Scope, Any] = suiteAll("type mismatch") {
test("parses the type mismatch error") {
val IsZIOTypeError = new IsZIOTypeErrorExtractor(Nil)

val regex =
raw"""type mismatch;
| found : zio.URIO[zio.Has[persistence.Persistence] with zio.console.Console with zio.console.Console,zio.ExitCode]
Expand All @@ -30,6 +33,8 @@ object RegexSpec extends ZIOSpecDefault {
}

test("parses the return type") {
val IsZIOTypeError = new IsZIOTypeErrorExtractor(Nil)

val regex =
raw"""type mismatch;
| found : zio.ZIO[io.github.gaelrenoux.tranzactio.doobie.Connection with R,E,A]
Expand All @@ -43,5 +48,38 @@ object RegexSpec extends ZIOSpecDefault {

assertTrue(found.A == "A", required.A == "A")
}

test("parses the custom types from config") {
val IsZIOTypeError = new IsZIOTypeErrorExtractor(List("this.is.a.Test"))

val regex =
raw"""type mismatch;
| found : this.is.a.Test[io.github.gaelrenoux.tranzactio.doobie.Connection with R,E,A]
| (which expands to) this.is.a.Test[zio.Has[doobie.util.transactor.Transactor[zio.Task]] with R,E,A]
| required: this.is.a.Test[io.github.gaelrenoux.tranzactio.doobie.Connection with R,Throwable,A]
| (which expands to) this.is.a.Test[zio.Has[doobie.util.transactor.Transactor[zio.Task]] with R,Throwable,A]""".stripMargin

val (found, required) = regex match {
case IsZIOTypeError(_, found, required) => (found, required)
}

assertTrue(found.A == "A", required.A == "A")
}


test("parses simple mismatch") {
val IsZIOTypeError = new IsZIOTypeErrorExtractor(List("this.is.a.Test"))

val regex =
raw"""type mismatch;
| found : this.is.a.Test[Int,Nothing,Int]
| required: this.is.a.Test[Any,String,Int] [11:40]""".stripMargin

val (found, _) = regex match {
case IsZIOTypeError(_, found, required) => (found, required)
}

assertTrue(found.R == Set("Int"))
}
}
}

0 comments on commit 79f81ef

Please sign in to comment.