O que é um linter? Construindo um analisador de código do zero em Rust
O editor sublinha um trecho que compila e talvez até funcione. Como ele percebeu o problema sem executar uma única instrução do programa?
se ativo == verdadeiro {
mostre(total)
}ativo == verdadeiro
──────┬──────────
simplifique para ativoVOCABULÁRIO VISUAL
As peças que entram em cena neste capítulo.
Um linter é uma ferramenta que lê código-fonte e procura padrões suspeitos, redundantes, confusos ou contrários às regras de um projeto. Ele não precisa iniciar o programa para descobrir que uma variável nunca foi usada ou que uma comparação booleana repete uma informação já conhecida.
Isso é análise estática: obter evidências a partir do texto e das estruturas derivadas dele. “Estática” não significa simples. O Crivo precisará reconhecer tokens, montar a árvore sintática, acompanhar declarações e usos e, nos próximos capítulos, estudar caminhos de controle.
PROGRAMA PULSO O CRIVO
var ativo = verdadeiro; lê a fonte
se ativo == verdadeiro { cria tokens e AST
mostre("ok"); percorre relações
} produz diagnósticos
nunca executa mostre| Ferramenta | Pergunta principal | Executa o programa? |
|---|---|---|
| Parser | a forma pertence à gramática? | não |
| Compilador | como traduzir o programa? | não necessariamente |
| Linter | há evidências de problema ou melhoria? | não |
| Teste | o comportamento observado corresponde ao esperado? | sim |
| Debugger | o que acontece durante uma execução? | sim |
O nome vem da ideia de peneirar: a fonte Pulso atravessa várias camadas e cada regra retém uma evidência específica. O Crivo é escrito em Rust, recebe um arquivo .pulso, produz diagnósticos com linha e coluna e retorna um código de saída útil para terminal e automação.
arquivo.pulso
↓ Lexer::scan
Vec<Token + Span>
↓ Parser::program
Vec<Stmt> — a AST
↓ Analyzer::block
Vec<Diagnostic>
↓ renderer
arquivo:linha:coluna aviso[regra]: mensagemNeste primeiro estado, o Crivo 0.1 implementa três regras: variável não usada, expressão sem efeito e comparação booleana redundante. Não faremos buscas com contains. Cada regra trabalha sobre nós da AST e posições produzidas pelo frontend.
O lexer caminha pelos bytes UTF-8 sem confundir byte com coluna. Para cada token, preservamos o intervalo no arquivo e a posição percebida pelo leitor:
pub struct Span {
pub start: usize,
pub end: usize,
pub line: usize,
pub column: usize,
}
pub struct Token {
pub kind: Kind,
pub span: Span,
}start é inclusivo e end é exclusivo. Essa convenção permite recortar &source[start..end] e, no capítulo 3, aplicar uma correção sem procurar novamente o texto. O lexer reconhece palavras da Pulso, identificadores, literais, delimitadores e operadores. Um caractere desconhecido produz erro de sintaxe com posição; não é ignorado silenciosamente.
O parser consome esses tokens e cria Stmt::Var, Stmt::If, Stmt::Expr e expressões como Expr::Name e Expr::Binary. A AST não guarda apenas “há uma variável”: ela liga o nome ao span exato que originou o nó.
$ unzip crivo-0.1.2-artigo-01.zip
$ cd crivo-0.1
$ cargo test
$ cargo run -- check exemplos/inicio.pulso
exemplos/inicio.pulso:2:1 aviso[unused-variable]:
variável nunca_usada declarada e nunca usada
exemplos/inicio.pulso:3:1 aviso[no-effect-expression]:
expressão sem efeito
exemplos/inicio.pulso:4:4 aviso[redundant-boolean]:
comparação booleana redundanteO primeiro -- separa argumentos do Cargo dos argumentos do nosso binário. Quando instalarmos o executável, a forma curta será crivo check arquivo.pulso. O download não presume essa instalação.
O código de saída é 0 sem diagnósticos, 1 quando regras encontram problemas e 2 para uso incorreto, arquivo ilegível ou sintaxe inválida. Assim, “o processo terminou” não é confundido com “o código passou no Crivo”.
Considere este programa Pulso:
var ativo = verdadeiro;
se ativo == verdadeiro {
2 + 2;
mostre("pronto");
}
var tentativas = 3;A sintaxe está correta. O compilador consegue gerar bytecode ou código nativo. Durante a execução, o texto aparece. Mesmo assim, há três sinais úteis: comparar um booleano com verdadeiro é redundante; 2 + 2 calcula um valor descartado; tentativas nunca é lida.
Já ensinamos o computador a entender, executar e compilar a Pulso. Agora vamos ensiná-lo a examinar o código e apontar problemas antes que o programa seja executado.
| Categoria | Exemplo | Quem costuma detectar |
|---|---|---|
| Erro de sintaxe | var x = ; | parser |
| Erro de compilação | nome inexistente ou tipo incompatível | compilador |
| Erro de execução | divisão por zero num caminho percorrido | runtime |
| Aviso | variável nunca lida | compilador ou linter |
| Qualidade | condição redundante | linter |
A fronteira não é universal: rustc, Clippy, ESLint e Ruff fazem escolhas diferentes. O nosso contrato é simples: o compilador garante que a Pulso possa ser traduzida; o Crivo procura padrões suspeitos e explica por que merecem atenção.
Nos artigos da linguagem Pulso e da VM Cadência, caracteres viraram tokens e tokens viraram uma árvore sintática abstrata — a AST. Não vamos reconstruir essa entrada.
FONTE PULSO
│ lexer + parser compartilhados
▼
AST
┌──┼───────────────┐
▼ ▼ ▼
INTERPRETADOR COMPILADOR CRIVO
executa nós transforma nós examina e enriquece nós
produz valores produz bytecode/IR produz diagnósticosO interpretador precisa saber o que fazer agora. O compilador pergunta como representar isto em outra máquina. O linter pergunta o que este trecho revela quando relacionado ao restante do programa. Nenhum diagnóstico exige chamar a VM ou o runtime.
Uma AST mínima poderia guardar apenas Binary(ativo, Equal, verdadeiro). Isso é suficiente para executar, mas insuficiente para sublinhar exatamente == verdadeiro. Cada nó relevante passa a carregar um span: o intervalo semiaberto [início, fim) em bytes UTF-8.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Span {
pub start: usize,
pub end: usize,
}
pub struct Spanned<T> {
pub node: T,
pub span: Span,
}
se ativo == verdadeiro │ └───────┬───┘ │ bytes 9..21 └ ativo: bytes 3..8 SourceFile converte offset → linha e coluna Span preserva offset → destaque exato
Bytes, caracteres e colunas não são sinônimos quando há acentos. O SourceFile guarda os inícios de linha e usa o texto original para converter o intervalo somente na hora de renderizar.
pub trait Rule {
fn name(&self) -> &'static str;
fn check_expr(&self, _expr: &Spanned<Expr>, _ctx: &mut Context) {}
fn check_stmt(&self, _stmt: &Spanned<Stmt>, _ctx: &mut Context) {}
}
pub struct Context<'a> {
pub source: &'a SourceFile,
pub diagnostics: Vec<Diagnostic>,
}
O contrato não promete que toda regra visite todo nó. Métodos padrão deixam cada implementação interessada apenas no que precisa. Um registro de regras recebe objetos independentes; o analisador controla o percurso uma única vez.
Esse percurso organizado é o visitor. Ele entra numa declaração, visita sua expressão, desce para operandos e blocos e volta. Centralizar a caminhada evita que cada regra esqueça um ramo da AST.
fn visit_expr(expr: &Spanned<Expr>, rules: &[Box<dyn Rule>], ctx: &mut Context) {
for rule in rules { rule.check_expr(expr, ctx); }
match &expr.node {
Expr::Binary { left, right, .. } => {
visit_expr(left, rules, ctx);
visit_expr(right, rules, ctx);
}
Expr::Call { callee, args } => { /* visita todos */ }
_ => {}
}
}
Uma caminhada compartilhada é pequena e previsível. No Crivo 0.2 algumas análises ganharão fases próprias, porque resolver nomes e fluxo exige estado que não cabe numa visita local.
aviso[boolean-comparison]: comparação booleana redundante
┌─ exemplos/inicio.pulso:2:10
│
2 │ se ativo == verdadeiro {
│ ^^^^^^^^^^^^^ use apenas `ativo`
│
= a comparação produz o mesmo valor que a variável
O modelo guarda rule, severity, message, arquivo, span, rótulo e nota. A renderização calcula linha, coluna e trecho. Separar os dados da aparência permitirá produzir JSON no terceiro artigo sem reexecutar análise.
Condição booleana redundante
Quando uma expressão x == verdadeiro aparece, a regra sugere x. Para x == falso, a forma equivalente seria !x, mas a versão 0.1 apenas diagnostica: ainda não temos um aplicador seguro.
Expressão sem efeito
Literais e operações aritméticas puras, usadas como comando, calculam e descartam. Chamadas não entram nessa lista porque podem imprimir, gravar ou alterar estado; sem um sistema de efeitos, o Crivo escolhe ser conservador.
Variável declarada e nunca usada
Ao entrar num bloco, registramos declarações; ao encontrar uma leitura, marcamos o nome. Ao sair, emitimos os não usados. Esta versão lida com um bloco simples. Escopos aninhados, parâmetros e sombreamento ficam para o 0.2 — e a limitação aparece claramente no diagnóstico e no README.
| Regra | Detecta | Não presume |
|---|---|---|
| boolean-comparison | x == verdadeiro | que toda comparação pode ser removida |
| no-effect-expression | 2 + 2; | que chamadas são puras |
| unused-variable | declaração nunca lida | escopos complexos nesta versão |
$ cargo run -- check exemplos/inicio.pulso
3 diagnósticos · 0 erros · 3 avisos
$ cargo test
test booleano_redundante ... ok
test expressao_sem_efeito ... ok
test variavel_nao_usada ... ok
test preserva_linha_coluna_utf8 ... ok
Os testes criam uma fonte, passam pelo frontend da Pulso, executam o analisador e comparam os diagnósticos. Não há VM no caminho. Um teste instala um contador no executor fictício e comprova que permanece em zero.
O Crivo 0.1 já possui fonte com posições, AST reaproveitada, visitor, regras independentes, diagnósticos acionáveis, CLI e testes. Mas ainda enxerga cada árvore localmente. Para perceber uma variável escondida, um retorno ausente ou uma atribuição perdida, ele precisará compreender relações.
O desenho foi guiado pela documentação oficial do Clippy sobre regras, pela especificação de diagnósticos do Language Server Protocol e pela arquitetura já construída para a Pulso. O Crivo não tenta cobrir a linguagem Rust, inferência industrial, macros ou compatibilidade de milhares de regras. Seu objetivo é deixar todo o mecanismo visível.
PROJETO COMPLETO · ESTADO EXATO DESTE CAPÍTULO
Baixe e execute o Crivo 0.1.
O pacote é independente, inclui o código-fonte em Rust, exemplos Pulso, testes e README. Ele contém somente o que foi construído até este artigo; as versões anteriores permanecem disponíveis em seus próprios endereços.