我想从给定的Scala项目中提取所有方法的调用图,这些方法是该项目自己的源代码的一部分。
据我所知,表示编译器无法实现这一点,它需要一直到实际的编译器(或编译器插件?)。
你能建议完整的代码吗?它可以安全地适用于大多数scala项目,但那些使用最古怪的动态语言功能的项目除外?对于调用图,我指的是包括class/trait + method
顶点的有向(可能是循环的)图,其中边a->B指示a可以调用B。
应避免对库的调用或将其"标记"为项目自身源之外的调用。
编辑:
请参阅我的宏天堂衍生原型解决方案,基于@dk14的线索,作为下面的答案。托管于githubhttps://github.com/matanster/sbt-example-paradise.
这是一个工作原型,它将必要的底层数据打印到控制台,作为概念验证。http://goo.gl/oeshdx.
工作原理
我把@dk14的概念改编成了宏天堂的顶级样板。
宏天堂允许您定义一个注释,该注释将宏应用于源代码中的任何注释对象。从那里,您可以访问编译器为源生成的AST,scala反射api可以用于探索AST元素的类型信息。Quasiquotes(词源来自haskell或其他什么)用于匹配相关元素的AST。
更多关于Quasiquotes
需要注意的一点是,准引号在AST上起作用,但乍一看它们是一个奇怪的api,而不是AST(!)的直接表示。AST是由天堂的宏注释为您挑选的,然后准引号是探索手头AST的工具:您可以使用准引号对抽象语法树进行匹配、切片和骰子。
关于准引号,需要注意的是,有固定的准引号模板用于匹配每种类型的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
.
.
.
很容易看出如何从这些数据中关联调用者和被调用者,以及如何筛选或标记项目源之外的调用目标。这就是scala2.11的全部内容。使用此代码,需要为每个源文件中的每个类/对象等准备一个注释。
剩下的挑战主要是:
仍然存在的挑战:
- 完成任务后,此操作将崩溃。铰链https://github.com/scalamacros/paradise/issues/67
- 需要找到一种方法,在不使用静态注释手动注释每个类和对象的情况下,最终将魔术应用于整个源文件。目前,这还很小,无可否认,无论如何,控制类的包含和忽略都有好处。在(几乎)每个顶级源文件定义之前植入注释的预处理阶段是一个不错的解决方案
- 打磨匹配器,使所有相关的定义都匹配——使这一点超越我简单粗略的测试,变得通用而坚实
思考的替代方法
无循环让人想起了一种完全相反的方法,它仍然坚持scala编译器的领域——它检查编译器为源代码生成的所有符号(就像我从源代码收集的一样多)。它所做的是检查循环引用(有关详细定义,请参阅repo)。每个符号都应该有足够的信息,以导出非循环需要生成的引用图。
受这种方法启发的解决方案可以,如果可行,定位父";所有者;而不是像非循环本身那样关注源文件连接的图。因此,只要付出一些努力,它就会恢复每个方法的类/对象所有权。不确定这个设计是否不会在计算上爆炸,也不确定如何确定地获得包含每个符号的类。
好处是这里不需要宏注释。不利的一面是,这不能像宏天堂所允许的那样随意使用运行时检测,这有时可能很有用。
它需要更精确的分析,但作为开始,这个简单的宏将打印所有可能的应用程序,但它需要宏天堂,所有跟踪的类都应该有@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)
-it(因为现在找到的树上的expr.tpe
等于null
),并找到这个调用方法实际上属于哪个类,所以生成的代码可能并不那么琐碎。
p.S.macroAnnotations为您提供了未检查的树(因为它在编译器阶段比常规宏更早),所以如果您想要进行类型检查,最好的方法是通过调用一些常规宏来包围要进行类型检查的代码段,并在该宏中处理它(您甚至可以传递一些静态参数)。树中由宏注释生成的每个常规宏都将照常执行。
Edit
这个答案的基本思想是完全绕过(相当复杂的)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行awk
、grep
、sort
和uniq
魔法就可以获得漂亮的邻接列表,所有的类都是节点,方法都是边。
我从来没有试过,这只是个主意。我不能保证Java反编译器在Scala代码上运行良好。