提取scala项目的完整调用图(艰难的)

mat*_*ter 24 scala scalac scala-compiler scala-macro-paradise

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

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

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

应避免或"标记"来自/来自图书馆的呼叫在项目自己的来源之外.

编辑:

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

mat*_*ter 5

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

这是如何工作的

我已经从宏观天堂的顶级样板上调整了@ dk14的概念.

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

更多关于Quasiquotes

值得注意的一点是,quasiquotes在AST上工作,但它们是一个奇怪的第一眼api而不是AST的直接表示(!).AST通过paradise的宏注释为你拾取,然后quasiquotes是探索手头AST的工具:你使用quasiquotes匹配,切片和切块抽象语法树.

关于quasiquotes的实际注意事项是有固定的quasiquote模板用于匹配每种类型的scala AST - 用于scala类定义的模板,用于scala方法定义的模板等.这些tempaltes都在这里提供,使它非常简单地匹配解构手头的AST到其有趣的成分.虽然模板看起来可能看起来令人生畏,但它们大多只是模仿scala语法的模板,您可以自由地将其中的$前置变量名称更改为感觉更符合您口味的名称.

我仍然需要进一步磨练我使用的quasiquote匹配,目前这些匹配并不完美.但是,我的代码似乎在许多情况下产生了期望的结果,并且将匹配精度达到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
  .
  .
  .
Run Code Online (Sandbox Code Playgroud)

很容易看出调用者和被调用者如何与这些数据相关联,以及如何过滤或标记出项目源外的调用目标.这完全适用于scala 2.11.使用此代码,需要在每个源文件中为每个类/对象/ etc添加注释.

仍然存在的挑战主要是:

剩下的挑战:

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

思考的另类方法

非循环使人想起一个相反的方法仍然坚持scala编译器的领域 - 它检查由编译器为源生成的所有符号(尽可能多地从源代码中收集).它的作用是检查循环引用(有关详细定义,请参阅repo).据推测,每个符号都附有足够的信息,以得出非循环需要生成的引用图.

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

好处是这里不需要宏观注释.缺点是,由于宏观天堂相当容易允许,因此无法使用运行时仪表,这可能有时很有用.

  • 顺便说一句,编程语言中"quasiquote"这个词的词源可以追溯到Lisps. (2认同)

And*_*kin 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;
Run Code Online (Sandbox Code Playgroud)

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

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);
}
Run Code Online (Sandbox Code Playgroud)

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

Scheduler.class.schedule(...)
ContextProvider.class.provideComputationContext(...)
SimpleComputationExecutor.class.fulfillPromise(...)
SimpleComputationExecutor.class.computeHere(...)
SimpleComputationExecutor.class.handleLocalResponses(...)
Run Code Online (Sandbox Code Playgroud)

因此,如果您需要一个快速而肮脏的解决方案,那么您很可能只需大约 10 行awk、和魔法即可获得漂亮的邻接列表,其中所有类作为节点,方法作为边缘。grepsortuniq

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

  • 我想我正确地理解了你的问题,我希望每个方法“f”都可以生成方法“g1,...,gN”的列表,这样“f”就可以用“g1”来实现, ...,gN`(即“f”可以在运行时调用每个“gK”)。然而,我仔细研究了反编译器的输出,现在我不得不承认我的建议可能行不通。 (2认同)