1

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)

Corey Woodfield
  • 350
  • 2
  • 10

1 Answers1

1

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.

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • 1
    Hmm. Not printing implicit or using makes sense, given that they do apply to the whole list. That doesn't explain why it's printed in the original but not in the copy though – Corey Woodfield Sep 14 '22 at 16:12