Skip to content

Commit

Permalink
OrganizeImports: don't leak state from one fix execution to another
Browse files Browse the repository at this point in the history
At best, this was causing memory leaks during an invocation. At worst, it was generating false negatives for unused import removals.
  • Loading branch information
bjaglin committed Feb 3, 2024
1 parent 9271030 commit 22cb445
Showing 1 changed file with 65 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,6 @@ class OrganizeImports(

private val wildcardGroupIndex: Int = matchers indexOf *

private val unusedImporteePositions: mutable.Set[Position] =
mutable.Set.empty[Position]

private val diagnostics: ArrayBuffer[Diagnostic] =
ArrayBuffer.empty[Diagnostic]

def this() = this(OrganizeImportsConfig())

override def description: String = "Organize import statements"
Expand Down Expand Up @@ -95,43 +89,49 @@ class OrganizeImports(
}

private def fixWithImplicitDialect(implicit doc: SemanticDocument): Patch = {
unusedImporteePositions ++= doc.diagnostics.collect {
case d if d.message == "Unused import" => d.position
}

val diagnostics: ArrayBuffer[Diagnostic] = ArrayBuffer.empty[Diagnostic]

val unusedImporteePositions = new UnusedImporteePositions

val (globalImports, localImports) = collectImports(doc.tree)

val globalImportsPatch =
if (globalImports.isEmpty) Patch.empty
else organizeGlobalImports(globalImports)
else
organizeGlobalImports(unusedImporteePositions, diagnostics)(
globalImports
)

val localImportsPatch =
if (!config.removeUnused || localImports.isEmpty) Patch.empty
else removeUnused(localImports)
else removeUnusedImports(unusedImporteePositions)(localImports)

diagnostics.map(Patch.lint).asPatch + globalImportsPatch + localImportsPatch
}

private def isUnused(importee: Importee): Boolean =
unusedImporteePositions contains positionOf(importee)

private def organizeGlobalImports(
unusedImporteePositions: UnusedImporteePositions,
diagnostics: ArrayBuffer[Diagnostic]
)(
imports: Seq[Import]
)(implicit doc: SemanticDocument): Patch = {
val noUnused = imports flatMap (_.importers) flatMap (removeUnused(_).toSeq)
val noUnused = imports flatMap (_.importers) flatMap (
removeUnusedImporters(unusedImporteePositions)(_).toSeq
)

val (implicits, noImplicits) =
if (!config.groupExplicitlyImportedImplicitsSeparately) (Nil, noUnused)
else partitionImplicits(noUnused)

val (fullyQualifiedImporters, relativeImporters) =
noImplicits partition isFullyQualified
noImplicits partition isFullyQualified(diagnostics)

// Organizes all the fully-qualified global importers.
val fullyQualifiedGroups: Seq[ImportGroup] = {
val expanded =
if (config.expandRelative) relativeImporters map expandRelative else Nil
groupImporters(fullyQualifiedImporters ++ expanded)
groupImporters(diagnostics)(fullyQualifiedImporters ++ expanded)
}

// Moves relative imports (when `config.expandRelative` is false) and
Expand Down Expand Up @@ -165,39 +165,49 @@ class OrganizeImports(
(insertionPatch + removalPatch).atomic
}

private def removeUnused(imports: Seq[Import]): Patch =
private def removeUnusedImports(
unusedImporteePositions: UnusedImporteePositions
)(
imports: Seq[Import]
): Patch =
Patch.fromIterable {
imports flatMap (_.importers) flatMap { case Importer(_, importees) =>
val hasUsedWildcard = importees exists { i =>
i.is[Importee.Wildcard] && !isUnused(i)
i.is[Importee.Wildcard] && !unusedImporteePositions(i)
}

importees collect {
case i @ Importee.Rename(_, to) if isUnused(i) && hasUsedWildcard =>
case i @ Importee.Rename(_, to)
if unusedImporteePositions(i) && hasUsedWildcard =>
// Unimport the identifier instead of removing the importee since
// unused renamed may still impact compilation by shadowing an
// identifier.
//
// See https://github.com/scalacenter/scalafix/issues/614
Patch.replaceTree(to, "_").atomic

case i if isUnused(i) =>
case i if unusedImporteePositions(i) =>
Patch.removeImportee(i).atomic
}
}
}

private def removeUnused(importer: Importer): Option[Importer] =
private def removeUnusedImporters(
unusedImporteePositions: UnusedImporteePositions
)(
importer: Importer
): Option[Importer] =
if (!config.removeUnused) Some(importer)
else {
val hasUsedWildcard = importer.importees exists { i =>
i.is[Importee.Wildcard] && !isUnused(i)
i.is[Importee.Wildcard] && !unusedImporteePositions(i)
}

var rewritten = false

val noUnused = importer.importees.flatMap {
case i @ Importee.Rename(from, _) if isUnused(i) && hasUsedWildcard =>
case i @ Importee.Rename(from, _)
if unusedImporteePositions(i) && hasUsedWildcard =>
// Unimport the identifier instead of removing the importee since
// unused renamed may still impact compilation by shadowing an
// identifier.
Expand All @@ -206,7 +216,7 @@ class OrganizeImports(
rewritten = true
Importee.Unimport(from) :: Nil

case i if isUnused(i) =>
case i if unusedImporteePositions(i) =>
rewritten = true
Nil

Expand Down Expand Up @@ -240,6 +250,8 @@ class OrganizeImports(
}

private def isFullyQualified(
diagnostics: ArrayBuffer[Diagnostic]
)(
importer: Importer
)(implicit doc: SemanticDocument): Boolean = {
val topQualifier = topQualifierOf(importer.ref)
Expand Down Expand Up @@ -312,10 +324,16 @@ class OrganizeImports(
)
}

private def groupImporters(importers: Seq[Importer]): Seq[ImportGroup] =
private def groupImporters(
diagnostics: ArrayBuffer[Diagnostic]
)(
importers: Seq[Importer]
): Seq[ImportGroup] =
importers
.groupBy(matchImportGroup) // Groups imports by importer prefix.
.mapValues(deduplicateImportees _ andThen organizeImportGroup)
.mapValues(
deduplicateImportees _ andThen organizeImportGroup(diagnostics)
)
.map { case (index, imports) => ImportGroup(index, imports) }
.toSeq
.sortBy(_.index)
Expand All @@ -333,13 +351,17 @@ class OrganizeImports(
}
}

private def organizeImportGroup(importers: Seq[Importer]): Seq[Importer] = {
private def organizeImportGroup(
diagnostics: ArrayBuffer[Diagnostic]
)(
importers: Seq[Importer]
): Seq[Importer] = {
val importeesSorted = locally {
config.groupedImports match {
case GroupedImports.Merge =>
mergeImporters(importers, aggressive = false)
mergeImporters(diagnostics)(importers, aggressive = false)
case GroupedImports.AggressiveMerge =>
mergeImporters(importers, aggressive = true)
mergeImporters(diagnostics)(importers, aggressive = true)
case GroupedImports.Explode =>
explodeImportees(importers)
case GroupedImports.Keep =>
Expand All @@ -363,6 +385,8 @@ class OrganizeImports(
}

private def mergeImporters(
diagnostics: ArrayBuffer[Diagnostic]
)(
importers: Seq[Importer],
aggressive: Boolean
): Seq[Importer] =
Expand Down Expand Up @@ -1078,6 +1102,17 @@ object OrganizeImports {
}
}

class UnusedImporteePositions(implicit doc: SemanticDocument) {
private val positions: Seq[Position] =
doc.diagnostics.toSeq.collect {
case d if d.message == "Unused import" => d.position
}

/** Returns true if the importee was marked as unused by the compiler */
def apply(importee: Importee): Boolean =
positions contains positionOf(importee)
}

implicit private class SymbolExtension(symbol: Symbol) {

/**
Expand Down

0 comments on commit 22cb445

Please sign in to comment.