Úpravy
Sloty se zapisují přiřazením, nahrazení a odstranění uzlu, práce se seznamy a nový uzel z fragmentu: jak strom měnit tak, aby zůstal konzistentní a po tisku se změnilo jen to, na co jste sáhli.
Zápis do slotu
Slot je vlastnost s hookem, takže se zapisuje přiřazením:
$if->cond = $parser->parseExpression('$order->isPaid()');
$node->replaceChild($old, $new);
$node->setSlot('cond', $expr); // když jméno slotu znáte až za běhu
Hook se postará o to, co byste jinak museli hlídat: odmítne hodnotu, která už má rodiče, adoptuje novou, pustí starou a ohlásí změnu indexu tokenů, který drží pořadí, řádky a sloupce. Typ vlastnosti odmítne hodnotu, která do slotu nepatří, ještě dřív.
Uzel patří právě jednomu stromu. Vložit uzel, který už rodiče má, je výjimka; clone dá hlubokou kopii
bez rodiče, kterou vložit lze.
Co nemá hook, hlídá jazyk: text a trivia tokenu jsou private(set) a mění se metodami (Trivia), položky seznamu jsou protected(set) a mění
se metodami seznamu, parent zapisuje strom sám. Není tedy jak strom rozbít zápisem vedle API.
Nahradit a odstranit
$node->replaceWith($other);
$node->remove();
$node->remove(CommentPolicy::Drop);
Mezi přiřazením do slotu a replaceWith() je rozdíl, na který se přijde až v diffu:
replaceWith() zachová trivia kolem starého uzlu, přiřazení ne.
$ternary->cond = $parser->parseExpression('$a !== []');
// $x = $a !== []? 'yes' : 'no';
$ternary->cond->replaceWith($parser->parseExpression('$a !== []'));
// $x = $a !== [] ? 'yes' : 'no';
Fragment z parseru má okraje prázdné, takže přiřazením se ztratí mezera, která patřila starému uzlu. Přiřazení
použijte tam, kde slot plníte poprvé nebo kde trivia řešíte sami; replaceWith() všude jinde.
remove() odstraní položku seznamu: uzel, který stál na řádcích sám, vezme řádky s sebou, jinak zůstane
okolní mezera. Komentáře uvnitř odstraňovaného uzlu se podle CommentPolicy přesunou k následujícímu tokenu
(výchozí), k předchozímu, nebo zahodí; ztratit komentář mlčky nejde.
Seznamy
$stmts->append($stmt);
$stmts->insert($index, $stmt);
$stmts->removeItem($stmt);
$args->append($arg); // oddělovač se odvodí z existujících, nebo ', '
$args->insert(0, $arg);
$args->setTrailingSeparator(null); // pryč s koncovou čárkou
SeparatedNodeList si oddělovače hlídá sám: při vložení odvodí chybějící čárku z těch, které
v seznamu jsou (nebo použije
, ` u jednořádkového seznamu), a ve víceřádkovém seznamu dá položce odsazení souseda; při odstranění vezme čárku za položkou, u poslední tu před ní. `removeItem()
odstraňuje položku ze seznamu, remove() na uzlu odstraňuje uzel sám; je to totéž z druhé strany a jmenuje se
to jinak jen proto, že PHP nedovolí dvě různé signatury.
NodeList příkazů odsazení za vás neřeší, protože příkaz může stát ledaskde. Vložený příkaz proto
dostane řádek a odsazení výslovně, a odsazení souseda si přečtěte dřív, než ho vložením připravíte
o začátek řádku:
$stmt = $parser->parseStatement('$this->log("total");');
$indent = $return->getFirstToken()->getLineIndentation();
$return->parent->insert($return->parent->indexOf($return), $stmt);
$return->getFirstToken()->ensureLeadingNewline();
$return->getFirstToken()->setIndentation($indent);
$stmt->getFirstToken()->setIndentation($indent);
Nový uzel
Nový kód se nestaví z tokenů, parsuje se z řetězce: Parser::parseExpression(),
parseStatement(), parseType(), parseName() vrátí odpojený uzel s prázdnými trivia na
okrajích, připravený k vložení. Kdo chce místo psaní řetězce klonovat existující uzel, musí klonu nejdřív vyčistit
okraje přes setEdgeTrivia(): klon si nese úvodní trivia originálu, a u prvního příkazu souboru je v nich i
<?php, které by se ve výstupu objevilo dvakrát.
Nejmenší možná úprava bývá změna textu jednoho tokenu a často stačí:
$call->name->text = 'count'; // jméno; hook ho přetokenizuje
$string->setValue('Hello', "'"); // hodnotu literálu i s uvozovkou
$token->setText('!==');
Co si strom hlídá sám a co ne
Sám si hlídá rodiče, index a FileNode::$revision, které roste s každou změnou; kdo potřebuje vědět,
jestli se něco změnilo, porovná revizi před a po, ale nepočítá s tím, že jedna úprava je jedno zvýšení, protože
složená úprava jako remove() jich udělá několik.
Nehlídá si dvě věci, které zůstávají na vás:
- Kanonická trivia. Konec řádku, který uzavírá řádek tokenu, patří do jeho koncových trivia, ne do úvodních
trivia dalšího tokenu.
ensureLeadingNewline()asetBlankLinesBefore()to dělají správně; kdo skládá trivia ručně, ať to dodrží, jinakgetTrailingSpace()zalomení neuvidí. - Význam. Strom vám dovolí nahradit podmínku čímkoli a odstranit
return; jestli to smíte, je otázka naisRepeatableRead(),matches()a na vás.
Po úpravě se tiskne Printer::print($file) nebo (string) $file a výstup je původní soubor se
změnami přesně tam, kde jste je udělali.