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.

verze: 1.0