Nette Documentation Preview

syntax
PhpSyntax
*********

.[perex]
Bezztrátový syntaktický strom PHP bez jediné závislosti: každý token, každá mezera i každý komentář má své místo a vytištěný strom je původní soubor bajt po bajtu. Knihovna pro nástroje, které mají kód číst a měnit.

Roky jsem si myslel, že parser pro PHP je vyřešená věc: je tu nikic/php-parser, stojí na něm PHPStan i Rector, co víc chtít. Pak jsem psal skript, který měl v pár stech souborech přejmenovat jedno volání, a zjistil jsem, kolik práce dá **nepřepsat všechno ostatní**. Přesně na tohle je PhpSyntax.


Strom, který nic nezahodí
=========================

Abstraktní syntaktický strom zahazuje, co pro význam programu není podstatné: mezery, prázdné řádky, komentáře uvnitř výrazů, závorky navíc. Pro analýzu je to správně. Jakmile ale chcete kód **změnit a vrátit zpátky**, potřebujete i to, co AST zahodil, jinak přetisknete soubor podle svých pravidel místo podle autorových.

PhpSyntax staví **konkrétní** strom: každý token zdrojáku má v něm svůj slot (`IfNode` má `ifKeyword`, `openParen`, `cond`, `closeParen`, `body`), a bílé znaky a komentáře visí na tokenech jako trivia. Smlouva zní:

```php
Printer::print($parser->parse($code)) === $code
```

Platí bajt po bajtu pro cokoli, co PHP přijme: BOM, hashbang, CR i CRLF, uzavírací tagy, inline HTML, `__halt_compiler()`. Je to invariant, který drží testy nad korpusem, a je to zároveň nejjednodušší test, jaký si nad vlastním kódem uděláte: parsuj, vytiskni, porovnej.


Kdy který
=========

Obě knihovny mají své místo a je poctivé říct které.

**php-parser**, když potřebujete vědět, co kód dělá: typy, tok řízení, vyhodnocení konstantních výrazů, obrovský ekosystém nad ním. Také když potřebujete strom i z kódu se syntaktickou chybou: php-parser umí zotavení a vrátí částečný strom, PhpSyntax na chybě skončí výjimkou, protože jeho gramatika zotavení nemá.

**PhpSyntax**, když se chcete kódu dotknout a zbytek nechat: přejmenovat volání, doplnit argument, přesunout komentář, srovnat mezery, a dostat diff přesně tak velký jako změna. Formátovače, migrační skripty, generátory, které upravují existující soubory, nástroje na hromadné přepisy. Největší z nich je [DressCode |dresscode:], checker a fixer stylu kódu, pro který PhpSyntax původně vznikl.


Instalace
=========

```shell
composer require phpsyntax/phpsyntax
```

