Skip to content

Commit

Permalink
OrganizeImports: favor Scala 3 syntax on Scala 3 sources
Browse files Browse the repository at this point in the history
  • Loading branch information
bjaglin committed Nov 23, 2023
1 parent e054ea7 commit 4493993
Show file tree
Hide file tree
Showing 19 changed files with 132 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,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 All @@ -42,9 +43,12 @@ import scalafix.v1.Symbol
import scalafix.v1.SymbolInformation
import scalafix.v1.XtensionSeqPatch
import scalafix.v1.XtensionTreeScalafix
import scala.meta.Dialect

class OrganizeImports(config: OrganizeImportsConfig)
extends SemanticRule("OrganizeImports") {
class OrganizeImports(
config: OrganizeImportsConfig,
implicit val dialect: Dialect
) extends SemanticRule("OrganizeImports") {
import OrganizeImports._
import ImportMatcher._

Expand All @@ -58,7 +62,7 @@ class OrganizeImports(config: OrganizeImportsConfig)
private val diagnostics: ArrayBuffer[Diagnostic] =
ArrayBuffer.empty[Diagnostic]

def this() = this(OrganizeImportsConfig())
def this() = this(OrganizeImportsConfig(), Dialect.current)

override def isLinter: Boolean = true

Expand Down Expand Up @@ -564,7 +568,12 @@ class OrganizeImports(config: OrganizeImportsConfig)

importer match {
case Importer(_, Importee.Wildcard() :: Nil) =>
syntax.patch(syntax.lastIndexOfSlice("._"), ".\u0001", 2)
val wildcardSyntax = Importee.Wildcard().syntax
syntax.patch(
syntax.lastIndexOfSlice(s".$wildcardSyntax"),
".\u0001",
2
)

case _ if importer.isCurlyBraced =>
syntax
Expand Down Expand Up @@ -685,6 +694,49 @@ class OrganizeImports(config: OrganizeImportsConfig)

Patch.addLeft(token, indented mkString "\n")
}

private def prettyPrintImportGroup(group: Seq[Importer]): String = {
group
.map(i => "import " + importerSyntax(i))
.mkString("\n")
}

/**
* HACK: The Scalafix pretty-printer decides to add spaces after open and
* before close braces in imports with multiple importees, i.e., `import a.{
* b, c }` instead of `import a.{b, c}`. On the other hand, renames are
* pretty-printed without the extra spaces, e.g., `import a.{b => c}`. This
* behavior is not customizable and makes ordering imports by ASCII order
* complicated.
*
* This function removes the unwanted spaces as a workaround. In cases where
* users do want the inserted spaces, Scalafmt should be used after running
* the `OrganizeImports` rule.
*/
private def importerSyntax(importer: Importer): String =
importer.pos match {
case pos: Position.Range =>
// Position found, implies that `importer` was directly parsed from the source code. Returns
// the original parsed text to preserve the original source level formatting.
pos.text

case Position.None =>
// Position not found, implies that `importer` is derived from certain existing import
// statement(s). Pretty-prints it.
val syntax = importer.syntax

// NOTE: We need to check whether the input importer is curly braced first and then replace
// the first "{ " and the last " }" if any. Naive string replacement is insufficient, e.g.,
// a quoted-identifier like "`{ d }`" may cause broken output.
(importer.isCurlyBraced, syntax lastIndexOfSlice " }") match {
case (_, -1) =>
syntax
case (true, index) =>
syntax.patch(index, "}", 2).replaceFirst("\\{ ", "{")
case _ =>
syntax
}
}
}

object OrganizeImports {
Expand Down Expand Up @@ -717,8 +769,28 @@ object OrganizeImports {
}
}

val maybeDialect = ScalaVersion.from(scalaVersion).map { scalaVersion =>
def extractSuffixForScalacOption(prefix: String) = {
scalacOptions
.filter(_.startsWith(prefix))
.lastOption
.map(_.stripPrefix(prefix))
}

// We only lookup the Scala 2 option (Scala 3 is `-source`), as the latest Scala 3
// dialect is used no matter what the actual minor version is anyway, and as of now,
// the pretty printer is just more permissive with the latest dialect.
val sourceScalaVersion =
extractSuffixForScalacOption("-Xsource:")
.flatMap(ScalaVersion.from(_).toOption)

scalaVersion.dialect(sourceScalaVersion)
}

if (!conf.removeUnused || hasWarnUnused)
Configured.ok(new OrganizeImports(conf))
Configured.ok(
new OrganizeImports(conf, maybeDialect.getOrElse(Dialect.current))
)
else if (hasCompilerSupport)
Configured.error(
"The Scala compiler option \"-Ywarn-unused\" is required to use OrganizeImports with"
Expand Down Expand Up @@ -776,48 +848,6 @@ object OrganizeImports {
}
}

private def prettyPrintImportGroup(group: Seq[Importer]): String =
group
.map(i => "import " + importerSyntax(i))
.mkString("\n")

/**
* HACK: The Scalafix pretty-printer decides to add spaces after open and
* before close braces in imports with multiple importees, i.e., `import a.{
* b, c }` instead of `import a.{b, c}`. On the other hand, renames are
* pretty-printed without the extra spaces, e.g., `import a.{b => c}`. This
* behavior is not customizable and makes ordering imports by ASCII order
* complicated.
*
* This function removes the unwanted spaces as a workaround. In cases where
* users do want the inserted spaces, Scalafmt should be used after running
* the `OrganizeImports` rule.
*/
private def importerSyntax(importer: Importer): String =
importer.pos match {
case pos: Position.Range =>
// Position found, implies that `importer` was directly parsed from the source code. Returns
// the original parsed text to preserve the original source level formatting.
pos.text

case Position.None =>
// Position not found, implies that `importer` is derived from certain existing import
// statement(s). Pretty-prints it.
val syntax = importer.syntax

// NOTE: We need to check whether the input importer is curly braced first and then replace
// the first "{ " and the last " }" if any. Naive string replacement is insufficient, e.g.,
// a quoted-identifier like "`{ d }`" may cause broken output.
(importer.isCurlyBraced, syntax lastIndexOfSlice " }") match {
case (_, -1) =>
syntax
case (true, index) =>
syntax.patch(index, "}", 2).replaceFirst("\\{ ", "{")
case _ =>
syntax
}
}

@tailrec private def topQualifierOf(term: Term): Term.Name =
term match {
case Term.Select(qualifier, _) => topQualifierOf(qualifier)
Expand Down Expand Up @@ -908,11 +938,11 @@ object OrganizeImports {
* Categorizes a list of `Importee`s into the following four groups:
*
* - Names, e.g., `Seq`, `Option`, etc.
* - Renames, e.g., `{Long => JLong}`, `{Duration => D}`, etc.
* - Unimports, e.g., `{Foo => _}`.
* - Renames, e.g., `{Long => JLong}`, `{Duration as D}`, etc.
* - Unimports, e.g., `{Foo => _}` or `{Foo as _}`.
* - Givens, e.g., `{given Foo}`.
* - GivenAll, i.e., `given`.
* - Wildcard, i.e., `_`.
* - Wildcard, i.e., `_` or `*`.
*/
object Importees {
def unapply(importees: Seq[Importee]): Option[
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package test.organizeImports

import scala.collection.immutable.{Map, Seq, Vector}
import scala.collection.mutable.*
import scala.concurrent.{Channel as Ch, *}
import scala.util.{Random as _, *}

object CoalesceImportees
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package test.organizeImports

import test.organizeImports.Givens._
import test.organizeImports.Givens.{B => B1, C => _, _, given}
import test.organizeImports.Givens.{B as B1, C as _, *, given}

object CoalesceImporteesGivensAndNames
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package test.organizeImports

import test.organizeImports.Givens.{C => C1, _}
import test.organizeImports.Givens.{C as C1, *}

object CoalesceImporteesNoGivens
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package test.organizeImports

import test.organizeImports.Givens._
import test.organizeImports.Givens.{A => A1, given}
import test.organizeImports.Givens.{A as A1, given}

object CoalesceImporteesNoNames
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package test.organizeImports

import scala.collection.immutable.Vector
import scala.collection.immutable.{Map => Dict}
import scala.collection.immutable.{Set as _, *}

object DeduplicateImportees
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package test.organizeImports

import test.organizeImports.GivenImports._
import test.organizeImports.GivenImports.{gamma => _, given Beta, given Zeta, given}
import test.organizeImports.GivenImports2.{alpha => _, beta => _}
import test.organizeImports.GivenImports.{gamma as _, given Beta, given Zeta, given}
import test.organizeImports.GivenImports2.{alpha as _, beta as _}
import test.organizeImports.GivenImports2.{given Gamma, given Zeta}

object GroupedGivenImportsMergeUnimports
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package test.organizeImports

import scala.collection.immutable._
import scala.collection.mutable.Map
import scala.collection.mutable.{Buffer as _, Seq as S, *}

object GroupedImportsExplodeMixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package test.organizeImports

import test.organizeImports.MergeImports.Dedup.{a, b as b1, c as _}

object GroupedImportsMergeDedup
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package test.organizeImports

import test.organizeImports.MergeImports.Rename1.{a as A, b as B, c, d}
import test.organizeImports.MergeImports.Rename2.{a as A, b as B, c}
import test.organizeImports.MergeImports.Rename2.{a, b}

object GroupedImportsMergeRenames
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package test.organizeImports

import test.organizeImports.MergeImports.Unimport1.{b as B, c as _, d, *}
import test.organizeImports.MergeImports.Unimport2.{a as _, b as _, c as C, d}

object GroupedImportsMergeUnimports
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package test.organizeImports

import test.organizeImports.MergeImports.Wildcard1.{b => B}
import test.organizeImports.MergeImports.Wildcard1.{d, *}
import test.organizeImports.MergeImports.Wildcard2.{a, b, *}

object GroupedImportsMergeWildcard

0 comments on commit 4493993

Please sign in to comment.