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. IfNodeifKeyword, openParen, condition, closeParen a body; TernaryNodecondition, 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á v body přímo ReturnNode. Se složenými závorkami je to BlockNode se seznamem statements.
  • else if je ElseNode, jehož tělo je IfNode; elseif je ElseIfNode. 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řídy null) a $members. Podobně FunctionLikeNode slibuje $params a $returnType u funkce, metody, closure, arrow funkce i hooku. Obě rozhraní se dají použít jako filtr v find().

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.