
# AST:n uudelleenkirjoitus

AST:n uudelleenkirjoitus on AST-haku, jossa on korvaava malli. Greph käyttää samaa vastaavuuden etsintää kuin [AST-haku](https://greph.inline0.dev/fi/docs/modes/ast) ja sijoittaa sitten uudelleenkirjoitetun mallin lähdekoodiin osuvien tavuvälien kohdalle. Ympäröivät välilyönnit, sisennykset ja kommentit säilyvät; osuva koodi tulostetaan uudelleen (katso [Osumien sisällä](#inside-a-match)).

## Esimerkkejä

```bash
# 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
```

## Korvaavat mallit

Korvaava malli käyttää samaa metamuuttujasyntaksia kuin hakukuvio. Kaapatut solmut sijoitetaan takaisin nimen perusteella:

| Kuviossa | Mallissa | Vaikutus |
|---|---|---|
| `$X` | `$X` | Lisää kaapattu solmu |
| `$$$ARGS` | `$$$ARGS` | Lisää kaapattu solmujono |
| `$NAME` (tunniste) | `$NAME` | Lisää kaapattu tunniste |

Kuviossa esiintyvät mutta mallista puuttuvat metamuuttujat jätetään pois uudelleenkirjoitetusta koodista. Nykyisin mallissa esiintyvää mutta kuviosta puuttuvaa metamuuttujaa ei hylätä: se tulostetaan sellaisenaan, joten `-r '$o->$m($$$A, $EXTRA)'` kirjoittaa koodiin `$EXTRA`-muuttujan ([#4](https://github.com/inline0/greph/issues/4)). Tarkista mallit ensin komennolla `--dry-run`.

## Tilat

| Valitsin | Toiminta |
|---|---|
| (ei mitään) | Käytä uudelleenkirjoitusta kaikkiin osuman sisältäviin tiedostoihin |
| `--dry-run` | Tulosta uudelleenkirjoitetun tiedoston sisältö stdoutiin, älä kirjoita sitä tiedostoon |
| `--interactive` | Pyydä `Rewrite <file>? [y/N]` ennen kunkin tiedoston kirjoittamista |

Oletusarvoisesti "kirjoitetaan kaikki, mikä muuttuu". Käytä ensin valitsinta `--dry-run`, kun kokeilet kuviota, ja poista valitsin, kun olet tyytyväinen muutoksiin.

## Muotoilun säilyttäminen

Greph ei muotoile tiedostoa uudelleen. Vastaavuuden etsintä tallentaa kunkin osuman tavuvälin alkuperäisestä lähdekoodista, ja uudelleenkirjoitin sijoittaa tulostetun mallin suoraan näihin väleihin. Koskematon koodi säilyy täsmälleen ennallaan, mukaan lukien:

- Sisennys ja rivin lopun välilyönnit
- Sisäiset ja omilla riveillään olevat kommentit
- Tyhjien rivien asettelu
- Merkkijonojen lainausmerkkityyli ja yhdistäminen
- PHP-avaus- ja sulkutunnisteen tyyli

Tämä on harkittu kompromissi täyden uudelleenmuotoilun sijaan: Greph ei siisti koodia, johon ei osunut, mutta ei myöskään tuota turhia muutoksia asiaan liittymättömille riveille.

### Osuman sisällä

Osuva väli korvataan mallilla, joka tulostetaan uudelleen PHP-Parsern tulostimella. Kaapatut solmut tulostetaan uudelleen sen sijaan, että ne kopioitaisiin lähdetekstistä. Nykyisin tämä tarkoittaa seuraavaa ([#2](https://github.com/inline0/greph/issues/2)):

- Kommentti, jota ei ole liitetty osuman sisällä seuraavaan solmuun, poistetaan. Tällaisia ovat esimerkiksi taulukon viimeisen alkion jälkeinen kommentti tai muuten tyhjän taulukon kommentti.
- Kaapattuihin solmuihin liitetyt kommentit säilyvät, mutta siirtyvät omalle rivilleen. Tämä voi muuttaa yksirivisen lausekkeen moniriviseksi.
- Osuman sisäiset rivinvaihdot eivät säily: monirivisestä `array(...)`-rakenteesta tulee yksirivinen `[...]`-rakenne, jos siinä ei ole tällaisia kommentteja.
- Literaalien kirjoitusasu normalisoidaan: `0x1F` muuttuu muotoon `0x1f`, `1_000` muotoon `1000`, escape-sekvenssit, kuten `"\x41"`, kirjoitetaan merkkinä (`"A"`), ja joustavista heredoc-rakenteista poistetaan sisennys.

```php
// before
$d = array(
    "a" => 1,  // trailing comment
);

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

Tarkista `--dry-run`-tuloste osumien varalta, jos niissä on kommentteja tai käsin muotoiltuja literaaleja.

## Moniriviset ja sisäkkäiset osumat

Moniriviset kuviot toimivat samoin kuin yksiriviset. Korvaava koodi tulostetaan uudelleen tulostimella, johon metamuuttujat on sijoitettu. Korvatun koodin asettelu määräytyy siis tulostimen eikä alkuperäisen osuman mukaan.

Greph kirjoittaa jokaisen tiedoston uudelleen yhdellä kertaa eikä koskaan käytä kahta päällekkäistä osumaa. Se käy osumat läpi tiedoston lopusta alkuun ja ohittaa osumat, jotka menevät päällekkäin jo uudelleenkirjoitetun osuman kanssa. Jos sisäkkäiset osumat alkavat eri kohdista, sisempi osuma kirjoitetaan uudelleen ja ulompi jätetään ennalleen:

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

Jos kaksi osumaa alkaa samasta kohdasta (esimerkiksi menetelmäketju, johon `$obj->$method($$$ARGS)` osuu), ulompi osuma kirjoitetaan uudelleen. Jos kuvio voi sisältyä itseensä, suorita uudelleenkirjoitus uudelleen, kunnes komento `--dry-run` ei tulosta mitään ([#3](https://github.com/inline0/greph/issues/3)). ast-grep kirjoittaa kahdesta sisäkkäisestä osumasta ulomman uudelleen, joten `vendor/bin/sg` ja ylävirran `sg` tuottavat tässä eri tulokset.

## Käyttö ohjelmallisesti

```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()` ei kirjoita tiedostoja itse. Muutosten tekemisestä (tai niiden ohittamisesta `--dry-run`- ja hylättyjen `--interactive`-tapausten yhteydessä) vastaa CLI. Näin facadea voivat turvallisesti kutsua automatisoidut agentit, jotka haluavat tarkistaa muutokset ennen niiden käyttöönottoa.

`RewriteResult` tuo näkyviin:

- `file`: absoluuttinen polku
- `originalContents`: tiedoston sisältö ennen uudelleenkirjoitusta
- `rewrittenContents`: tiedoston sisältö uudelleenkirjoituksen jälkeen
- `replacementCount`: rakenteellisten korvausten määrä
- `changed()`: apufunktio, joka palauttaa arvon `true`, kun `originalContents !== rewrittenContents`

## Milloin uudelleenkirjoitusta kannattaa käyttää

Uudelleenkirjoitus on oikea työkalu, kun:

- Muunnos on paikallinen ja rakenteellinen (ei koko tiedoston uudelleenjärjestelyä).
- Haluat deterministisen ja toistettavan refaktoroinnin, jonka voit ajaa uudelleen, kunnes muutoksia ei enää synny.
- Haluat esikatsella muutoksen ennen sen käyttöönottoa.
- Haluat integroida refaktoroinnin skriptiin tai CI-työhön suorittamatta ulkoista binääritiedostoa.

Uudelleenkirjoitus ei ole oikea työkalu, kun:

- Muunnos riippuu tyyppitiedoista, joita PHP-Parser ei pysty ratkaisemaan yhdestä tiedostosta.
- Muunnos edellyttää tuontien lisäämistä tai poistamista tiedostorajojen yli.
- Haluat muotoilijan refaktoroinnin sijaan (käytä PHP-CS-Fixer).