Knihovna vyžaduje PHP 8.4 nebo novější a rozšíření `tokenizer`. Jinak **nemá žádnou závislost**: `composer.json` žádá PHP a tokenizer a nic jiného, takže se nemá o co přetahovat s tím, co už v projektu je. Kód pro novější PHP přečte i na starším běhu, viz [Parsování |parsing#Novější syntaxe na starším PHP].


Na ukázku
=========

```php
use PhpSyntax\Nodes\Expression\FunctionCallNode;
use PhpSyntax\Nodes\NameNode;
use PhpSyntax\Parser\Parser;
use PhpSyntax\Printer;

$file = (new Parser)->parse(file_get_contents('Order.php'));

foreach ($file->find(FunctionCallNode::class) as $call) {
	if ($call->name instanceof NameNode && $call->name->text === 'sizeof') {
		$call->name->text = 'count';
	}
}

file_put_contents('Order.php', Printer::print($file));
```

Přejmenovalo se každé volání `sizeof`, a to je také jediné, co se v souboru změnilo. Kde se pak strom prochází, jak se mění a co ví o jménech, říkají další stránky: [Parsování a tisk |parsing], [Uzly a sloty |tree], [Trivia |trivia], [Procházení |traversal], [Úpravy |mutation], [Analýzy |analyses] a [reference uzlů |nodes].


Výkon
=====

Parser je generovaný LALR(1) automat nad gramatikou převzatou z php-parseru, tisk je pouhé spojení tokenů. Na běžném stroji projde parse a tisk celého adresáře `vendor/` s třemi sty soubory a 1,2 MB kódu za necelou sekundu, tedy zhruba dvě milisekundy na soubor. Index pozic tokenů se staví líně a po úpravě se neobnovuje celý, takže cena změny následované dotazem odpovídá vzdálenosti mezi nimi, ne velikosti souboru.

PhpSyntax

Bezztrátový syntaktický strom PHP bez jediné závislosti: každý token, každá mezera i každý komentář má své místo a vytištěný strom je původní soubor bajt po bajtu. Knihovna pro nástroje, které mají kód číst a měnit.

Roky jsem si myslel, že parser pro PHP je vyřešená věc: je tu nikic/php-parser, stojí na něm PHPStan i Rector, co víc chtít. Pak jsem psal skript, který měl v pár stech souborech přejmenovat jedno volání, a zjistil jsem, kolik práce dá nepřepsat všechno ostatní. Přesně na tohle je PhpSyntax.

Strom, který nic nezahodí

Abstraktní syntaktický strom zahazuje, co pro význam programu není podstatné: mezery, prázdné řádky, komentáře uvnitř výrazů, závorky navíc. Pro analýzu je to správně. Jakmile ale chcete kód změnit a vrátit zpátky, potřebujete i to, co AST zahodil, jinak přetisknete soubor podle svých pravidel místo podle autorových.

PhpSyntax staví konkrétní strom: každý token zdrojáku má v něm svůj slot (IfNodeifKeyword, openParen, cond, closeParen, body), a bílé znaky a komentáře visí na tokenech jako trivia. Smlouva zní:

Printer::print($parser->parse($code)) === $code

Platí bajt po bajtu pro cokoli, co PHP přijme: BOM, hashbang, CR i CRLF, uzavírací tagy, inline HTML, __halt_compiler(). Je to invariant, který drží testy nad korpusem, a je to zároveň nejjednodušší test, jaký si nad vlastním kódem uděláte: parsuj, vytiskni, porovnej.

Kdy který

Obě knihovny mají své místo a je poctivé říct které.

php-parser, když potřebujete vědět, co kód dělá: typy, tok řízení, vyhodnocení konstantních výrazů, obrovský ekosystém nad ním. Také když potřebujete strom i z kódu se syntaktickou chybou: php-parser umí zotavení a vrátí částečný strom, PhpSyntax na chybě skončí výjimkou, protože jeho gramatika zotavení nemá.

PhpSyntax, když se chcete kódu dotknout a zbytek nechat: přejmenovat volání, doplnit argument, přesunout komentář, srovnat mezery, a dostat diff přesně tak velký jako změna. Formátovače, migrační skripty, generátory, které upravují existující soubory, nástroje na hromadné přepisy. Největší z nich je DressCode, checker a fixer stylu kódu, pro který PhpSyntax původně vznikl.

Instalace

composer require phpsyntax/phpsyntax

Knihovna vyžaduje PHP 8.4 nebo novější a rozšíření tokenizer. Jinak nemá žádnou závislost: composer.json žádá PHP a tokenizer a nic jiného, takže se nemá o co přetahovat s tím, co už v projektu je. Kód pro novější PHP přečte i na starším běhu, viz Parsování.

Na ukázku

use PhpSyntax\Nodes\Expression\FunctionCallNode;
use PhpSyntax\Nodes\NameNode;
use PhpSyntax\Parser\Parser;
use PhpSyntax\Printer;

$file = (new Parser)->parse(file_get_contents('Order.php'));

foreach ($file->find(FunctionCallNode::class) as $call) {
	if ($call->name instanceof NameNode && $call->name->text === 'sizeof') {
		$call->name->text = 'count';
	}
}

file_put_contents('Order.php', Printer::print($file));

Přejmenovalo se každé volání sizeof, a to je také jediné, co se v souboru změnilo. Kde se pak strom prochází, jak se mění a co ví o jménech, říkají další stránky: Parsování a tisk, Uzly a sloty, Trivia, Procházení, Úpravy, Analýzy a reference uzlů.

Výkon

Parser je generovaný LALR(1) automat nad gramatikou převzatou z php-parseru, tisk je pouhé spojení tokenů. Na běžném stroji projde parse a tisk celého adresáře vendor/ s třemi sty soubory a 1,2 MB kódu za necelou sekundu, tedy zhruba dvě milisekundy na soubor. Index pozic tokenů se staví líně a po úpravě se neobnovuje celý, takže cena změny následované dotazem odpovídá vzdálenosti mezi nimi, ne velikosti souboru.