E se não houver um analisador estático para o seu idioma favorito?

Bem, se o seu idioma favorito significa russo, inglês etc., esse é outro hub . E se a linguagem de programação ou marcação, é claro, escreva você mesmo o analisador! À primeira vista, isso é muito difícil, mas, felizmente, existem ferramentas multilíngues prontas que são relativamente fáceis de adicionar suporte a um novo idioma. Hoje mostrarei como adicionar o suporte à linguagem Modelica ao analisador PMD com uma quantidade de tempo bastante pequena.


A propósito, você sabe o que pode degradar a qualidade da base de código obtida de uma sequência de solicitações de recebimento ideais? O fato de que programadores de terceiros copiaram partes do código do projeto existente em seus patches em vez de abstração alfabética. Você deve admitir que, até certo ponto, é ainda mais difícil capturar essa banalidade do que o código de baixa qualidade - é de alta qualidade e até mesmo cuidadosamente depurado. Portanto, a verificação local não é suficiente aqui, é preciso ter em mente toda a base de códigos, mas isso não é fácil para uma pessoa ... Então: se adicionar suporte completo ao Modelica (sem criar regras específicas) ao estado "pode ​​executar verificações primitivas" levou-me cerca de uma semana; em seguida, o suporte para apenas o detector de copiar e colar pode ser adicionado em um dia!


O que mais é Modelica?


Modelica é, como o nome sugere, uma linguagem para escrever modelos de sistemas físicos. De fato, não apenas físico: é possível descrever processos químicos, comportamento quantitativo de populações de animais etc. - isso é descrito por sistemas de equações diferenciais da forma der(X) = f(X) , onde X é o vetor de incógnitas. Partes imperativas de código também são suportadas. As equações diferenciais parciais não são explicitamente suportadas, mas é possível dividir a área de estudo em partes (como provavelmente teríamos feito em uma linguagem de uso geral) e, em seguida, anote as equações de cada elemento, reduzindo o problema à anterior. O truque de Modelka é que a solução para esse der(X) = f(X) está com o compilador: você pode alterar o solucionador nas configurações , a equação não precisa ser linear etc. Em suma, existem algumas vantagens (escrevi a fórmula do livro - e funcionou) e os contras (com mais abstração, obtemos menos controle). Uma introdução ao Modelika é o tópico de um artigo separado (que já apareceu várias vezes em Habré), e até de um ciclo inteiro, hoje me interessa como aberta e com várias implementações, mas, infelizmente, ainda é um padrão úmido.


Além disso, o Modelika, por um lado, possui tipificação estática (o que nos ajudará a escrever algumas análises significativas mais rapidamente); por outro lado, ao instanciar um modelo, não é necessário que o compilador verifique completamente a biblioteca inteira (portanto, um analisador estático é muito útil para capturar "adormecido"). erros). Por fim, ao contrário de alguns C ++, para os quais há uma nuvem de analisadores e compiladores estáticos com recursos bonitos, e o mais importante, consulte modelos C ++ diagnóstico de erros, compiladores Os modelos ainda geram periodicamente um erro interno do compilador, o que significa que há espaço para ajudar o usuário mesmo com um analisador bastante simples.


O que é PMD?


Vou responder música de bicicleta. Uma vez eu quis fazer uma pequena solicitação de pull no ambiente de desenvolvimento do OpenModelica. Vendo como o salvamento do modelo é processado em outra parte do código, notei uma pequena e não muito clara parte interna de quatro linhas de código que suportavam algum tipo de invariante. Não entendo com que tipo de editor ele interage, mas percebendo que, do ponto de vista desse trecho de código, minha tarefa é completamente idêntica, eu apenas o coloco em uma função para que eu possa reutilizá-lo e não quebrá-lo. Menteiner disse que é maravilhoso, só então substituir esse código por uma chamada de função nos vinte lugares restantes ... Decidi não me envolver ainda e apenas fiz outra cópia, observando que, de alguma forma, eu precisaria pentear tudo de uma vez sem misturar com o patch atual. Pesquisando no Google, encontrei o CPD (Copy-paste Detector) - parte do analisador estático PMD - que suporta ainda mais idiomas do que o próprio analisador. Depois de configurá-lo na base de código do OMEdit, eu esperava ver essas duas dúzias de peças de quatro linhas. Eu simplesmente não os vi (cada um deles simplesmente não excedeu o limite no número de tokens), mas vi, por exemplo, a repetição de quase cinquenta linhas de código C ++. Como eu já disse, é improvável que o mentor simplesmente copie uma peça gigantesca de outro arquivo. Mas ele poderia facilmente perder isso no PR - porque o código, por definição, já atendia a todos os padrões do projeto! Quando compartilhei a observação com o mentor, ele concordou que seria necessário fazer a limpeza como parte de uma tarefa separada.


