Análise estática de grandes volumes de código Python: experiência no Instagram. Parte 2

Hoje publicamos a segunda parte da tradução do material dedicado à análise estática de grandes volumes de código Python do lado do servidor no Instagram.



A primeira parte

Programadores que estão cansados ​​de aprender


Considerando que possuímos cerca de cem de nossas próprias regras de fiapos, a contabilidade pedante das recomendações emitidas por essas regras pode resultar rapidamente em perda de tempo dos desenvolvedores. Seria melhor gastar o tempo gasto na correção do estilo do código ou na eliminação de padrões obsoletos para criar algo novo e desenvolver o projeto.

Descobrimos que quando os programadores veem muitas notificações vindas do linter, eles começam a ignorar todas essas mensagens. Isso também se aplica a notificações importantes.
Suponha que decidamos declarar a função fn obsoleta e, em vez disso, use a função com um nome melhor. Se você não informar os desenvolvedores sobre isso, eles não saberão que não precisam mais usar a função fn . Pior ainda, eles não sabem o que usar em vez dessa função. Nessa situação, você pode criar uma regra de linter. Mas qualquer grande base de código já conterá muitas regras. Como resultado, é provável que uma importante notificação de ponteiro seja perdida no monte de notificações de bugs menores.


Linter é muito exigente e o "sinal útil" pode facilmente se perder no "ruído"

O que devemos fazer com isso?

Você pode corrigir automaticamente muitos problemas detectados pelo linter. Se o próprio linter puder ser comparado com a documentação que aparece onde é necessário, essas correções automáticas são uma refatoração de código que é executada onde é necessário. Dado o grande número de desenvolvedores trabalhando no Instagram, é quase impossível treinar cada um deles em nossas melhores técnicas de escrita de código. A inclusão de recursos de correção automática de código no sistema nos permite educar os desenvolvedores sobre novas técnicas quando eles não estão cientes dessas técnicas. Isso nos ajuda a atualizar rapidamente os desenvolvedores. As correções automáticas, além disso, permitiram que os programadores se concentrassem em coisas importantes, em vez de focar em pequenas alterações de código monótonas. Em geral, pode-se observar que as correções automáticas de código são mais eficazes e úteis em termos de treinamento aos desenvolvedores do que as simples notificações por linter.

Então, como criar um sistema para correção automática de código? Um fiapo baseado em árvore de sintaxe nos fornece informações sobre um nó disfuncional. Como resultado, não precisamos criar lógica para detectar problemas, pois já temos as regras correspondentes para o linter! Como sabemos sobre qual nó específico não nos convém e sobre onde está localizado o código-fonte, podemos, sem arriscar estragar alguma coisa, por exemplo, substituir o nome da função fn por add . Isso é adequado para corrigir violações únicas das regras que são executadas quando essas violações são detectadas. Mas e se introduzirmos uma nova regra para o linter, o que significa que pode haver centenas de fragmentos de código na base de código que não estão em conformidade com essa regra? Todas essas inconsistências podem ser corrigidas com antecedência?

Mods de código


Um codemod é apenas uma maneira de encontrar problemas e fazer alterações no código fonte. Os codemods são baseados em script. Codemod pode ser pensado como "refatoração de esteróides". A variedade de tarefas resolvidas pelos modos de código é extremamente ampla: desde as mais simples, como renomear uma variável em uma função, até as mais complexas, como reescrever uma função, para que ele aceite um novo argumento. Ao trabalhar com o codemod, os mesmos conceitos são usados ​​na operação do linter. Mas, em vez de informar o programador sobre o problema, como o linter, o modo de código resolve automaticamente esse problema.

Como escrever um codemod? Considere um exemplo. Aqui queremos parar de usar get_global . Nessa situação, você pode usar o linter, mas não se sabe quanto tempo levará para corrigir o código inteiro. Além disso, essa tarefa será distribuída entre muitos desenvolvedores. Ao mesmo tempo, mesmo que o projeto use um sistema de correção automática de código, pode levar algum tempo para processar todo o código.


Queremos evitar o uso de get_global e, em vez disso, usar variáveis ​​de instância

