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

scalafix.internal.pc.ScalafixGlobal.scala Maven / Gradle / Ivy

The newest version!
package scala.meta.internal.pc

import java.io.File
import java.{util => ju}

import scala.collection.compat._
import scala.collection.mutable
import scala.reflect.NameTransformer
import scala.reflect.internal.{Flags => gf}
import scala.reflect.io.VirtualDirectory
import scala.tools.nsc.Settings
import scala.tools.nsc.interactive.Global
import scala.tools.nsc.reporters.StoreReporter
import scala.util.Failure
import scala.util.Try
import scala.util.control.NonFatal
import scala.{meta => m}

import scala.meta.internal.semanticdb.scalac.SemanticdbOps
import scala.meta.io.AbsolutePath

object ScalafixGlobal {
  def newCompiler(
      cp: List[AbsolutePath],
      options: List[String],
      symbolReplacements: Map[String, String]
  ): Try[ScalafixGlobal] = {
    val classpath = cp.mkString(File.pathSeparator)
    val vd = new VirtualDirectory("(memory)", None)
    val settings = new Settings
    settings.Ymacroexpand.value = "discard"
    settings.outputDirs.setSingleOutput(vd)
    settings.classpath.value = classpath
    settings.YpresentationAnyThread.value = true
    if (classpath.isEmpty) {
      settings.usejavacp.value = true
    }
    val (isSuccess, unprocessed) =
      settings.processArguments(options, processAll = true)
    (isSuccess, unprocessed) match {
      case (true, Nil) =>
        Try(
          new ScalafixGlobal(settings, new StoreReporter(), symbolReplacements)
        )
      case (isSuccess, unprocessed) =>
        Failure(
          new Exception(
            s"newGlobal failed while processing Arguments. " +
              s"Status is $isSuccess, unprocessed arguments are $unprocessed"
          )
        )
    }
  }
}