Por conseguinte, o PMD (Detector de Erros do Programa) é um analisador estático facilmente extensível. Talvez ele não calcule o conjunto de valores que uma variável pode receber (embora quem sabe ...), mas para adicionar regras a ela, você nem precisa conhecer Java e, de alguma forma, alterar seu código! O fato é que a primeira coisa que ele, e não surpreendentemente, está criando arquivos AST com códigos-fonte. E como é a árvore de análise de origem? Para a árvore de análise XML! Portanto, você pode descrever as regras simplesmente como solicitações XPath - para as quais elas correspondem, em seguida, emitimos um aviso. Eles ainda têm um depurador gráfico para as regras! Regras mais complexas, é claro, podem ser escritas diretamente em Java como visitantes do AST.


Consequência : O PMD pode ser usado não apenas para as regras rígidas e universais que os programadores Java severos comprometeram com o código do analisador, mas também para o estilo de codificação local - mesmo se você inserir seu próprio ruleset.xml local em cada repositório!


Nível 1: encontre copiar e colar automaticamente


Em princípio, adicionar suporte para um novo idioma no CPD geralmente é muito simples. Não vejo sentido em recontar a documentação "como fazer" - é muito clara, estruturada e passo a passo. Para recontar uma coisa dessas - jogue apenas em um telefone danificado. É melhor descrever o que espera por você (TLDR: tudo bem) :


  • O analisador (PMD e CPD) é desenvolvido no github no repositório pmd / pmd
  • O depurador de regra visual foi movido para um repositório pmd / pmd-designer separado. Observe que o jar-nickname final é incorporado automaticamente na distribuição binária do PMD , que o Gradle coletará para você no repositório anterior. Você não precisa clonar especialmente o pmd-designer para isso.
  • O projeto possui uma documentação do desenvolvedor . O que eu li foi muito detalhado. É verdade que está um pouco desatualizado, mas isso é tratado pela segunda solicitação pull :

Vou avisar que estou desenvolvendo no Ubuntu. No Windows, ele também deve funcionar perfeitamente - tanto em termos de qualidade quanto no sentido de uma maneira ligeiramente diferente de iniciar ferramentas.


Então, para adicionar um novo idioma ao CPD, você precisa ...


  • ATENÇÃO: se você deseja suporte total para o PMD antes do lançamento do PMD 7, é melhor ir diretamente para o nível 2, pois o suporte normal para a maneira fácil através da gramática Antlr pronta aparecerá, de acordo com os rumores, na mesma versão 7, mas por enquanto você só gasta tempo (embora e um pouco ...)
  • Bifurque o repositório pmd / pmd .
  • Encontre no antlr / grammars-v4 uma gramática pronta para o seu idioma - é claro, se o idioma for interno, você deverá escrevê-lo, mas para o Modelika, por exemplo, foi encontrado. Aqui, é claro, você precisa cumprir as formalidades das licenças - não sou advogado, mas pelo menos preciso especificar a fonte de onde copiei.
  • Depois disso, você precisa criar o pmd-<your language name> , adicioná-lo ao Gradle e colocar o arquivo de gramática lá. Além disso, depois de ler duas páginas de documentação não estressante, refaça o script de montagem do módulo Go, algumas classes para carregar o módulo através da reflexão, bem, há uma coisinha ...
  • Corrija a saída de referência em um dos testes, porque agora o CPD suporta mais um idioma! Como você encontra esse teste? Muito fácil: ele quer quebrar a construção .
  • LUCRO! É muito simples, desde que exista uma gramática pronta

