Подтвердить что ты не робот

Извлечение полного графика вызовов проекта scala (жесткого)

Я хотел бы извлечь из данного проекта Scala, график вызовов всех методов, которые являются частью собственного источника проекта.

Насколько я понимаю, компилятор презентации не включил это, и ему необходимо полностью отказаться от реального компилятора (или плагина компилятора?).

Можете ли вы предложить полный код, который будет безопасно работать для большинства проектов Scala, но те, которые используют самые сумасшедшие функции динамического языка? для графа вызовов я имею в виду направленный (возможно, циклический) график, содержащий вершины class/trait + method, где ребро A → B указывает, что A может вызывать B.

Звонки в/из библиотек следует избегать или "отмечать" как вне собственного источника проекта.

Спасибо!

EDIT:

Посмотрите мое прототипное решение на основе макроса, основанное на лидерстве @dk14, в качестве ответа ниже. Хостинг на github на http://goo.gl/oeshdx.

4b9b3361

Ответ 1

Здесь рабочий прототип, который выводит необходимые базовые данные на консоль в качестве доказательства концепции. http://goo.gl/oeshdx.

Как это работает

Я адаптировал концепции из @dk14 на верхнем шаблоне из macro paradise.

Macro paradise позволяет вам определить аннотацию, которая будет применять ваш макрос по любому аннотированному объекту в исходном коде. Оттуда у вас есть доступ к AST, который генерирует компилятор для источника, а scala отражение api может использоваться для изучения информации о типе элементов AST. Квазиквадраты (этимология от haskell или что-то еще) используются для соответствия AST для соответствующих элементов.

Подробнее о Quasiquotes

В целом важно отметить, что квазиквадраты работают над АСТ, но они являются странными в первом взгляде апи, а не прямым представлением АСТ (!). АСТ подбирается для вас с помощью аннотаций райских макросов, а затем квазиквадраты - это инструмент для изучения АСТ под рукой: вы сопоставляете, нарезаете и кубите абстрактное синтаксическое дерево, используя квазиквартины.

Практическое замечание о квазикварталах состоит в том, что существуют фиксированные квазиквазотные шаблоны для сопоставления каждого типа scala AST - шаблон для определения класса scala, шаблон для определения метода scala и т.д. Эти tempaltes представлены здесь, что очень просто сопоставить и разложить 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. Используя этот код, нужно будет добавить аннотацию к каждому классу/объекту/etc в каждом исходном файле.

Проблемы, которые остаются в основном:

Оставшиеся проблемы:

  • Это происходит после выполнения задания. Hinging on https://github.com/scalamacros/paradise/issues/67
  • Нужно найти способ в конечном счете применить магию ко всем исходным файлам без ручного аннотирования каждого класса и объекта со статической аннотацией. На данный момент это довольно незначительно, и, по общему признанию, есть преимущества в том, что они могут контролировать классы, чтобы включать и игнорировать в любом случае. Стадия предварительной обработки, которая внедряет аннотацию перед (почти) каждым определением исходного файла верхнего уровня, будет одним из приятных решений.
  • Похороны состыков, чтобы все и только соответствующие определения были сопоставлены - сделать это общим и твердым за пределами моего упрощенного и беглого тестирования.

Альтернативный подход к размышлению

acyclic напоминает совершенно противоположный подход, который все еще придерживается царства компилятора scala - он проверяет все сформированные символы для источника, компилятором (столько, сколько я собираюсь из источника). То, что он делает, это проверка циклических ссылок (см. Репо для подробного определения). Каждый символ, предположительно, имеет достаточную информацию, прикрепленную к нему, чтобы получить график ссылок, который acyclic должен генерировать.

Решение, основанное на этом подходе, может, если это возможно, найти родительский "владелец" каждого символа, а не сосредоточиться на графике источника файлы как ациклические. Таким образом, с некоторым усилием он восстановит свойство класса/объекта для каждого метода. Не уверен, что этот дизайн не будет расходоваться в расчете, а также как детерминистически получить класс, охватывающий каждый символ.

Потенциал роста заключался бы в том, что здесь нет необходимости в макрообменах. Недостатком является то, что это не может посыпать инструменты во время выполнения, так как макро-рай довольно легко позволяет, что иногда может быть полезно.

Ответ 2

Требуется более точный анализ, но в начале этого простого макроса будут напечатаны все возможные приложения, но для этого требуется макро-рай, и все прослеживаемые классы должны иметь @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 [email protected]"def $method[..$tt] (..$params): $typ = $body" =>
                         analizeBody(name.toString, method.toString, body)
                         x

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

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




    c.Expr[Any](output)
  }
}

Input:

  @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 дают вам неконтролируемое дерево (как на предыдущем этапе компилятора, чем обычные макросы), поэтому, если вы хотите что-то typechecked, лучший способ состоит в том, что фрагмент кода, который вы хотите проверить typecheck, с вызовом некоторого вашего обычного макроса и обрабатывать его внутри этого макрос (вы можете даже передать некоторые статические параметры). Каждый регулярный макрос внутри дерева, создаваемый макро-аннотацией, будет выполнен как обычно.

Ответ 3

Edit
Основная идея в этом ответе заключалась в том, чтобы полностью обойти (довольно сложный) компилятор Scala и извлечь конец из созданных файлов .class в конце. Оказалось, что декомпилятор с достаточно подробным выходом может свести проблему к основным манипуляциям с текстом. Однако после более детального изучения выяснилось, что это не так. Можно просто вернуться к квадрату, но с запутанным кодом Java вместо исходного кода Scala. Таким образом, это предложение действительно не работает, хотя есть некоторые основания для работы с окончательными .class файлами вместо промежуточных структур, используемых внутри компилятором Scala.
/Edit

Я не знаю, есть ли там инструменты, которые делают это из коробки (я предполагаю, что вы это проверили). У меня есть только очень грубое представление о компиляторе презентации. Но если все, что вы хотите, - извлечь граф с помощью методов как узлов и потенциальных вызовов методов как ребер, у меня есть предложение для быстрого и грязного решения. Это будет работать только в том случае, если вы хотите использовать его для какой-либо визуализации, это совсем не поможет вам, если вы хотите выполнить некоторые умные операции рефакторинга.

Если вы хотите попытаться построить такой генератор графов самостоятельно, это может оказаться намного проще, чем вы думаете. Но для этого вам нужно пройти весь путь, даже мимо компилятора. Просто возьмите скомпилированные файлы .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 wizardry, чтобы получить приятную смежность списки со всеми вашими классами как узлы и методы как ребра.

Я никогда не пробовал, это просто идея. Я не могу гарантировать, что декомпиляторы Java хорошо работают с кодом Scala.