Para resolver esse problema, podemos, juntamente com a regra do linter que o detecta, escrever um código-modo. Acreditamos que permitir que padrões e APIs desatualizados deixem gradualmente o código irá distrair os desenvolvedores e diminuir a legibilidade do código. Preferimos remover imediatamente o código obsoleto e não observar como ele desaparece gradualmente do projeto.

Dado o volume do nosso código e o número de desenvolvedores ativos, isso geralmente significa eliminar automaticamente designs obsoletos. Se conseguirmos limpar rapidamente o código de padrões obsoletos, isso significa que podemos manter a produtividade de todos os desenvolvedores do Instagram.

Então, como fazer um codemod? Como substituir apenas o fragmento de código que nos interessa, preservando os comentários, o recuo e tudo mais? Existem ferramentas baseadas em uma árvore de sintaxe específica (como o que o LibCST cria) que permitem modificar o código com precisão cirúrgica e salvar todas as construções auxiliares nele. Como resultado, se precisarmos alterar o nome da função de fn para add na árvore abaixo, podemos escrever o nome add vez de fn no nó Name e, em seguida, gravar a árvore no disco!


O modo de código pode ser feito escrevendo o nome add no nó Name em vez do nome fn. Em seguida, a árvore alterada pode ser gravada no disco. Você pode ler mais sobre isso na documentação do LibCST.

Agora que conhecemos um pouco os mods de código, vamos dar uma olhada em um exemplo prático. Os funcionários do Instagram estão trabalhando duro para tornar a base de código do projeto totalmente digitada. Kodmody os ajuda seriamente neste assunto.

Se temos um certo conjunto de funções não tipadas que precisam ser digitadas, podemos tentar gerar os tipos retornados por elas pela inferência de tipo usual! Por exemplo, se uma função retornar valores de apenas um tipo primitivo, simplesmente atribuímos esse tipo de valor de retorno à função. Se a função retornar valores de um tipo lógico, por exemplo, se comparar algo com algo ou verificar algo, podemos atribuir a ele o tipo de valor de retorno bool . Descobrimos que, no decorrer do trabalho prático com a base de código do Instagram, essa é uma operação bastante segura.


Descobrindo os tipos de valores retornados por funções

Mas e se a função não retornar explicitamente nenhum valor ou implicitamente retornar None ? Se a função não retornar explicitamente nada, poderá ser atribuído o tipo None .

Isso, diferentemente do exemplo anterior, pode ser mais perigoso devido à existência de padrões comuns que os desenvolvedores usam. Por exemplo, em um método de classe base, você pode lançar uma exceção NotImplemented e, nos métodos das subclasses que substituem esse método, você pode retornar uma string. É importante observar que todas essas técnicas são heurísticas, mas os resultados de sua aplicação geralmente se mostram corretos. Como resultado, eles podem ser considerados úteis.


Funções que não retornam nada

Expandindo módulos de código com Pyre


Vamos dar um passo adiante. O Instagram usa o Pyre, um sistema de verificação de tipo estático completo semelhante ao mypy. O uso do Pyre nos permite verificar os tipos em uma base de código. E se usássemos os dados gerados pelo Pyre para expandir os recursos dos codemods? A seguir, é apresentado um exemplo desses dados. É fácil ver que há quase tudo o que você precisa para corrigir automaticamente as anotações de tipo!

 $ pyre ƛ Found 2 type errors! testing/utils.py:7:0 Missing return annotation [3]: Returning `SomeClass` but no return type is specified. testing/utils.py:10:0 Missing return annotation [3]: Returning `testing.other.SomeOtherClass` but no return type is specified. 

O Pyre durante o trabalho realiza uma análise detalhada da ordem de execução de cada função. Como resultado, essa ferramenta pode, às vezes, com uma probabilidade muito alta, assumir que uma função não anotada deve retornar. Isso significa que, se o Pyre acredita que a função retorna um tipo simples, atribuímos a ela o tipo de retorno. No entanto, agora, em potencial, precisamos processar comandos de importação também. Isso significa que precisamos saber se algo é importado ou declarado localmente. Mais tarde, abordaremos brevemente esse tópico.

