Uzly a sloty

Uzly a sloty pojmenované rolí, tokeny, seznamy s oddělovači a jména: jak je strom postavený a kde najít, 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, cond, closeParen a body; TernaryNodecond, question, if, colon a else. Párové oddělovače se vždy jmenují open* a close*, seznamy jsou v množném čísle (stmts, args, 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->cond;        // ExpressionNode, třeba PropertyFetchNode
$if->body;        // StatementNode, u složených závorek BlockNode

$if->cond = $parser->parseExpression('$order->isPaid()');

Za přiřazením není magie, ale 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.

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, když if má pět, je alternativní syntaxí: if (…): … endif; používá tutéž třídu se sloty colon, stmts, endKeyword a semicolon místo body. Prázdné sloty se 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 stmts.
  • 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, takže „je to nějaká třída“ je jeden instanceof.

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);

Položky seznamu jsou protected(set): mění se jen jeho metodami (append(), insert(), removeItem()), aby seznam vždy věděl, 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.

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;        // NameRole::ClassLike, Function, Constant, Namespace podle místa ve stromu
$name->isKeyword();
$name->equals('App\Model\Order');

text je zapisovatelný, a to je nejkratší cesta, jak jméno přepsat: $call->name->text = 'count' přetokenizuje jméno na nové a nechá kolem něj všechno, jak bylo.

Role se odvozuje z místa ve stromu: totéž Foo je třída za new, funkce před ( a konstanta jinde. Co jméno znamená doopravdy, tedy včetně importů a jmenného prostoru, říká až analýza NameResolver, 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 bere dvě věci najednou: hodnotu a případně uvozovku, kterou se má zapsat.

$string->setValue("x'y", "'");   // 'x\'y'

Heredoc odpoví hasInterpolation() dřív, než se dá jeho value číst; interpolovaný řetězec je vlastní uzel se svými částmi.

Kořen

FileNode je kořen: stmts 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.

verze: 1.0