ver - Extracción del gráfico de llamadas completo de un proyecto de scala(difícil)
como ver historial de llamadas (3)
Me gustaría extraer de un proyecto Scala determinado, el gráfico de llamadas de todos los métodos que forman parte de la propia fuente del proyecto.
Según tengo entendido, el compilador de presentación no lo habilita, y requiere bajar hasta el compilador real (o un complemento del compilador).
¿Puede sugerir un código completo, que funcione de manera segura para la mayoría de los proyectos de scala pero aquellos que usan las características de lenguaje dinámico más extravagantes? para el gráfico de llamadas, me refiero a un gráfico dirigido (posiblemente cíclico) que comprende class/trait + method
vértices class/trait + method
donde un borde A -> B indica que A puede llamar a B.
Las llamadas a / desde bibliotecas se deben evitar o "marcar" como fuera de la propia fuente del proyecto.
EDITAR:
Vea mi solución prototipo derivado de macro paraíso, basada en el liderazgo de @ dk14, como una respuesta a continuación. Alojado en github en https://github.com/matanster/sbt-example-paradise .
Aquí está el prototipo de trabajo, que imprime los datos subyacentes necesarios en la consola como prueba de concepto. http://goo.gl/oeshdx .
Como funciona esto
He adaptado los conceptos de @dk14 en la parte superior de macro paraíso .
Macro Paradise le permite definir una anotación que aplicará su macro sobre cualquier objeto anotado en su código fuente. Desde allí, tiene acceso al AST que el compilador genera para la fuente, y se puede usar la interfaz de reflexión de Scala para explorar la información de tipo de los elementos AST. Las quasiquotes (la etimología es de haskell o algo así) se utilizan para hacer coincidir el AST para los elementos relevantes.
Más sobre los cuasiquotes.
Lo importante que se debe tener en cuenta en general es que los quasiquotes funcionan sobre un AST, pero son una API extraña a primera vista y no una representación directa del AST (!). El AST es recogido por la macro anotación de paradise, y luego las quasiquotes son la herramienta para explorar el AST en cuestión: usted combina, corta y corta el árbol de sintaxis abstracta usando cuasiquotes.
Lo práctico a tener en cuenta sobre las cuasiquotes es que hay plantillas de cuasiquotas fijas para hacer coincidir cada tipo de scala AST: una plantilla para una definición de clase scala, una plantilla para una definición de método scala, etc. Todos estos tempaltes se proporcionan aquí , por lo que es muy Sencillo de igualar y deconstruir el AST a la mano de sus interesantes constituyentes. Si bien las plantillas pueden parecer desalentadoras a primera vista, en su mayoría son solo plantillas que imitan la sintaxis de Scala, y puede cambiar libremente los nombres de las variables $
prependidas en ellas a nombres que se sientan más agradables a su gusto.
Todavía necesito perfeccionar las coincidencias de quasiquote que uso, que actualmente no son perfectas. Sin embargo, mi código parece producir el resultado deseado para muchos casos, y es posible que sea posible hacer una precisión de las coincidencias con una precisión del 95%.
Salida de muestra
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
.
.
.
Es fácil ver cómo se pueden correlacionar las personas que llaman y las personas a partir de estos datos, y cómo se pueden filtrar o marcar los destinos de llamadas que se encuentran fuera del origen del proyecto. Esto es todo para Scala 2.11. Usando este código, uno necesitará anteponer una anotación a cada clase / objeto / etc en cada archivo fuente.
Los desafíos que quedan son en su mayoría:
Retos pendientes:
- Esto se estrella después de hacer el trabajo. Dependiendo de https://github.com/scalamacros/paradise/issues/67
- Debe encontrar una manera de aplicar la magia a los archivos de origen completos sin anotar manualmente cada clase y objeto con la anotación estática. Esto es bastante menor por ahora, y hay que admitir que hay beneficios por poder controlar las clases para incluirlas e ignorarlas de todos modos. Una etapa de preprocesamiento que implante la anotación antes de (casi) todas las definiciones de archivos fuente de nivel superior sería una buena solución.
- Perfeccionar los comparadores de tal manera que todas y solo las definiciones relevantes coincidan, para hacer que esto sea general y sólido más allá de mis pruebas simples y rápidas.
Enfoque alternativo para reflexionar
acyclic trae a la mente un enfoque totalmente opuesto que aún se adhiere al reino del compilador de scala: inspecciona todos los símbolos generados para la fuente, por el compilador (tanto como recojo de la fuente). Lo que hace es verificar las referencias cíclicas (ver el repositorio para una definición detallada). Supuestamente, cada símbolo tiene suficiente información adjunta para derivar la gráfica de referencias que el acyclic necesita generar.
Una solución inspirada por este enfoque puede, si es posible , ubicar al "propietario" padre de cada símbolo en lugar de centrarse en el gráfico de las conexiones de los archivos de origen como lo hace el propio acíclico. Así, con un poco de esfuerzo recuperaría la propiedad de clase / objeto de cada método. No estoy seguro si este diseño no explotaría computacionalmente, ni cómo obtener de manera determinista la clase que abarca cada símbolo.
La ventaja sería que no hay necesidad de anotaciones de macro aquí. El inconveniente es que esto no puede esparcir la instrumentación en tiempo de ejecución, ya que el macro paraíso lo permite con bastante facilidad, lo que a veces puede ser útil.
Requiere un análisis más preciso, pero como comienzo, esta simple macro imprimirá todas las aplicaciones posibles, pero requiere de macro-paraíso y todas las clases trazadas deben tener @trace
anotación de @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)
}
}
Entrada:
@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) = ???
}
Salida (como advertencias del compilador, pero puede salir al archivo en lugar de println):
Warning:scalac: MyF.call: call2
Warning:scalac: MyF.call: call3
Warning:scalac: MyF.call: cl
Por supuesto, es posible que desee c.typeCheck(input)
(como ahora expr.tpe
en los árboles encontrados es igual a null
) y encontrar a qué clase pertenece realmente este método de llamada, por lo que el código resultante puede no ser tan trivial.
Las anotaciones de macro de PS le dan un árbol no verificado (ya que está en la etapa anterior del compilador que en las macroses normales), por lo que si desea que se verifique algo tipográficamente, la mejor forma es rodear el trozo de código que desea chequear con la llamada de algunos de sus macros regulares, y procesarlo dentro Esta macro (incluso puede pasar algunos parámetros estáticos). Cada macro regular dentro del árbol producida por macro-anotación - se ejecutará como de costumbre.
Editar
La idea básica en esta respuesta fue omitir completamente el compilador Scala (bastante complejo) y extraer el gráfico de los archivos .class
generados al final. Parecía que un descompilador con una salida suficientemente detallada podría reducir el problema a la manipulación básica del texto. Sin embargo, después de un examen más detallado, resultó que este no es el caso. Uno simplemente regresaría al punto de partida, pero con el código de Java confuso en lugar del código original de Scala. Por lo tanto, esta propuesta realmente no funciona, aunque hay algunas razones para trabajar con los archivos .class
finales en lugar de las estructuras intermedias utilizadas internamente por el compilador Scala.
/Editar
No sé si hay herramientas por ahí que lo hacen fuera de la caja (supongo que lo has comprobado). Solo tengo una idea muy aproximada de lo que es el compilador de presentación. Pero si todo lo que desea es extraer un gráfico con métodos como nodos y posibles llamadas de métodos como bordes, tengo una propuesta para una solución rápida y sucia. Esto solo funcionaría si desea usarlo para algún tipo de visualización, no le ayuda en absoluto si desea realizar algunas operaciones de refactorización inteligentes.
En caso de que desee intentar construir un generador de gráficos de este tipo, puede resultar mucho más sencillo de lo que cree. Pero para esto, es necesario bajar todo el camino, incluso más allá del compilador . Simplemente tome sus archivos .class
compilados , y use algo como el decompilador java CFR en él.
Cuando se usa en un único archivo .class
compilado, CFR generará una lista de clases de las que depende la clase actual (aquí uso mi pequeño proyecto como ejemplo):
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;
Luego escupirá un código de aspecto horrible, que podría tener este aspecto (pequeño extracto):
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);
}
Lo que uno debería notar aquí es que todos los métodos vienen con su firma completa, incluida la clase en la que están definidos , por ejemplo:
Scheduler.class.schedule(...)
ContextProvider.class.provideComputationContext(...)
SimpleComputationExecutor.class.fulfillPromise(...)
SimpleComputationExecutor.class.computeHere(...)
SimpleComputationExecutor.class.handleLocalResponses(...)
Entonces, si necesita una solución rápida y sucia, podría ser que podría salirse con solo 10 líneas de awk
, grep
, sort
y uniq
wizardry para obtener listas de adyacencia agradables con todas sus clases como nodos y métodos como bordes .
Nunca lo he probado, es solo una idea. No puedo garantizar que los descompiladores de Java funcionen bien en el código Scala.