Back-End

15 fev, 2018

NEAL, a plataforma open source agnóstica de linguagem da Uber

Publicidade

Todos os dias, os engenheiros da Uber escrevem centenas de commits na nossa base de código móvel, com cada patch novo exigindo uma revisão de código detalhada para detectar erros que possam afetar a experiência do usuário.

Para tornar as análises de código mais fáceis, nós construímos o Not Exactly a Linter (NEAL), uma ferramenta open source agnóstica de linguagem que permite que os engenheiros escrevam regras personalizadas baseadas em sintaxe, automatizando as seções do processo de revisão de código. Neste artigo, discutimos os benefícios do NEAL e mostramos como você pode usá-lo para suas próprias revisões de código.

Introduzindo o NEAL

Para revisores de partes mais críticas da base de código móvel da Uber, o número de novos diffs pode ser esmagador, diminuindo o rendimento e a produtividade do desenvolvedor. Esses obstáculos são exacerbados pela nossa diversidade geográfica; com equipes de engenharia em 12 locais em todo o mundo, as revisões bloqueadas podem resultar em longas mudanças, tornando mais difícil resolver problemas.

Além dessas considerações do dia a dia, existem muitos padrões de design que queremos impor ou evitar em nossa base de código como um todo. Alguns deles são cobertos por linters existentes, mas muitos outros padrões são específicos da arquitetura RIB da Uber. No início de 2017, percebemos que precisávamos investir em uma solução de revisão de código mais automatizada que eliminaria os obstáculos associados ao crescimento confiável e eficiente de uma base de código multilinguagem em escala.

Depois de experimentar ferramentas open source existentes, rapidamente percebemos que a maioria das opções não atendeu aos nossos requisitos de extensibilidade e flexibilidade. Enquanto algumas foram construídas para linguagens específicas, muitas outras só permitiram expressões regulares personalizadas ou engenheiros necessários para escrever programas quando eles queriam criar novas regras. Precisávamos de uma solução que permitisse aos engenheiros revisarem rapidamente o código – independentemente da linguagem – e gerarem novas regras. Então, decidimos começar do zero e construir a nossa própria.

O NEAL permite que os engenheiros escrevam seu próprio conjunto de regras para cobrir qualquer parte da base de código, levando o processo de revisão do código a um passo mais perto da automação completa. De fato, para tornar esse processo mais fácil, o NEAL usa sua própria linguagem específica de domínio.

Na Uber, usamos o NEAL extensivamente para:

  • Melhorar a confiabilidade de nossos testes, verificando padrões, como primitivas de concorrência e testes assíncronos.
  • Controlar o crescimento do nosso tamanho binário, evitando recursos de linguagem que resultem em grandes quantidades de código de máquina.
  • Aplicar restrições de alto nível no código do executável, como o código que pode ser executado como parte do fluxo do núcleo da Uber e garantir que os plugins recém-incorporados possam ser desativados com segurança.
  • E a parte mais emocionante? Graças ao NEAL, as regras mencionadas acima foram escritas por engenheiros em nossa base de código móvel.

Como ele funciona?

O NEAL foi projetado para ser completamente extensível em três dimensões:

  • Linguagem: os provedores de árvore de sintaxe abstrata (AST) adicionam suporte para linguagens como um analisador ou envolvendo um analisador existente.
  • Formatação: os repórteres adicionam facilmente novos formatos para produzir violações.
  • Criação de regras: com o NEAL, as regras definem ações a serem tomadas quando padrões específicos são encontrados durante as revisões de código.

Nós decidimos usar o linting baseado em AST (em vez de linters tradicionais baseados em expressão), porque ele facilita a expressão de padrões mais complexos, como estruturas profundamente aninhadas e padrões recursivos. Esse método também é menos propenso a erros, uma vez que os usuários não precisam responder a texto sem sentido no código-fonte, como espaços em branco, comentários e ordem de itens (por exemplo, `public static` vs` static public`).

