AST (Abstract Syntax Tree)

AST (Abstract Syntax Tree) é uma representação gráfica do código fonte utilizado principalmente pelos compiladores para ler o código e gerar os binários de destino.

Por exemplo, o AST deste exemplo de código:

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

Terá este aspecto:

A transformação do código fonte para um AST, é um padrão muito comum no processamento de qualquer tipo de dados estruturados. O fluxo de trabalho típico é baseado em “Criar um analisador que converte dados brutos em um formato baseado em gráficos, que podem então ser consumidos por um mecanismo de renderização”.

Esse é basicamente o processo de conversão de dados brutos em objetos fortemente digitados na memória que podem ser manipulados programmaticamente.

Aqui está um outro exemplo da ferramenta online realmente legal astexplorer.net:

Note como na imagem acima a linguagem DOT texto bruto foi convertido em uma árvore de objetos (que também pode ser consumido/salvado como um arquivo json).

Como um desenvolvedor se você for capaz de ‘ver o código ou dados brutos como um gráfico’, você terá feito uma incrível mudança de paradigma que o ajudará tremendamente através do seu cuidador.

Por exemplo eu usei ASTs e analisadores personalizados para:

  • escrever testes que verificam o que está realmente acontecendo no código (fácil quando você tem acesso a um modelo de objeto do código)
  • consumir dados de log que os parsers normais baseados em ‘regex’ lutaram para entender
  • apresentar análise estática em código escrito em linguagens personalizadas
  • transformar dumps de dados em in-objetos de memória (que depois podem ser facilmente transformados e filtrados)
  • criar arquivos transformados com fatias de múltiplos arquivos de código fonte (que eu chamei de MethodStreams e CodeStreams) – Veja exemplo abaixo para saber como isto se parece na prática
  • reformar refatoração de código personalizado (por exemplo, para corrigir automaticamente problemas de segurança no código) – Veja exemplo abaixo para saber como isto se parece na prática

Quando se constrói um analisador para uma determinada fonte de dados, um marco importante é a capacidade de ida e volta, de poder ir do AST, de volta ao texto original (sem perda de dados).

É exatamente assim que a refatoração de código nos IDEs funciona (por exemplo, quando você renomeia uma variável e todas as instâncias dessa variável são renomeadas).

Aqui está como funciona essa capacidade de ida e volta:

  1. iniciar com o arquivo A (ou seja o código fonte)
  2. criar o AST do arquivo A
  3. criar o arquivo B como transformação do AST
  4. arquivo A é igual ao arquivo B (byte por byte)

Quando isso é possível, torna-se fácil mudar o código programmaticamente, já que estaremos manipulando objetos fortemente digitados sem nos preocuparmos com a criação do código fonte sintático correto (que agora é uma transformação do AST).

Testes de escrita usando objetos ASTs

Após você começar a ver seu código fonte (ou dados que você consome) como ‘apenas um analisador AST longe de ser objetos que você pode manipular’, uma palavra inteira de oportunidades e capacidades se abrirá para você.

Um bom exemplo é como detectar um determinado padrão no código fonte que você quer ter certeza que ocorre em um grande número de arquivos, digamos, por exemplo, que você quer: “certifique-se de que um determinado método de Autorização (ou validação de dados) é chamado em cada método de serviços web expostos”?

Este não é um problema trivial, pois a menos que você seja capaz de programar um teste que verifique esta chamada, suas únicas opções são:

  1. escreva um ‘documento/página wiki padrão’ que define esse requisito, e certifique-se de que todos os desenvolvedores o leiam, o entendam e, mais importante, o sigam
  2. verifique manualmente se esse padrão/requisito foi implementado corretamente (nas revisões de código Pull Requests)
  3. tente usar automação com ferramentas baseadas em ‘regex’ (comerciais ou de código aberto), e perceber que é realmente difícil obter bons resultados com isso
  4. fallback nos testes manuais de GQ (e revisões de Segurança) para pegar quaisquer blind-spots

Mas, quando você tem a capacidade de escrever testes que chequem esse requisito, aqui está o que acontece:

  1. escrever testes que consumam o AST do código para poder verificar muito explicitamente se o padrão/requisito foi implementado/codificado corretamente
  2. via comentários no arquivo de teste, a documentação pode ser gerada a partir do código do teste (i.e. nenhum passo extra necessário para criar documentação para este padrão/requisito)
  3. executar esses testes como parte da compilação local e como parte do pipeline principal do CI
  4. com um teste falho, os desenvolvedores saberão o mais rápido possível uma vez que um problema tenha sido descoberto, e podem corrigi-lo muito rapidamente

