go-critical: o analisador estático mais teimoso do Go


Estamos anunciando um novo linter (analisador estático) para o Go , que também é uma caixa de proteção para prototipar suas idéias no mundo da análise estática.


O go-critic é construído em torno das seguintes observações:


  • É melhor ter uma implementação "suficientemente boa" do teste do que não tê-lo
  • Se a verificação for controversa, isso não significa que não possa ser útil. Marcamos como "opinativo" e derramamos
  • Escrever um linter a partir do zero é geralmente mais difícil do que adicionar uma nova verificação a uma estrutura existente se a estrutura em si for fácil de entender.

Neste post, veremos o uso e a arquitetura do go-critical, algumas das verificações implementadas nele , e também descreveremos as principais etapas para adicionar nossa função de analisador a ele.


Início rápido


$ cd $GOPATH $ go get -u github.com/go-critic/go-critic/... $ ./bin/gocritic check-package strings $GOROOT/src/strings/replace.go:450:22: unslice: could simplify s[:] to s $GOROOT/src/strings/replace.go:148:2: elseif: should rewrite if-else to switch statement $GOROOT/src/strings/replace.go:156:3: elseif: should rewrite if-else to switch statement $GOROOT/src/strings/replace.go:219:3: elseif: should rewrite if-else to switch statement $GOROOT/src/strings/replace.go:370:1: paramTypeCombine: func(pattern string, value string) *singleStringReplacer could be replaced with func(pattern, value string) *singleStringReplacer $GOROOT/src/strings/replace.go:259:2: rangeExprCopy: copy of r.mapping (256 bytes) can be avoided with &r.mapping $GOROOT/src/strings/replace.go:264:2: rangeExprCopy: copy of r.mapping (256 bytes) can be avoided with &r.mapping $GOROOT/src/strings/strings.go:791:1: paramTypeCombine: func(s string, cutset string) string could be replaced with func(s, cutset string) string $GOROOT/src/strings/strings.go:800:1: paramTypeCombine: func(s string, cutset string) string could be replaced with func(s, cutset string) string $GOROOT/src/strings/strings.go:809:1: paramTypeCombine: func(s string, cutset string) string could be replaced with func(s, cutset string) string $GOROOT/src/strings/strings.go:44:1: unnamedResult: consider to give name to results $GOROOT/src/strings/strings.go:61:1: unnamedResult: consider to give name to results $GOROOT/src/strings/export_test.go:28:3: rangeExprCopy: copy of r.mapping (256 bytes) can be avoided with &r.mapping $GOROOT/src/strings/export_test.go:42:1: unnamedResult: consider to give name to results 

(A formatação dos avisos foi editada; os originais estão disponíveis na essência .)


O utilitário gocritic pode verificar pacotes individuais pelo caminho de importação ( check-package ) e também percorrer recursivamente todos os diretórios ( check-project ). Por exemplo, você pode verificar todo o $GOROOT ou $GOPATH com um único comando:


 $ gocritic check-project $GOROOT/src $ gocritic check-project $GOPATH/src 

Há suporte para a "lista branca" de verificações, a fim de listar explicitamente quais verificações devem ser executadas (sinalizador -enable ). Por padrão, todas as verificações que não estão marcadas com o ícone Experimental ou VeryOpinionated são VeryOpinionated .


Estão previstas integrações no golangci-lint e no gometalinter .


Como tudo começou


Ao realizar a próxima revisão de código do projeto Go, ou ao auditar alguma biblioteca de terceiros, é possível observar os mesmos problemas repetidamente.


Infelizmente, não foi possível encontrar um linter que diagnosticasse essa classe de problemas.


Sua primeira etapa pode ser uma tentativa de categorizar o problema e entrar em contato com os autores do linter existente, sugerindo que eles adicionem uma nova verificação. As chances de sua proposta ser aceita são altamente dependentes do projeto e podem ser bastante baixas. Mais provavelmente, meses de expectativa se seguirão.


Mas e se a verificação for completamente ambígua e puder ser percebida por alguém como subjetiva demais ou insuficientemente precisa?


Talvez faça sentido tentar escrever esse cheque você mesmo?


