25

我想从给定的 Scala 项目中提取所有方法的调用图,这些方法是项目自身源代码的一部分。

据我了解,演示编译器并没有启用它,它需要一直向下到实际的编译器(或编译器插件?)。

您能否建议完整的代码,它可以安全地适用于大多数 scala 项目,但那些使用最古怪的动态语言功能的项目?对于调用图,我的意思是一个有向(可能是循环的)图,包括class/trait + method顶点,其中边 A -> B 表示 A 可以调用 B。

应避免调用/从库调用或“标记”为项目自身源之外的调用。

编辑:

请参阅我的宏天堂衍生原型解决方案,基于@dk14 的领导,作为下面的答案。托管在https://github.com/matanster/sbt-example-paradise的 github 上。

4

3 回答 3

5

这是工作原型,它将必要的基础数据打印到控制台作为概念证明。http://goo.gl/oeshdx

这是如何工作的

我已经适应了来自宏观天堂顶级样板的@dk14 的概念。

宏天堂允许您定义一个注释,将您的宏应用于源代码中的任何注释对象。从那里您可以访问编译器为源代码生成的 AST,并且可以使用 scala 反射 API 来探索 AST 元素的类型信息。Quasiquotes(词源来自haskell或其他东西)用于匹配相关元素的AST。

更多关于准报价

通常需要注意的重要一点是,quasiquotes 在 AST 上工作,但它们是一个乍一看很奇怪的 api,而不是 AST 的直接表示(!)。AST 由天堂的宏注释为您挑选,然后准引号是探索手头 AST 的工具:您使用准引号匹配、切片和切块抽象语法树。

关于quasiquotes的实际需要注意的是,有固定的quasiquote模板用于匹配每种类型的scala AST - scala类定义的模板,scala方法定义的模板等。这些模板都在这里提供,使其非常很容易将手头的 AST 与其有趣的成分进行匹配和解构。虽然这些模板乍一看可能令人望而生畏,但它们大多只是模仿 scala 语法的模板,您可以自由地将其中的$前置变量名称更改为更符合您口味的名称。

我仍然需要进一步磨练我使用的准引用匹配,这些匹配目前并不完美。但是,我的代码似乎在许多情况下都能产生所需的结果,并且将匹配精度提高到 95% 可能是可行的。

样本输出

found class B
class B has method doB
found object DefaultExpander
object DefaultExpander has method foo
object DefaultExpander has method apply
  which calls Console on object scala of type package scala
  which calls foo on object DefaultExpander.this of type object DefaultExpander
  which calls <init> on object new A of type class A
  which calls doA on object a of type class A
  which calls <init> on object new B of type class B
  which calls doB on object b of type class B
  which calls mkString on object tags.map[String, Seq[String]](((tag: logTag) => "[".+(Util.getObjectName(tag)).+("]")))(collection.this.Seq.canBuildFrom[String]) of type trait Seq
  which calls map on object tags of type trait Seq
  which calls $plus on object "[".+(Util.getObjectName(tag)) of type class String
  which calls $plus on object "[" of type class String
  which calls getObjectName on object Util of type object Util
  which calls canBuildFrom on object collection.this.Seq of type object Seq
  which calls Seq on object collection.this of type package collection
  .
  .
  .

很容易看出调用者和被调用者如何从这些数据中关联起来,以及如何过滤或标记项目源之外的调用目标。这就是 scala 2.11 的全部内容。使用此代码,需要为每个源文件中的每个类/对象/等添加注释。

剩下的挑战主要是:

剩下的挑战:

  1. 完成工作后会崩溃。取决于https://github.com/scalamacros/paradise/issues/67
  2. 需要找到一种方法最终将魔法应用于整个源文件,而无需使用静态注释手动注释每个类和对象。这在目前来说是相当小的,并且不可否认,能够控制类以包含和忽略无论如何都是有好处的。在(几乎)每个顶级源文件定义之前植入注释的预处理阶段将是一个不错的解决方案。
  3. 磨练匹配器,以便匹配所有且仅相关的定义 - 使其超出我简单粗略的测试范围内的通用性和可靠性。

思考的替代方法

非循环让我想到了一种完全相反的方法,它仍然坚持 scala 编译器的领域——它检查编译器为源生成的所有符号(就像我从源收集的一样多)。它的作用是检查循环引用(有关详细定义,请参见 repo)。每个符号都应该附有足够的信息,以得出非循环需要生成的引用图。

如果可行,受这种方法启发的解决方案可能会定位每个符号的父“所有者”,而不是像非循环本身那样关注源文件连接图。因此,通过一些努力,它将恢复每个方法的类/对象所有权。不确定这种设计是否不会在计算上爆炸,也不知道如何确定性地获得包含每个符号的类。

好处是这里不需要宏注释。不利的一面是,这不能像宏天堂那样轻易地允许使用运行时检测,这有时可能很有用。

于 2015-04-27T20:28:23.757 回答
4

它需要更精确的分析,但作为开始,这个简单的宏将打印所有可能的应用程序,但它需要宏天堂并且所有跟踪的类都应该有@trace注释:

class trace extends StaticAnnotation { 
  def macroTransform(annottees: Any*) = macro tracerMacro.impl
}

object tracerMacro {

