Saltar para o conteúdo

Refatorar código de forma estrutural

Este guia mostra-lhe como conseguir que um agente faça uma alteração mecânica e ampla — um codemod, uma migração em massa de uma API, um padrão que quer ver desaparecer — usando pesquisa e reescrita estruturais, com a alteração preparada para revisão antes de algo ser escrito.

O ast_grep faz corresponder código pela forma. O ast_edit reescreve-o. O resolve confirma ou descarta.

As ferramentas estruturais abrangem 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. Precisam de ter as gramáticas do tree-sitter preparadas, o que acontece sempre numa build distribuída — uma gramática em falta impede o servidor, em vez de degradar a pesquisa.

Nunca reescreva algo que não tenha observado. Peça ao agente para pesquisar:

Encontre todos os locais onde chamamos dispose() a um nullable que acabámos de verificar contra null.

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

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

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

A que importa: um $NAME repetido tem de capturar o mesmo texto.

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

encontra uma guarda que descarta aquilo que testou. Não encontra if (a != null) b.dispose() — que é normalmente o erro que estava à procura. Nenhuma pesquisa textual consegue exprimir isso.

Duas coisas a experimentar, por ordem:

  1. Torne-o mais pequeno. 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 pretendia.
  2. Indique a linguagem. O language: só é inferido quando o path indica um único ficheiro.

Um padrão que nenhum contexto de análise aceita é reportado como não analisável — a ferramenta di-lo, em vez de reportar “sem correspondências” para algo que nunca estava a ser lido tal como 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.

Ainda não está nada no disco. Leia a lista, verifique alguns dos locais que a pesquisa reportou e, em seguida:

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

ou

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

A razão de fazer isto estruturalmente é que é amplo. As alterações amplas são exatamente onde uma reescrita com bom aspeto quebra um tipo. Peça diagnósticos depois de confirmar:

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

file: "*" executa o próprio verificador do projeto (dart analyze, tsc --noEmit, cargo check, go build) em vez de uma consulta ficheiro a ficheiro — a forma certa de pergunta depois de uma alteração que tocou em quarenta ficheiros.

Digamos que log(msg, level) vai passar a ser log(level: level, message: msg).

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

Observe as ocorrências. Se algumas delas forem um log de dois argumentos diferente, restrinja o padrão limitando o path ao diretório onde está o que pretende, em vez de tentar exprimir 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 a seguir lsp(action: "diagnostics", file: "*").

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