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.

verze: 1.0