  def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {

    import c.universe._
    val inputs = annottees.map(_.tree).toList
    def analizeBody(name: String, method: String, body: c.Tree) = body.foreach {
      case q"$expr(..$exprss)" => println(name + "." + method + ": " + expr)
      case _ =>
    }


    val output = inputs.head match {
      case q"class $name extends $parent { ..$body }" =>
        q"""
            class $name extends $parent {
              ..${
                   body.map {
                       case x@q"def $method[..$tt] (..$params): $typ = $body" =>
                         analizeBody(name.toString, method.toString, body)
                         x

                       case x@q"def $method[..$tt]: $typ = $body" =>
                         analizeBody(name.toString, method.toString, body)
                        x

                   }
                 }
            }
          """
      case x => sys.error(x.toString)
    }




    c.Expr[Any](output)
  }
}

输入:

  @trace class MyF {
    def call(param: Int): Int = {
      call2(param)
      if(true) call3(param) else cl()
    }
    def call2(oaram: Int) = ???
    def cl() = 5
    def call3(param2: Int) = ???
  }

输出(作为编译器的警告,但您可以输出到文件而不是 println):

  Warning:scalac: MyF.call: call2
  Warning:scalac: MyF.call: call3
  Warning:scalac: MyF.call: cl

当然,您可能想要c.typeCheck(input)它(因为现在expr.tpe找到的树是 equals null)并找到这个调用方法实际上属于哪个类,所以生成的代码可能不是那么简单。

PS macroAnnotations 为您提供未经检查的树(因为它在比常规宏更早的编译器阶段),因此如果您想要进行类型检查 - 最好的方法是围绕您要进行类型检查的代码段调用一些常规宏,并在内部处理它这个宏(你甚至可以传递一些静态参数)。由宏注释生成的树内的每个常规宏都将照常执行。

于 2015-04-24T12:12:19.403 回答
2

编辑
这个答案的基本思想是完全绕过(相当复杂的)Scala 编译器,.class最后从生成的文件中提取图形。具有足够详细输出的反编译器似乎可以将问题减少到基本的文本操作。然而,经过更详细的检查,事实证明并非如此。一种是回到原点,但使用混淆的 Java 代码而不是原始的 Scala 代码。.class所以这个提议并没有真正起作用,尽管使用最终文件而不是 Scala 编译器内部使用的中间结构背后有一些基本原理。
/编辑

我不知道是否有开箱即用的工具(我假设您已经检查过)。我只有一个非常粗略的想法,演示编译器是什么。但是,如果您只想提取一个图,其中方法作为节点,方法的潜在调用作为边,我有一个快速而简单的解决方案的建议。这仅在您想将其用于某种可视化时才有效,如果您想执行一些巧妙的重构操作,它根本无济于事。

如果您想尝试自己构建这样的图形生成器,结果可能比您想象的要简单得多。但是为此,您需要一路向下,甚至经过编译器。只需获取已编译 .class的文件,然后在其上使用 CFR java 反编译器之类的东西。

在单个编译.class文件上使用时,CFR 将生成当前类所依赖的类列表(这里我以我的小宠物项目为例):

import akka.actor.Actor;
import akka.actor.ActorContext;
import akka.actor.ActorLogging;
import akka.actor.ActorPath;
import akka.actor.ActorRef;
import akka.actor.Props;
import akka.actor.ScalaActorRef;
import akka.actor.SupervisorStrategy;
import akka.actor.package;
import akka.event.LoggingAdapter;
import akka.pattern.PipeToSupport;
import akka.pattern.package;
import scala.Function1;
import scala.None;
import scala.Option;
import scala.PartialFunction;
...
(very long list with all the classes this one depends on)
...
import scavenger.backend.worker.WorkerCache$class;
import scavenger.backend.worker.WorkerScheduler;
import scavenger.backend.worker.WorkerScheduler$class;
import scavenger.categories.formalccc.Elem;

然后它会吐出一些看起来很可怕的代码,可能看起来像这样(小摘录):

public PartialFunction<Object, BoxedUnit> handleLocalResponses() {
    return SimpleComputationExecutor.class.handleLocalResponses((SimpleComputationExecutor)this);
}

public Context provideComputationContext() {
    return ContextProvider.class.provideComputationContext((ContextProvider)this);
}

public ActorRef scavenger$backend$worker$MasterJoin$$_master() {
    return this.scavenger$backend$worker$MasterJoin$$_master;
}

@TraitSetter
public void scavenger$backend$worker$MasterJoin$$_master_$eq(ActorRef x$1) {
    this.scavenger$backend$worker$MasterJoin$$_master = x$1;
}

public ActorRef scavenger$backend$worker$MasterJoin$$_masterProxy() {
    return this.scavenger$backend$worker$MasterJoin$$_masterProxy;
}

@TraitSetter
public void scavenger$backend$worker$MasterJoin$$_masterProxy_$eq(ActorRef x$1) {
    this.scavenger$backend$worker$MasterJoin$$_masterProxy = x$1;
}

public ActorRef master() {
    return MasterJoin$class.master((MasterJoin)this);
}

这里应该注意的是,所有方法都带有完整的签名,包括定义它们的类,例如:

Scheduler.class.schedule(...)
ContextProvider.class.provideComputationContext(...)
SimpleComputationExecutor.class.fulfillPromise(...)
SimpleComputationExecutor.class.computeHere(...)
SimpleComputationExecutor.class.handleLocalResponses(...)

因此,如果您需要一个快速而简单的解决方案,您很可能只需要大约 10 行awkgrep和魔法就可以得到很好的邻接列表sortuniq其中所有类都作为节点,方法作为边。

我从来没有尝试过,这只是一个想法。我不能保证 Java 反编译器在 Scala 代码上运行良好。

于 2015-04-22T00:53:51.127 回答