Que benefícios obtemos ao adicionar automaticamente informações de tipo que são facilmente exibidas no código? Bem, tipos são documentação! Se a função for totalmente digitada, o desenvolvedor não precisará ler seu código para descobrir os recursos de sua chamada e os recursos de usar o que ela retorna.

 def get_description(page: WikiPage) -> Optional[str]:    if page.draft:        return None    return page.metadata["description"]  # <-    ? 

Muitos de nós já encontramos código Python semelhante. A base de código do Instagram também tem algo semelhante. Se a função get_description não get_description sido get_description , você precisará procurar em vários módulos para descobrir o que ela retorna. Ao mesmo tempo, mesmo se falamos de funções mais simples, cujos tipos de valores de retorno são fáceis de derivar, suas variantes digitadas são percebidas mais facilmente do que as não tipadas.

Além disso, o Pyre não verifica a operação correta do corpo da função se a função não estiver completamente anotada. No exemplo a seguir, a chamada para some_function falhará. Seria bom saber sobre isso antes que o código entre em produção.

 def some_function(in: int) -> bool:    return in > 0 def some_other_function():    if some_function("bla"): # <-             print("Yay!") 

Nesse caso, podemos descobrir um erro semelhante após o código entrar em produção. O fato é que some_other_function não possui anotação de tipo de retorno. Se o anotássemos usando nossos mecanismos heurísticos, usando o tipo deduzido automaticamente None , teríamos descoberto um problema com os tipos antes que ele pudesse causar problemas. Obviamente, este é um exemplo artificial, mas no Instagram esses problemas são graves. Se você tiver milhões de linhas de código, poderá, no processo de revisão de código, perder coisas que parecem completamente óbvias em um exemplo simples.

No Instagram, os métodos acima baseados em tipos deduzidos automaticamente permitiram digitar cerca de 10% das funções. Como resultado, as pessoas não precisavam mais editar manualmente milhares e milhares de funções. As vantagens do código digitado são óbvias, mas isso, no contexto de nossa conversa, leva a outra vantagem importante. Uma base de código totalmente digitada abre possibilidades ainda maiores para o processamento de código usando os modos de código.

Se confiarmos nas anotações de tipo, isso significa que o Pyre pode abrir possibilidades adicionais para nós. Vejamos novamente o exemplo em que renomeamos as funções. E se a entidade que estamos renomeando for representada por um método de classe e não por uma função global?


Function é um método de classe

Se você combinar as informações de tipo recebidas do Pyre e o modo de código que renomeia as funções, poderá, inesperadamente, fazer correções para onde a função é chamada e onde é declarada! Neste exemplo, como sabemos o que está no lado esquerdo da construção a.fn , também sabemos que é seguro alterar essa construção para a.add .

Análise estática mais avançada



O Python possui quatro tipos de escopos: escopo global, escopo no nível de classe e função, escopo aninhado

A análise do escopo nos permite usar codemods ainda mais poderosos. Lembre-se de um dos exemplos acima, em que falamos sobre o fato de adicionar anotações de tipo também pode significar a necessidade de trabalhar com comandos de importação? Se o sistema analisar o escopo, isso significa que podemos saber quais tipos usados ​​no arquivo estão presentes nele, graças aos comandos de importação, que são declarados localmente e quais estão ausentes. Da mesma forma, se você souber que uma variável global se sobrepõe a um argumento de função, evite alterar acidentalmente o nome desse argumento ao renomear uma variável global.

Sumário


Em nossa busca para corrigir todos os erros no código do Instagram, entendemos uma coisa. Consiste no fato de que a pesquisa do código que precisa ser corrigido geralmente é mais importante que a própria correção. Os programadores geralmente precisam resolver tarefas simples - como renomear funções, adicionar argumentos a métodos ou dividir módulos em partes. Tudo isso é comum, mas o tamanho da nossa base de código significa que uma pessoa não poderá encontrar todas as linhas que precisam ser alteradas. É por isso que é tão importante combinar os recursos dos codemods com a análise estática confiável. Isso nos permite encontrar com mais confiança as partes do código que precisam ser alteradas, o que significa que nos permite tornar os modos de código mais seguros e mais poderosos.

Caros leitores! Você usa mods de código?


Source: https://habr.com/ru/post/pt473770/


All Articles