AST (Abstract Syntax Tree)

El AST (Abstract Syntax Tree) es una representación gráfica del código fuente utilizada principalmente por los compiladores para leer el código y generar los binarios de destino.

Por ejemplo, el AST de este ejemplo de código:

while b ≠ 0
if a > b
a := a − b
else
b := b − a
return a

Tendrá el siguiente aspecto:

La transformación del código fuente a un AST, es un patrón muy común cuando se procesa cualquier tipo de datos estructurados. El flujo de trabajo típico se basa en «La creación de un analizador que convierte los datos en bruto en un formato basado en gráficos, que luego puede ser consumido por un motor de renderizado».

Esto es básicamente el proceso de convertir los datos en bruto en objetos en memoria fuertemente tipados que pueden ser manipulados mediante programación.

Aquí hay otro ejemplo de la herramienta online realmente genial astexplorer.net:

Nota cómo en la imagen de arriba el texto crudo del lenguaje DOT fue convertido en un árbol de objetos (que también puede ser consumido/guardado como un archivo json).

Como desarrollador si eres capaz de «ver el código o los datos en bruto como un gráfico», habrás hecho un cambio de paradigma increíble que te ayudará enormemente a través de tu cuidador.

Por ejemplo he utilizado ASTs y parsers personalizados para:

  • escribir pruebas que comprueban lo que realmente está sucediendo en el código (fácil cuando tienes acceso a un modelo de objetos del código)
  • consumir datos de registro que los analizadores normales basados en «regex» se esforzaban por entender
  • realizar un análisis estático del código escrito en lenguajes personalizados
  • transformar los volcados de datos en objetos enobjetos en memoria (que luego pueden ser fácilmente transformados y filtrados)
  • crear archivos transformados con rebanadas de múltiples archivos de código fuente (que llamé MethodStreams y CodeStreams) – Véase el ejemplo a continuación para ver cómo se ve esto en la práctica
  • realizar refactorización de código personalizado (por ejemplo, para corregir automáticamente los problemas de seguridad en el código) – Véase el ejemplo a continuación para ver cómo se ve esto en la práctica

Cuando se construye un analizador para una fuente de datos particular, un hito clave es la capacidad de ida y vuelta, de ser capaz de ir desde el AST, de vuelta al texto original (sin pérdida de datos).

Así es exactamente como funciona la refactorización de código en los IDEs (por ejemplo cuando se renombra una variable y se renombran todas las instancias de esa variable).

Así es como funciona este viaje de ida y vuelta:

  1. comienza con el archivo A (es decir el código fuente)
  2. crear el AST del fichero A
  3. crear el fichero B como transformación desde el AST
  4. el fichero A es igual al fichero B (byte a byte)

Cuando esto es posible, se hace fácil cambiar el código programáticamente, ya que estaremos manipulando objetos fuertemente tipados sin preocuparnos de la creación de código fuente sintácticamente correcto (que ahora es una transformación desde el AST).

Escribir pruebas usando objetos AST

Una vez que empieces a ver tu código fuente (o los datos que consumes) como ‘sólo un parser AST lejos de ser objetos que puedes manipular’, toda una palabra de oportunidades y capacidades se abrirá para ti.

Un buen ejemplo es cómo detectar un patrón particular en el código fuente que quieres asegurarte de que ocurre en un gran número de archivos, digamos por ejemplo que quieres: «asegurarse de que un método particular de Autorización (o validación de datos) se llama en cada método de servicios web expuestos»?

Este no es un problema trivial, ya que a menos que seas capaz de escribir programáticamente una prueba que compruebe esta llamada, tus únicas opciones son:

  1. escribir un ‘documento estándar/página wiki’ que defina ese requisito, y asegurarte de que todos los desarrolladores lo lean, lo entiendan y, lo que es más importante, lo sigan
  2. comprobar manualmente si ese estándar/requisito se ha implementado correctamente (en las revisiones de código de Pull Requests)
  3. intentar utilizar la automatización con herramientas basadas en ‘regex’ (comerciales o de código abierto), y darse cuenta de que es muy difícil obtener buenos resultados
  4. recurrir a las pruebas manuales de QA (y a las revisiones de seguridad) para detectar cualquier punto ciego

Pero, cuando se tiene la capacidad de escribir pruebas que comprueben este requisito, esto es lo que ocurre:

  1. escribir pruebas que consumen el AST del código para poder comprobar de forma muy explícita si el estándar/requisito se implementó/codificó correctamente
  2. mediante comentarios en el archivo de prueba, la documentación puede generarse a partir del código de prueba (es decir.es decir, no se requiere ningún paso adicional para crear la documentación para este estándar/requisito)
  3. ejecutar esas pruebas como parte de la construcción local y como parte de la tubería principal de CI
  4. al tener una prueba fallida, los desarrolladores sabrán ASAP una vez que se ha descubierto un problema, y pueden arreglarlo muy rápidamente

