Skip to content

Commit

Permalink
OrganizeImports: use new Scala 3 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 689323b
Show file tree
Hide file tree
Showing 52 changed files with 360 additions and 108 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import scala.collection.mutable
import scala.collection.mutable.ArrayBuffer
import scala.util.Try

import scala.meta.Dialect
import scala.meta.Import
import scala.meta.Importee
import scala.meta.Importee.GivenAll
Expand All @@ -28,6 +29,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 @@ -43,8 +45,10 @@ import scalafix.v1.SymbolInformation
import scalafix.v1.XtensionSeqPatch
import scalafix.v1.XtensionTreeScalafix

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,122 @@ 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. Use
// the original parsed text to preserve the original source level formatting, but patch
// importee that have specific Scala 3 syntax.
val syntax = new StringBuilder(pos.text)
def patchSyntax(
t: Tree,
newSyntax: String,
stripEnclosingBraces: Boolean = false
) = {
val start = t.pos.start - pos.start
syntax.replace(start, t.pos.end - pos.start, newSyntax)
val end = t.pos.start - pos.start + newSyntax.length

if (stripEnclosingBraces)
(
syntax.take(start).lastIndexOf('{'),
syntax.indexOf('}', end)
) match {
case (from, to) if from != -1 && to != -1 =>
syntax.delete(end, to + 1)
syntax.delete(from, start)
case _ =>
}
}
val Importer(_, importees) = importer
val optionalBraces =
importees.length == 1 && dialect.allowAsForImportRename
// traverse & patch backwards to avoid shifting indices
importees.reverse.foreach {
case i @ Importee.Rename(from, to) =>
patchSyntax(i, i.copy().syntax, optionalBraces)
case i @ Importee.Unimport(name) =>
patchSyntax(i, i.copy().syntax, optionalBraces)
case i @ Importee.Wildcard() =>
patchSyntax(i, i.copy().syntax)
case _ =>
}
syntax.toString

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

implicit private class ImporterExtension(importer: Importer) {

/** Checks whether the `Importer` is curly-braced when pretty-printed. */
def isCurlyBraced: Boolean = {
val importees @ Importees(_, renames, unimports, _, _, _) =
importer.importees

importees.length > 1 ||
((renames.length == 1 || unimports.length == 1) && !dialect.allowAsForImportRename)
}

/**
* Returns an `Importer` with all the `Importee`s that are selected from the
* input `Importer` and satisfy a predicate. If all the `Importee`s are
* selected, the input `Importer` instance is returned to preserve the
* original source level formatting. If none of the `Importee`s are
* selected, returns a `None`.
*/
def filterImportees(f: Importee => Boolean): Option[Importer] = {
val filtered = importer.importees filter f
if (filtered.length == importer.importees.length) Some(importer)
else if (filtered.isEmpty) None
else Some(importer.copy(importees = filtered))
}

/** Returns true if the `Importer` contains a standalone wildcard. */
def hasWildcard: Boolean = {
val Importees(_, _, unimports, _, _, wildcard) = importer.importees
unimports.isEmpty && wildcard.nonEmpty
}

/** Returns true if the `Importer` contains a standalone given wildcard. */
def hasGivenAll: Boolean = {
val Importees(_, _, unimports, _, givenAll, _) = importer.importees
unimports.isEmpty && givenAll.nonEmpty
}
}
}

object OrganizeImports {
Expand Down Expand Up @@ -717,8 +842,29 @@ object OrganizeImports {
}
}

// TODO: add an option to force a dialect with many tests
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 +922,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 +1012,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 Expand Up @@ -967,40 +1071,4 @@ object OrganizeImports {
def infoNoThrow(implicit doc: SemanticDocument): Option[SymbolInformation] =
Try(symbol.info).toOption.flatten
}

implicit private class ImporterExtension(importer: Importer) {

/** Checks whether the `Importer` is curly-braced when pretty-printed. */
def isCurlyBraced: Boolean = {
val importees @ Importees(_, renames, unimports, _, _, _) =
importer.importees
renames.nonEmpty || unimports.nonEmpty || importees.length > 1
}

/**
* Returns an `Importer` with all the `Importee`s that are selected from the
* input `Importer` and satisfy a predicate. If all the `Importee`s are
* selected, the input `Importer` instance is returned to preserve the
* original source level formatting. If none of the `Importee`s are
* selected, returns a `None`.
*/
def filterImportees(f: Importee => Boolean): Option[Importer] = {
val filtered = importer.importees filter f
if (filtered.length == importer.importees.length) Some(importer)
else if (filtered.isEmpty) None
else Some(importer.copy(importees = filtered))
}

/** Returns true if the `Importer` contains a standalone wildcard. */
def hasWildcard: Boolean = {
val Importees(_, _, unimports, _, _, wildcard) = importer.importees
unimports.isEmpty && wildcard.nonEmpty
}

/** Returns true if the `Importer` contains a standalone given wildcard. */
def hasGivenAll: Boolean = {
val Importees(_, _, unimports, _, givenAll, _) = importer.importees
unimports.isEmpty && givenAll.nonEmpty
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ OrganizeImports.groupedImports = Merge
*/
package test.organizeImports

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ OrganizeImports.groupedImports = Explode
package test.organizeImports

import test.organizeImports.ExplodeImports.FormatPreserving.g1.{ a, b }
import test.organizeImports.ExplodeImports.FormatPreserving.g2.{ c => C, _ }
import test.organizeImports.ExplodeImports.FormatPreserving.g2.{ c => C, _ }

object ExplodeImportsFormatPreserving
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import scala.concurrent._
import scala.concurrent.duration
import scala.concurrent.{Promise, Future}

import test.organizeImports.QuotedIdent.{`a.b` => ab}
import test.organizeImports.QuotedIdent.`a.b`.`{ d }`.e
import test.organizeImports.QuotedIdent.`a.b`.`{ d }`.{ e => E }
import test.organizeImports.QuotedIdent.`a.b`
import test.organizeImports.QuotedIdent.`a.b`.{c => _, _}
import test.organizeImports.QuotedIdent._

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package test.organizeImports

import test.organizeImports.QuotedIdent._
import test.organizeImports.QuotedIdent.{`a.b` => ab}
import test.organizeImports.QuotedIdent.`a.b`
import test.organizeImports.QuotedIdent.`a.b`.{c => _, _}
import test.organizeImports.QuotedIdent.`a.b`.`{ d }`.{ e => E }
import test.organizeImports.QuotedIdent.`a.b`.`{ d }`.e

import scala.concurrent._
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.*
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,5 +1,5 @@
package test.organizeImports

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

object CoalesceImporteesNoGivensNoNames
Loading

0 comments on commit 689323b

Please sign in to comment.