Uzly a sloty
Z čeho se strom skládá: uzly a jejich sloty pojmenované podle role, tokeny, seznamy s oddělovači, jména a literály. A kde se dozvíte, co který uzel nese.
Sloty
Každá konstrukce jazyka má svou třídu uzlu a každý uzel má sloty pojmenované podle role, ne podle textu.
IfNode má ifKeyword, openParen, condition, closeParen a
body; TernaryNode má condition, question, then,
colon a else. Párové oddělovače se vždy jmenují open* a close*, seznamy
jsou v množném čísle (statements, arguments, members), slot, který může být
prázdný, je ?. Všechny třídy leží v PhpSyntax\Nodes a úplný seznam se sloty je v referenci uzlů.
Slot je obyčejná vlastnost, takže se čte i zapisuje přiřazením:
$if = $file->findFirst(IfNode::class);
$if->ifKeyword; // Token 'if'
$if->condition; // ExpressionNode, třeba PropertyFetchNode
$if->body; // StatementNode, u složených závorek BlockNode
$if->condition = $parser->parseExpression('$order->isPaid()');
Za přiřazením není žádná magie, ale property hook: uzel si novou hodnotu adoptuje, starou pustí a ohlásí to indexu tokenů. Co to znamená pro trivia kolem nahrazeného uzlu, říká stránka Úpravy stromu.
Jména slotů jedné třídy vypíše konstanta Slots, v pořadí zdrojáku:
IfNode::Slots; // ifKeyword, openParen, cond, closeParen, body, colon, stmts, elseifs, else, endKeyword, semicolon
Že jich je jedenáct, i když if má pět částí, je kvůli alternativní syntaxi: zápis
if (…): … endif; používá tutéž třídu, jen se sloty colon, statements,
endKeyword a semicolon místo body. Prázdné sloty se ve výstupu nikde neobjeví.
Děti uzlu vrací getChildren() v pořadí zdrojáku, prázdné sloty vynechané. Je to jediná cesta k dětem:
uzel není iterovatelný, aby existoval jeden způsob a byl jím obyčejné pole.
foreach ($class->getChildren() as $child) {
// NodeList (attributes), ModifiersNode, Token 'class', IdentifierNode, Token '{', NodeList (members), Token '}'
}
Dvě pravidla, která šetří překvapení:
- Tělo řídicí struktury je
StatementNode, ne nutně blok:if ($a) return;má vbodypřímoReturnNode. Se složenými závorkami je toBlockNodese seznamemstatements. else ifjeElseNode, jehož tělo jeIfNode;elseifjeElseIfNode. Třídy, rozhraní, traity a výčty jsou různé uzly (ClassNode,InterfaceNode,TraitNode,EnumNode), ale všechny i s anonymní třídou implementujíClassLikeNode, které slibuje$name(u anonymní třídynull) a$members. PodobněFunctionLikeNodeslibuje$paramsa$returnTypeu funkce, metody, closure, arrow funkce i hooku. Obě rozhraní se dají použít jako filtr vfind().
Tokeny
Listy stromu jsou Token: druh (kind), text a dvě pole trivia, úvodní a koncová. Druh je buď
konstanta TokenKind (TokenKind::Variable, TokenKind::DoubleArrow), nebo u jednoznakových
tokenů ordinální hodnota znaku; is() se ptá pohodlně na obojí a přijme i text operátoru:
$token->is(TokenKind::Variable);
$token->is('(', '[');
is() s textem nikdy nematchne obsah řetězce, jen operátory a interpunkci, takže
$token->is('(') je bezpečné i pro token řetězce '('. Text tokenu je přesně to, co stálo ve
zdrojáku, i tam, kde PHP počítá do tokenu mezery: přetypování ( int ), inline HTML, obsah heredocu.
Seznamy
NodeList<T> je seznam bez oddělovačů (příkazy, členy třídy, skupiny atributů),
SeparatedNodeList<T> seznam s oddělovači mezi položkami (parametry, argumenty, položky pole, importy),
který si pamatuje i oddělovače a případnou koncovou čárku:
$args->getItems(); // list<ArgumentNode|VariadicPlaceholderNode>
$args->getSeparators(); // list<Token>, čárky
$args->hasTrailingSeparator(); // koncová čárka
count($args);
$args->indexOf($item);
foreach ($args as $arg) {
// seznamy jsou iterovatelné a Countable
}
Položky seznamu jsou protected(set), takže se mění jen jeho vlastními metodami (append(),
insert(), removeItem()); díky tomu seznam vždycky ví, co v něm je. Mezera mezi dvěma čárkami
destrukturace ([, $b] = …) je EmptyArrayItemNode, aby seznam měl na každém místě položku;
modifikátory (public static readonly) drží ModifiersNode v pořadí zdrojáku.
Slot, ve kterém seznam sedí, se jmenuje podle toho, co drží: statements, members,
parameters, a tam, kde by jméno jen zopakovalo jméno uzlu, prostě items. Argumenty volání jsou tedy
$call->arguments->items, ne $call->arguments->arguments.
Jména
NameNode je jméno třídy, funkce, konstanty nebo jmenného prostoru; je to jeden token, včetně klíčových
slov, která gramatika na místě jména připouští (static, array, readonly). Umí
o sobě říct víc než text, a to vlastnostmi:
$name->text; // 'App\Model\Order', jak je zapsané
$name->kind; // NameKind::Unqualified, Qualified, FullyQualified, Relative
$name->parts; // ['App', 'Model', 'Order']
$name->shortName; // 'Order'
$name->role; // SymbolKind::ClassLike, Function nebo Constant podle místa ve stromu
$name->isKeyword();
$name->isDeclaration(); // zavádí jméno symbol (use, namespace), místo aby na něj odkazovalo?
$name->isReference(); // odkazuje na symbol, který má smysl překládat?
$name->equals('App\Model\Order');
Vlastnost text je zapisovatelná a je to nejkratší cesta, jak jméno přepsat:
$call->name->text = 'count' jméno přetokenizuje na nové a všechno kolem nechá, jak bylo. Nové jméno mimo
strom vyrobí NameNode::fromText('Nette\Utils\Strings'), jméno členu pak
IdentifierNode::fromText('render').
Role se odvozuje z místa ve stromu: totéž Foo je třída za new, funkce před ( a
konstanta jinde. Třídu a jmenný prostor drží PHP v jedné tabulce jmen, takže obojí je ClassLike;
use Foo; totiž zavádí zároveň třídu Foo i prefix Foo\Bar a rozlišit to nejde. Co
jméno znamená doopravdy, tedy včetně importů a jmenného prostoru, říká až analýza NameResolver, viz Analýzy.
Role ale říká jen to, do které tabulky jmen zápis patří, ne jestli na nějaký symbol vůbec odkazuje. Na to je
isReference() a odpoví false ve třech případech: jméno symbol zavádí (use,
namespace), pojmenovává vestavěný typ tam, kde se píše typ (int, array), nebo je to
self, static či parent na místě třídy, protože ta zastupují konkrétní třídu jen
tam, kde jsou napsaná. Klíčové slovo na místě jména naopak reference je: readonly(...) volá funkci toho
jména. Právě na tohle se ptejte, než jméno pošlete resolveru, viz Analýzy.
IdentifierNode je jméno členu, návěští, hooku nebo aliasu (->name, ::CONST,
as Alias), také jeden token, i když je to rezervované slovo. Slot, který drží jméno, se jmenuje
name, takže text typu se čte $type->name->text.
Skaláry
Literály si drží text tak, jak byl zapsán, a hodnotu, kterou znamenají, dávají jako vlastnost:
$int->token->text; // '0o755'
$int->value; // 493
$int->base; // 8
$string->token->text; // '"a\nb"'
$string->value; // "a\nb" se skutečným koncem řádku
$string->quote; // '"'
Zápis hodnoty je metoda, protože přijímá dvě věci najednou: hodnotu a případně uvozovku, kterou se má zapsat. O escapování se stará uzel, ne vy:
$string->setValue("x'y", "'"); // 'x\'y'
StringNode::fromValue("it's", '"'); // nový uzel: "it's"
StringNode::fromValue('plain'); // 'plain'
Co fromValue() zapíše, to $value přečte zpátky; drží to test knihovny. Holé jméno offsetu
v interpolaci ("$row[label]") není StringNode, ale UnquotedStringNode, aby
StringNode znamenal jednu věc a $quote bylo vždycky ' nebo ".
U heredocu se nejdřív zeptejte hasInterpolation(), teprve pak se dá číst jeho value. Ta hodnota
je tělo, ze kterého se nejdřív odečte odsazení uzavíracího návěští a teprve potom se čtou escape sekvence, přesně
jak to dělá PHP: odsazení je fyzické, kdežto to, co vyrobí \n, žádný řádek zdrojáku není a odsazení se
z něj neubírá. Interpolovaný řetězec je vlastní uzel poskládaný ze svých částí.
Importy
Příkaz use má jedinou třídu UseNode, ať je zapsaný jakkoli. Skupinový zápis
(use App\{A, B};) je jen zápisová varianta téhož příkazu, ne jiný uzel, takže má navíc vyplněné sloty
prefix, openBrace a closeBrace a odpoví na isGroup(). Každá položka umí
říct, co doopravdy importuje a jakého druhu to je, i když prefix a klíčové slovo function stojí
o patro výš:
$use->kind; // SymbolKind::ClassLike, Function nebo Constant
foreach ($use->items as $item) {
$item->fullName; // 'App\A', i u skupinového zápisu
$item->kind; // vlastní druh, jinak druh příkazu
$item->getStatement(); // UseNode, ke kterému patří
}
Položka znamená to, co říká příkaz, ve kterém stojí, prefix skupiny v to počítaje. Proto se položka mezi
příkazy nestěhuje, ale píše se znovu, a to metodou addImport(), která ji zapíše tak, jak ten který příkaz
své položky píše: celým jménem v prostém importu, relativně k prefixu ve skupině:
$use->addImport('App\Model\Order'); // use App\Model\Order;
$use->addImport('App\Model\Order', 'Entity'); // use App\Model\Order as Entity;
Jméno, které pod prefix skupiny nespadá, metoda odmítne výjimkou, místo aby zapsala import, který znamená něco jiného. Bez třetího parametru jde nová položka na konec seznamu.
Kořen
FileNode je kořen: statements se všemi příkazy souboru, přičemž příkazy za
namespace App; jsou vnořené do NamespaceNode, takže každý jmenný prostor má své příkazy jako
děti. Poslední token souboru je vždy EndOfFile s prázdným textem, na kterém visí, co je za posledním
příkazem. FileNode::$revision roste s každou změnou stromu; hodí se k tomu poznat, že se něco změnilo, ne
k počítání změn.