@@ -779,57 +779,71 @@ class OrganizeImports(
779779
780780 ps.foreach(_.begComment.foreach(appendBegComment))
781781 i.begComment.foreach(appendBegComment)
782- if (single != null ) single.begComment.foreach(appendBegComment)
783- sb.append(" import " ).append(refSyntax(i.ref)).append('.' )
784- if (single != null ) {
785- val isCurly = single.isCurlyBraced
786- val useOuterSpace = isCurly && single
787- .originalPrototype()
788- .parent
789- .exists(_.hasSpaceInCurly)
790- if (isCurly) {
782+ if (i.isUnchangedFromSource) {
783+ // An already-organized wrapped import: re-emit its source text as is,
784+ // leaving its layout to the formatter. Interior comments come along
785+ // in the token stream; comments around the import are printed as
786+ // usual, above and below this branch.
787+ // Continuation lines are re-indented on insertion, like printed ones.
788+ val srcIndent = " " * i.parent.fold(0 )(_.pos.startColumn)
789+ val srcLines = i.tokens.syntax.linesIterator
790+ sb.append(" import " ).append(srcLines.next())
791+ srcLines.foreach(l =>
792+ sb.append('\n ' ).append(l.stripPrefix(srcIndent))
793+ )
794+ } else {
795+ if (single != null ) single.begComment.foreach(appendBegComment)
796+ sb.append(" import " ).append(refSyntax(i.ref)).append('.' )
797+ if (single != null ) {
798+ val isCurly = single.isCurlyBraced
799+ val useOuterSpace = isCurly && single
800+ .originalPrototype()
801+ .parent
802+ .exists(_.hasSpaceInCurly)
803+ if (isCurly) {
804+ sb.append('{' )
805+ if (useOuterSpace) sb.append(' ' )
806+ }
807+ sb.append(importeeSyntax(single))
808+ if (isCurly) {
809+ if (useOuterSpace) sb.append(' ' )
810+ sb.append('}' )
811+ }
812+ single.endComment.foreach(appendEndComment)
813+ } else {
814+ val lines = i.importees.iterator.map(_.pos.startLine).filter(_ >= 0 )
815+ val isMultiline = lines.hasNext && {
816+ val line = lines.next()
817+ lines.exists(_ != line)
818+ }
819+ val origImporters = i.importees
820+ .flatMap(_.originalPrototype().parent)
821+ .distinct
822+ val useOuterSpace =
823+ ! isMultiline && origImporters.exists(_.hasSpaceInCurly)
824+ // Preserve a trailing comma if the (single) source importer had one.
825+ val trailingComma = isMultiline && (origImporters match {
826+ case Seq (origImporter : Importer ) => origImporter.hasTrailingComma
827+ case _ => false
828+ })
791829 sb.append('{' )
792- if (useOuterSpace) sb.append(' ' )
793- }
794- sb.append(treeSyntax(single))
795- if (isCurly) {
796- if (useOuterSpace) sb.append(' ' )
830+ val sep = if (isMultiline) " \n " else " "
831+ if (isMultiline) sb.append(sep)
832+ else if (useOuterSpace) sb.append(' ' )
833+ val sblen = sb.length
834+ i.importees.foreach { i2 =>
835+ if (sb.length > sblen) sb.append(',' ).append(sep)
836+ val proto = i2.originalPrototype()
837+ proto.begComment.foreach(appendBegComment)
838+ sb.append(importeeSyntax(i2))
839+ proto.endComment.foreach(appendEndComment)
840+ }
841+ if (isMultiline) {
842+ if (trailingComma) sb.append(',' )
843+ sb.append('\n ' )
844+ } else if (useOuterSpace) sb.append(' ' )
797845 sb.append('}' )
798846 }
799- single.endComment.foreach(appendEndComment)
800- } else {
801- val lines = i.importees.iterator.map(_.pos.startLine).filter(_ >= 0 )
802- val isMultiline = lines.hasNext && {
803- val line = lines.next()
804- lines.exists(_ != line)
805- }
806- val origImporters = i.importees
807- .flatMap(_.originalPrototype().parent)
808- .distinct
809- val useOuterSpace =
810- ! isMultiline && origImporters.exists(_.hasSpaceInCurly)
811- // Preserve a trailing comma if the (single) source importer had one.
812- val trailingComma = isMultiline && (origImporters match {
813- case Seq (origImporter : Importer ) => origImporter.hasTrailingComma
814- case _ => false
815- })
816- sb.append('{' )
817- val sep = if (isMultiline) " \n " else " "
818- if (isMultiline) sb.append(sep)
819- else if (useOuterSpace) sb.append(' ' )
820- val sblen = sb.length
821- i.importees.foreach { i2 =>
822- if (sb.length > sblen) sb.append(',' ).append(sep)
823- val proto = i2.originalPrototype()
824- proto.begComment.foreach(appendBegComment)
825- sb.append(treeSyntax(i2))
826- proto.endComment.foreach(appendEndComment)
827- }
828- if (isMultiline) {
829- if (trailingComma) sb.append(',' )
830- sb.append('\n ' )
831- } else if (useOuterSpace) sb.append(' ' )
832- sb.append('}' )
833847 }
834848 i.endComment.foreach(appendEndComment)
835849 ps.foreach(_.endComment.foreach(appendEndComment))
@@ -1152,23 +1166,73 @@ object OrganizeImports {
11521166 Try (symbol.info).toOption.flatten
11531167 }
11541168
1155- @ inline
1169+ /**
1170+ * Emits an importer ref segment by segment: only leaf names can be taken from
1171+ * the source, so inter-segment trivia (`a . b`, a ref wrapped over several
1172+ * lines) can never leak into the output.
1173+ */
11561174 private def refSyntax (ref : Term )(implicit dialect : Dialect ): String =
11571175 ref match {
1158- case Term .Select (qual, name) if ! name.pos.isEmpty =>
1159- refSyntax(qual) + " ." + name.pos.text
1160- case _ if ! ref.pos.isEmpty =>
1161- ref.pos.text
1162- case Term .Select (qual, name) =>
1163- refSyntax(qual) + " ." + treeSyntax(name)
1164- case _ =>
1165- treeSyntax(ref)
1176+ case t : Term .Name => nameSyntax(t)
1177+ case Term .Select (qual, name) => refSyntax(qual) + " ." + nameSyntax(name)
1178+ case _ => treeSyntax(ref)
11661179 }
11671180
1168- @ inline
1181+ /**
1182+ * Canonical, dialect-normalized syntax. Use for comparison keys ONLY (dedup,
1183+ * grouping, sorting): two importers/importees that mean the same thing must
1184+ * produce the same key regardless of how they were spelled in the source, so
1185+ * ordering here deliberately follows the dialect-normalized spelling rather
1186+ * than e.g. an author's backticks (see `refSyntax`/`importeeSyntax` for
1187+ * output, and https://github.com/scalacenter/scalafix/pull/2500 for why these
1188+ * two must not be conflated).
1189+ */
11691190 private def treeSyntax (tree : Tree )(implicit dialect : Dialect ): String =
11701191 tree.reprint()
11711192
1193+ /**
1194+ * Emits a leaf `Name` as it was spelled in the source, so that backquotes
1195+ * around identifiers the target dialect does not consider keywords are not
1196+ * dropped (soft keywords, e.g. `` `export` ``,
1197+ * https://github.com/scalacenter/scalafix/issues/2480).
1198+ *
1199+ * NB: always reads the name's own position, never `originalPrototype()` --
1200+ * the latter would resurrect pre-rewrite text for a node produced by
1201+ * `copy(field = ...)` (e.g. `expandRelative`'s `replaceTopQualifier`).
1202+ */
1203+ private def nameSyntax (name : Name )(implicit dialect : Dialect ): String =
1204+ name.pos match {
1205+ case p : Position .Range => p.text
1206+ case _ => treeSyntax(name)
1207+ }
1208+
1209+ private def typeSyntax (tpe : Type )(implicit dialect : Dialect ): String =
1210+ tpe match {
1211+ case t : Type .Name => nameSyntax(t)
1212+ case Type .Select (qual, name) => refSyntax(qual) + " ." + nameSyntax(name)
1213+ case _ => treeSyntax(tpe) // Type.Apply, Type.Project, ...
1214+ }
1215+
1216+ /**
1217+ * Emits an importee: leaf names come from the source, every connective token
1218+ * (`=>` vs `as`, `given`) is produced for the target dialect, so a dialect
1219+ * migration keeps the source spelling of the names it moves. Separators match
1220+ * the pretty-printer's, so output is unchanged for unescaped names.
1221+ */
1222+ private def importeeSyntax (importee : Importee )(implicit
1223+ dialect : Dialect
1224+ ): String = {
1225+ def arrow = if (dialect.allowAsForImportRename) " as" else " =>"
1226+ importee match {
1227+ case Importee .Name (name) => nameSyntax(name)
1228+ case Importee .Rename (name, rename) =>
1229+ s " ${nameSyntax(name)} $arrow ${nameSyntax(rename)}"
1230+ case Importee .Unimport (name) => s " ${nameSyntax(name)} $arrow _ "
1231+ case Importee .Given (tpe) => " given " + typeSyntax(tpe)
1232+ case _ => treeSyntax(importee) // Wildcard, GivenAll
1233+ }
1234+ }
1235+
11721236 implicit private class ImporteeExtension (val importee : Importee )
11731237 extends AnyVal {
11741238
@@ -1237,6 +1301,40 @@ object OrganizeImports {
12371301 tokens.getWideOpt(idx).exists(_.is[Token .Comma ])
12381302 }
12391303
1304+ def spansMultipleLines : Boolean =
1305+ importer.pos.startLine != importer.pos.endLine
1306+
1307+ /**
1308+ * Would pretty-printing under `dialect` rewrite `=>` to `as` or `_` to `*`?
1309+ */
1310+ def needsDialectRewrite (implicit dialect : Dialect ): Boolean =
1311+ importer.importees.exists {
1312+ case i : Importee .Wildcard =>
1313+ i.tokens.exists(_.is[Token .Underscore ]) ==
1314+ dialect.allowStarWildcardImport
1315+ case i @ (_ : Importee .Rename | _ : Importee .Unimport ) =>
1316+ i.tokens.exists(_.is[Token .RightArrow ]) ==
1317+ dialect.allowAsForImportRename
1318+ case _ => false
1319+ }
1320+
1321+ /**
1322+ * Checks whether this `Importer` can be re-emitted verbatim from its own
1323+ * source text, so its layout (indentation, alignment, trailing comma) is
1324+ * left to the formatter rather than re-flowed by this rule.
1325+ *
1326+ * The signal is position presence: a node the pipeline rewrote is a
1327+ * `copy()` and carries no position (same invariant `nameSyntax` relies on),
1328+ * so an `Importer` that still has its parsed `Position.Range` was not
1329+ * restructured -- same qualifier, same importees, same order. Only wrapped
1330+ * imports qualify: single-line ones are still normalized (`import a.{ B }`
1331+ * -> `import a.B`), and one needing a dialect migration is being rewritten.
1332+ */
1333+ def isUnchangedFromSource (implicit dialect : Dialect ): Boolean =
1334+ importer.pos.isInstanceOf [Position .Range ] &&
1335+ importer.spansMultipleLines &&
1336+ ! importer.needsDialectRewrite
1337+
12401338 }
12411339
12421340 implicit private class TreeExtension (val tree : Tree ) extends AnyVal {
0 commit comments