Reescrita AST
A reescrita AST é uma pesquisa AST com um modelo de substituição. Greph executa o mesmo comparador da pesquisa AST e, em seguida, insere o modelo reescrito no código-fonte nos intervalos de bytes correspondentes. Os espaços em branco, a indentação e os comentários ao redor são preservados; o próprio código correspondente é impresso novamente (consulte Dentro de uma correspondência).
Exemplos rápidos
# Convert array() literals to []
./vendor/bin/greph -p 'array($$$ITEMS)' -r '[$$$ITEMS]' --dry-run src
./vendor/bin/greph -p 'array($$$ITEMS)' -r '[$$$ITEMS]' src
# isset ternary -> null coalesce
./vendor/bin/greph -p 'isset($X) ? $X : $Y' -r '$X ?? $Y' --dry-run src
# Rename a method call
./vendor/bin/greph -p '$obj->oldName($$$ARGS)' -r '$obj->newName($$$ARGS)' src
# Confirm each file before writing
./vendor/bin/greph -p 'array($$$ITEMS)' -r '[$$$ITEMS]' --interactive src
Modelos de substituição
O modelo de substituição usa a mesma sintaxe de metavariáveis do padrão de pesquisa. Os nós capturados são inseridos novamente pelo nome:
| No padrão | No modelo | Efeito |
|---|---|---|
$X |
$X |
Insere o nó capturado |
$$$ARGS |
$$$ARGS |
Insere a sequência de nós capturada |
$NAME (identificador) |
$NAME |
Insere o identificador capturado |
As metavariáveis que aparecem no padrão, mas não no modelo, são removidas do código reescrito. Uma metavariável que aparece no modelo, mas não no padrão, não é rejeitada atualmente: ela é impressa literalmente, então -r '$o->$m($$$A, $EXTRA)' grava uma variável $EXTRA no código (#4). Verifique os modelos com --dry-run primeiro.
Modos
| Sinalizador | Comportamento |
|---|---|
| (nenhum) | Aplica a reescrita a todos os arquivos correspondentes |
--dry-run |
Imprime o conteúdo do arquivo reescrito na saída padrão, sem gravá-lo |
--interactive |
Solicita confirmação Rewrite <file>? [y/N] antes de gravar cada arquivo |
O padrão é “gravar tudo o que mudar”. Use --dry-run primeiro ao ajustar um padrão e remova o sinalizador quando estiver satisfeito com as diferenças.
Preservação da formatação
Greph não reformata o arquivo. O comparador registra o intervalo de bytes de cada correspondência no código-fonte original, e o reescritor insere o modelo renderizado diretamente nesses intervalos. O código não alterado permanece exatamente como estava, incluindo:
- Indentação e espaços em branco no fim da linha
- Comentários em linha e isolados
- Disposição das linhas em branco
- Estilo de aspas e concatenação de strings
- Estilo das tags de abertura/fechamento PHP
Essa é uma escolha deliberada em troca da reformatação completa: Greph não vai “arrumar” o código que não correspondeu, mas também não introduz diferenças ruidosas em linhas sem relação.
Dentro de uma correspondência
O próprio intervalo correspondente é substituído pelo modelo renderizado usando o impressor de código de PHP-Parser, que imprime novamente os nós capturados em vez de copiá-los como texto-fonte. Atualmente, isso significa (#2):
- Um comentário que não esteja associado a um nó seguinte dentro da correspondência é removido. Por exemplo, um comentário após o último item de array ou um comentário em um array vazio.
- Comentários associados a nós capturados são mantidos, mas passam para a própria linha, o que pode transformar uma expressão de uma linha em várias linhas.
- Quebras de linha dentro da correspondência não são mantidas: um
array(...)de várias linhas sem esses comentários se torna um[...]de uma linha. - A grafia de literais é normalizada:
0x1Fse torna0x1f,1_000se torna1000, sequências de escape como"\x41"são escritas como o caractere ("A"), e heredocs flexíveis perdem a indentação.
// before
$d = array(
"a" => 1, // trailing comment
);
// after -p 'array($$$ITEMS)' -r '[$$$ITEMS]'
$d = ["a" => 1];
Revise a saída de --dry-run para encontrar correspondências que contenham comentários ou literais formatados manualmente.
Correspondências multilinha e aninhadas
Padrões multilinha funcionam da mesma forma que os de uma linha. A substituição é renderizada pelo impressor de código com as metavariáveis inseridas, então a disposição do código substituído vem do impressor, não da correspondência original.
Greph reescreve cada arquivo em uma única passagem e nunca aplica duas correspondências sobrepostas. Percorre as correspondências de trás para a frente e ignora qualquer uma que se sobreponha a outra já reescrita. Para correspondências aninhadas que começam em posições diferentes, a interna é reescrita e a externa permanece como estava:
// before
$b = array(1, array(2));
// after one run of -p 'array($$$ITEMS)' -r '[$$$ITEMS]'
$b = array(1, [2]);
// after a second run
$b = [1, [2]];
Quando duas correspondências começam na mesma posição (por exemplo, uma cadeia de métodos correspondente a $obj->$method($$$ARGS)), a correspondência externa é reescrita. Quando um padrão pode ser aninhado, execute a reescrita novamente até que --dry-run não imprima nada (#3). ast-grep reescreve a correspondência externa de duas aninhadas, então vendor/bin/sg e o sg upstream produzem resultados diferentes aqui.
Uso programático
use Greph\Greph;
use Greph\Ast\AstSearchOptions;
$rewrites = Greph::rewriteAst(
'array($$$ITEMS)',
'[$$$ITEMS]',
'src',
new AstSearchOptions(jobs: 4),
);
foreach ($rewrites as $result) {
if (!$result->changed()) {
continue;
}
echo "{$result->file}: {$result->replacementCount} replacement(s)\n";
file_put_contents($result->file, $result->rewrittenContents);
}
Greph::rewriteAst() não grava arquivos por conta própria. O CLI é responsável por confirmar as alterações (ou ignorá-las em casos de --dry-run e --interactive recusado). Isso torna a fachada segura para chamadas de agentes automatizados que queiram inspecionar as diferenças antes de aplicá-las.
RewriteResult expõe:
file: caminho absolutooriginalContents: o conteúdo do arquivo antes da reescritarewrittenContents: o conteúdo do arquivo após a reescritareplacementCount: número de substituições estruturaischanged(): auxiliar que retornatruequandooriginalContents !== rewrittenContents
Quando usar rewrite
Rewrite é a ferramenta certa quando:
- A transformação é local e estrutural (não uma reestruturação do arquivo inteiro).
- Você quer uma refatoração determinística e repetível, que possa executar novamente até que nada mude.
- Você quer visualizar a alteração antes de aplicá-la.
- Você quer integrar a refatoração a um script ou trabalho de CI sem invocar um binário externo.
Rewrite não é a ferramenta certa quando:
- A transformação depende de informações de tipos que PHP-Parser não consegue resolver a partir de um único arquivo.
- A transformação precisa adicionar ou remover imports além dos limites do arquivo.
- Você quer um formatador em vez de uma refatoração (use PHP-CS-Fixer).