Pular para o conteúdo

Refatorar código de forma estrutural

Este guia mostra como fazer um agente realizar uma alteração mecânica ampla — um codemod, uma migração de API em massa, um padrão que você quer eliminar — usando busca e reescrita estruturais, com a alteração preparada para revisão antes que qualquer coisa seja escrita.

O ast_grep casa código por forma. O ast_edit o reescreve. O resolve confirma ou descarta.

As ferramentas estruturais cobrem as linguagens que o grafo de código analisa: Dart, JavaScript, TypeScript, TSX, PHP, Python, Rust, Zig, C, C++, Go, Java, Ruby, C#, Swift, Kotlin, R, Assembly, MATLAB e Ada. Elas precisam das gramáticas do tree-sitter instaladas, o que sempre acontece em uma build distribuída — uma gramática ausente derruba o servidor em vez de degradar a busca.

Nunca reescreva algo que você não examinou. Peça ao agente para pesquisar:

Encontre todos os lugares em que chamamos dispose() em um nullable que acabamos de verificar quanto a null.

ast_grep(pattern: "if ($X != null) $X.dispose()", language: "dart")

A saída é file:line mais o código-fonte correspondente de cada local.

Token Corresponde a
$NAME Qualquer nó único, capturado
$$$NAME Qualquer sequência de irmãos — argumentos, instruções
$_ Qualquer coisa, capturada sem nome

O que importa: um $NAME repetido precisa capturar o mesmo texto.

if ($X != null) $X.dispose()

encontra uma guarda que descarta o que ela testou. Não encontra if (a != null) b.dispose() — que normalmente é o bug que você procurava. Nenhuma busca textual consegue expressar isso.

Duas coisas para tentar, nesta ordem:

  1. Deixe-o menor. foo($X) antes de if (a) { foo($X); }. Um padrão que não é código válido por si só muitas vezes não é a construção que você quis dizer.
  2. Nomeie a linguagem. language: só é inferido quando path nomeia um único arquivo.

Um padrão que nenhum contexto de análise aceita é reportado como não analisável — a ferramenta diz isso em vez de reportar “nenhuma correspondência” para algo que nunca estava sendo lido do jeito que parece.

ast_edit(
pattern: "if ($X != null) $X.dispose()",
rewrite: "$X?.dispose()",
language: "dart",
)

O resultado é uma proposta:

(proposed) ast_edit: if ($X != null) $X.dispose() → $X?.dispose()
12 replacements in 4 files:
lib/features/a/x.dart (3)
lib/features/b/y.dart (5)
…
Nothing has been written. Call resolve with edit_id "edit_1" and action
"accept" to apply it, or "discard" to drop it.

Nada está no disco ainda. Leia a lista, confira alguns dos locais que a busca reportou e então:

resolve(edit_id: "edit_1", action: "accept")

ou

resolve(edit_id: "edit_1", action: "discard")

O ponto de fazer isso de forma estrutural é que a mudança é ampla. Alterações amplas são exatamente onde uma reescrita de aparência plausível quebra um tipo. Peça diagnósticos depois de confirmar:

lsp(action: "diagnostics", file: "*")

file: "*" executa o verificador do próprio projeto (dart analyze, tsc --noEmit, cargo check, go build) em vez de uma consulta por arquivo — o formato certo de pergunta depois de uma alteração que tocou quarenta arquivos.

Digamos que log(msg, level) esteja virando log(level: level, message: msg).

ast_grep(pattern: "log($MSG, $LEVEL)", language: "dart")

Examine os resultados. Se alguns deles forem um log de dois argumentos diferente, restrinja o padrão limitando path ao diretório que contém o que você quer, em vez de tentar expressar a diferença no padrão.

ast_edit(
pattern: "log($MSG, $LEVEL)",
rewrite: "log(level: $LEVEL, message: $MSG)",
path: "lib/features/telemetry",
language: "dart",
)

Depois resolve, e então lsp(action: "diagnostics", file: "*").

Situação Ferramenta melhor
Renomear um símbolo lsp_rename — ele segue referências, não formas
Encontrar uma string, um comentário ou um nome de arquivo search (conteúdo) / search_files (nomes)
“O que quebra se eu mudar isto?” O grafo de código
Uma linguagem que as gramáticas não cobrem search mais edit