Skip to content

Commit

Permalink
Add scalafix-interfaces with Java APIs for reflective invocation
Browse files Browse the repository at this point in the history
This commit adds a new 8kb module scalafix-interfaces with Java-only
interfaces exposing a public API to invoke Scalafix via reflection.
This API is intended for build tools such as sbt, gradle or IDEs like
IntelliJ.

This API is inspired by how zinc Java APIs and Dotty Java interfaces
are designed. A document describing alternative approaches can be
seen here
https://docs.google.com/document/d/1Y1MVMjVQ8P25YEI3uvh86gg3n61WZng0hr1qRUS_S6I/edit#heading=h.1fe4wp3hxrrj

This commit is large for two reasons

1. Several large files were split into multiple files with limited changes
2. This commit makes several improvements to the linter and reporting pipeline.

The old reporting pipeline had several problems:

- it was difficult to get the rule name and category id separately
  because they were merged into a single string.
- based on my personal observation, the old `LintCategory` and `LintMessage`
  were not intuitive and several contributed linter rules used them
  in weird ways. I hope the new `LintMessage` interface and `LintID`
  case class makes it more intuitive what the role of category IDs and
  explanations is. Rule authors are able to create custom classes
  that extend `LintMessage`. This design is similar to how Dotty
  error messages are designed and expensive values such as explanations
  can be computed on-demand.
- the ScalafixReporter trait contains a lot of complex methods with
  questionable value. Now it contains a single method matching
  the Java interface API.
- the cli did not have an extensible way to hook into the reporting
  of linter messages. Now it's possible to collect linter messages
  programmatically the same way compiler diagnostics are reported
  via zinc.

This commit introduces *almost* no breaking changes to the public API. All
existing rules and tests in this repo are unchanged.  Users that
previously called `LintMessage.copy` (should be rare) must migrate to
use `LintMessage.EagerLintMessage.copy` instead.
  • Loading branch information
olafurpg committed Aug 7, 2018
1 parent 6a0c120 commit 33a7b3e
Show file tree
Hide file tree
Showing 69 changed files with 1,970 additions and 960 deletions.
17 changes: 16 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ inThisBuild(

noPublish

lazy val interfaces = project
.in(file("scalafix-interfaces"))
.settings(
javaHome.in(Compile) := {
// force javac to fork by setting javaHome to get error messages during compilation,
// see https://github.com/sbt/zinc/issues/520
Some(file(sys.props("java.home")).getParentFile)
},
moduleName := "scalafix-interfaces",
crossVersion := CrossVersion.disabled,
crossScalaVersions := List(scala212),
autoScalaLibrary := false
)

lazy val core = project
.in(file("scalafix-core"))
.settings(
Expand Down Expand Up @@ -51,7 +65,7 @@ lazy val cli = project
"org.apache.commons" % "commons-text" % "1.2"
)
)
.dependsOn(reflect)
.dependsOn(reflect, interfaces)

