Treci la conținut

Rescriere AST

Rescrierea AST înseamnă căutare AST cu un șablon de înlocuire. Greph folosește același mecanism de potrivire ca căutarea AST, apoi inserează șablonul rescris în sursă la intervalele de octeți potrivite. Spațiile albe din jur, indentarea și comentariile sunt păstrate; codul potrivit este reimprimat (consultați În interiorul unei potriviri).

Exemple rapide

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

Șabloane de înlocuire

Șablonul de înlocuire folosește aceeași sintaxă a metavariabilelor ca tiparul de căutare. Nodurile capturate sunt inserate după nume:

În tipar În șablon Efect
$X $X Inserează nodul capturat
$$$ARGS $$$ARGS Inserează secvența de noduri capturată
$NAME (identificator) $NAME Inserează identificatorul capturat

Metavariabilele care apar în tipar, dar nu și în șablon, sunt eliminate din codul rescris. O metavariabilă care apare în șablon, dar nu și în tipar, nu este respinsă deocamdată: este afișată literal, așa că -r '$o->$m($$$A, $EXTRA)' scrie o variabilă $EXTRA în cod (#4). Verificați mai întâi șabloanele cu --dry-run.

Moduri

Opțiune Comportament
(niciuna) Aplică rescrierea tuturor fișierelor potrivite
--dry-run Afișează în stdout conținutul rescris al fișierului, fără să-l scrie
--interactive Solicită confirmarea pentru Rewrite <file>? [y/N] înainte de a scrie fiecare fișier

În mod implicit, se aplică „scrie tot ce se schimbă”. Folosiți mai întâi --dry-run când ajustați un tipar, apoi eliminați opțiunea când sunteți mulțumit de diferențe.

Păstrarea formatării

Greph nu reformatează fișierul. Mecanismul de potrivire înregistrează intervalul de octeți al fiecărei potriviri din sursa originală, iar rescrierea inserează șablonul rezultat direct în aceste intervale. Codul neatins rămâne exact la fel, inclusiv:

  • Indentarea și spațiile albe de la sfârșitul liniilor
  • Comentariile inline și cele autonome
  • Dispunerea liniilor goale
  • Stilul ghilimelelor și concatenarea șirurilor
  • Stilul etichetelor de deschidere/închidere PHP

Acesta este un compromis intenționat față de reformatarea completă: Greph nu va „aranja” codul pe care nu l-a potrivit, dar nici nu va introduce diferențe inutile în linii fără legătură.

În interiorul unei potriviri

Intervalul potrivit este înlocuit de șablonul redat prin imprimanta elegantă a PHP-Parser, iar nodurile capturate sunt reimprimate, nu copiate ca text sursă. Deocamdată, asta înseamnă (#2):

  • Un comentariu care nu este atașat unui nod următor din potrivire este eliminat, de exemplu un comentariu la sfârșitul unui tablou sau unul dintr-un tablou gol.
  • Comentariile atașate nodurilor capturate sunt păstrate, dar mutate pe propria linie; o expresie de o singură linie poate ajunge să ocupe mai multe linii.
  • Sfârșiturile de linie din potrivire nu sunt păstrate: un array(...) pe mai multe linii, fără astfel de comentarii, devine un [...] pe o singură linie.
  • Forma literalilor este normalizată: 0x1F devine 0x1f, 1_000 devine 1000, secvențele de escape precum "\x41" sunt scrise ca acel caracter ("A"), iar heredoc-urile flexibile își pierd indentarea.
PHP
// before
$d = array(
    "a" => 1,  // trailing comment
);

// after -p 'array($$$ITEMS)' -r '[$$$ITEMS]'
$d = ["a" => 1];

Revizuiți rezultatul --dry-run pentru potriviri care conțin comentarii sau literali formatați manual.

Potriviri pe mai multe linii și imbricate

Tiparele pe mai multe linii funcționează la fel ca cele pe o singură linie. Înlocuirea este redată de imprimanta elegantă cu metavariabilele substituite, așa că dispunerea codului înlocuit este stabilită de imprimantă, nu de potrivirea originală.

Greph rescrie fiecare fișier dintr-o singură trecere și nu aplică niciodată două potriviri suprapuse. Parcurge potrivirile de la sfârșitul fișierului spre început și omite orice potrivire care se suprapune cu una deja rescrisă. Pentru potriviri imbricate care încep la poziții diferite, rescrie potrivirea interioară și o lasă neschimbată pe cea exterioară:

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]];

Când două potriviri încep la aceeași poziție (de exemplu, un lanț de metode potrivit de $obj->$method($$$ARGS)), este rescrisă potrivirea exterioară. Când un tipar se poate imbrica, rulați rescrierea din nou până când --dry-run nu mai afișează nimic (#3). ast-grep rescrie potrivirea exterioară dintre două potriviri imbricate, așa că vendor/bin/sg și sg din amonte produc aici rezultate diferite.

Utilizare programatică

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() nu scrie fișiere de unul singur. CLI este responsabil de aplicarea modificărilor (sau de omiterea lor în cazurile --dry-run și --interactive refuzate). Astfel, fațada poate fi apelată în siguranță de agenți automatizați care vor să inspecteze diferențele înainte de a le aplica.

RewriteResult expune:

  • file: cale absolută
  • originalContents: conținutul fișierului înainte de rescriere
  • rewrittenContents: conținutul fișierului după rescriere
  • replacementCount: numărul de înlocuiri structurale
  • changed(): funcție ajutătoare care returnează true când originalContents !== rewrittenContents

Când să folosiți rescrierea

Rescrierea este instrumentul potrivit când:

  • Transformarea este locală și structurală (nu restructurarea întregului fișier).
  • Doriți o refactorizare deterministă și repetabilă, pe care să o puteți relua până când nu se mai schimbă nimic.
  • Doriți să previzualizați modificarea înainte de a o aplica.
  • Doriți să integrați refactorizarea într-un script sau într-o sarcină CI fără a invoca un binar extern.

Rescrierea nu este instrumentul potrivit când:

  • Transformarea depinde de informații despre tipuri pe care PHP-Parser nu le poate determina dintr-un singur fișier.
  • Transformarea trebuie să adauge sau să elimine importuri dincolo de limitele fișierului.
  • Doriți un formatter, nu o refactorizare (folosiți PHP-CS-Fixer).