-
Notifications
You must be signed in to change notification settings - Fork 185
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add scalafix-interfaces with Java APIs for reflective invocation #783
Conversation
} | ||
|
||
test("runMain") { | ||
// This is a full integration test that stresses the full breadth of the scalafix-interfaces API |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is a full integration test for the new reflective API. Gives an idea how it will be used.
* @throws ScalafixException in case of errors during classloading, most likely caused | ||
* by an incorrect classloader argument. | ||
*/ | ||
static Scalafix classloadInstance(ClassLoader classLoader) throws ScalafixException { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the entrypoint for reflectively loading a Scalafix
instance.
* @implNote This interface is not intended for extension, the only implementation of this interface | ||
* should live in the Scalafix repository. | ||
*/ | ||
public interface ScalafixMainArgs { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Builder interface to construct arguments for the scalafix cli.
else "" | ||
|
||
s"[${category.id}] $message$explanation" | ||
trait LintMessage { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LintMessage
became a trait so that it's possible (but not necessary) to implement custom message types
class UnusedCode(val position: Position) extends LintMessage { ... }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question whether basic values should have default implementations 🤔 For example categoryID
and explanation
could be empty string and severity
could default to error. I'll go ahead and update that ASAP.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
0386f17
to
33a7b3e
Compare
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.
@@ -0,0 +1,66 @@ | |||
package scalafix.internal.interfaces | |||
import java.util.Optional |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
newline between 1 and 2 plz.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Re-opened scalameta/scalafmt#1069 This started happening more frequently now because intellij-scala uses Scalafmt after refactorings.
/** | ||
* @return The name of the rule that produced this diagnostic. | ||
*/ | ||
String ruleName(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For example: RemoveUnusedImport
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
* Empty if the rule only reports diagnostics of a single | ||
* category. | ||
*/ | ||
String categoryID(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Example?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess in Disable.get
, get
is the categoryID
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
* @param paths Files and directories to run Scalafix on. Directories are recursively expanded | ||
* for files matching the patterns <code>*.scala</code> and <code>*.sbt</code>. | ||
*/ | ||
ScalafixMainArgs withPaths(List<Path> paths); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hum, how is it expanded? How do you exclude directories?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added withExcludedPaths
and updated the docstring to mention it's recommended to pass in only files with this programmatic API. Directory expansion is only supported to make cli invocations ergonomic.
* | ||
* @param diagnostic the reported diagnostic. | ||
*/ | ||
void reportDiagnostic(ScalafixDiagnostic diagnostic); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bit limited compared to what scalafix can do.
It would be useful to have a list of files that were modified. Ideally, it would be useful to have interactive rewrites where the user can make a choice. For example for collection.Seq in the new collection design.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, I want to expand this interface with handlers for fixing files but I decided to leave it for the future since currently I only need to hook into linter messages from sbt-scalafix. I opened #784 to continue the discussion
|
||
def check(name: String, original: String, expected: String): Unit = { | ||
test(name) { | ||
val marker = "@@".r |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we reuse this: https://github.com/scalameta/scalameta/blob/master/tests/shared/src/test/scala/scala/meta/tests/tokenizers/BaseTokenizerCoverageSuite.scala ?
It looks really neat:
https://github.com/scalameta/scalameta/blob/master/tests/shared/src/test/scala/scala/meta/tests/tokenizers/TokenizerCoverageSuite.scala
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I personally dislike unicode characters since they're inconvenient to write. However, I refactored the test to use
val startMarker = '→'
val stopMarker = '←'
for consistency with the Scalameta tests. The extraction implementation stayed unchanged.
@@ -44,15 +44,22 @@ class CliSemanticSuite extends BaseCliSuite { | |||
} | |||
) | |||
|
|||
checkSemantic( | |||
name = "MissingSemanticDB", | |||
args = Array(), // no --classpath |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we keep this comment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
Files.createDirectories(d) | ||
Files.createDirectories(src) | ||
val semicolon = src.resolve("Semicolon.scala") | ||
// This rule is published to Maven Central to simplify testing --tool-classpath. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer to see this in the build.
I would: add example-scalafix-rule project and make the test task depends on publishLocal of example-scalafix-rule
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree it would be good to bake this into the build but I opted for this because it's closer to the environment end-users will use this functionality (I've been bitten by different in custom build environments and end-user environments).
I want this integration test to be as self-contained as possible so that readers don't have to read build.sbt to understand what's going on. I can imagine this test case will be a good reference point for people who want to write unit tests when integrating with Scalafix.
import scalafix.testkit.DiffAssertions | ||
|
||
class ScalafixImplSuite extends FunSuite with DiffAssertions { | ||
def semanticdbPluginPath(): String = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we get this from BuildInfo?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BuildInfo doesn't robustly support task generators, we could make it a resource generator but that would make the test harder to read IMO. I inlined it here since that makes the test more self-contained (instead of magically pulling in info from the build) and avoids the need to regenerate this information on every compile/run instead of only when we run ScalafixImplSuite
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the review @MasseGuillaume !
@@ -0,0 +1,66 @@ | |||
package scalafix.internal.interfaces | |||
import java.util.Optional |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Re-opened scalameta/scalafmt#1069 This started happening more frequently now because intellij-scala uses Scalafmt after refactorings.
* @param paths Files and directories to run Scalafix on. Directories are recursively expanded | ||
* for files matching the patterns <code>*.scala</code> and <code>*.sbt</code>. | ||
*/ | ||
ScalafixMainArgs withPaths(List<Path> paths); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added withExcludedPaths
and updated the docstring to mention it's recommended to pass in only files with this programmatic API. Directory expansion is only supported to make cli invocations ergonomic.
* | ||
* @param diagnostic the reported diagnostic. | ||
*/ | ||
void reportDiagnostic(ScalafixDiagnostic diagnostic); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, I want to expand this interface with handlers for fixing files but I decided to leave it for the future since currently I only need to hook into linter messages from sbt-scalafix. I opened #784 to continue the discussion
/** | ||
* @return The name of the rule that produced this diagnostic. | ||
*/ | ||
String ruleName(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
* Empty if the rule only reports diagnostics of a single | ||
* category. | ||
*/ | ||
String categoryID(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
import scalafix.testkit.DiffAssertions | ||
|
||
class ScalafixImplSuite extends FunSuite with DiffAssertions { | ||
def semanticdbPluginPath(): String = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BuildInfo doesn't robustly support task generators, we could make it a resource generator but that would make the test harder to read IMO. I inlined it here since that makes the test more self-contained (instead of magically pulling in info from the build) and avoids the need to regenerate this information on every compile/run instead of only when we run ScalafixImplSuite
.
Files.createDirectories(d) | ||
Files.createDirectories(src) | ||
val semicolon = src.resolve("Semicolon.scala") | ||
// This rule is published to Maven Central to simplify testing --tool-classpath. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree it would be good to bake this into the build but I opted for this because it's closer to the environment end-users will use this functionality (I've been bitten by different in custom build environments and end-user environments).
I want this integration test to be as self-contained as possible so that readers don't have to read build.sbt to understand what's going on. I can imagine this test case will be a good reference point for people who want to write unit tests when integrating with Scalafix.
@@ -44,15 +44,22 @@ class CliSemanticSuite extends BaseCliSuite { | |||
} | |||
) | |||
|
|||
checkSemantic( | |||
name = "MissingSemanticDB", | |||
args = Array(), // no --classpath |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
|
||
def check(name: String, original: String, expected: String): Unit = { | ||
test(name) { | ||
val marker = "@@".r |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I personally dislike unicode characters since they're inconvenient to write. However, I refactored the test to use
val startMarker = '→'
val stopMarker = '←'
for consistency with the Scalameta tests. The extraction implementation stayed unchanged.
@ShaneDelmore @xeno-by I am curious to hear your thoughts about the new public Java interface API. The internal changes for |
Gonna merge to unblock a new milestone release, happy to address feedback in followup PRs. |
This upgrade introduced a new Java API in a module scalafix-interfaces that exposes a public API for build tools and IDEs scalacenter/scalafix#783. This means sbt-scalafix no longer needs to invoke the fragile `main(args: Array[String]): Unit` interface. This also means it's possible to hook into the linter reporting pipeline and use the sbt logger for diagnostics.
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 PR is large for two reasons
The old reporting pipeline had several problems:
because they were merged into a single string.
LintCategory
andLintMessage
were not intuitive and several contributed linter rules used them
in weird ways. I hope the new
LintMessage
interface andLintID
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 Dottyerror messages are designed and expensive values such as explanations
can be computed on-demand.
questionable value. Now it contains a single method matching
the Java interface API.
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.
Existing rules and tests in this repo are unchanged. Users that
previously called
LintMessage.copy
(should be rare) must migrate touse
LintMessage.EagerLintMessage.copy
instead.