lazy val testsShared = project
.in(file("scalafix-tests/shared"))
Expand Down Expand Up @@ -123,6 +137,7 @@ lazy val unit = project
libraryDependencies ++= coursierDeps ++ testsDeps,
libraryDependencies ++= List(
jgit,
semanticdbPluginLibrary,
scalatest
),
compileInputs.in(Compile, compile) := {
Expand Down
1 change: 1 addition & 0 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ object Dependencies {
def googleDiff = "com.googlecode.java-diff-utils" % "diffutils" % "1.3.0"

def metacp = "org.scalameta" %% "metacp" % scalametaV
def semanticdbPluginLibrary = "org.scalameta" % "semanticdb-scalac-core" % scalametaV cross CrossVersion.full
def scalameta = "org.scalameta" %% "contrib" % scalametaV
def symtab = "org.scalameta" %% "symtab" % scalametaV
def scalatest = "org.scalatest" %% "scalatest" % "3.2.0-SNAP10"
Expand Down
5 changes: 2 additions & 3 deletions project/ScalafixBuild.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,15 @@ object ScalafixBuild extends AutoPlugin with GhpagesKeys {
lazy val noPublish = Seq(
mimaReportBinaryIssues := {},
mimaPreviousArtifacts := Set.empty,
publishArtifact := false,
publish := {},
publishLocal := {}
skip in publish := true
)
lazy val supportedScalaVersions = List(scala211, scala212)
lazy val isFullCrossVersion = Seq(
crossVersion := CrossVersion.full
)
lazy val warnUnusedImports = "-Ywarn-unused-import"
lazy val compilerOptions = Seq(
"-target:jvm-1.8",
warnUnusedImports,
"-deprecation",
"-encoding",
Expand Down
2 changes: 1 addition & 1 deletion scalafix-cli/src/main/scala/scalafix/cli/ExitStatus.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import scala.collection.mutable

sealed abstract case class ExitStatus(code: Int, name: String) {
def isOk: Boolean = code == ExitStatus.Ok.code
def is(exit: ExitStatus): Boolean = (code & exit.code) != 0
override def toString: String = s"$name=$code"
}

Expand All @@ -26,7 +27,6 @@ object ExitStatus {
val Ok,
UnexpectedError,
ParseError,
ScalafixError,
CommandLineError,
MissingSemanticdbError,
StaleSemanticdbError,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package scalafix.internal.interfaces

import scala.meta.inputs.Position
import scalafix.interfaces.ScalafixDiagnostic
import scalafix.interfaces.ScalafixMainCallback
import scalafix.internal.config
import scalafix.internal.config.ScalafixReporter
import scalafix.internal.util.LintSyntax
import scalafix.lint.LintDiagnostic
import scalafix.lint.LintID
import scalafix.lint.LintSeverity

object MainCallbackImpl {

def default: ScalafixMainCallback = fromScala(config.ScalafixReporter.default)

def fromScala(underlying: config.ScalafixReporter): ScalafixMainCallback =
new ScalafixMainCallback {
override def reportDiagnostic(d: ScalafixDiagnostic): Unit = {
val diagnostic = ScalafixDiagnosticImpl.fromJava(d)
if (diagnostic.id == LintID.empty) {
underlying.report(
diagnostic.message,
diagnostic.position,
diagnostic.severity)
} else {
underlying.lint(diagnostic)
}
}
}

def fromJava(underlying: ScalafixMainCallback): ScalafixReporter =
new ScalafixReporter {
override def lint(d: LintDiagnostic): Unit = {
val diagnostic = ScalafixDiagnosticImpl.fromScala(d)
underlying.reportDiagnostic(diagnostic)
}
def report(msg: String, pos: Position, sev: LintSeverity): Unit = {
val diagnostic = ScalafixDiagnosticImpl.fromScala(
new LintSyntax.EagerLintDiagnostic(msg, pos, sev, "", LintID.empty)
)
underlying.reportDiagnostic(diagnostic)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package scalafix.internal.interfaces

import java.util.Optional
import scala.meta.inputs.Position
import scalafix.interfaces.ScalafixInput
import scalafix.interfaces.ScalafixPosition
import scalafix.internal.util.PositionSyntax._

object PositionImpl {
def optionalFromScala(pos: Position): Optional[ScalafixPosition] =
if (pos == Position.None) Optional.empty()
else Optional.of(fromScala(pos))
def fromScala(pos: Position): ScalafixPosition =
new ScalafixPosition {
override def formatMessage(severity: String, message: String): String =
pos.formatMessage(severity, message)
override def startOffset(): Int = pos.start
override def startLine(): Int = pos.startLine
override def startColumn(): Int = pos.startColumn
override def endOffset(): Int = pos.end
override def endLine(): Int = pos.endLine
override def endColumn(): Int = pos.endColumn
override def input(): ScalafixInput =
ScalafixInputImpl.fromScala(pos.input)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package scalafix.internal.interfaces
import java.util.Optional
import scala.meta.inputs.Input
import scala.meta.inputs.Position
import scalafix.interfaces.ScalafixDiagnostic
import scalafix.interfaces.ScalafixLintID
import scalafix.interfaces.ScalafixPosition
import scalafix.interfaces.ScalafixSeverity
import scalafix.lint.LintDiagnostic
import scalafix.lint.LintID
import scalafix.lint.LintSeverity

object ScalafixDiagnosticImpl {
def fromScala(diagnostic: LintDiagnostic): ScalafixDiagnostic =
new ScalafixDiagnostic {
override def severity(): ScalafixSeverity = diagnostic.severity match {
case LintSeverity.Info => ScalafixSeverity.INFO
case LintSeverity.Warning => ScalafixSeverity.WARNING
case LintSeverity.Error => ScalafixSeverity.ERROR
}
override def message(): String = diagnostic.message
override def explanation(): String = diagnostic.explanation
override def position(): Optional[ScalafixPosition] =
PositionImpl.optionalFromScala(diagnostic.position)
override def lintID(): Optional[ScalafixLintID] =
if (diagnostic.id == LintID.empty) {
Optional.empty()
} else {
Optional.of(new ScalafixLintID {
override def ruleName(): String = diagnostic.id.rule
override def categoryID(): String = diagnostic.id.categoryID
})
}
}

def fromJava(diagnostic: ScalafixDiagnostic): LintDiagnostic =
new LintDiagnostic {
override def message: String = diagnostic.message()
override def position: Position = {
if (diagnostic.position().isPresent) {
val spos = diagnostic.position().get
val input = Input.VirtualFile(
spos.input().filename(),
spos.input().text().toString
)
Position.Range(input, spos.startOffset(), spos.endOffset())
} else {
Position.None
}
}
override def severity: LintSeverity = diagnostic.severity() match {
case ScalafixSeverity.INFO => LintSeverity.Info
case ScalafixSeverity.WARNING => LintSeverity.Warning
case ScalafixSeverity.ERROR => LintSeverity.Error
}
override def explanation: String = diagnostic.explanation()
override def id: LintID = {
if (diagnostic.lintID().isPresent) {
val lintID = diagnostic.lintID().get
LintID(lintID.ruleName(), lintID.categoryID())
} else {
LintID.empty
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package scalafix.internal.interfaces
import scalafix.cli.ExitStatus
import scalafix.interfaces.ScalafixError

object ScalafixErrorImpl {
private lazy val statusToError: Map[ExitStatus, ScalafixError] = {
val ok :: from = ExitStatus.all
assert(ok.isOk)
val to = ScalafixError.values().toList
assert(from.length == to.length, s"$from != $to")
val map = from.zip(to).toMap
map.foreach {
case (key, value) =>
assert(
key.name.toLowerCase() == value.toString.toLowerCase,
s"$key != $value"
)
}
map
}

def fromScala(exit: ExitStatus): Array[ScalafixError] = {
val buf = Array.newBuilder[ScalafixError]
ExitStatus.all.foreach { code =>
if (exit.is(code))
buf += statusToError(code)
}
buf.result()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package scalafix.internal.interfaces

import scalafix.Versions
import scalafix.interfaces.Scalafix
import scalafix.interfaces.ScalafixError
import scalafix.interfaces.ScalafixMainArgs
import scalafix.internal.v1.MainOps

final class ScalafixImpl extends Scalafix {

override def toString: String =
s"""Scalafix v${scalafixVersion()}"""

override def runMain(args: ScalafixMainArgs): Array[ScalafixError] = {
val exit =
MainOps.run(Array(), args.asInstanceOf[ScalafixMainArgsImpl].args)
ScalafixErrorImpl.fromScala(exit)
}

override def newMainArgs(): ScalafixMainArgs =
ScalafixMainArgsImpl()

override def mainHelp(screenWidth: Int): String = {
MainOps.helpMessage(screenWidth)
}

override def scalafixVersion(): String =
Versions.version
override def scalametaVersion(): String =
Versions.scalameta
override def supportedScalaVersions(): Array[String] =
Versions.supportedScalaVersions.toArray
override def scala211(): String =
Versions.scala211
override def scala212(): String =
Versions.scala212
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package scalafix.internal.interfaces

import java.nio.CharBuffer
import java.nio.file.Path
import java.util.Optional
import scala.meta.inputs.Input
import scala.{meta => m}
import scalafix.interfaces.ScalafixInput

object ScalafixInputImpl {
def fromScala(input: m.Input): ScalafixInput =
new ScalafixInput {
override def text(): CharSequence = input match {
case Input.VirtualFile(_, value) => value
case _ => CharBuffer.wrap(input.chars)
}
override def filename(): String = input.syntax
override def path(): Optional[Path] = input match {
case Input.File(path, _) => Optional.of(path.toNIO)
case _ => Optional.empty()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package scalafix.internal.interfaces

import java.io.PrintStream
import java.net.URLClassLoader
import java.nio.charset.Charset
import java.nio.file.Path
import java.util
import metaconfig.Conf
import scala.meta.io.AbsolutePath
import scala.meta.io.Classpath
import scalafix.interfaces.ScalafixMainArgs
import scalafix.interfaces.ScalafixMainCallback
import scalafix.interfaces.ScalafixMainMode
import scalafix.internal.v1.Args
import scala.collection.JavaConverters._

final case class ScalafixMainArgsImpl(args: Args = Args.default)
extends ScalafixMainArgs {

override def withRules(rules: util.List[String]): ScalafixMainArgs =
copy(args = args.copy(rules = rules.asScala.toList))

override def withToolClasspath(
classLoader: URLClassLoader): ScalafixMainArgs =
copy(args = args.copy(toolClasspath = classLoader))

override def withPaths(paths: util.List[Path]): ScalafixMainArgs =
copy(
args = args.copy(
files = paths.asScala.iterator.map(AbsolutePath(_)(args.cwd)).toList)
)

override def withWorkingDirectory(path: Path): ScalafixMainArgs = {
require(path.isAbsolute, s"working directory must be relative: $path")
copy(args = args.copy(cwd = AbsolutePath(path)))
}

override def withConfig(path: Path): ScalafixMainArgs =
copy(args = args.copy(config = Some(AbsolutePath(path)(args.cwd))))

override def withMode(mode: ScalafixMainMode): ScalafixMainArgs = mode match {
case ScalafixMainMode.TEST =>
copy(args = args.copy(test = true))
case ScalafixMainMode.IN_PLACE =>
copy(args = args.copy(stdout = false))
case ScalafixMainMode.STDOUT =>
copy(args = args.copy(stdout = true))
case ScalafixMainMode.AUTO_SUPPRESS_LINTER_ERRORS =>
copy(args = args.copy(autoSuppressLinterErrors = true))
}

override def withArgs(args: util.List[String]): ScalafixMainArgs = {
val decoder = Args.decoder(this.args)
val newArgs = Conf
.parseCliArgs[Args](args.asScala.toList)
.andThen(c => c.as[Args](decoder))
.get
copy(args = newArgs)
}

override def withPrintStream(out: PrintStream): ScalafixMainArgs =
copy(args = args.copy(out = out))

override def withClasspath(path: util.List[Path]): ScalafixMainArgs =
copy(
args = args.copy(
classpath = Classpath(
path.asScala.iterator.map(AbsolutePath(_)(args.cwd)).toList))
)

override def withSourceroot(path: Path): ScalafixMainArgs = {
require(path.isAbsolute, s"sourceroot must be relative: $path")
copy(args = args.copy(sourceroot = Some(AbsolutePath(path)(args.cwd))))
}

override def withMainCallback(
callback: ScalafixMainCallback): ScalafixMainArgs =
copy(args = args.copy(callback = callback))

override def withCharset(charset: Charset): ScalafixMainArgs =
copy(args = args.copy(charset = charset))

}
Loading

0 comments on commit 33a7b3e

Please sign in to comment.