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.