Agora, estando na raiz do repositório pmd, você pode digitar ./mvnw clean verify , enquanto em pmd-dist/target você terá, entre outras coisas, distribuição binária na forma de um arquivo zip que você precisa descompactar e executar usando ./bin/run.sh cpd --minimum-tokens 100 --files /path/to/source/dir --language <your language name> no diretório descompactado. Em princípio, você pode ../mvnw clean verify partir do seu novo módulo, o que acelerará drasticamente a montagem, mas será necessário colocar corretamente o apelido do jar montado na distribuição binária descompactada (por exemplo, montada uma vez após o registro de um novo módulo).


Nível 2: localizando erros e violações do guia de estilo


Como eu disse, o PMD 7 promete suporte total ao Antlr. Se você, como eu, não quiser esperar pela liberação do mar, precisará obter uma descrição da gramática do idioma no formato JJTree de algum lugar. Talvez você possa fortalecer o suporte de um analisador arbitrário - a documentação diz que isso é possível, mas eles não dizem exatamente ... Acabei de pegar o modelica.g4 do mesmo repositório com gramáticas para o Anltr como base e refazê-lo manualmente no JJTree. Naturalmente, se a gramática acabou sendo um processamento da existente, indique novamente a fonte, verifique a conformidade com as licenças e. etc.


A propósito, para uma pessoa que é bem versada em todos os tipos de geradores de analisadores, é improvável que isso seja uma surpresa. Antes disso, eu o usava seriamente, a menos que eu mesmo escrevesse regulares e combinadores de analisadores no Scala. Portanto, a coisa óbvia, de fato, me entristeceu a princípio: a AST, é claro, vou obter do modelica.g4 , mas não parece muito claro e "utilizável": haverá nuvens de nós extras nele, e se você não olhar os tokens , mas apenas em nós, nem sempre é claro onde, por exemplo, o ramo then termina e else começa.


Novamente, não vou recontar a documentação do JJTree e um bom tutorial - desta vez, não porque o original brilha com detalhes e clareza, mas porque eu mesmo não a descobri completamente, mas a documentação foi retransmitida incorretamente, mas com confiança, obviamente pior do que não recontar. É melhor deixar uma pequena pista, descoberta ao longo do caminho:


  • Em primeiro lugar, o código de descrição do analisador JavaCC assume inserções Java que serão inscritas no analisador gerado
  • Não se confunda com o fato de que, ao criar um AST, sintaxe como [ Expression() ] significa opcional e, no contexto da descrição de tokens - escolha de um personagem, como em uma expressão regular. Tanto quanto eu entendo a explicação dos desenvolvedores de PMD, essas são construções semelhantes que têm um significado tão diferente - legado, senhor ...
  • Para o nó raiz (no meu caso, StoredDefinition ), você deve especificar seu tipo em vez de void (ou seja, ASTStoredDefiniton )
  • Usando a sintaxe #void após o nome do nó, é possível ocultá-la da árvore analisada (ou seja, ela afetará apenas qual é a origem correta e o que não é, e como os outros nós serão aninhados)
  • Usando uma construção do formulário void SimpleExpression() #SimpleExpression(>1) podemos dizer que o nó deve ser mostrado no AST resultante, se tiver mais de um descendente. Isso é muito conveniente ao descrever expressões com muitos operadores com prioridades diferentes: ou seja, do ponto de vista do analisador, a constante solitária 1 será algo como LogicExpression(AdditiveExpression(MultiplicativeExperssion(Constant(1)))) - insira todos os n níveis de prioridades de operação - mas o código do analisador obterá Constant(1)
  • O nó possui uma image variável padrão (consulte setImage , setImage ), que geralmente setImage a "essência" desse nó: por exemplo, para um nó correspondente ao nome de uma variável local, é lógico copiar o token correspondente com o identificador para a image (por padrão, todos os tokens de as árvores serão jogadas fora, por isso, vale a pena copiar o significado nelas, de qualquer forma, se for algo variável, e não apenas palavras-chave)
  • LOOKAHEAD - bem, esta é uma música separada, até um capítulo separado na documentação é dedicado a ela
    • Em termos gerais, no JavaCC, se você acessar um nó, não poderá revertê-lo e tentar analisar de forma diferente, mas poderá olhar com antecedência e decidir se deve ou não ir
    • no caso mais simples, ao ver um aviso JavaCC, você acabou de dizer no cabeçalho LOOKAHEAD = n e você recebe erros de análise misteriosos, porque, no caso geral, parece que não pode resolver todos os problemas (bem, exceto que, ao definir alguns bilhões de tokens, você realmente obtém uma prévia de tudo, mas não do fato de que funciona dessa maneira .. .)
    • na frente do nome do nó incorporado, você pode indicar explicitamente com base em quantos tokens aqui você pode definitivamente tomar a decisão final
    • se, no caso geral, não houver um número fixo de tokens, você pode dizer "vá aqui, se anteriormente, a partir deste ponto, conseguimos combinar esse prefixo - e depois a descrição usual da subárvore"
    • tenha cuidado: no caso geral, o JavaCC não pode verificar a exatidão das diretivas LOOKAHEAD - confia em você, portanto, pelo menos, descubra a prova matemática de por que esse lookahead é suficiente ...

