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

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

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

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

Вызовы в/из библиотек следует избегать или «помечать» как находящиеся за пределами собственного источника проекта.

ИЗМЕНИТЬ:

См. Мое прототипное решение, полученное из макрорайя, основанное на руководстве @ dk14, в качестве ответа ниже. Размещено на github по адресу https://github.com/matanster/sbt-example-paradise.


person matanster    schedule 22.04.2015    source источник
comment
Если вам действительно нравится дополнять его кодом для отдельного извлечения иерархий типов/классов и взаимосвязей примесей, это также очень ценится.   -  person matanster    schedule 24.04.2015
comment
Инструмент Degraph делает это на уровне класса, а не (пока) на уровне метода. Но я не вижу концептуальной проблемы, чтобы расширить его до уровня метода. Но комментарий/редактирование @Andrey Tyukin заставляет меня думать, что я что-то упускаю. (Примечание: я автор Degraph, и Degraph анализирует файлы классов с помощью ASM)   -  person Jens Schauder    schedule 24.04.2015
comment
Компоновщик Scala.js содержит анализатор графа вызовов с детализацией на уровне методов. Однако на данный момент он работает с классами стиля JVM (один класс, множественное наследование интерфейсов), но знает модули Scala (также известные как объекты). Кроме того, он не может обрабатывать файлы только из вашего проекта (но может применяться апостериорное сопоставление имен). Наконец, он не сможет обрабатывать циклические зависимости *.java/*.scala. Если это приемлемо, я составлю полный ответ о том, как это сделать.   -  person gzm0    schedule 25.04.2015
comment
В каком смысле он не может обрабатывать циклические зависимости? никогда не остановится?   -  person matanster    schedule 27.04.2015
comment
@matt Ваша ссылка не работает   -  person Peanut    schedule 29.04.2015


Ответы (3)


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

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

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

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

Подробнее о квазицитатах

Вообще важно отметить, что квазикавычки работают с 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
  .
  .
  .

Из этих данных легко увидеть, как можно сопоставить вызывающих и вызываемых абонентов и как можно отфильтровать или выделить цели вызовов за пределами источника проекта. Это все для scala 2.11. Используя этот код, нужно будет добавить аннотацию к каждому классу/объекту/и т. д. в каждом исходном файле.

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

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

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

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

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

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

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

person matanster    schedule 27.04.2015
comment
Кстати, этимология слова квазицитата в языках программирования восходит к Лиспу. - person Eugene Burmako; 08.05.2015
comment
Позже я реализовал все это как плагин компилятора. Оглядываясь назад, было действительно глупо выбирать макро-рай. - person matanster; 20.10.2015

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

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

person dk14    schedule 24.04.2015
comment
Если я правильно понимаю, этот макрос соответствует синтаксису определения метода (?), но улавливает только подмножество способов, которыми метод может быть определен в Scala. Например. двойные списки параметров, метод, который пропускает знак равенства, и другие случаи, которые используются даже в простых проектах scala (например, в моем собственном). Так что это больше похоже на обратное проектирование синтаксиса, а не на безопасное использование мудрости компилятора/плагина, не так ли? хотя аннотации макросов хороши, это полезно знать. - person matanster; 24.04.2015
comment
Можете ли вы использовать typeCheck внутри макроса? это работает во время компиляции? - person matanster; 24.04.2015
comment
компилятор/плагин будет иметь дело с тем же AST, что и макрос. На самом деле макрос работает с небольшим подмножеством компилятора-API, поэтому единого способа сделать это нет - вам придется разбираться со всеми нюансами самостоятельно. Я думаю, что этот макрос будет в порядке без знака равенства - person dk14; 24.04.2015
comment
2) да, должен, но AST должен быть действительно компилируемым - вы не можете просто передать кусок классового AST - person dk14; 24.04.2015
comment
Спасибо :-) какие версии Scala и макрорайя вы использовали для запуска? хороший подход. - person matanster; 24.04.2015
comment
В идеале, хотя должен быть подход, использующий архитектуру компилятора, я предполагаю, что именно так IDE, наполовину адаптированные к scala, пытаются получить требуемый интеллект. Помимо AST, компилятор в конечном итоге пишет байт-код для методов, поэтому я бы предположил, что он знает больше, чем просто AST (на 27 этапах обработки!). - person matanster; 24.04.2015
comment
Paradise 2.0.1 + scala 2.10/2.11, в качестве прототипа я использовал свой другой проект: github.com/dk14/ println-трассировщик - person dk14; 24.04.2015
comment
AST — это результат парсинга, который переходит на следующие уровни компиляторов, макрос — это обработчик на одном из этих уровней. После этого (может быть, еще несколько уровней) - AST переходит к этапу, который генерирует байткод, но, насколько я знаю, этот этап недоступен даже для компилятора-плагина. Если вы хотите работать напрямую с нетипизированным байт-кодом, вам может помочь asm - person dk14; 24.04.2015
comment
просто упомянем - чтобы быть быстрым, компилятор scala довольно грязный, изменчивый, небезопасный и не чистый. Это напоминает мне программирование на C/C++, так что просто упомяну - person dk14; 24.04.2015
comment
Ну, если это не медлительный слон в комнате философии Scala, который становится заметен тут и там... Интересно, не разрушит ли макро-рай более крупных зверей, таких как библиотеки, которые сами используют макросы (для предоставления DSL или иным образом). Поскольку здесь вы только аннотируете каждый класс как точку привязки, возможно, неблагоприятного взаимодействия не будет. Будут ли библиотечные макросы расширяться компилятором до или после ваших собственных? - person matanster; 24.04.2015
comment
Сначала он расширяет макро-аннотацию (это на более раннем уровне компиляции) - поэтому у вас здесь непроверенное дерево (по сравнению с обычным макросом). На самом деле проверка типов может быть здесь проблемой, но ее можно решить, окружив фрагменты кода, которые вы хотите обработать, с помощью вашего обычного макроса (просто окружите его приложением вашей макро-функции). После расширения макроаннотации - все штатные макросы (в том числе добавленные вами) будут выполнены. Этот трюк должен сработать — по крайней мере, я рекомендовал его проекту jscala, и они до сих пор его используют. - person dk14; 24.04.2015
comment
Мальчик квазицитирует один симпатичный API для манипулирования AST. Думаю, здесь показано, как деконструировать определение метода, из которого вы получаете уверенность в том, что совпадение в вашем коде поймает все определения метода. - person matanster; 25.04.2015
comment
вы также можете получить определения функций, например val a = (z: Int) => call4 и тела val/var в целом (возможно, даже тела внутренних объектов и классов). P.S. Еще одна неожиданно хорошая вещь в том, что Tree.foreach (а также Tree.find) используют глубокий обход, поэтому можно найти запрошенную структуру повсюду в вашем коде. - person dk14; 25.04.2015
comment
Меня немного смущает поддержка scala 2.11 в github.com/dk14/println-tracer и макро рай. В клонированном виде print-tracer, кажется, строится против 2.10. Переход на сборку против 2.11.5 приводит к небольшому количеству ошибок компиляции. Насколько результат сборки 2.10 может анализировать проекты 2.11? Я бы предпочел попробовать макро-рай в коде scala 2.11, но не вижу стабильной версии макро-рая, созданной с помощью 2.11 на сонатипе. - person matanster; 27.04.2015
comment
ой. Я зафиксировал scala 2.11.0-совместимую версию. 2.0.1-paradise все еще в порядке, так как здесь кроссбилд (cross CrossVersion.full), вы можете попробовать обновиться до более новой версии, но я их еще не проверял. - person dk14; 27.04.2015
comment
отвечая на вопрос, я не думаю, что макрос, скомпилированный для 2.10.0, может нормально выполняться в компиляторе 2.11.0. По крайней мере, мой println-tracer вообще не работал бы без последнего коммита. - person dk14; 27.04.2015

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

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

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

При использовании в одном скомпилированном файле .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.

person Andrey Tyukin    schedule 22.04.2015
comment
Спасибо, но идея в том, чтобы получить граф вызовов, а не граф, который сопоставляет классы с их методами. Для получения правильных результатов вам действительно нужно использовать компилятор, чтобы придумать класс, к которому применяется каждый метод (или получить его из файлов классов в качестве альтернативы, следуя вашему подходу) - person matanster; 22.04.2015
comment
Я думаю, что правильно понял ваш вопрос, я надеялся, что для каждого метода f можно будет сгенерировать список методов g1,...,gN, так что f будет реализован в терминах g1,...,gN (то есть f может вызывать каждый gK во время выполнения). Однако я присмотрелся к тому, что выдает декомпилятор, и теперь должен признать, что мое предложение, скорее всего, не сработает. - person Andrey Tyukin; 22.04.2015
comment
это было хорошее направление мысли (!) Я высоко ценю как отношение, так и дух дальнейшего пересмотра!! - person matanster; 22.04.2015
comment
Если вы хотите работать на уровне байт-кода, два инструмента, которые анализируют граф вызовов, — это Classycle и ProGuard. Оба они прекрасно работают с байт-кодом, сгенерированным компилятором Scala. - person Seth Tisue; 26.04.2015