A superfície de um módulo
Como o analisador lê o que outro arquivo oferece, sem executá-lo — e quando ele prefere calar.
P.naoExiste() e P.criar(1, 2, 3) são acusados antes de rodar, mesmo quando P vem de outro arquivo. É a checagem que mais importa em sistema grande: num arquivo de 40 linhas o erro aparece na primeira execução; num de 200 arquivos, a maioria das chamadas cruza módulo, e todas elas eram invisíveis.
Como ele sabe, sem executar#
superficie.py lê o outro arquivo com o lexer e o parser, e não com o interpretador. Do que encontra, guarda o que atravessa a fronteira:
| Guarda | Para conferir |
|---|---|
| os nomes exportados | P.naoExiste — o membro que não existe |
| a aridade de cada ação | P.criar(1, 2, 3) com dois parâmetros |
| o tipo de cada parâmetro | D.valor_de(texto) onde se espera Integer |
| o tipo de retorno | P.criar(…).clientte — o campo errado, do outro lado |
| os campos de cada record | o mesmo, um nível adiante |
O tipo de retorno atravessar é o que mais rende. Sem ele, uma ação que declara -> Pedido virava um valor sem tipo no outro arquivo, e o campo com nome quase certo passava no check.
A tradução de vocabulário#
O -> Pedido declarado lá é P.Pedido aqui. Devolver o nome nu faria o analisador procurar um record que este arquivo não declara — e a primeira versão fazia isso, acusando o código certo:
Parameter 'p' of 'P.com_total' expects Pedido but got P.PedidoUm falso alarme no caminho mais comum de um projeto modular ensina a desligar a verificação inteira. Hoje a tradução acontece nos dois lados, e quando não dá para concluir o analisador cala.
Quando ele cala#
| Cala quando | Porque |
|---|---|
| o outro arquivo não compila | uma superfície lida de código quebrado é palpite |
| há ciclo de import | não há ordem em que a leitura termine |
| a profundidade (4) acaba | seguir a cadeia inteira levaria o check a minutos |
o relay nomeia algo que só existe em execução | o nome pode ser qualquer coisa |
| o módulo não exporta aquele tipo | o tipo é interno, e o nome daqui não o alcança |
Em todos esses casos a superfície devolve aberta, e a conferência volta a ficar em silêncio. Um falso alarme é pior que um silêncio.
O escopo de quem chama#
A inferência usa o escopo de quem chama, e não o global. Parece detalhe e não é: D.valor_de(n) dentro de action f(n) virou "Undefined name 'n'" — 649 falsos alarmes num projeto gerado de 252 arquivos, um por uso de parâmetro numa chamada entre módulos.
E a suíte passava: os primeiros testes chamavam no nível de topo, onde o escopo global é o certo. O bug só aparecia dentro de uma ação, que é onde quase todo código vive. Quem pegou foi rodar o check no projeto grande.
O cache#
A superfície é guardada por (caminho, mtime). Sem isso, 200 arquivos importando três vizinhos cada levariam o check de 0,7 s a mais de um minuto — e um analisador que demora um minuto não roda a cada salvar, que é quando ele vale.