All Downloads are FREE. Search and download functionalities are using the official Maven repository.

fix.SortImports.scala Maven / Gradle / Ivy

The newest version!
package fix

import scala.meta._
import scala.meta.tokens.Token.{ Comment, LF }

import fix.SortImportsConfig.Swap
import metaconfig.generic.Surface
import metaconfig.{ generic, ConfDecoder, ConfEncoder, Configured }
import scalafix.v1._

final case class SortImportsConfig(
  blocks: List[String],
  asciiSort: Boolean
)

object SortImportsConfig {

  val default: SortImportsConfig                       = SortImportsConfig(Nil, asciiSort = true)
  implicit val surface: Surface[SortImportsConfig]     = generic.deriveSurface[SortImportsConfig]
  implicit val decoder: ConfDecoder[SortImportsConfig] = generic.deriveDecoder[SortImportsConfig](default)
  implicit val encoder: ConfEncoder[SortImportsConfig] = generic.deriveEncoder[SortImportsConfig]

  final class Swap(val value: (Import, String)) extends AnyVal {
    def from: Import = value._1
    def to: String   = value._2
  }
}

class SortImports(config: SortImportsConfig) extends SyntacticRule("SortImports") {
  def this() = this(SortImportsConfig.default)

  override def description: String =
    "Sorts imports and groups them based on configuration"
  override def isRewrite: Boolean = true

  override def withConfiguration(config: Configuration): Configured[Rule] =
    config.conf
      .getOrElse("SortImports")(this.config)
      .map(new SortImports(_))

  private val importOrdering: ImportOrdering =
    if (config.asciiSort) new DefaultSort else new WildcardAndGroupFirstSort

  override def fix(implicit doc: SyntacticDocument): Patch = {

    // Traverse full code tree. Stop when import branches are found and add them to last list in buf
    // If an empty line is found add an empty list to buf
    val importGroupsWithEmptyLines: List[ImportGroup] = ImportGroupTraverser.retrieveImportGroups(doc.tree)

    // Contains groups of imports
    val importGroups: List[ImportGroup] = importGroupsWithEmptyLines.filter(_.nonEmpty)

    // Trailing comments
    val comments: Map[Import, Comment] = ImportGroup(importGroups.flatten).trailingComment(doc.tokens, doc.comments)

    // Remove all newlines within import groups
    val removeLinesPatch: List[Patch] = importGroups.flatMap { importGroup =>
      doc.tokens.collect {
        case token: LF if importGroup.containPosition(token.pos) => token
      }
    }.map(Patch.removeToken)

    val removeSemicolonsPatch: List[Patch] = importGroups.flatMap { importGroup =>
      doc.tokens.collect {
        case token: Token.Semicolon if importGroup.containPosition(token.pos) || importGroup.trailedBy(token.pos) =>
          token
      }
    }.map(Patch.removeToken)

    // Remove comments and whitespace between imports and comments
    val removeCommentsPatch: Iterable[Patch] = comments.values.map(Patch.removeToken)
    val removeCommentSpacesPatch: Iterable[Patch] = comments.flatMap {
      case (imp, comment) =>
        (0 to comment.pos.start - imp.pos.end).map { diff =>
          new Token.Space(Input.None, comment.dialect, imp.pos.end + diff)
        }
    }.map(Patch.removeToken)

    val configBlocks = {
      val originalBlocks = config.blocks.map {
        case Block.Default.string => Block.Default
        case block if block.startsWith(Block.RegexPrefix.Prefix) =>
          new Block.RegexPrefix(block.substring(Block.RegexPrefix.Prefix.length))
        case block => new Block.StaticPrefix(block)
      }

      // If DefaultBlock is not found in the SortImports rule, prepend it
      if (originalBlocks.contains(Block.Default)) {
        originalBlocks
      } else {
        originalBlocks :+ Block.Default
      }
    }

    // Sort config blocks by length desc so groupByBlock give precedence to more specific group
    val sortedConfigBlocks = configBlocks.sortBy(-_.string.length)

    // Sort each group of imports
    val sorted: Seq[Seq[String]] = importGroups.map { importGroup =>
      // Group based on SortImports rule
      // In case of import list, the first element in the list is significant
      val groupedImports: Map[Block, ImportGroup] = importGroup.groupByBlock(sortedConfigBlocks)

      // Sort grouped imports and convert to strings
      val strImportsSorted: Seq[String] = configBlocks
        .foldLeft(Seq[Seq[String]]()) { (acc, configBlock) =>
          groupedImports.get(configBlock) match {
            case Some(blockImports) =>
              val semiColons = blockImports.trailingSemicolon(doc.tokens)
              val strImports = blockImports
                .sortWith(importOrdering)
                .map { imp =>
                  val semi = if (semiColons.contains(imp)) ";" else ""
                  comments.get(imp).fold(s"$imp$semi")(comment => s"$imp$semi $comment")
                }
                .toSeq
              acc :+ (strImports.init :+ (strImports.last + '\n'))
            case _ => acc
          }
        }
        .flatten

      // Remove extra newline on end of imports
      strImportsSorted.init :+ strImportsSorted.last.dropRight(1)
    }

    val combined: List[List[Swap]] =
      importGroups
        .zip(sorted)
        .map {
          case (importGroup, strImportGroupSorted) => importGroup.value.zip(strImportGroupSorted).map(new Swap(_))
        }

    // Create patches using sorted - unsorted pairs
    // Essentially imports are playing musical chairs
    val patches: List[Patch] =
      combined.flatMap(importSwaps =>
        importSwaps.init.map(trade => Patch.replaceTree(trade.from, s"${trade.to}\n")) :+ Patch
          .replaceTree(importSwaps.last.from, importSwaps.last.to)
      )

    List(
      patches,
      removeLinesPatch,
      removeSemicolonsPatch,
      removeCommentsPatch,
      removeCommentSpacesPatch
    ).flatten.asPatch
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy