fca4f13be6/org.scala-refactoring.library/src/main/scala/scala/tools/refactoring/implementations/MoveClass.scala

User picture

Commiter: Mirko Stocker

Author: Mirko Stocker

Revision: fca4f13be6


File Size: 12.3 KB

(19 Jan 06:40 UTC) 4 months ago

Set the version numbers in all places, and improvements to Move Class.

 
Show/hide line numbers
package scala.tools.refactoring
package implementations

import scala.tools.nsc.util.SourceFile
import scala.tools.refactoring.analysis.CompilationUnitDependencies
import scala.tools.refactoring.common.{TreeExtractors, NewFileChange, InteractiveScalaCompiler, Change}
import scala.tools.refactoring.transformation.TreeFactory
import scala.collection.mutable.ListBuffer
import scala.tools.refactoring.common.TextChange

abstract class MoveClass extends MultiStageRefactoring with TreeFactory with analysis.Indexes with TreeExtractors with InteractiveScalaCompiler with CompilationUnitDependencies {

  import global._

  /**
   * Returns Some if the user selected a single ImplDef in the source file.
   * None means that all ImplDefs should be moved.
   */
  type PreparationResult = Option[ImplDef]

  /**
   * We don't really need the preparation result in the refactoring, instead
   * the user has the option of ignoring the selected ImplDef, therefore we
   * take a copy of the PreparationResult as a parameter.
   * */
  case class RefactoringParameters(packageName: String, moveSingleImpl: PreparationResult)

  /**
   * We can only move classes from files that contain a single package.
   * Note that
   *
   *   package a
   *   package b
   *
   * is considered a single package, but
   *
   *   package a
   *   class A
   *   package b
   *
   * is not.
   */
  def prepare(s: Selection) = {
    def hasSinglePackageDeclaration = topPackageDef(s.root.asInstanceOf[PackageDef]) match {
      case PackageDef(_, stats) => stats forall (stmt => stmt.isInstanceOf[ImplDef] || stmt.isInstanceOf[Import])
      case _ => false
    }

    val topLevelImpls = topLevelImplDefs(s)

    s.findSelectedOfType[ImplDef] match {
      // If there is only one ImplDef in the file, we simply move
      // all impls. This doesn't matter from the refactoring's
      // perspective, but is used in the IDE to determine if it's
      // possible to split a class from a multiple-definition file.
      case Some(singleImplDef) if topLevelImpls.size == 1 =>
        Right(None)
      case Some(singleImplDef) =>
        Right(Some(singleImplDef))
      case None if hasSinglePackageDeclaration =>
        Right(None) // Move all ImplDefs
      case _ =>
        Left(PreparationError("Files with multiple packages cannot be moved."))
    }
  }

  def perform(selection: Selection, preparationResult: PreparationResult, parameters: RefactoringParameters): Either[RefactoringError, List[Change]] = {

    val toMove = parameters.moveSingleImpl

    val ancestors = toMove.toList flatMap ancestorSymbols

    trace("Selected ImplDef: %s, in package %s, move to %s", toMove map(_.nameString) getOrElse "ALL", ancestors map (_.nameString) mkString ("."), parameters)

    /*
     * We need to handle two different cases:
     *
     *  1) A single class from the existing file is moved to a new one and
     *     there are other classes remaining in the file.
     *
     *  2) The complete file is moved, this is easier because we simply move
     *     the file and don't need to create a new source file.
     * */
    val movedClassChanges = if(moveOnlyPartOfSourceFile(selection, parameters)) {

      val newFileChanges = {
        val moveClass = {
          val statsToMove = parameters.moveSingleImpl.toList
          createRenamedTargetPackageTransformation(parameters, statsToMove, toMove.get)
        }
        val changes = transformFile(selection.file, moveClass)
        changes map (_.toNewFile(parameters.packageName))
      }

      newFileChanges ++ removeClassFromOldFileAndAddImportToNewIfNecessary(selection, parameters)

    } else {
      val statsToMove = topLevelStats(selection)
      val transformation = createRenamedTargetPackageTransformation(parameters, statsToMove, selection.root)
      transformFile(selection.file, transformation)
    }

    /*
     * We need to adapt the imports of all the files that reference one of the moved classes.
     * This include imports to the moved classes and fully qualified names.
     * */
    val otherFiles = adaptDependentFiles(selection, toMove, parameters.packageName)

    Right(movedClassChanges ++ otherFiles)
  }