Este es un ejemplo perfecto de cómo escalar la arquitectura y los requisitos de seguridad, de una manera que está incrustada dentro del Ciclo de Vida de Desarrollo de Software.

Necesitamos ASTs para entornos heredados y en la nube

Cuanto más te adentras en los ASTs, más te das cuenta de que son capas de abstracción entre diferentes capas o dimensiones. Y lo que es más importante, permiten la manipulación de los datos de una capa concreta de forma programática.

Pero cuando miras los entornos actuales de legado y de la nube (la parte que llamamos ‘Infraestructura como código’), lo que verás son grandes partes de esos ecosistemas que hoy no tienen analizadores AST para convertir su realidad en objetos programables.

Esta es una gran área de investigación, en la que te centrarías en la creación de DSLs (Lenguajes Específicos de Dominio) ya sea para sistemas heredados o para aplicaciones en la nube (escoge uno ya que cada uno tendrá conjuntos de materiales fuente completamente diferentes). Un ejemplo del tipo de DSL que necesitamos es un lenguaje que describa y codifique el comportamiento de las funciones Lambda (es decir, los recursos que necesitan para ejecutarse, y cuál es el comportamiento esperado de la función Lambda)

MethodStreams

Uno de los ejemplos más potentes de manipulación de AST que he visto, es la función MethodStreams que añadí a la Plataforma O2.

Con esta función pude crear programáticamente un archivo basado en el árbol de llamadas de un método concreto. Este archivo contenía todo el código fuente relevante para ese método original (generado a partir de múltiples archivos), y supuso una enorme diferencia a la hora de hacer revisiones de código.

Para entender por qué hice esto, empecemos por el problema que tenía.

Atrás, en 2010, estaba haciendo una revisión de código de una aplicación .Net que tenía un millón de líneas de código. Pero sólo estaba mirando los métodos de WebServices, que sólo cubrían una pequeña parte de esa base de código (lo cual tenía sentido ya que esos eran los métodos expuestos a internet). Sabía cómo encontrar esos métodos expuestos a internet, pero para entender cómo funcionaban, tenía que mirar cientos de archivos, que eran los que contenían código en la ruta de ejecución de esos métodos.

Como en la Plataforma O2 ya tenía un parser de C# muy potente y soporte de refactorización de código (implementado para la función REPL), pude escribir rápidamente un nuevo módulo que:

  1. partiendo del método X del servicio web
  2. calculara todos los métodos llamados desde ese método X
  3. calculara todos los métodos llamados por 2. (recursivamente)
  4. capturó los objetos AST de todos los métodos identificados por los pasos anteriores
  5. creó un nuevo archivo con todos los objetos de 4.

Este nuevo archivo fue sorprendente, ya que contenía SOLO el código que necesito leer durante mi revisión de seguridad.

Pero la cosa mejoró, ya que en esta situación, pude añadir los RegExs de validación (aplicados a todos los métodos de WebServices) en la parte superior del archivo, y añadir el código fuente de los Stored Procedures relevantes en la parte inferior del archivo.

Algunos de los archivos generados tenían más de 3k líneas de código, lo cual era una simplificación masiva de los más de 20 archivos que los contenían (que tenían probablemente más de 50k líneas de código).

Este es un buen ejemplo de que puedo hacer un mejor trabajo, al tener acceso a un amplio conjunto de capacidades y técnicas (en este caso la capacidad de manipular programáticamente el código fuente)

Este tipo de manipulación de AST es un área de investigación que recomiendo encarecidamente para que usted se centre en (que también le dará un conjunto de herramientas masivas para sus actividades de codificación del día a día). Por cierto, si vas por este camino, también echa un vistazo a los CodeStreams de la Plataforma O2 que son una evolución de la tecnología MethodStreams. CodeStreams le dará un flujo de todas las variables que son tocadas por una variable de origen en particular (lo que en el análisis estático se llama análisis de flujo Taint y Taint Checking)

Arreglar el código en tiempo real (o en tiempo de compilación)

Otro ejemplo realmente genial del poder de la manipulación de AST es el PoC que escribí en 2011 sobre Arreglar/Codificar el código .NET en tiempo real (en este caso Response.Write), donde muestro cómo agregar programáticamente una corrección de seguridad a un método vulnerable por diseño.

Aquí está el aspecto de la UI, donde el código de la izquierda, se transformó programáticamente al código de la derecha (añadiendo el método extra AntiXSS.HtmlEncode wrapper)

Aquí está el código fuente que hace la transformación y el arreglo del código (nótese el ida y vuelta del código):

En 2018, la forma de implementar este flujo de trabajo de forma amigable para el desarrollador, es crear automáticamente un Pull Request con esos cambios extra.

Deja una respuesta

Tu dirección de correo electrónico no será publicada.