Por padrão, o NEAL é fornecido com provedores Swift e Python AST, formatos de comando compatíveis com a linha e compatíveis com arc, e sem regras. No entanto, você pode encontrar algumas das regras implementadas pelo NEAL que utilizamos na Uber em nossa página no GitHub.

Usando o NEAL

O NEAL não é fornecido com nenhuma regra, então deixe-nos ver como um engenheiro adicionaria uma à base de código de um aplicativo.

Suponhamos que temos uma função que executa uma computação muito cara (deixe-nos chamar de expensiveComputation), e queremos garantir que ela não será chamada a partir de qualquer inicialização de classe, o que faria com que o aplicativo não respondesse inesperadamente.

Um programa ofensivo pode parecer assim:

// test.swift
func expensiveComputation() { /* … */ }

class App {
init() {
expensiveComputation()
}
}

Primeiro, usamos NEAL para extrair o AST para identificar quais nomes de nó usar em nossas regras:

neal --print-ast test.swift
[ FunctionDeclaration {
FunctionName = Identifier { Value = "expensiveComputation" }
},
ClassDeclaration {
ClassName = Identifier { Value = "Application" }
ClassBody = [
InitializerDeclaration {
InitializerBody = [
CallExpression {
Callee = Identifier { Value = "expensiveComputation" }
Arguments = []
}] }] }]

(o AST foi abreviado aqui por brevidade)

Então, nós escrevemos uma regra para coincidir com esse exemplo, da seguinte maneira:

// test.rules

// First we have to give a name to this rule
rule NoExpensiveInitializer {

// Then we have to specify which language are we targeting, followed by
// the first node we want to match, in this case ClassDeclaration
Swift::ClassDeclaration {
// node matchers can be nested to go deeper into the AST
InitializerDeclaration {
// additionally, matchers can also have predicates
CallExpression where Callee == "expensiveComputation" {
// once we found the pattern we wanted, we can take an action
fail("`expensiveComputation` should not be called from class initializers")
}
}
}
}

Agora, se executamos o NEAL nesse código, devemos ver algo como:

$ neal --rules test.rules test.swift

[1 of 1]: Analysing test.swift
On file test.swift: (NoExpensiveInitializer)

3 | class Application {
4 | init() {
5 | expensiveComputation()
~ | ^
6 | }
7 | }

error: `expensiveComputation` should not be called from class initializers

Alternativamente, podemos criar um arquivo de configuração mínima para agilizar o processo, da seguinte forma:

{
"rules": [ "test.rules" ]
}

Uma vez enviado, podemos alcançar o mesmo resultado, simplesmente executando:

Como as regras implementadas pelo NEAL são baseadas na sintaxe, elas só podem corresponder ao que está escrito, e não ao que o programa realmente faz. Por exemplo, no nosso exemplo acima, o NEAL só capturará o erro se a chamada expensiveComputation for chamada diretamente do inicializador, mas não se ela for chamada de forma transitiva, ou seja, o inicializador chama x e x chama a expensiveComputation. Isso pode ser contornado com convenções e anotações fornecidas pelo usuário, mas essa funcionalidade não é suportada out of the box.

Próximos passos

Para dar aos outros a liberdade e a extensibilidade da configuração dos parâmetros de seus próprios processos automatizados de revisão de código, damos ao NEAL de volta à comunidade open source.

Se o NEAL despertou seu interesse, você pode aprender mais visitando nosso repositório no GitHub e lendo nossa documentação. Entre em contato com a Uber Engineering via GitHub Issues se precisar de ajuda para começar ou, melhor ainda, gostaria de contribuir com outras linguagens para o projeto!

***

Este artigo é do Uber Engineering. Ele foi escrito por Tadeu Zagallo. A tradução foi feita pela Redação iMasters com autorização. Você pode conferir o original em: https://eng.uber.com/neal/