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

Scalafix command for scala-cli with basic optionns and tests #2968

Open
wants to merge 13 commits into
base: main
Choose a base branch
from

Conversation

Vigorge
Copy link

@Vigorge Vigorge commented Jun 17, 2024

Resolves Issue #647

@Vigorge Vigorge marked this pull request as draft June 17, 2024 10:12
@Gedochao
Copy link
Contributor

Hey, thanks for the contribution!
I'll make sure to review this as soon as I am available, but given the volume of code included, I might need a couple days to get to it.

@Gedochao
Copy link
Contributor

Gedochao commented Jun 25, 2024

NOTE: After this is cleaned up and merged, we'll want to merge the scalafix sub-command with the fix sub-command (there's no point to keep them separate).
Individual fix features would probably be enabled/disabled by flags.
We'll do that as a follow-up, no need to worry about it in this PR.

Copy link

@bjaglin bjaglin left a comment

Choose a reason for hiding this comment

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

Thanks for starting this 🤩

I am not familiar with scala-cli at all, but as the maintainer of sbt-scalafix and scalafix, here is some feedback FWIW!

@tgodzik
Copy link
Member

tgodzik commented Jun 26, 2024

Thanks @bjaglin!

@Vigorge if you are at any point overwhelmed by amount of comments, we are here to help. You already did a lot of great work 😍

@Vigorge
Copy link
Author

Vigorge commented Jul 4, 2024

Thanks @bjaglin!

@Vigorge if you are at any point overwhelmed by amount of comments, we are here to help. You already did a lot of great work 😍

Thanks a lot for such an impactful feedback! These comments are really helpful and I'll surely take them into account. I really hope that I will resolve them in a few days and add some more features. Unfortunately, I had to take a little break, but now am fully committed to work more on this PR now

@Gedochao
Copy link
Contributor

Gedochao commented Aug 8, 2024

Not sure why didn't the CI run, but a rebase from main seems to be necessary, anyway.

@SethTisue
Copy link
Contributor

Great to see activity on this — I think this could be big for Scalafix, in terms of people being able to use it more easily.

@Vigorge Vigorge marked this pull request as ready for review August 12, 2024 12:27
@Vigorge
Copy link
Author

Vigorge commented Aug 12, 2024

Hi, sorry for taking such a long time to resolve all those threads and thanks a lot again for such feedback.

The PR is ready for review.

@bjaglin
Copy link

bjaglin commented Aug 12, 2024

Thanks for the follow-ups @Vigorge 👍 I will take a look at the PR tomorrow.

Copy link

@bjaglin bjaglin left a comment

Choose a reason for hiding this comment

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

Hey @Vigorge, thanks for all the follow-ups/additions. Having a semantic rule tested against all Scala versions is a great achievement!

I went through a thorough second review to avoid any surprise in the third/final round. From my very own perspective (again, I am not familiar with scala-cli), here are the outstanding comments to be addressed before this could be released

  • workspace / current directory injection seems broken - I did not manage to use the CLI
  • diagnostics/linters are ignored under --check
  • using scalafix-interfaces with native-image is impossible and raises a cryptic error - a more actionnable error message should be added until this is sorted out in a further PR

case CommandLineError =>
"A command-line argument was parsed incorrectly"
case MissingSemanticdbError =>
"A semantic rewrite was run on a source file that has no associated META-INF/semanticdb/.../*.semanticdb"
Copy link

Choose a reason for hiding this comment

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

nit: all scalafix rules are not rewrites

Suggested change
"A semantic rewrite was run on a source file that has no associated META-INF/semanticdb/.../*.semanticdb"
"A semantic rule was run on a source file that has no associated META-INF/semanticdb/.../*.semanticdb"

evaluation.getErrorMessage.toScala.toList
}
else
customScalafixInstance.run().map(prepareErrorMessage).toList
Copy link

@bjaglin bjaglin Aug 13, 2024

Choose a reason for hiding this comment

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

Scalafix already outputs human readable messages to stdout by default, so users will get duplicate error messages.

$ launcher --power scalafix Test.scala
The `scalafix` sub-command is experimental
Please bear in mind that non-ideal user experience should be expected.
If you encounter any bugs or have feedback to share, make sure to reach out to the maintenance team at https://github.com/VirtusLab/scala-cli
No rules requested to run
No rules were provided to Scalafix so nothing happened