go-critic existe para se tornar um lar para testes experimentais que são mais fáceis de implementar por nós do que anexá-los a analisadores estáticos existentes. O dispositivo go-critic minimiza a quantidade de contexto e ações necessárias para adicionar uma nova verificação - podemos dizer que você precisa adicionar apenas um arquivo (sem contar os testes).


Como funciona o crítico


Um crítico é um conjunto de regras que descrevem as propriedades da verificação e os microcontroladores que implementam a inspeção de código para conformidade com uma regra.


Um aplicativo que incorpora um linter (por exemplo, cmd / gocritic ou golangci-lint ) recebe uma lista de regras suportadas, as filtra de uma maneira específica, cria uma função de verificação para cada regra selecionada e inicia cada uma delas no pacote em estudo.


O trabalho de adicionar um novo verificador se resume a três etapas principais:


  1. Adicionando testes.
  2. A implementação da verificação em si.
  3. Adicionando documentação para o linter.

Examinaremos todos esses pontos usando a regra captLocal de exemplo, que requer a ausência de nomes locais começando com uma letra maiúscula.



Adicionando testes


Para adicionar dados de teste para uma nova verificação, você precisa criar um novo diretório em lint / testdata .


Cada um desses diretórios deve ter um arquivo positive_tests.go , que descreve exemplos de código nos quais a verificação deve funcionar. Para testar a ausência de falsos positivos, os testes são complementados com um código "correto", no qual a nova verificação não deve encontrar nenhum problema ( negative_tests.go ).


Exemplos:


 // lint/testdata/positive_tests.go /// consider `in' name instead of `IN' /// `X' should not be capitalized /// `Y' should not be capitalized /// `Z' should not be capitalized func badFunc1(IN, X int) (Y, Z int) { /// `V' should not be capitalized V := 1 return V, 0 } 

 // lint/testdata/negative_tests.go func goodFunc1(in, x int) (x, y int) { v := 1 return v, 0 } 

Você pode executar testes após adicionar um novo linter.


Implementação de verificação


Crie um arquivo com o nome do verificador: lint/captLocal_checker.go .
Por convenção, todos os arquivos de _checker têm o sufixo _checker .


 package lint //  “Checker”    . type captLocalChecker struct { checkerBase upcaseNames map[string]bool } 

checkerBase é um tipo que deve ser incorporado em cada verificador.
Ele fornece implementações padrão, o que permite escrever menos código em cada ponteiro.
Entre outras coisas, checkerBase inclui um ponteiro para lint.context , que contém informações de tipo e outros metadados sobre o arquivo que está sendo verificado.


O campo upcaseNames conterá uma tabela de nomes conhecidos, que ofereceremos para substituir pela versão strings.ToLower(name) . Para os nomes que não estão contidos no mapa, será sugerido não usar uma letra maiúscula, mas nenhuma substituição correta será fornecida.


O estado interno é inicializado uma vez para cada instância.
O método Init() deve ser definido apenas para os linters que precisam executar a inicialização preliminar.


 func (c *captLocalChecker) Init() { c.upcaseNames = map[string]bool{ "IN": true, "OUT": true, "INOUT": true, } } 

Agora você precisa definir a própria função de verificação.
No caso de captLocal , precisamos verificar todo o ast.Ident local que introduz novas variáveis.


Para verificar todas as definições locais de nomes, você deve implementar um método com a seguinte assinatura no seu verificador:


 VisitLocalDef(name astwalk.Name, initializer ast.Expr) 

A lista de interfaces de visitantes disponíveis pode ser encontrada no arquivo lint / internal / visitor.go .
captLocal implementa LocalDefVisitor .


 //  ast.Expr,         //  .      . func (c *captLocalChecker) VisitLocalDef(name astwalk.Name, _ ast.Expr) { switch { case c.upcaseNames[name.ID.String()]: c.warnUpcase(name.ID) case ast.IsExported(name.ID.String()): c.warnCapitalized(name.ID) } } func (c *captLocalChecker) warnUpcase(id *ast.Ident) { c.ctx.Warn(id, "consider `%s' name instead of `%s'", strings.ToLower(id.Name), id) } func (c *captLocalChecker) warnCapitalized(id ast.Node) { c.ctx.Warn(id, "`%s' should not be capitalized", id) } 

Por convenção, os métodos que geram avisos geralmente são colocados em métodos separados. Existem raras exceções, mas seguir esta regra é considerado uma boa prática.


