Skip to content
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

Merged
merged 3 commits into from
Aug 9, 2018

Conversation

olafurpg
Copy link
Contributor

@olafurpg olafurpg commented Aug 7, 2018

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

  1. Several large files were split into multiple files with almost no code 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 internal 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.
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.

}

test("runMain") {
// This is a full integration test that stresses the full breadth of the scalafix-interfaces API
Copy link
Contributor Author

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 {
Copy link
Contributor Author

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 {
Copy link
Contributor Author

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 {
Copy link
Contributor Author

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 { ... }

Copy link
Contributor Author

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@olafurpg olafurpg force-pushed the interfaces branch 2 times, most recently from 0386f17 to 33a7b3e Compare August 7, 2018 16:06
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
Copy link
Contributor

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.

Copy link
Contributor Author

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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example: RemoveUnusedImport

Copy link
Contributor Author

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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example?

Copy link
Contributor

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.

Copy link
Contributor Author

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);
Copy link
Contributor

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?

Copy link
Contributor Author

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);
Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

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
Copy link
Contributor

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?

Copy link
Contributor Author

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.
Copy link
Contributor

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

Copy link
Contributor Author

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 = {
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

@olafurpg olafurpg left a 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
Copy link
Contributor Author

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);
Copy link
Contributor Author

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);
Copy link
Contributor Author

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();
Copy link
Contributor Author

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();
Copy link
Contributor Author

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 = {
Copy link
Contributor Author

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.
Copy link
Contributor Author

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
Copy link
Contributor Author

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
Copy link
Contributor Author

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.

@olafurpg
Copy link
Contributor Author

olafurpg commented Aug 8, 2018

@ShaneDelmore @xeno-by I am curious to hear your thoughts about the new public Java interface API. The internal changes for ScalafixReporter are less interesting and require less scrutiny, most of the work there was to synchronize the internal implementation with the public Java interface API.

@olafurpg
Copy link
Contributor Author

olafurpg commented Aug 9, 2018

Gonna merge to unblock a new milestone release, happy to address feedback in followup PRs.

@olafurpg olafurpg merged commit d1b7ff1 into scalacenter:master Aug 9, 2018
@olafurpg olafurpg deleted the interfaces branch August 9, 2018 05:42
olafurpg added a commit to olafurpg/sbt-scalafix that referenced this pull request Aug 9, 2018
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants