Przejdź do treści

Przepisywanie AST

Przepisywanie AST to wyszukiwanie AST z szablonem zastępczym. Greph używa tego samego mechanizmu dopasowywania co wyszukiwanie AST, a następnie wstawia przepisany szablon do źródła w dopasowanych zakresach bajtów. Otaczające białe znaki, wcięcia i komentarze są zachowywane; sam dopasowany kod jest drukowany ponownie (zob. Wewnątrz dopasowania).

Szybkie przykłady

Powłoka
# 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

Szablony zastępcze

Szablon zastępczy używa tej samej składni metazmiennych co wzorzec wyszukiwania. Przechwycone węzły są wstawiane z powrotem według nazwy:

We wzorcu W szablonie Efekt
$X $X Wstaw przechwycony węzeł
$$$ARGS $$$ARGS Wstaw przechwyconą sekwencję węzłów
$NAME (identyfikator) $NAME Wstaw przechwycony identyfikator

Metazmienne występujące we wzorcu, ale nie w szablonie, są pomijane w przepisanym kodzie. Metazmienna występująca w szablonie, ale nie we wzorcu, nie jest obecnie odrzucana: zostaje wypisana dosłownie, więc -r '$o->$m($$$A, $EXTRA)' zapisuje zmienną $EXTRA w kodzie (#4). Najpierw sprawdź szablony za pomocą --dry-run.

Tryby

Flaga Działanie
(brak) Zastosuj przepisywanie do każdego pasującego pliku
--dry-run Wypisz zawartość przepisanych plików na standardowe wyjście, ale niczego nie zapisuj
--interactive Poproś o potwierdzenie Rewrite <file>? [y/N] przed zapisaniem każdego pliku

Domyślnie „zapisuj wszystkie zmiany”. Najpierw użyj --dry-run podczas dopracowywania wzorca, a następnie usuń flagę, gdy uznasz różnice za odpowiednie.

Zachowanie formatowania

Greph nie formatuje ponownie pliku. Mechanizm dopasowywania zapisuje zakres bajtów każdego dopasowania w oryginalnym źródle, a mechanizm przepisujący wstawia wyrenderowany szablon bezpośrednio w tych zakresach. Nietknięty kod pozostaje dokładnie taki jak wcześniej, w tym:

  • Wcięcia i końcowe białe znaki
  • Komentarze w wierszu i samodzielne
  • Układ pustych wierszy
  • Styl cudzysłowów i konkatenacji w ciągach znaków
  • Styl znaczników otwierających i zamykających PHP

To celowy kompromis w porównaniu z pełnym formatowaniem: Greph nie „porządkuje” kodu, którego nie dopasowało, ale też nie wprowadza niepotrzebnych różnic w niezwiązanych wierszach.

Wewnątrz dopasowania

Sam dopasowany zakres zostaje zastąpiony szablonem przetworzonym przez drukarkę AST w PHP-Parser; przechwycone węzły są drukowane ponownie, a nie kopiowane jako tekst źródłowy. Obecnie oznacza to (#2):

  • Komentarz, który nie jest przypisany do kolejnego węzła wewnątrz dopasowania, zostaje usunięty, na przykład komentarz na końcu po ostatnim elemencie tablicy lub komentarz w pustej tablicy.
  • Komentarze przypisane do przechwyconych węzłów są zachowywane, ale przenoszone do osobnego wiersza, co może zamienić jednowierszowe wyrażenie w kilka wierszy.
  • Podziały wierszy wewnątrz dopasowania nie są zachowywane: wielowierszowe array(...) bez takich komentarzy staje się jednowierszowym [...].
  • Zapis literałów jest normalizowany: 0x1F staje się 0x1f, 1_000 staje się 1000, sekwencje ucieczki takie jak "\x41" są zapisywane jako znak ("A"), a elastyczne heredoki tracą wcięcia.
PHP
// before
$d = array(
    "a" => 1,  // trailing comment
);

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

Sprawdź wynik --dry-run pod kątem dopasowań zawierających komentarze lub ręcznie sformatowane literały.

Dopasowania wielowierszowe i zagnieżdżone

Wzorce wielowierszowe działają tak samo jak jednowierszowe. Zastępnik jest renderowany przez drukarkę AST po podstawieniu metazmiennych, więc układ zastępowanego kodu pochodzi z drukarki, a nie z oryginalnego dopasowania.

Greph przepisuje każdy plik w jednym przebiegu i nigdy nie stosuje dwóch nakładających się dopasowań. Przechodzi po dopasowaniach od końca pliku do początku i pomija te, które nakładają się na już przepisane. W przypadku zagnieżdżonych dopasowań zaczynających się w różnych przesunięciach przepisuje dopasowanie wewnętrzne, a zewnętrzne pozostawia bez zmian:

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

Gdy dwa dopasowania zaczynają się na tej samej pozycji (na przykład łańcuch metod dopasowany przez $obj->$method($$$ARGS)), przepisywane jest dopasowanie zewnętrzne. Jeśli wzorzec może być zagnieżdżony, uruchamiaj przepisywanie ponownie, aż --dry-run niczego nie wypisze (#3). ast-grep przepisuje zewnętrzne z dwóch zagnieżdżonych dopasowań, więc vendor/bin/sg i nadrzędny sg dają tu różne wyniki.

Użycie programowe

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() samodzielnie nie zapisuje plików. Za zatwierdzanie zmian (lub pomijanie ich w przypadkach --dry-run i odrzuconych --interactive) odpowiada CLI. Dzięki temu z fasady mogą bezpiecznie korzystać zautomatyzowani agenci, którzy chcą sprawdzić różnice przed zastosowaniem zmian.

RewriteResult udostępnia:

  • file: ścieżka bezwzględna
  • originalContents: zawartość pliku przed przepisaniem
  • rewrittenContents: zawartość pliku po przepisaniu
  • replacementCount: liczba zamian strukturalnych
  • changed(): funkcja pomocnicza zwracająca true, gdy originalContents !== rewrittenContents

Kiedy używać przepisywania

Przepisywanie sprawdzi się, gdy:

  • Transformacja jest lokalna i strukturalna (nie obejmuje przebudowy całego pliku).
  • Chcesz deterministycznego, powtarzalnego refaktoryzowania, które możesz ponawiać, aż nic się nie zmieni.
  • Chcesz podejrzeć zmianę przed jej zastosowaniem.
  • Chcesz zintegrować refaktoryzację ze skryptem lub zadaniem CI bez uruchamiania zewnętrznego pliku binarnego.

Przepisywanie nie sprawdzi się, gdy:

  • Transformacja zależy od informacji o typach, których PHP-Parser nie potrafi ustalić na podstawie pojedynczego pliku.
  • Transformacja wymaga dodania lub usunięcia importów poza granicami pliku.
  • Potrzebujesz formatowania, a nie refaktoryzacji (użyj PHP-CS-Fixer).