Adicionando documentação


Outro método de implementação necessário é o InitDocumentation :


 func (c *captLocalChecker) InitDocumentation(d *Documentation) { d.Summary = "Detects capitalized names for local variables" d.Before = `func f(IN int, OUT *int) (ERR error) {}` d.After = `func f(in int, out *int) (err error) {}` } 

Normalmente, basta preencher 3 campos:


  • Summary - uma descrição da ação de validação em uma frase.
  • Before - código antes da correção.
  • Código posterior após correção (não deve causar um aviso).

Geração de documentação

Gerar documentação novamente não é um pré-requisito para um novo interlocutor; talvez em um futuro próximo essa etapa seja totalmente automatizada. Mas se você ainda deseja verificar a aparência do arquivo de redução de preço, use o comando make docs . O arquivo docs/overview.md será atualizado.


Registre um novo linter e execute testes


O toque final está registrando um novo linter:


 //   captLocal_checker.go init . func init() { addChecker(&captLocalChecker{}, attrExperimental, attrSyntaxOnly) } 

addChecker espera um ponteiro para o valor zero do novo ponteiro. A seguir, vem o argumento variável, permitindo que você passe zero ou mais atributos que descrevem as propriedades da implementação da regra.


attrSyntaxOnly é um marcador opcional para linters que não usam informações de tipo em sua implementação, o que permite executá-las sem executar verificações de tipo. golangci-lint marca esses linters com a bandeira "fast" (porque correm muito mais rápido).


attrExperimental é um atributo atribuído a todas as novas implementações. A remoção deste atributo é possível somente após a estabilização da verificação implementada.


Agora que o novo linter está registrado por meio do addChecker, você pode executar os testes:


 #  GOPATH: $ go test -v github.com/go-critic/go-critic/lint #  GOPATH/src/github.com/go-critic/go-critic: $ go test -v ./lint #  ,      make: $ make test 

Mesclagem otimista (quase)


Ao considerar solicitações de recebimento, tentamos aderir à estratégia de mesclagem otimista . Isso é expresso principalmente na aceitação daqueles PRs aos quais o revisor pode ter algumas reivindicações, em particular puramente subjetivas. Imediatamente após a injeção desse patch, um PR pode seguir o revisor, que corrige essas deficiências, o autor do patch original é adicionado ao CC (cópia).


Também temos dois marcadores linter que podem ser usados ​​para evitar bandeiras vermelhas na ausência de consenso total:


  1. Experimental : uma implementação pode ter uma grande quantidade de falso positivo, ser ineficaz (a origem do problema é identificada) ou "cair" em algumas situações. Você pode infundir essa implementação se a marcar com o atributo attrExperimental . Às vezes, com a ajuda do experimental, são indicadas essas verificações que não encontraram um bom nome no primeiro commit.
  2. VeryOpinionated : se o teste puder ter defensores e inimigos, vale a pena marcá-lo com o atributo attrVeryOpinionated . Dessa forma, podemos evitar rejeitar idéias sobre o estilo do código que podem não corresponder ao gosto de alguns esquilos.

Experimental é uma propriedade de implementação potencialmente temporária e corrigível. VeryOpinionated é uma propriedade de regra mais fundamental que é independente de implementação.


É recomendável criar um tíquete [checker-request] no github antes de enviar a implementação, mas se você já tiver enviado uma solicitação pull, poderá abrir o problema correspondente.


Para mais detalhes sobre o processo de desenvolvimento, consulte CONTRIBUTING.md .
As regras básicas estão listadas na seção principal de regras .


Palavras de despedida


Você pode participar do projeto não apenas adicionando um novo linter.
Existem muitas outras maneiras:


  • Experimente-o em seus projetos ou em projetos de código aberto grandes / conhecidos e relate falsos positivos, falsos negativos e outras deficiências. Ficaríamos gratos se você também adicionar uma nota sobre o problema encontrado / corrigido na página de troféus .
  • Sugira idéias para novas inspeções. É o suficiente para criar um problema no nosso rastreador.
  • Adicione testes para linters existentes.

go-critic critica seu código Go com as vozes de todos os programadores envolvidos em seu desenvolvimento. Todos podem criticar, portanto - participe!


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


All Articles