Procházení
Hledání uzlů, Traverser s řízením průchodu, sousední tokeny a pozice, a dvě otázky, na které strom odpoví za vás.
Hledání
Nejkratší cesta k uzlům dané třídy vede přes find() a findFirst():
foreach ($file->find(FunctionCallNode::class) as $call) {
// ...
}
$class = $file->findFirst(ClassNode::class);
Obě berou třídu uzlu, nebo predikát, když třída nestačí:
$long = $file->find(fn($node) => $node instanceof StringNode && strlen($node->value) > 80);
find() vrací pole, tedy snímek pořízený předem, takže je bezpečné během procházení strom měnit.
Prochází uzly, ne tokeny; token se najde přes uzel, ve kterém sedí, nebo procházením od getFirstToken() přes
getNext().
Traverser
Když potřebujete vidět všechno v pořadí zdrojáku, včetně tokenů, nebo reagovat na vstup i výstup z uzlu, je na to
Traverser:
use PhpSyntax\Traverser;
(new Traverser)->traverse($file, function (Node|Token $node) {
// enter
}, function (Node|Token $node) {
// leave
});
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.
Callback může průchod řídit návratovou hodnotou:
(new Traverser)->traverse($file, function ($node) {
if ($node instanceof ClosureNode) {
return Traverser::DontTraverseChildren; // do tělíčka closure nechceme
} elseif ($node instanceof HaltCompilerNode) {
return Traverser::StopTraversal; // dál už nic zajímavého není
}
});
Cokoli jiného, co callback vrátí, průchod ignoruje, takže se nemusíte hlídat, co z něj vypadne.
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 každý nástroj
$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 je každý nástroj nad ním neřešil znovu.
K nim patří ExpressionNode::isDereferenced(), které řekne, jestli rodič sahá na člen nebo prvek výrazu,
což rozhoduje, zda kolem něj smějí zmizet závorky.