Skip to content

Commit

Permalink
Support OrganizeImports.removeUnused in Scala 3
Browse files Browse the repository at this point in the history
Once this PR scala/scala3#17835
has merged and released, scalafix-organize-imports should be
able to run OrganizeImports.removeUnused based on the
diagnostics information in SemanticDB emit from Scala3 compiler.

In order to make OrganizeImports rule to work with Scala 3,
this commit added a few adjustments to the rule.
  • Loading branch information
tanishiking committed Oct 26, 2023
1 parent f9672da commit 11fc77d
Showing 1 changed file with 48 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package scalafix.internal.rule
import scala.annotation.tailrec
import scala.collection.mutable
import scala.collection.mutable.ArrayBuffer
import scala.util.Failure
import scala.util.Success
import scala.util.Try

import scala.meta.Import
Expand All @@ -28,6 +30,7 @@ import metaconfig.ConfEncoder
import metaconfig.ConfOps
import metaconfig.Configured
import metaconfig.internal.ConfGet
import scalafix.internal.config.ScalaVersion
import scalafix.internal.rule.ImportMatcher.*
import scalafix.internal.rule.ImportMatcher.---
import scalafix.internal.rule.ImportMatcher.parse
Expand Down Expand Up @@ -72,7 +75,8 @@ class OrganizeImports(config: OrganizeImportsConfig)

override def fix(implicit doc: SemanticDocument): Patch = {
unusedImporteePositions ++= doc.diagnostics.collect {
case d if d.message == "Unused import" => d.position
// Scala2 says "Unused import" while Scala3 says "unused import"
case d if d.message.toLowerCase == "unused import" => d.position
}

val (globalImports, localImports) = collectImports(doc.tree)
Expand All @@ -88,8 +92,16 @@ class OrganizeImports(config: OrganizeImportsConfig)
diagnostics.map(Patch.lint).asPatch + globalImportsPatch + localImportsPatch
}

private def isUnused(importee: Importee): Boolean =
unusedImporteePositions contains positionOf(importee)
private def isUnused(importee: Importee): Boolean = {
// positionOf returns the position of `bar` for `import foo.{bar => baz}`
// this position matches with the diagnostics from Scala2,
// but Scala3 diagnostics has a position for `bar => baz`, which doesn't match with
// the return value of `positionOf`.
// We could adjust the behavior of `positionOf` based on Scala version,
// but this implementation just checking the unusedImporteePosition includes the importee pos, for simplicity.
val pos = positionOf(importee)
unusedImporteePositions.exists(unused => unused.start <= pos.start && pos.end <= unused.end)
}

private def organizeGlobalImports(
imports: Seq[Import]
Expand Down Expand Up @@ -707,32 +719,40 @@ object OrganizeImports {
scalacOptions: List[String],
scalaVersion: String
): Configured[Rule] = {
val hasCompilerSupport = scalaVersion.startsWith("2")

val hasWarnUnused = hasCompilerSupport && {
val warnUnusedPrefix = Set("-Wunused", "-Ywarn-unused")
val warnUnusedString = Set("-Xlint", "-Xlint:unused")
scalacOptions exists { option =>
(warnUnusedPrefix exists option.startsWith) || (warnUnusedString contains option)
}
}
ScalaVersion.from(scalaVersion).map { v =>
v.isScala2 || (
v.isScala3 &&
v.minor.getOrElse(0) >= 3 &&
v.patch.getOrElse(0) >= 1
)
} match {
case Failure(exception) => Configured.error(exception.getMessage())
case Success(hasCompilerSupport) =>
val hasWarnUnused = hasCompilerSupport && {
val warnUnusedPrefix = Set("-Wunused", "-Ywarn-unused")
val warnUnusedString = Set("-Xlint", "-Xlint:unused")
scalacOptions exists { option =>
(warnUnusedPrefix exists option.startsWith) || (warnUnusedString contains option)
}
}

if (!conf.removeUnused || hasWarnUnused)
Configured.ok(new OrganizeImports(conf))
else if (hasCompilerSupport)
Configured.error(
"The Scala compiler option \"-Ywarn-unused\" is required to use OrganizeImports with"
+ " \"OrganizeImports.removeUnused\" set to true. To fix this problem, update your"
+ " build to use at least one Scala compiler option like -Ywarn-unused-import (2.11"
+ " only), -Ywarn-unused, -Xlint:unused (2.12.2 or above) or -Wunused (2.13 only)."
)
else
Configured.error(
"\"OrganizeImports.removeUnused\" is not supported on Scala 3 as the compiler is"
+ " not providing enough information. Run the rule with"
+ " \"OrganizeImports.removeUnused\" set to false to organize imports while keeping"
+ " potentially unused imports."
)
if (!conf.removeUnused || hasWarnUnused)
Configured.ok(new OrganizeImports(conf))
else if (hasCompilerSupport)
Configured.error(
"The Scala compiler option \"-Ywarn-unused\" is required to use OrganizeImports with"
+ " \"OrganizeImports.removeUnused\" set to true. To fix this problem, update your"
+ " build to use at least one Scala compiler option like -Ywarn-unused-import (2.11"
+ " only), -Ywarn-unused, -Xlint:unused (2.12.2 or above) or -Wunused (2.13 only)."
)
else
Configured.error(
"\"OrganizeImports.removeUnused\"" + s"is not supported on $scalaVersion as the compiler is"
+ " not providing enough information. Please upgrade Scala 3 version to 3.3.1 or greater."
+ " Otherwise, run the rule with \"OrganizeImports.removeUnused\" set to false"
+ " to organize imports while keeping potentially unused imports."
)
}
}

private def buildImportMatchers(
Expand Down

0 comments on commit 11fc77d

Please sign in to comment.