Pular para o conteúdo

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

Shell
# 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: 0x1F se torna 0x1f, 1_000 se torna 1000, sequências de escape como "\x41" são escritas como o caractere ("A"), e heredocs flexíveis perdem a indentação.
PHP
// 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:

PHP
// 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

PHP
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 absoluto
  • originalContents: o conteúdo do arquivo antes da reescrita
  • rewrittenContents: o conteúdo do arquivo após a reescrita
  • replacementCount: número de substituições estruturais
  • changed(): auxiliar que retorna true quando originalContents !== 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).