  private def addRequiredImportsForExtractedClass(toMove: Tree, targetPackageName: String) = traverseAndTransformAll {
    locatePackageLevelImports &> transformation[(PackageDef, List[Import], List[Tree]), Tree] {
      case (p, existingImports, others) =>
            
        val oi = new OrganizeImports {
          val global: MoveClass.this.global.type = MoveClass.this.global
            
          object NeededImports extends Participant {
            def apply(trees: List[Import]) = {
              
              val imps = neededImports(toMove) filterNot { imp =>
                // We don't want to add imports for types that are
                // declared in `toMove`.
                val declaration = index.declaration(imp.symbol)
                declaration map (toMove.pos includes _.pos) getOrElse false
              }
              mkImportTrees(imps, targetPackageName)
            }
          }
        }
        // TODO: Use IDE settings
        val imports = scala.Function.chain(oi.NeededImports :: oi.SortImports :: Nil)(existingImports)         
        // When we move the whole file, we only want to add imports to the originating package
        p copy (stats = imports ::: others) replaces p
    }
  }
  
  /**
   * Returns a transformation that creates the contents of the target file.
   * */
  private def createRenamedTargetPackageTransformation(parameters: RefactoringParameters, implsToMove: List[Tree], importsFor: Tree) = {

    val targetPackages = parameters.packageName.split("\\.").toList

    val findFirstPackageToRename = filter {
      case pkg: PackageDef if pkg.stats.contains(implsToMove.head /*!*/) =>
        true
      case pkg: PackageDef =>
        val packages = ancestorSymbols(pkg) map (_.nameString)
        val found = !(targetPackages startsWith packages)
        found
    }

    val changePackageDeclaration = transform {
      case pkg @ PackageDef(pid, stats) =>

        val surroundingPackages = originalParentOf(pkg) match {
          case Some(parentPkg: PackageDef) =>
            ancestorSymbols(parentPkg) map (_.nameString)
          case _ => Nil
        }

        val newPid = if(targetPackages.startsWith(surroundingPackages)) {
          (targetPackages.drop(surroundingPackages.size))
        } else {
          targetPackages
        }

        if(newPid.isEmpty) {
          PackageDef(Ident(nme.EMPTY_PACKAGE_NAME), implsToMove)
        } else {
          PackageDef(pid = Ident(newPid mkString ".") replaces pid, stats = implsToMove) replaces pkg
        }
    }

    val insertImports = addRequiredImportsForExtractedClass(importsFor, parameters.packageName)

    traverseAndTransformAll(findFirstPackageToRename &> changePackageDeclaration) &> insertImports
  }

  /**
   * Returns the list of changes that adapt the old file (only when we don't move the complete file)
   * */
  private def removeClassFromOldFileAndAddImportToNewIfNecessary(selection: Selection, parameters: RefactoringParameters) = {

    val toMove = parameters.moveSingleImpl.get

    val referencesToMovedClass = index.references(toMove.symbol)

    val referencesInOriginalFile = referencesToMovedClass.filter(_.pos.source.file == selection.file)

    def hasRelativeReferenceToMovedClass = referencesInOriginalFile.exists {
      // TODO check if the complete qualifier has a range!
      case Select(qual, _) if qual.pos.isRange => false
      // check the position to exclude self references
      case t: RefTree if toMove.pos.includes(t.pos) => false
      case _ => true
    }

    val removeClassFromOldFile = replaceTree(toMove, EmptyTree)

    val trans = if(hasRelativeReferenceToMovedClass) {
      removeClassFromOldFile &> addImportTransformation(List(parameters.packageName + "." + toMove.nameString))
    } else {
      removeClassFromOldFile
    }

    transformFile(selection.file, trans)
  }

