-
Notifications
You must be signed in to change notification settings - Fork 185
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add scalafix-interfaces with Java APIs for reflective invocation
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
Showing
69 changed files
with
1,970 additions
and
960 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
46 changes: 46 additions & 0 deletions
46
scalafix-cli/src/main/scala/scalafix/internal/interfaces/MainCallbackImpl.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
|
||
} |
26 changes: 26 additions & 0 deletions
26
scalafix-cli/src/main/scala/scalafix/internal/interfaces/PositionImpl.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
66 changes: 66 additions & 0 deletions
66
scalafix-cli/src/main/scala/scalafix/internal/interfaces/ScalafixDiagnosticImpl.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} | ||
} |
30 changes: 30 additions & 0 deletions
30
scalafix-cli/src/main/scala/scalafix/internal/interfaces/ScalafixErrorImpl.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
37 changes: 37 additions & 0 deletions
37
scalafix-cli/src/main/scala/scalafix/internal/interfaces/ScalafixImpl.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
23 changes: 23 additions & 0 deletions
23
scalafix-cli/src/main/scala/scalafix/internal/interfaces/ScalafixInputImpl.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} | ||
} |
83 changes: 83 additions & 0 deletions
83
scalafix-cli/src/main/scala/scalafix/internal/interfaces/ScalafixMainArgsImpl.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
|
||
} |
Oops, something went wrong.