diff --git a/docs/rules/OrganizeImports.md b/docs/rules/OrganizeImports.md index 748de82cfd..5939f2533b 100644 --- a/docs/rules/OrganizeImports.md +++ b/docs/rules/OrganizeImports.md @@ -1328,3 +1328,74 @@ object RemoveUnused { val long: JLong = JLong.parseLong("0") } ``` + +`targetDialect` +-------------- + +Specifies the [wildcards and renames](https://docs.scala-lang.org/scala3/reference/changed-features/imports.html) +syntax used when imports are coalesced or rewritten. + +### Value type + +Enum: `Auto | Scala2 | Scala3` + +#### `Auto` + +Infer the dialect from compilation settings (Scala version or `-Xsource` when +provided). This is safe only for sources that are not cross-compiled (see +rationale below). + +#### `Scala2` + +Use `_` as wildcard and `=>` for renames. + +#### `Scala3` + +Use `*` as wildcard and `as` for renames. + +### Default value + +`Scala2` + +Rationale: `Auto` is not a safe default for projects cross-compiling with +Scala 3.x and Scala 2.x without `-Xsource:3`, as the Scala 3.x Scalafix run +would introduce Scala 2.x compilation errors. + +### Examples + +#### `Scala2` + +```conf +OrganizeImports { + targetDialect = Scala2 +} +``` + +Before: + +```scala +``` + +After: + +```scala +``` + +#### `Scala3` + +```conf +OrganizeImports { + targetDialect = Scala3 +} +``` + +Before: + +```scala +``` + +After: + +```scala +``` + diff --git a/scalafix-rules/src/main/scala/scalafix/internal/rule/OrganizeImports.scala b/scalafix-rules/src/main/scala/scalafix/internal/rule/OrganizeImports.scala index 923387ba1e..b37bd86eb5 100644 --- a/scalafix-rules/src/main/scala/scalafix/internal/rule/OrganizeImports.scala +++ b/scalafix-rules/src/main/scala/scalafix/internal/rule/OrganizeImports.scala @@ -20,6 +20,7 @@ import scala.meta.Tree import scala.meta.XtensionClassifiable import scala.meta.XtensionCollectionLikeUI import scala.meta.XtensionSyntax +import scala.meta.dialects import scala.meta.inputs.Position import scala.meta.tokens.Token @@ -29,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 @@ -801,9 +803,37 @@ object OrganizeImports { } } + val targetDialect = conf.targetDialect match { + case TargetDialect.Auto => + 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) + } + .getOrElse(Dialect.current) + case TargetDialect.Scala2 => + dialects.Scala212 + case TargetDialect.Scala3 => + dialects.Scala3 + } + if (!conf.removeUnused || hasWarnUnused) Configured.ok( - new OrganizeImports(conf, Dialect.current) + new OrganizeImports(conf, targetDialect) ) else if (hasCompilerSupport) Configured.error( @@ -952,11 +982,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[ diff --git a/scalafix-rules/src/main/scala/scalafix/internal/rule/OrganizeImportsConfig.scala b/scalafix-rules/src/main/scala/scalafix/internal/rule/OrganizeImportsConfig.scala index d9703df4a9..889e8a5234 100644 --- a/scalafix-rules/src/main/scala/scalafix/internal/rule/OrganizeImportsConfig.scala +++ b/scalafix-rules/src/main/scala/scalafix/internal/rule/OrganizeImportsConfig.scala @@ -75,6 +75,18 @@ object Preset { ConfEncoder.instance(v => Conf.Str(v.toString)) } +sealed trait TargetDialect +object TargetDialect { + case object Auto extends TargetDialect + case object Scala2 extends TargetDialect + case object Scala3 extends TargetDialect + + implicit def reader: ConfDecoder[TargetDialect] = + ReaderUtil.oneOf(Auto, Scala2, Scala3) + implicit def writer: ConfEncoder[TargetDialect] = + ConfEncoder.instance(v => Conf.Str(v.toString)) +} + final case class OrganizeImportsConfig( blankLines: BlankLines = BlankLines.Auto, coalesceToWildcardImportThreshold: Option[Int] = None, @@ -88,7 +100,8 @@ final case class OrganizeImportsConfig( importSelectorsOrder: ImportSelectorsOrder = ImportSelectorsOrder.Ascii, importsOrder: ImportsOrder = ImportsOrder.Ascii, preset: Preset = Preset.DEFAULT, - removeUnused: Boolean = true + removeUnused: Boolean = true, + targetDialect: TargetDialect = TargetDialect.Scala2 ) object OrganizeImportsConfig {