Procházení

Traverser, potomci a předci, sousední tokeny a pozice: jak se ve stromu pohybovat a jak se ptát, zda jsou dva výrazy stejné nebo bezpečně opakovatelné.

Shora dolů

Traverser projde strom do hloubky v pořadí zdrojáku a zavolá enter při vstupu do uzlu nebo tokenu a leave při výstupu:

use PhpSyntax\Traverser;

$calls = [];
(new Traverser(enter: function (Node|Token $node) use (&$calls) {
	if ($node instanceof FunctionCallNode || $node instanceof MethodCallNode) {
		$calls[] = $node->getFirstToken()->getLine() . ': ' . $node;
	}
}))->traverse($file);

Callback smí uzel, který dostal, nahradit nebo odstranit; traverser pak do něj ani do jeho náhrady nesestoupí, leave pro něj nezavolá, sourozence odpojené mezitím přeskočí a uzly vložené mezitím nechá dalšímu průchodu. Díky tomu se dá měnit strom přímo během průchodu bez zvláštního režimu.

Když nepotřebujete pořadí ani události, stačí snímek:

foreach ($file->getDescendants(FunctionCallNode::class) as $call) {
	// ...
}

getDescendants() vrátí pole všech potomků dané třídy (nebo všech), a protože je to pole pořízené předem, je bezpečné během procházení strom měnit.

Zdola nahoru a do stran

Každý uzel i token zná svého rodiče ($node->parent) a od něj se dá jít výš:

$method = $call->findAncestor(MethodNode::class);   // nejbližší předek dané třídy, nebo null
$class = $call->findAncestor(ClassLikeNode::class);

Po tokenech se chodí do stran: $token->getNext() a getPrevious() je následující a předchozí token v souboru bez ohledu na strukturu. $node->getFirstToken() a getLastToken() jsou okraje uzlu, takže „token před uzlem“ je $node->getFirstToken()->getPrevious().

Uzel se chová jako řetězec: (string) $node je jeho text včetně trivia uvnitř, což se hodí do hlášek i při ladění.

Pozice

$token->getLine();       // řádek v aktuálním stavu stromu
$token->getColumn();     // sloupec
$token->getOffset();     // bajtový offset
$token->originalLine;    // řádek v původním souboru, nikdy se nemění
$node->getStartLine();   // řádky uzlu
$node->getEndLine();

Rozdíl mezi getLine() a originalLine je podstatný, jakmile strom měníte: první sleduje změny, druhé je pozice, kterou zná uživatel ze svého souboru, a do hlášení patří ta druhá. U uzlu vloženého za běhu je originalLine null. Pozice počítá index tokenů, který se staví líně a po změně stromu se opravuje jen tam, kde je potřeba, takže dotaz hned po změně stojí tolik, jak daleko od změny leží.

Dvě otázky, které si klade polovina pravidel

$a->matches($b);          // jsou ty dva uzly stejné, token po tokenu, bez ohledu na mezery?
$expr->isRepeatableRead(); // dá se výraz vyhodnotit podruhé, aniž by se něco změnilo?

matches() porovnává texty tokenů, ne trivia, takže $a->b a $a -> b si odpovídají. isRepeatableRead() říká true pro proměnné, přístupy k vlastnostem a prvkům, konstanty a literály, a false pro volání, new, přiřazení, ++ a cokoli, co může mít vedlejší účinek. Dohromady odpovídají na otázku „opakuje tenhle ternár svou podmínku a smím ji vyhodnotit jen jednou“, a jsou ve stromu právě proto, aby si ji každé pravidlo neřešilo znovu.

verze: 1.0