Agora que você tem uma descrição da gramática do idioma no formato JJTree, essas 14 etapas simples ajudarão você a adicionar suporte ao idioma. A maioria deles tem o formato "crie uma classe semelhante à implementação para java ou vm, mas adaptada". Observarei apenas os recursos típicos, alguns deles aparecerão na documentação principal se eles aceitarem minha solicitação de recebimento de documentação :


  • Comentando a remoção de todos os arquivos gerados no script de montagem alljavacc.xml (que está no seu novo módulo), você pode transferi-los para a árvore de origem do target/generated-sources . Mas melhor não. Provavelmente, apenas uma pequena parte será alterada; portanto, é melhor excluir apenas algumas: elas viram a necessidade de alterar a implementação padrão, copiadas para a árvore de origem, adicionadas à lista de arquivos excluídos, reconstruídas - e agora você gerencia o arquivo - especificamente esse arquivo . Caso contrário, será difícil descobrir exatamente o que foi alterado, e o suporte dificilmente pode ser chamado de agradável.
  • agora que você possui uma implementação do modo PMD "principal", também pode pendurar facilmente no analisador JJTree uma ligação para CPD, semelhante ao Java ou a alguma outra implementação disponível.
  • Lembre-se de implementar um método que retorna o nome do host para consultas XPath. Na implementação padrão, obtém-se recursão infinita (o nome do nó via toString e vice-versa) ou qualquer outra coisa, em geral, por isso, também não é possível olhar para a árvore no PMD Designer e, sem isso, a gramática é realmente triste.
  • parte dos registros de componentes é feita adicionando arquivos de texto de pontos de entrada de nome de classe totalmente qualificados ao META-INF/services
  • o que pode ser descrito declarativamente nas regras (por exemplo, uma descrição detalhada da verificação e exemplos de erros) é descrito não no código, mas na category/<language name>/<ruleset>.xml - em qualquer caso, você deverá registrar suas regras lá
  • ... mas ao implementar os testes, aparentemente, algum mecanismo de descoberta automática, talvez caseiro, é usado ativamente, portanto
    • se você for informado, "adicione um teste trivial para cada versão do idioma" - é melhor não discutir, eles dizem "eu não preciso disso, funciona assim" - talvez esse seja o mecanismo de detecção automática
    • se você vir um teste para uma regra específica com um corpo de classe contendo apenas um comentário // no additional unit tests , eles não são testes, eles apenas se encontram nos recursos na forma de descrição XML dos dados de entrada e reações esperadas do analisador, imediatamente: alguns corretos e alguns exemplos incorretos.

Uma missão pequena, mas importante: terminar o PMD Designer


Talvez você possa depurar tudo sem um visualizador. Mas porque? Em primeiro lugar, terminar é muito simples. Em segundo lugar, ajudará muito os usuários que não estão familiarizados com Java: eles são fáceis e simples (se isso se aplica ao XPath), bem, ou pelo menos sem recompilar o PMD, serão capazes de descrever padrões simples do que eles não gostam (no caso mais simples - um guia de estilo como "o nome de um pacote de modelo sempre começa com um p minúsculo").


