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, condition, closeParen, body, colon, statements, 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. Sloty, které daný zápis nepoužívá, jsou
null.
Děti uzlu vrací getChildren() v pořadí zdrojáku; sloty s null vynechá, prázdný seznam
(třeba elseifs u if bez elseif) ale mezi dětmi je. Je to jediná cesta k dětem: uzel
není iterovatelný, aby existoval jeden způsob a byl jím obyčejné pole. Výjimkou jsou jen seznamy, viz níže.
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$parameters(u hooku bez závoreknull) a$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 = $call->arguments->items;
$args->getItems(); // list<ArgumentNode|VariadicPlaceholderNode|ArgumentPlaceholderNode>
$args->getSeparators(); // list<Token>, čárky
$args->hasTrailingSeparator(); // koncová čárka
count($args);
$args->isEmpty();
$args->indexOf($item);
$args->findSeparatorOf($item); // čárka, která s položkou odejde
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, v typu, za
extends, instanceof nebo před ::, funkce před ( a konstanta jen jako
samostatný výraz ($x = Foo;). 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, namespaceSeparator, 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 přidá do existujícího příkazu a
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 App\Model\Item;
$use->addImport('App\Model\Order'); // use App\Model\Item, App\Model\Order;
$use->addImport('App\Model\Cart', 'Basket'); // use App\Model\Item, App\Model\Order, App\Model\Cart as Basket;
// use App\Model\{Item};
$group->addImport('App\Model\Order'); // use App\Model\{Item, Order};
Nový samostatný příkaz use se vyrobí jako každý jiný příkaz, parseStatement(), a vloží se
do seznamu příkazů, viz Úpravy stromu.
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í to, co je za posledním
řádkem kódu: prázdné řádky a komentáře pod ním. Komentář na řádku posledního příkazu patří podle pravidla
o trivia k tomu příkazu. 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.