  private def topLevelStats(selection: Selection): List[Tree] = {
    topPackageDef(selection.root.asInstanceOf[PackageDef]).stats collect {
      case impl: ImplDef => impl
      case imp:  Import  => imp
    }
  }

  private def topLevelImplDefs(selection: Selection): List[ImplDef] = {
    topLevelStats(selection) collect {
      case impl: ImplDef => impl
    }
  }

  private def moveOnlyPartOfSourceFile(selection: Selection, parameters: RefactoringParameters) = {
    parameters.moveSingleImpl.isDefined && topLevelImplDefs(selection).size > 1
  }

  private def adaptDependentFiles(selection: Selection, toMove: Option[ImplDef], newFullPackageName: String): Iterable[Change] = {

    def referencesToMovedClasses(moved: List[ImplDef]): Map[SourceFile, List[(ImplDef, List[Tree])]] = {

      val referencesPerFile = collection.mutable.Map[SourceFile, List[(ImplDef, List[Tree])]]()

      def addToMap(impl: ImplDef) {
        index.references(impl.symbol) groupBy (_.pos.source) foreach {
          case (src, references) if src.file != selection.file =>
            val old = referencesPerFile.getOrElse(src, List[(ImplDef, List[Tree])]())
            referencesPerFile(src) = (impl, references) :: old
          case _ => ()
        }
      }

      moved foreach addToMap

      referencesPerFile.toMap
    }

    val referencesFromOtherFiles = referencesToMovedClasses {
      toMove map (List(_)) getOrElse topLevelImplDefs(selection)
    } flatMap {
      case (sourceFile, entries) => entries.toList map {
        case (implDef, references) => (sourceFile, implDef, references)
      }
    }

    referencesFromOtherFiles flatMap {
      case (sourceFile, implDef, references) =>

        val referencedName = implDef.nameString

        val alreadyHasImportSelector = references.exists(_.isInstanceOf[ImportSelectorTree])

        def hasReferenceWithoutFullName = references.exists {
          // TODO check if the complete qualifier has a Range!
          case Select(qual, _) if qual.pos.isRange => false
          case t: RefTree => true
        }

        if(!alreadyHasImportSelector && hasReferenceWithoutFullName) {

          val addImport = new AddImportStatement { val global = MoveClass.this.global }
          addImport.addImport(sourceFile.file, newFullPackageName + "." + referencedName)

        } else {

          def hasMovedName(s: ImportSelector) = s.name.toString == referencedName

          val adaptImports = transform {

            /*
             * The import has a single selector that imports the class we move.
             * */
            case pkg @ PackageDef(_, stats) if stats.exists {
              case Import(_, selector :: Nil) => hasMovedName(selector)
              case _ => false
            } =>
              /*
               * We are lucky and can replace the import expression.
               * */
              pkg copy (stats = stats map {
                case imp @ Import(_, selector :: Nil) if hasMovedName(selector) =>
                  imp copy (expr = Ident(newFullPackageName)) replaces imp
                case stmt => stmt
              }) copyAttrs pkg

            /*
             * The import has multiple selectors with one of them being the class we move.
             * */
            case pkg @ PackageDef(_, stats) if stats.exists {
              case Import(_, selectors) => selectors.exists(hasMovedName)
              case _ => false
            } =>
              /*
               * Remove the obsolete selector and add a new import with the new package name.
               * */
              pkg copy (stats = stats flatMap {
                case imp @ Import(_, selectors) if selectors.exists(hasMovedName) =>
                  val selector = selectors.find(hasMovedName).get
                  List(
                      imp copy (selectors = selectors.filterNot(_ == selector)) replaces imp,
                      Import(Ident(newFullPackageName), selector :: Nil))
                case stmt =>
                  List(stmt)
              }) copyAttrs pkg

            case s @ Select(qualifier, _) if references.contains(s) &&
                qualifier.pos.isRange /* qualifier is visible in the source code */ =>
              s copy (qualifier = Ident(newFullPackageName)) replaces s

            case ref: Ident if references.contains(ref) && !alreadyHasImportSelector =>
              Ident(newFullPackageName + "." + ref.name)
          }

          transformFile(sourceFile.file, traverseAndTransformAll(adaptImports))
        }
    }
  }
}