scalascalametascalafix

Why is scala.meta.Term.Param#toString dropping modifiers?


I'm trying to rename the parameter of an anonymous function using a semantic scalafix plugin. The relevant code looks like this:

  case Term.Apply(func, args) =>
    args.collect { case Term.Block(List(Term.Function(List(arg), _))) =>
      Patch.replaceTree(arg, arg.copy(name = Term.Name("components")).toString())

The problem is, this is changing { implicit foo => to { components => (i.e. it's dropping the implicit modifier). I initially thought it was being dropped by the copy method for some reason, but I added some printlns and that's not the case: the implicit modifier exists on the copy, but just isn't being included in the toString output. Anyone know what's going on here? And how I can get the implicit to be included in the output?

printlns:

      println("***********ORIGINAL***********")
      println("toString:\t" + arg.toString())
      println("name:\t\t" + arg.name)
      println("modifiers:\t" + arg.mods)
      println("syntax:\t\t" + arg.syntax)
      println("structure:\t" + arg.structure)
      println("***********COPY***********")
      val copy = arg.copy(name = Term.Name("components"))
      println("toString:\t" + copy.toString())
      println("name:\t\t" + copy.name)
      println("modifiers:\t" + copy.mods)
      println("syntax:\t\t" + copy.syntax)
      println("structure:\t" + copy.structure)

output:

***********ORIGINAL***********
toString:   implicit app
name:       app
modifiers:  List(implicit)
syntax:     implicit app
structure:  Term.Param(List(Mod.Implicit), Term.Name("app"), None, None)
***********COPY***********
toString:   components
name:       components
modifiers:  List(implicit)
syntax:     components
structure:  Term.Param(List(Mod.Implicit), Term.Name("components"), None, None)

(notice that the copy has implicit in its list of modifiers, but it doesn't show up in the outputs of toString or syntax)


Solution

  • The thing is that when Scalameta (4.5.13) prints a Term.Param it skips Mod.Implicit and Mod.Using

      case t: Term.Param =>
        // NOTE: `implicit/using` in parameters is skipped as it applies to whole list
        printParam(t, t.mods.filterNot(x => x.is[Mod.Implicit] || x.is[Mod.Using]))
    

    Then it prints List[List[Term.Param]] correctly

    implicit def syntaxParamss: Syntax[List[List[Term.Param]]] = Syntax { paramss =>
      def usingImplicit(params: List[Term.Param]): Show.Result = {
        if (params.exists(_.mods.exists(_.is[Mod.Using])))
          s("using ", r(params, ", "))
        else
          w("implicit ", r(params, ", "), params.exists(_.mods.exists(_.is[Mod.Implicit])))
      }
      r(
        paramss.map(params => {
          s(
            "(",
            usingImplicit(params),
            ")"
          )
        }),
        ""
      )
    }
    

    but this doesn't help us.

    The easiest fix is just to add implicit when necessary

    doc.tree.collect {
      case Term.Apply(func, args) =>
        args.collect {
          case Term.Block(List(Term.Function(List(arg), _))) =>
            val res = arg.copy(name = Term.Name("components"))
            val prefix = if (res.mods.exists(_.is[Mod.Implicit])) "implicit " else ""
            Patch.replaceTree(arg, prefix + res.toString)
        }.asPatch
    }.asPatch
    

    why it's printed in the original but not in the copy though

    Because Scalameta prints differently newly parsed trees and transformed/generated trees. For the former it preserves their original string representation with original formatting. For the latter it prints them with corresponding instance of scala.meta.prettyprinters.Show i.e. skips implicit for a parameter etc.

    arg.toString calls scala.meta.internal.prettyprinters.TreeSyntax.apply[Term.Param](Scala213).apply(arg).

    The method TreeSyntax.apply is

    def apply[T <: Tree](dialect: Dialect): Syntax[T] = {
      // NOTE: This is the current state of the art of smart prettyprinting.
      // If we prettyprint a tree that's just been parsed with the same dialect,
      // then we retain formatting. Otherwise, we don't, even in the tiniest.
      // I expect to improve on this in the nearest future, because we had it much better until recently.
      Syntax { (x: T) =>
        x.origin match {
          // NOTE: Options don't really matter,
          // because if we've parsed a tree, it's not gonna contain lazy seqs anyway.
          // case Origin.Parsed(_, originalDialect, _) if dialect == originalDialect && options == Options.Eager =>
          case o @ Origin.Parsed(_, `dialect`, _) => s(o.position.text)
          case _ => reprint(x)(dialect)
        }
      }
    }
    

    Here in the pattern matching for Origin.Parsed (the origin of a newly parsed tree) the method returns Result.Str, for Origin.None (the origin of a transformed/generated tree) it returns Result.Sequence.

    println(arg) // implicit y: Boolean
    println(arg.structure) // Term.Param(List(Mod.Implicit), Term.Name("y"), Some(Type.Name("Boolean")), None)
    println(arg.getClass) // class scala.meta.Term$Param$TermParamImpl
    println(arg.origin) // Parsed(Input.VirtualFile("fix/Scalafixdemo.scala", "... implicit y: Boolean => ..."),Scala213,TokenStreamPosition(45,51))
    println(TreeSyntax.apply[Term.Param](Scala213).apply(arg).getClass) 
    // class scala.meta.prettyprinters.Show$Str
    
    val res = arg.copy(name = Term.Name("components"))
    println(res) // components: Boolean
    println(res.structure) // Term.Param(List(Mod.Implicit), Term.Name("components"), Some(Type.Name("Boolean")), None)
    println(res.getClass) // class scala.meta.Term$Param$TermParamImpl
    println(res.origin) // None
    println(TreeSyntax.apply[Term.Param](Scala213).apply(res).getClass) 
    // class scala.meta.prettyprinters.Show$Sequence
    

    The method scala.meta.internal.trees.InternalTree#origin is private[meta] so if you play with it put your rule into the package scala.meta.

    Term.Param is not a case class and .copy is not a method of a case class. arg and res are actually instances of macro-generated class Term.Param.TermParamImpl.