Este é um exemplo perfeito de como escalar a arquitetura e os requisitos de segurança, de uma forma que está embutida dentro do Ciclo de Vida de Desenvolvimento de Software.

Precisamos de ASTs para ambientes legados e em nuvem

Quanto mais você entra em ASTs, mais você percebe que eles são camadas de abstração entre diferentes camadas ou dimensões. Mais importante ainda, elas permitem a manipulação dos dados de uma determinada camada de uma forma programática.

Mas quando você olha para os atuais ambientes legados e em nuvem (a parte que chamamos de ‘Infraestrutura como código’), o que você verá são grandes partes desses ecossistemas que hoje não têm analisadores de ASTs para converter sua realidade em objetos programáveis.

Esta é uma grande área de pesquisa, onde você se concentraria na criação de DSLs (Domain Specific Languages) para sistemas legados ou para aplicações em nuvem (escolha uma vez que cada um terá conjuntos completos de materiais-fonte diferentes). Um exemplo do tipo de DSL que precisamos é uma linguagem para descrever e codificar o comportamento das funções Lambda (ou seja, os recursos que eles precisam executar, e qual é o comportamento esperado da função Lambda)

MethodStreams

Um dos exemplos mais poderosos de manipulação AST que já vi, é a funcionalidade MethodStreams que adicionei à plataforma O2.

Com esta funcionalidade fui capaz de programar a criação de um arquivo baseado na árvore de chamadas de um determinado método. Esse arquivo continha todo o código fonte relevante àquele método original (gerado a partir de vários arquivos), e fez uma enorme diferença ao fazer revisões de código.

Para entender por que eu fiz isso, vamos começar com o problema que eu tinha.

Back em 2010 eu estava fazendo uma revisão de código de uma aplicação .Net que tinha um milhão de linhas de código. Mas eu estava apenas olhando para os métodos WebServices, que cobriam apenas uma pequena parte dessa base de código (o que fazia sentido, já que esses eram os métodos expostos à internet). Eu sabia como encontrar esses métodos expostos à internet, mas para entender como eles funcionavam, eu tinha que olhar para centenas de arquivos, que eram os arquivos que continham o código no caminho de execução desses métodos.

Desde que na plataforma O2 eu já tinha um parser C# muito forte e suporte a refatoração de código (implementado para a funcionalidade REPL), eu fui capaz de escrever rapidamente um novo módulo que:

  1. começando no método de serviço web X
  2. calculei todos os métodos chamados por esse método X
  3. calculei todos os métodos chamados por 2. (recursivamente)
  4. capturou os objetos AST de todos os métodos identificados pelos passos anteriores
  5. criou um novo arquivo com todos os objetos de 4.

Este novo arquivo foi incrível, pois continha SOMENTE o código que eu preciso ler durante minha revisão de segurança.

Mas o evento ficou melhor, pois nesta situação, consegui adicionar os RegExs de validação (aplicados a todos os métodos WebServices) ao topo do arquivo, e adicionar o código fonte dos Stored Procedures relevantes na parte inferior do arquivo.

alguns dos arquivos gerados tinham 3k+ linhas de código, o que foi uma simplificação massiva dos 20+ arquivos que os continham (que tinham provavelmente 50k+ linhas de código).

Aqui está um bom exemplo de que sou capaz de fazer um trabalho melhor, tendo acesso a um vasto conjunto de capacidades e técnicas (neste caso a capacidade de manipular programmaticamente o código fonte)

Este tipo de manipulação AST é uma área de investigação que recomendo vivamente para que se concentrem (o que também vos dará um enorme conjunto de ferramentas para as vossas actividades diárias de codificação). Btw, se você seguir este caminho, verifique também os CodeStreams da plataforma O2 que são uma evolução da tecnologia MethodStreams. CodeStreams lhe dará um fluxo de todas as variáveis que são tocadas por uma determinada variável fonte (o que em análise estática é chamado de Taint flow analysis e Taint Checking)

Fixing code in real time (ou em tempo de compilação)

Outro exemplo muito legal do poder da manipulação AST é o PoC que escrevi em 2011 em Fixing/Encoding .NET code in real time (neste caso Response.Write), onde mostro como programmaticamente adicionar uma correção de segurança a um método de projeto vulnerável.

Aqui é como a IU era, onde o código da esquerda foi transformado programmaticamente para o código da direita (adicionando o método extra AntiXSS.HtmlEncode wrapper)

Aqui é o código fonte que faz a transformação e a correção do código (note a ida e volta do código):

Em 2018, a forma de implementar este fluxo de trabalho de forma amigável para o desenvolvedor, é criar automaticamente um Pull Request com essas alterações extras.

Deixe uma resposta

O seu endereço de email não será publicado.