class ScalafixGlobal(
    settings: Settings,
    val storeReporter: StoreReporter,
    symbolReplacements: Map[String, String]
) extends Global(settings, storeReporter, "Scalafix rules global") { compiler =>

  lazy val gsymbolReplacements: Map[String, Symbol] = (for {
    (key, value) <- symbolReplacements.toSeq
    gkey <- inverseSemanticdbSymbols(key)
    if semanticdbSymbol(gkey) == key
    gvalue <- inverseSemanticdbSymbols(value)
    if semanticdbSymbol(gvalue) == value
  } yield (key, gvalue)).toMap

  override val shorthands: Set[String] = Set.empty

  def inverseSemanticdbSymbols(symbol: String): List[Symbol] = {
    import scala.meta.internal.semanticdb.Scala._
    if (!symbol.isGlobal) return Nil

    def EncodedTermName(str: String): TermName = TermName(
      NameTransformer.encode(str)
    )
    def EncodedTypeName(str: String): TypeName = TypeName(
      NameTransformer.encode(str)
    )

    def loop(s: String): List[Symbol] = {
      if (s.isNone || s.isRootPackage) rootMirror.RootPackage :: Nil
      else if (s.isEmptyPackage) rootMirror.EmptyPackage :: Nil
      else if (s.isPackage) {
        try {
          rootMirror.staticPackage(s.stripSuffix("/").replace("/", ".")) :: Nil
        } catch {
          case NonFatal(_) =>
            Nil
        }
      } else {
        val (desc, parent) = DescriptorParser(s)
        val parentSymbol = loop(parent)

        def tryMember(sym: Symbol): List[Symbol] =
          sym match {
            case NoSymbol =>
              Nil
            case owner =>
              desc match {
                case Descriptor.None =>
                  Nil
                case Descriptor.Type(value) =>
                  val member = owner.info.decl(EncodedTypeName(value)) :: Nil
                  if (sym.isJava)
                    owner.info.decl(EncodedTermName(value)) :: member
                  else member
                case Descriptor.Term(value) =>
                  owner.info.decl(EncodedTermName(value)) :: Nil
                case Descriptor.Package(value) =>
                  owner.info.decl(EncodedTermName(value)) :: Nil
                case Descriptor.Parameter(value) =>
                  owner.paramss.flatten.filter(
                    _.name.containsName(EncodedTermName(value))
                  )
                case Descriptor.TypeParameter(value) =>
                  owner.typeParams.filter(
                    _.name.containsName(EncodedTypeName(value))
                  )
                case Descriptor.Method(value, _) =>
                  owner.info
                    .decl(EncodedTermName(value))
                    .alternatives
                    .iterator
                    .filter(sym => semanticdbSymbol(sym) == s)
                    .toList
              }
          }

        parentSymbol.flatMap(tryMember)
      }
    }

    try loop(symbol).filterNot(_ == NoSymbol)
    catch {
      case NonFatal(e) =>
        println(s"invalid SemanticDB symbol: $symbol\n${e.getMessage}")
        Nil
    }
  }

  class MetalsGlobalSemanticdbOps(val global: compiler.type)
      extends SemanticdbOps
  lazy val semanticdbOps = new MetalsGlobalSemanticdbOps(compiler)

  def semanticdbSymbol(symbol: Symbol): String = {
    import semanticdbOps._
    symbol.toSemantic
  }
  def inverseSemanticdbSymbol(sym: String): Symbol =
    inverseSemanticdbSymbols(sym).headOption.getOrElse(NoSymbol)

  def renamedSymbols(context: Context): collection.Map[Symbol, Name] = {
    val result = mutable.Map.empty[Symbol, Name]
    context.imports.foreach { imp =>
      lazy val pre = imp.qual.tpe
      imp.tree.selectors.foreach { sel =>
        if (sel.rename != null) {
          val member = pre.member(sel.name)
          result(member) = sel.rename
          member.companion match {
            case NoSymbol =>
            case companion =>
              result(companion) = sel.rename
          }
        }
      }
    }
    result
  }
  lazy val renameConfig: collection.Map[Symbol, Name] =
    Map[String, String](
      "scala/collection/mutable/" -> "mutable.",
      "java/util/" -> "ju."
    ).map { case (sym, name) =>
      val nme =
        if (name.endsWith("#")) TypeName(name.stripSuffix("#"))
        else if (name.endsWith(".")) TermName(name.stripSuffix("."))
        else TermName(name)
      inverseSemanticdbSymbol(sym) -> nme
    }.view
      .filterKeys(_ != NoSymbol)
      .toMap

  private def backtickify(
      tpe: Type,
      loop: (Type, Option[ShortName]) => Type,
      withPrefix: Boolean
  ): Type =
    tpe match {
      case TypeRef(pre, sym, args)
          if Identifier.needsBacktick(sym.decodedName) &&
            sym != definitions.ByNameParamClass && // `=> T` is OK
            !sym.isPackageObject &&
            !definitions.isRepeated(sym) &&
            !sym.isAbstractType &&
            sym.owner != definitions.ScalaPackageClass =>
        val rawPrettyName = Identifier.backtickWrapWithoutCheck(sym.decodedName)
        val prefix = if (withPrefix) pre.prefixString else ""
        if (args.isEmpty) {
          new PrettyType(prefix + rawPrettyName)
        } else {
          val typeArgsStr =
            args.map(arg => loop(arg, None)).map(_.toString()).mkString(", ")
          new PrettyType(prefix + s"$rawPrettyName[$typeArgsStr]")
        }
      case TypeRef(p, sym, args) =>
        val pre = if (withPrefix) p else NoPrefix
        TypeRef(pre, sym, args.map(arg => loop(arg, None)))
      case _ =>
        tpe
    }

  def shortType(longType: Type, history: ShortenedNames): Type = {
    val isVisited = mutable.Set.empty[(Type, Option[ShortName])]
    val cached = new ju.HashMap[(Type, Option[ShortName]), Type]()
    def loop(tpe: Type, name: Option[ShortName]): Type = {
      val key = tpe -> name
      // NOTE(olafur) Prevent infinite recursion, see https://github.com/scalameta/metals/issues/749
      if (isVisited(key)) return cached.getOrDefault(key, tpe)
      isVisited += key
      val result = tpe match {
        case TypeRef(_, sym, List(arg)) if sym.fullName == "scala.Tuple1" =>
          new PrettyType(s"Tuple1[${loop(arg, None)}]")
        case tpe @ TypeRef(pre, sym, args) =>
          if (history.isSymbolInScope(sym, pre)) {
            backtickify(tpe, loop, false)
          } else {
            val ownerSymbol = pre.termSymbol
            history.config.get(ownerSymbol) match {
              case Some(rename)
                  if history.tryShortenName(ShortName(rename, ownerSymbol)) =>
                TypeRef(
                  new PrettyType(rename.toString),
                  sym,
                  args.map(arg => loop(arg, None))
                )
              case _ =>
                history.renames.get(sym) match {
                  case Some(rename)
                      if history.nameResolvesToSymbol(rename, sym) =>
                    TypeRef(
                      NoPrefix,
                      sym.newErrorSymbol(rename),
                      args.map(arg => loop(arg, None))
                    )
                  case _ if history.isSymbolInScope(sym, pre) =>
                    TypeRef(
                      NoPrefix,
                      sym,
                      args.map(arg => loop(arg, None))
                    )
                  case _ =>
                    if (
                      sym.isAliasType &&
                      (sym.isAbstract ||
                        sym.overrides.lastOption.exists(_.isAbstract))
                    ) {

                      // Always dealias abstract type aliases but leave concrete aliases alone.
                      // trait Generic { type Repr /* dealias */ }
                      // type Catcher[T] = PartialFunction[Throwable, T] // no dealias
                      loop(tpe.dealias, name)
                    } else if (history.owners(pre.typeSymbol)) {
                      if (history.nameResolvesToSymbol(sym.name, sym)) {
                        TypeRef(NoPrefix, sym, args.map(arg => loop(arg, None)))
                      } else {
                        TypeRef(
                          ThisType(pre.typeSymbol),
                          sym,
                          args.map(arg => loop(arg, None))
                        )
                      }
                    } else {
                      val preparedType = TypeRef(
                        loop(pre, Some(ShortName(sym))),
                        sym,
                        args
                      )

                      backtickify(preparedType, loop, true)
                    }
                }
            }
          }
        case SingleType(pre, sym) =>
          if (sym.isScalaPackageObject) {
            // NOTE(olafur): special-case scala package object because
            // `Type.toString()` doesn't print the "scala." prefix in
            // "scala.Seq[T]" even when it's needed.
            loop(ThisType(sym.owner), name)
          } else if (sym.hasPackageFlag || sym.isPackageObjectOrClass) {
            val dotSyntaxFriendlyName = name.map { name0 =>
              if (name0.symbol.isStatic) name0
              else {
                // Use the prefix rather than the real owner to maximize the
                // chances of shortening the reference: when `name` is directly
                // nested in a non-statically addressable type (class or trait),
                // its original owner is that type (requiring a type projection
                // to reference it) while the prefix is its concrete owner value
                // (for which the dot syntax works).
                // https://docs.scala-lang.org/tour/inner-classes.html
                // https://danielwestheide.com/blog/the-neophytes-guide-to-scala-part-13-path-dependent-types/
                ShortName(name0.symbol.cloneSymbol(sym))
              }
            }
            if (history.tryShortenName(dotSyntaxFriendlyName)) NoPrefix
            else tpe
          } else {
            if (history.isSymbolInScope(sym, pre)) SingleType(NoPrefix, sym)
            else {
              pre match {
                case ThisType(psym) if history.isSymbolInScope(psym, pre) =>
                  SingleType(NoPrefix, sym)
                case _ =>
                  SingleType(loop(pre, Some(ShortName(sym))), sym)
              }
            }
          }
        case ThisType(sym) =>
          if (history.tryShortenName(name)) NoPrefix
          else {
            val owners = sym.ownerChain
            val prefix = owners.indexWhere { owner =>
              owner.owner != definitions.ScalaPackageClass &&
              history.tryShortenName(
                Some(ShortName(owner.name, owner))
              )
            }
            if (prefix < 0) {
              new PrettyType(history.fullname(sym))
            } else {
              val names = owners
                .take(prefix + 1)
                .reverse
                .map(s => m.Term.Name(s.nameSyntax))
              val ref = names.tail.foldLeft(names.head: m.Term.Ref) {
                case (qual, name) => m.Term.Select(qual, name)
              }
              new PrettyType(ref.syntax)
            }
          }
        case ConstantType(Constant(sym: TermSymbol))
            if sym.hasFlag(gf.JAVA_ENUM) =>
          loop(SingleType(sym.owner.thisPrefix, sym), None)
        case ConstantType(Constant(tpe: Type)) =>
          ConstantType(Constant(loop(tpe, None)))
        case SuperType(thistpe, supertpe) =>
          SuperType(loop(thistpe, None), loop(supertpe, None))
        case RefinedType(parents, decls) =>
          RefinedType(parents.map(parent => loop(parent, None)), decls)
        case AnnotatedType(annotations, underlying) =>
          AnnotatedType(annotations, loop(underlying, None))
        case ExistentialType(quantified, underlying) =>
          ExistentialType(
            quantified.map(sym => sym.setInfo(loop(sym.info, None))),
            loop(underlying, None)
          )
        case PolyType(tparams, resultType) =>
          PolyType(tparams, resultType.map(t => loop(t, None)))
        case NullaryMethodType(resultType) =>
          loop(resultType, None)
        case TypeBounds(lo, hi) =>
          TypeBounds(loop(lo, None), loop(hi, None))
        case MethodType(params, resultType) =>
          MethodType(params, loop(resultType, None))
        case ErrorType =>
          definitions.AnyTpe
        case t => t
      }
      cached.putIfAbsent(key, result)
      result
    }

    longType match {
      case ThisType(_) => longType
      case _ => loop(longType, None)
    }
  }

  case class ShortName(
      name: Name,
      symbol: Symbol
  ) {
    def isRename: Boolean = symbol.name != name
    def asImport: String = {
      val ident = Identifier(name)
      if (isRename) s"${Identifier(symbol.name)} => ${ident}"
      else ident
    }
    def owner: Symbol = symbol.owner
  }
  object ShortName {
    def apply(sym: Symbol): ShortName =
      ShortName(sym.name, sym)
  }

  class ShortenedNames(
      val missingImports: mutable.Map[Name, ShortName] = mutable.Map.empty,
      val lookupSymbol: Name => List[NameLookup] = _ => Nil,
      val config: collection.Map[Symbol, Name] = Map.empty,
      val renames: collection.Map[Symbol, Name] = Map.empty,
      val owners: collection.Set[Symbol] = Set.empty
  ) {
    def this(context: Context) =
      this(lookupSymbol = { name =>
        context.lookupSymbol(name, _ => true) :: Nil
      })

    def fullname(sym: Symbol): String = {
      if (topSymbolResolves(sym)) sym.fullNameSyntax
      else s"_root_.${sym.fullNameSyntax}"
    }

    def topSymbolResolves(sym: Symbol): Boolean = {
      // Returns the package `a` for the symbol `_root_/a/b.c`
      def topPackage(s: Symbol): Symbol = {
        val owner = s.owner
        if (
          s.isRoot || s.isRootPackage || s == NoSymbol || s.owner.isEffectiveRoot || s == owner
        ) {
          s
        } else {
          topPackage(owner)
        }
      }
      val top = topPackage(sym)
      nameResolvesToSymbol(top.name.toTermName, top)
    }

    def isSymbolInScope(sym: Symbol, prefix: Type = NoPrefix): Boolean = {
      nameResolvesToSymbol(sym.name, sym, prefix)
    }
    def nameResolvesToSymbol(
        name: Name,
        sym: Symbol,
        prefix: Type = NoPrefix
    ): Boolean = {
      lookupSymbol(name) match {
        case Nil => true
        case lookup =>
          lookup.exists {
            case LookupSucceeded(qual, symbol) =>
              symbol.isKindaTheSameAs(sym) && {
                prefix == NoPrefix ||
                prefix.isInstanceOf[PrettyType] ||
                qual.tpe.computeMemberType(symbol) <:<
                  prefix.computeMemberType(sym)
              }
            case l => l.symbol.isKindaTheSameAs(sym)
          }
      }
    }

    def tryShortenName(short: ShortName): Boolean = {
      val ShortName(name, sym) = short
      missingImports.get(name) match {
        case Some(ShortName(_, other)) =>
          other.isKindaTheSameAs(sym)
        case _ =>
          val results =
            Iterator(lookupSymbol(name), lookupSymbol(name.otherName))
          results.flatten.filter(_ != LookupNotFound).toList match {
            case Nil =>
              // Missing imports must be addressable via the dot operator
              // syntax (as type projection is not allowed in imports).
              // https://lptk.github.io/programming/2019/09/13/type-projection.html
              if (
                sym.isStaticMember || // Java static
                sym.owner.ownerChain.forall { s =>
                  // ensure the symbol can be referenced in a static manner, without any instance
                  s.isPackageClass || s.isPackageObjectClass || s.isModule
                }
              ) {
                missingImports(name) = short
                true
              } else false
            case lookup =>
              lookup.forall(_.symbol.isKindaTheSameAs(sym))
          }
      }
    }
    def tryShortenName(name: Option[ShortName]): Boolean =
      name match {
        case Some(short) =>
          tryShortenName(short)
        case _ =>
          false
      }

  }

  implicit class XtensionNameMetals(name: Name) {
    def otherName: Name =
      if (name.isTermName) name.toTypeName
      else name.toTermName
  }
  implicit class XtensionSymbolMetals(sym: Symbol) {
    def isScalaPackageObject: Boolean = {
      sym.isPackageObject &&
      sym.owner == definitions.ScalaPackageClass
    }
    def javaClassSymbol: Symbol = {
      if (sym.isJavaModule && !sym.hasPackageFlag) sym.companionClass
      else sym
    }
    def nameSyntax: String = {
      if (sym.isEmptyPackage || sym.isEmptyPackageClass) "_empty_"
      else if (sym.isRootPackage || sym.isRoot) "_root_"
      else sym.nameString
    }
    def fullNameSyntax: String = {
      val out = new java.lang.StringBuilder
      def loop(s: Symbol): Unit = {
        if (
          s.isRoot || s.isRootPackage || s == NoSymbol || s.owner.isEffectiveRoot
        ) {
          out.append(Identifier(s.nameSyntax))
        } else {
          loop(s.effectiveOwner.enclClass)
          out.append('.').append(Identifier(s.name))
        }
      }
      loop(sym)
      out.toString
    }
    def isLocallyDefinedSymbol: Boolean = {
      sym.isLocalToBlock && sym.pos.isDefined
    }

    def asInfixPattern: Option[String] =
      if (
        sym.isCase &&
        !Character.isUnicodeIdentifierStart(sym.decodedName.head)
      ) {
        sym.primaryConstructor.paramss match {
          case (a :: b :: Nil) :: _ =>
            Some(s"${a.decodedName} ${sym.decodedName} ${b.decodedName}")
          case _ => None
        }
      } else {
        None
      }

    def isKindaTheSameAs(other: Symbol): Boolean = {
      if (other == NoSymbol) sym == NoSymbol
      else if (sym == NoSymbol) false
      else if (sym.hasPackageFlag) {
        // NOTE(olafur) hacky workaround for comparing module symbol with package symbol
        other.fullName == sym.fullName
      } else {
        other.dealiased == sym.dealiased ||
        other.companion == sym.dealiased ||
        semanticdbSymbol(other.dealiased) == semanticdbSymbol(sym.dealiased)
      }
    }

    def snippetCursor: String = sym.paramss match {
      case Nil =>
        "$0"
      case Nil :: Nil =>
        "()$0"
      case _ =>
        "($0)"
    }

    def isDefined: Boolean =
      sym != null &&
        sym != NoSymbol &&
        !sym.isErroneous

    def isNonNullaryMethod: Boolean =
      sym.isMethod &&
        !sym.info.isInstanceOf[NullaryMethodType] &&
        !sym.paramss.isEmpty

    def isJavaModule: Boolean =
      sym.isJava && sym.isModule

    def hasTypeParams: Boolean =
      sym.typeParams.nonEmpty ||
        (sym.isJavaModule && sym.companionClass.typeParams.nonEmpty)

    def requiresTemplateCurlyBraces: Boolean = {
      sym.isTrait || sym.isInterface || sym.isAbstractClass
    }
    def isTypeSymbol: Boolean =
      sym.isType ||
        sym.isClass ||
        sym.isTrait ||
        sym.isInterface ||
        sym.isJavaModule

    def dealiasedSingleType: Symbol =
      if (sym.isValue) {
        sym.info.resultType match {
          case SingleType(_, dealias) => dealias
          case _ => sym
        }
      } else {
        sym
      }
    def dealiased: Symbol =
      if (sym.isAliasType) sym.info.dealias.typeSymbol
      else if (sym.isValue) dealiasedSingleType
      else sym
  }

  /**
   * A `Type` with custom pretty-printing representation, not used for
   * typechecking.
   *
   * NOTE(olafur) Creating a new `Type` subclass is a hack, a better long-term
   * solution would be to implement a custom pretty-printer for types so that we
   * don't have to rely on `Type.toString`.
   */
  class PrettyType(
      override val prefixString: String,
      override val safeToString: String
  ) extends Type {
    def this(string: String) =
      this(string + ".", string)
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy