
Neste artigo, falarei sobre a nova biblioteca de análise estática go-ruleguard
(e utilitário) que adapta o gogrep
para uso dentro de linters.
Característica distintiva: você descreve as regras da análise estática em uma DSL tipo Go especial, que no início da ruleguard
de ruleguard
transforma em um conjunto de diagnósticos. Talvez essa seja uma das ferramentas mais facilmente configuráveis para implementar inspeções personalizadas no Go.
Como bônus, falaremos sobre go/analysis
e seus antecessores .
Extensibilidade da análise estática
Existem muitos linters para o Go, alguns dos quais podem ser expandidos. Geralmente, para estender o linter, você precisa escrever o código Go usando a API especial do linter.
Existem duas maneiras principais: Ir plugins e monólito. O monólito implica que todas as verificações (incluindo as pessoais) estão disponíveis no estágio de compilação.
revive
requer que novas verificações sejam incluídas em seu kernel para expansão. Além disso, os plug-ins go-critic
Crit, que permitem coletar extensões, independentemente do código principal. Ambas as abordagens implicam que você implemente manipulações go/ast
e go/types
no Go usando a API do linter. Mesmo verificações simples exigem muito código .
go/analysis
visa simplificar a imagem pelo fato de que a "estrutura" do linter se torna quase idêntica, mas não resolve o problema da complexidade da implementação técnica dos próprios diagnósticos.
Digressão em `loader` e` go / packages`
Quando você escreve um analisador para o Go, seu objetivo final é interagir com o AST e os tipos, mas antes que você possa fazer isso, o código-fonte precisa ser "carregado" da maneira correta. Para simplificar, o conceito de carregamento inclui análise , verificação de tipo e importação de dependências .
A primeira etapa na simplificação desse pipeline foi o pacote go/loader
, que permitirá que você "baixe" tudo o que precisa através de algumas chamadas. Tudo estava quase bem, e então ele se tornou obsoleto em favor de go/packages
. go/packages
possui uma API ligeiramente aprimorada e, em teoria, funciona bem com os módulos.
Agora, é melhor não usar nenhuma das opções acima diretamente para escrever analisadores, porque go/analysis
deu go/packages
algo que nenhuma das soluções anteriores possuía - uma estrutura para o seu programa. Agora podemos usar o paradigma de go/analysis
ditado e reutilizar os analisadores com mais eficiência. Esse paradigma tem pontos controversos, por exemplo, go/analysis
adequado para análises no nível de um pacote e suas dependências, mas não será fácil fazer uma análise global sobre ele sem truques de engenharia astutos.
go/analysis
também simplifica os testes do analisador .
O que é regra?

go-ruleguard
é um utilitário de análise estática que, por padrão, não inclui uma única verificação.
As regras de regras de regras ruleguard
carregadas no início, a partir de um arquivo gorules
especial que descreve declarativamente os padrões de código nos quais os avisos devem ser emitidos. Este arquivo pode ser editado livremente por usuários de ruleguard
.
Não é necessário gorules
programa de controle para conectar novas verificações, para que as regras dos gorules
possam ser chamadas dinâmicas .
O programa de controle de ruleguard
é mais ou menos assim:
package main import ( "github.com/quasilyte/go-ruleguard/analyzer" "golang.org/x/tools/go/analysis/singlechecker" ) func main() { singlechecker.Main(analyzer.Analyzer) }
Ao mesmo tempo, o analyzer
implementado por meio do pacote ruleguard
, que você deve usar se quiser usá-lo como uma biblioteca.
ruleguard VS revive
Pegue um exemplo simples, mas do mundo real: suponha que desejamos evitar chamadas em tempo de runtime.GC()
em nossos programas. No revive, já existe um diagnóstico separado para isso, chamado "call-to-gc"
.
Implementação de call-to-gc (70 linhas no Elven)
package rule import ( "go/ast" "github.com/mgechev/revive/lint" )
Agora compare com como isso é feito no go-ruleguard
:
package gorules import "github.com/quasilyte/go-ruleguard/dsl/fluent" func callToGC(m fluent.Matcher) { m.Match(`runtime.GC()`).Report(`explicit call to the garbage collector`) }
Nada mais, exatamente o que realmente importa - runtime.GC
e a mensagem que precisa ser emitida caso a regra seja acionada.
Você pode perguntar: isso é tudo? Comecei especificamente com um exemplo tão simples para mostrar quanto código pode ser necessário para um diagnóstico muito trivial no caso da abordagem tradicional. Eu prometo que haverá exemplos mais emocionantes.
Início rápido
go-critic
possui um diagnóstico rangeExprCopy
que localiza cópias de matriz potencialmente inesperadas no código.
Este código é iterado sobre uma cópia da matriz:
var xs [2048]byte for _, x := range xs {
A correção para esse problema é adicionar um caractere:
var xs [2048]byte - for _, x := range xs { // Copies 2048 bytes + for _, x := range &xs { // No copy // Loop body. }
Provavelmente, você não precisa dessa cópia, e o desempenho da versão corrigida é sempre melhor. Você pode esperar até que o compilador Go melhore, ou pode detectar esses lugares no código e corrigi-los hoje usando o mesmo go-critic
.
Esse diagnóstico pode ser implementado no idioma dos gorules
(arquivo rules.go
):
package gorules import "github.com/quasilyte/go-ruleguard/dsl/fluent" func _(m fluent.Matcher) { m.Match(`for $_, $_ := range $x { $*_ }`, `for $_, $_ = range $x { $*_ }`). Where(m["x"].Addressable && m["x"].Type.Size >= 128). Report(`$x copy can be avoided with &$x`). At(m["x"]). Suggest(`&$x`) }
A regra localiza todos for-range
loops de for-range
que ambas as variáveis iteráveis são usadas (este é o caso que leva à cópia). A expressão iterável $x
deve ser addressable
e deve ser maior que o limite selecionado em bytes.
Report()
define a mensagem a ser emitida para o usuário, e Suggest()
descreve um modelo de quickfix
que pode ser usado em seu editor via gopls (LSP), bem como interativamente se a ruleguard
chamada com o argumento ruleguard
(retornaremos a isso). At()
anexa o aviso e o quickfix
a uma parte específica do modelo. Precisamos disso para substituir $x
por &$x
, em vez de reescrever o loop inteiro.
Report()
e Suggest()
aceitam uma sequência na qual as expressões capturadas pelo modelo de Match()
podem ser interpoladas. A variável predefinida $$
significa "todo o fragmento capturado" (como $0
em expressões regulares).
Crie o arquivo rangecopy.go
:
package example
Agora podemos executar o ruleguard
:
$ ruleguard -rules rules.go -fix rangecopy.go rangecopy.go:12:20: builtins copy can be avoided with &builtins
Se depois analisarmos rangecopy.go
, veremos uma versão fixa, porque o ruleguard
foi chamado com o parâmetro ruleguard
.
As regras mais simples podem ser depuradas sem criar um arquivo gorules
:
$ ruleguard -c 1 -e 'm.Match(`return -1`)' rangecopy.go rangecopy.go:17:2: return -1 16 } 17 return -1 18 }
Graças ao uso de go/analysis/singlechecker
, temos a opção -c
, que nos permite exibir as linhas de contexto especificadas junto com o próprio aviso. Controlar esse parâmetro é um pouco contra-intuitivo: o valor padrão é -c=-1
, que significa "sem contexto", e -c=0
produzirá uma linha de contexto (a indicada pelo diagnóstico).
Aqui estão algumas gorules
mais interessantes dos gorules
:
- Modelos de tipo que permitem especificar os tipos esperados. Por exemplo, o
map[$t]$t
expressão map[$t]$t
descreve todos os mapas com o tipo de valor correspondente ao tipo de chave e *[$len]$elem
captura todos os ponteiros para matrizes. - Pode haver várias regras dentro de uma função,
e as próprias funções devem ser chamadas de grupos de regras . - As regras no grupo são aplicadas uma após a outra, na ordem em que são definidas. A primeira regra acionada cancela a comparação com as regras restantes. Isso é importante não apenas para otimização, mas também para regras especializadas para casos específicos. Um exemplo em que isso é útil é a regra de reescrever
$x=$x+$y
para $x+=$y
, para o caso com $y=1
você deseja oferecer $x++
, não $x+=1
.
Mais informações sobre o DSL usado podem ser encontradas em docs/gorules.md
.
Mais exemplos
package gorules import "github.com/quasilyte/go-ruleguard/dsl/fluent" func exampleGroup(m fluent.Matcher) {
Se não houver uma chamada Report()
para a regra, a mensagem de Suggest()
será usada. Isso permite, em alguns casos, evitar duplicação.
Filtros de tipo e subexpressões podem verificar várias propriedades. Por exemplo, as propriedades Pure
e Const
são úteis:
Var.Pure
significa que a expressão não tem efeitos colaterais.Var.Const
significa que a expressão pode ser usada em um contexto constante (por exemplo, a dimensão de uma matriz).
Para nomes package-qualified
para package-qualified
nas condições Where()
, você precisa usar o método Import()
. Por conveniência, todos os pacotes padrão foram importados para você; portanto, no exemplo acima, não precisamos fazer importações adicionais.
ações de correção rápida go/analysis
O suporte para o quickfix
por go/analysis
para nós.
No modelo de go/analysis
, o analisador gera diagnósticos e fatos . Os diagnósticos são enviados aos usuários e os fatos devem ser usados por outros analisadores.
O diagnóstico pode ter um conjunto de correções sugeridas , cada uma descrevendo como alterar os códigos-fonte no intervalo especificado para corrigir o problema encontrado pelo diagnóstico.
A descrição oficial está disponível em go/analysis/doc/suggested_fixes.md
.
Conclusão

Experimente a ruleguard
de ruleguard
em seus projetos e, se encontrar um erro ou desejar solicitar um novo recurso, abra um problema .
Se você ainda acha difícil criar um aplicativo de ruleguard
, aqui estão alguns exemplos:
- Implemente seu próprio diagnóstico para o Go.
- Atualize ou refatore automaticamente o código com
-fix
. - Coleta de estatísticas de código com o processamento
-json
do resultado do -json
.
Planos de desenvolvimento para ruleguard
de ruleguard
em um futuro próximo:
Links e recursos úteis