Diferentemente de outros erros que são imediatamente visíveis, os problemas com o PMD Designer são bastante traiçoeiros: parece que você já entendeu que a inscrição Java no lado direito do menu não é um botão, mas uma lista suspensa da seleção de idioma O_o, na qual ele já apareceu Modelica, porque um novo módulo com registro de pontos de entrada apareceu no caminho de classe. Mas aqui você escolhe seu idioma, baixa um arquivo de teste e vê o AST. E parece ser uma vitória, mas de alguma forma é preto e branco, e a subárvore destacada pode ser destacada no texto - embora não, o destaque esteja lá, mas é atualizado de forma torta - e, no entanto, como eles não tentaram destacar as correspondências encontradas com o XPath ... Já calculando a quantidade de trabalho, você pensa na próxima solicitação pull, mas decide acidentalmente mudar a linguagem para Java e baixar algum código fonte do próprio PMD ... Ah! É colorido! .. E o destaque da subárvore funciona! Uh ... e acontece que normalmente destaca as correspondências encontradas e grava trechos de texto na caixa à direita da solicitação ... Parece que quando uma exceção ocorre no código JavaFX durante a renderização da interface, ela interrompe a renderização, mas não é impressa no console ...


Em geral, você só precisa adicionar uma classe ma-a-scarlet para destacar a sintaxe com base em expressões regulares. No meu caso, era net.sourceforge.pmd.util.fxdesigner.util.codearea.syntaxhighlighting.ModelicaSyntaxHighlighter , que precisa ser registrado na classe AvailableSyntaxHighlighters . Observe que essas duas alterações ocorrem no repositório pmd-designer , cujo artefato de montagem precisa ser colocado em sua distribuição binária.


No final, é algo parecido com isto (GIF retirado do README no repositório do PMD Designer):


PMD Designer no trabalho


Subtotal


Se você concluiu todos esses níveis, agora tem:


  • detector de copiar e colar
  • mecanismo de regras
  • visualizador para depurar o AST e trazê-lo para uma forma conveniente de análise (como já vimos, nem todas as gramáticas do mesmo idioma são igualmente úteis!)
  • o mesmo visualizador para depurar regras XPath que seus usuários podem escrever sem recompilar o PMD e geralmente conhecer Java (XPath, é claro, também não é BASIC, mas é pelo menos uma linguagem de consulta padrão e não local)

Espero que você também entenda o fato de que a gramática agora é uma API estável para sua implementação de suporte a idiomas - não a altere (ou melhor, a função de converter a fonte em AST descrita por ela), a menos que seja absolutamente necessário e, se você tiver alterado, notifique como uma alteração de interrupção, e então os usuários ficarão chateados: provavelmente nem todos escreverão testes para suas regras, mas é muito triste quando as regras verificaram o código e pararam sem aviso prévio - quase como um backup, que de repente quebrou, e um ano atrás ...


A história não termina aí: pelo menos algumas regras úteis precisam ser escritas.


Mas isso não é tudo: o PMD suporta nativamente escopos e declarações. Cada nó AST tem escopo associado: o corpo da classe, função, loop ... O arquivo inteiro, na pior das hipóteses! E em todo escopo, há uma lista de definições (declarações) que ela contém diretamente. Como em outros casos, propõe-se implementar por analogia com outras linguagens, por exemplo, Modelika (mas no momento da redação, a lógica na minha solicitação pull é, francamente, bruta). scopes declarations visitor, - ScopeAndDeclarationFinder , — , , , - , read-only AST. , .


 public class ModelicaHandler extends AbstractLanguageVersionHandler { // ... @Override public VisitorStarter getSymbolFacade() { return new VisitorStarter() { @Override public void start(Node rootNode) { new SymbolFacade().initializeWith((ASTStoredDefinition) rootNode); } }; } } 

Conclusão


PMD . , «» Clang Static Analyzer , . , CPD ( ), .

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


All Articles