The scalafix framework output (not to be mixed up with the scalafix rules effective output, see comment above) can be silenced or re-routed via withPrintStream(). As the internal log entries can carry more information than the status code, I would recommend removing the custom error messages, and either mapping a PrintStream delegating to the scala-cli logger (that's what sbt-scalafix does) or actually simply keeping the default PrintStream (stdout), for the same reason as in the comment above.

test("external rule") {
val unnamedParamsInputsContent: String =
"""//> using options -P:semanticdb:synthetics:on
|//> using compileOnly.dep "com.github.jatcwang::scalafix-named-params:0.2.4"
Copy link

Choose a reason for hiding this comment

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

Will possibility of loading custom rules via compileOnly.dep be obvious to a scala-cli user? It wasn't to me, so a mention of it in the docs may be useful.


@Group(HelpGroup.Format.toString)
@Tag(tags.experimental)
@HelpMessage("Fail the invocation instead of applying fixes")
Copy link

Choose a reason for hiding this comment

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

nit: some rules won't emit rewrites (they are just linters), in which case --check does not change anything, so I would be more specific here

Suggested change
@HelpMessage("Fail the invocation instead of applying fixes")
@HelpMessage("Fail the invocation if rewrites are needed")

implicit lazy val help: Help[ScalafixOptions] = Help.derive

val cmdName = "scalafix"
private val helpHeader = "Fixes Scala code according to scalafix rules."
Copy link

Choose a reason for hiding this comment

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

nit: adding the linter aspect of scalafix to the description

Suggested change
private val helpHeader = "Fixes Scala code according to scalafix rules."
private val helpHeader = "Run Scalafix rules to lint or rewrite Scala code."

Comment on lines 139 to 142
val externalDeps =
options.shared.dependencies.compileOnlyDependency ++ successfulBuildOpt.map(
_.options.classPathOptions.extraCompileOnlyDependencies.values.flatten.map(_.value.render)
).getOrElse(Seq.empty)
Copy link

Choose a reason for hiding this comment

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

Not blocking for this PR as the experimental flag allows iterating

Even if that's likely the most harmless way to get scala-cli to resolve an artifact (either via a directive or the corresponding CLI flag I assume), this approach has several limitations

  • it might impact the build by evicting some transient dependencies
  • the fact that this is not in the base class probably shows, but it won't allow loading custom rules when running against Scala 3 sources (unless there is some kind of fallback to 2.13 artifacts built-in?), since for now Scalafix is not cross-published to Scala 3, as the mapping above shows

val scalafix = ScalafixInterface
.fetchAndClassloadInstance(scalaBinaryVersion)
.newArguments()
.withWorkingDirectory(workspace.toNIO)
Copy link

Choose a reason for hiding this comment

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

workspace seems to reference an ephemeral directory that does not contain (or no longer contains?) the source file when I run the output of ./mill -i show 'cli[].launcher' against a source in a random directory

➜  /tmp cat Test.scala
//> using options -Wunused:all

package foo

object Hello {
  def main(args: Array[String]): Unit = {
    val name = "John"
    println("Hello")
  }
}
➜  /tmp /Users/brice.jaglin/git/scala/scala-cli/out/cli/3.3.3/launcher.dest/launcher --power scalafix --rules=RemoveUnused ./Test.scala
Some utilized features are marked as experimental:
 - `scalafix` sub-command
 - `--rules=RemoveUnused` option
Please bear in mind that non-ideal user experience should be expected.
If you encounter any bugs or have feedback to share, make sure to reach out to the maintenance team at https://github.com/VirtusLab/scala-cli
[warn] ./Test.scala:7:9
[warn] unused local definition
[warn]     val name = "John"
[warn]         ^^^^
error: /Users/brice.jaglin/Library/Caches/ScalaCli/virtual-projects/a7/project-c4a6da38/Test.scala is not a file
Something unexpected happened running Scalafix

Comment on lines 76 to 79
val s = inputs.sourceFiles().collect {
case sc: Script => sc.path
case sc: SourceScalaFile => sc.path
}
Copy link

Choose a reason for hiding this comment

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

nit: scalafix is doing the same check (I think), so I'd remove this, in case we add support for other extensions upstream

val relPaths = sourcePaths.map(_.toNIO.getFileName)

val scalafix = ScalafixInterface
.fetchAndClassloadInstance(scalaBinaryVersion)
Copy link

Choose a reason for hiding this comment

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

Unsurprisingly, this fails when using the native-image bundle because of the dynamic nature of scalafix-interfaces.

➜  /tmp /Users/brice.jaglin/git/scala/scala-cli/out/cli/3.3.3/base-image/nativeImage.dest/scala-cli --power scalafix Test.scala
The `scalafix` sub-command is experimental
Please bear in mind that non-ideal user experience should be expected.
If you encounter any bugs or have feedback to share, make sure to reach out to the maintenance team at https://github.com/VirtusLab/scala-cli
Error: scalafix.interfaces.ScalafixException: Failed to load 'scalafix-interfaces.properties' to lookup versions
For more details, please see '/private/tmp/.scala-build/stacktraces/1723547209-8851074228942378765.log'
➜  /tmp cat /private/tmp/.scala-build/stacktraces/1723546008-8191378505818715998.log
scalafix.interfaces.ScalafixException: Failed to load 'scalafix-interfaces.properties' to lookup versions
  scalafix.interfaces.Scalafix.fetchAndClassloadInstance(Scalafix.java:126)
  scalafix.interfaces.Scalafix.fetchAndClassloadInstance(Scalafix.java:85)
  scala.cli.commands.scalafix.Scalafix$.runCommand(Scalafix.scala:100)
  scala.cli.commands.scalafix.Scalafix$.runCommand(Scalafix.scala:56)
  scala.cli.commands.ScalaCommand.run(ScalaCommand.scala:388)
  scala.cli.commands.ScalaCommand.run(ScalaCommand.scala:369)
  caseapp.core.app.CaseApp.main(CaseApp.scala:157)
  scala.cli.commands.ScalaCommand.main(ScalaCommand.scala:354)
  caseapp.core.app.CommandsEntryPoint.main(CommandsEntryPoint.scala:169)
  scala.cli.ScalaCliCommands.main(ScalaCliCommands.scala:126)
  scala.cli.ScalaCli$.main0(ScalaCli.scala:295)
  scala.cli.ScalaCli$.main(ScalaCli.scala:119)
  scala.cli.ScalaCli.main(ScalaCli.scala)
java.lang.NullPointerException: inStream parameter is null
  java.base@17.0.6/java.util.Objects.requireNonNull(Objects.java:233)
  java.base@17.0.6/java.util.Properties.load(Properties.java:407)
  scalafix.interfaces.Scalafix.fetchAndClassloadInstance(Scalafix.java:124)
  scalafix.interfaces.Scalafix.fetchAndClassloadInstance(Scalafix.java:85)
  scala.cli.commands.scalafix.Scalafix$.runCommand(Scalafix.scala:100)
  scala.cli.commands.scalafix.Scalafix$.runCommand(Scalafix.scala:56)
  scala.cli.commands.ScalaCommand.run(ScalaCommand.scala:388)
  scala.cli.commands.ScalaCommand.run(ScalaCommand.scala:369)
  caseapp.core.app.CaseApp.main(CaseApp.scala:157)
  scala.cli.commands.ScalaCommand.main(ScalaCommand.scala:354)
  caseapp.core.app.CommandsEntryPoint.main(CommandsEntryPoint.scala:169)
  scala.cli.ScalaCliCommands.main(ScalaCliCommands.scala:126)
  scala.cli.ScalaCli$.main0(ScalaCli.scala:295)
  scala.cli.ScalaCli$.main(ScalaCli.scala:119)
  scala.cli.ScalaCli.main(ScalaCli.scala)

We could probably fix these property lookups by telling GraalVM about it, but the classloading just after would fail anyway.

I am afraid we will need to run Scalafix through the scalafix-cli launcher (like scalafmt apparently?) instead of scalafix-interfaces. It would be a bit more cumbersome to pass CLI args, and a couple of advanced features wouldn't be available, but it should work overall. Or maybe there is a way to classload Scalafix within bloop somehow @tgodzik ? It would have the benefit of keeping classloaders warm across invocations.

Anyway, to unlock this PR, I guess we could add an early return if the native-image runtime is detected, and handle native-image compatibility in a later PR? I assume the native-image bundle is the most popular distribution channel though... 😢

Copy link
Author

Choose a reason for hiding this comment

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

Ouch! You're right, it seems that it won't make a lot of sence if it doens't work in native image.
I'll try to replace interfaces by scalafix-cli launcher and see how it is going to turn out.

Comment on lines 159 to 167
val evaluation = customScalafixInstance.evaluate()
if (evaluation.isSuccessful)
evaluation.getFileEvaluations.foldLeft(List.empty[String]) {
case (errors, fileEvaluation) =>
val problemMessage = fileEvaluation.getErrorMessage.toScala.orElse(
fileEvaluation.previewPatchesAsUnifiedDiff.toScala
)
errors ++ problemMessage
}
Copy link

@bjaglin bjaglin Aug 13, 2024

Choose a reason for hiding this comment

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

With this implementation, --check effectively ignores diagnostics (linters)

➜  /tmp cat Test.scala
package foo

final object Hello {
  def main(args: Array[String]): Unit = {
    val name = null
  }
}

Expected failure (no --check)

➜  /tmp /Users/brice.jaglin/git/scala/scala-cli/out/cli/3.3.3/launcher.dest/launcher --power scalafix --rules=DisableSyntax --scalafix-arg=--settings.DisableSyntax.noNulls=true Test.scala && echo OK
Some utilized features are marked as experimental:
 - `scalafix` sub-command
 - `--rules=DisableSyntax` option
 - `--scalafix-arg=--settings.DisableSyntax.noNulls=true` option
Please bear in mind that non-ideal user experience should be expected.
If you encounter any bugs or have feedback to share, make sure to reach out to the maintenance team at https://github.com/VirtusLab/scala-cli
/private/tmp/Test.scala:5:16: error: [DisableSyntax.null] null should be avoided, consider using Option instead
    val name = null
               ^^^^
A Scalafix linter error was reported

Unexpected success (with --check)

➜  /tmp /Users/brice.jaglin/git/scala/scala-cli/out/cli/3.3.3/launcher.dest/launcher --power scalafix --rules=DisableSyntax --scalafix-arg=--settings.DisableSyntax.noNulls=true Test.scala --check && echo OK
Some utilized features are marked as experimental:
 - `scalafix` sub-command
 - `--rules=DisableSyntax` option
 - `--scalafix-arg=--settings.DisableSyntax.noNulls=true` option
 - `--check` option
Please bear in mind that non-ideal user experience should be expected.
If you encounter any bugs or have feedback to share, make sure to reach out to the maintenance team at https://github.com/VirtusLab/scala-cli
OK

Instead of evaluating manually and reconstructing logs from lints/rewrites in each file (which might result in missing upcoming features), you should call run() with the --check flag. To capture output, you can redirect the ScalafixMainCallback (effectively the output of the scalafix rules) to the scala-cli logger (that's what sbt-scalafix does), or probably more pragmatic, just keep the default which is dumping to stdout, since moving to scalafix-cli for native-image (see other comment) won't allow this level of integration in the future anyway.

Copy link

Choose a reason for hiding this comment

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

This shows that invocation output (either with or without --check for a linter rule such as DisableSyntax, and with --check for a rewrite rule such as RedundantSyntax) should be asserted in the tests.

@Gedochao
Copy link
Contributor

@Vigorge how's the progress on this? Can we unblock you somehow?

@Vigorge
Copy link
Author

Vigorge commented Sep 17, 2024

@Vigorge how's the progress on this? Can we unblock you somehow?

HI, still am working on the native image solution and gonna add feature in few days.

Thanks for support! I will comment here if some help will be needed.

@dos65
Copy link

dos65 commented Oct 16, 2024

@Gedochao Hi, we did changes in MR to make scalafix work both in jvm and native builds. Could you approve CI run?

@dos65
Copy link

dos65 commented Oct 18, 2024

Update. I need one additionial iteration to go trough comments from @bjaglin
(add a bit more docs and tests) but in general I hope the PR is close to completion.

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.

scalafix command to run library migration & hygiene rules
6 participants