Pravidla o mezerách
Mezeru mezi dvěma tokeny nikdo nepřepisuje procedurou. Pravidlo o mezerách vysloví nárok a engine rozhodne, opraví a ohlásí. Jak takové pravidlo napsat.
Proč nárok, ne oprava
Kdyby pravidlo o mezeře za čárkou psalo mezeru samo a pravidlo o zalomení dlouhého seznamu psalo konec řádku samo, sešly by se na téže mezeře dvě procedury a výsledek by závisel na tom, která běžela později. Proto pravidla o mezerách v DressCode nic nepíší. Řeknou, co má mezi dvěma tokeny být, a engine, který vidí všechny nároky najednou, rozhodne, opraví a hlášení vypíše pod jménem pravidla, jehož nárok vyhrál. Odtud plyne to, co uživatel vidí: každá mezera má jednoho vlastníka a dvě pravidla, která by chtěla totéž místo, jsou chyba konfigurace, ne loterie.
Nárok
Claim má čtyři složky a pravidlo nárokuje jen ty, na kterých mu záleží:
| složka | co říká | hodnoty |
|---|---|---|
space |
vodorovná mezera, když tokeny sdílejí řádek | Space::None, Single, AtLeastSingle, a varianty SingleOrTabs,
AtLeastSingleOrTabs, které nechají tabulátor zarovnávající sloupce |
line |
stojí druhý token na řádku prvního, nebo na dalším | Line::Same, Line::Next |
blank |
kolik prázdných řádků, když je tam zalomení | počet, nebo rozsah [min, max] s null jako otevřeným koncem |
blankBelowComment |
prázdné řádky mezi komentářem v mezeře a tokenem | totéž |
Nejčastější nároky mají továrny, které vracejí sdílené instance: Claim::none(), single(),
atLeastSingle(), sameLine(), nextLine(), blank(1). Nárok na víc složek nebo
nárok s odůvodněním se staví konstruktorem: new Claim(Space::None, line: Line::Same),
new Claim(line: Line::Next, because: 'the line is 135 characters long'); odůvodnění engine připojí za čárku ke
zprávě.
Prázdné řádky se počítají nad komentářem, který stojí na vlastních řádcích nad tokenem, protože komentář
patří ke kódu pod ním; jen před zavírací závorkou a koncem souboru patří komentář k tomu, co je nad ním. Řádky
mezi posledním komentářem a tokenem jsou vlastní složka blankBelowComment, kterou nárokuje třeba pravidlo
o mezeře mezi dokumentačním komentářem a deklarací.
Kde nárok stojí
Pravidlo implementuje GapRule a v getClaims() řekne, u kterých slotů kterých uzlů má nárok,
před hodnotou slotu a za ní:
final class BlankLineBeforeReturnRule extends Rule implements GapRule
{
public function getVisitedTypes(): array
{
return [];
}
public function getClaims(): array
{
return [
'*' => [
'stmts:item' => [
fn(Gap $gap) => $gap->value instanceof ReturnNode && $gap->index > 0 ? Claim::blank(1) : null,
null,
],
],
];
}
}
Klíč první úrovně je třída uzlu, nebo * pro každý uzel, který slot má. Klíč druhé úrovně je slot,
slot:item pro každou položku seznamu, slot:separator pro jeho oddělovače. Hodnota je dvojice
nároků před a za: Claim, null pro žádný, nebo closure, která dostane Gap a vrátí
nárok nebo null. Nárok u slotu s uzlem platí pro jeho první token (před) nebo poslední (za).
Ukázka výše říká: před každým příkazem seznamu, který je return a není první, má být jeden
prázdný řádek. Pravidlo nenavštěvuje nic (getVisitedTypes() je prázdné), protože všechnu práci dělá
engine z nároků. Hlášení zní Expected 1 blank line before the return, 0 found a skládá ho engine z toho,
u čeho nárok stojí; totéž platí pro A single space after the comma nebo
A line break before the opening brace. Autor pravidla zprávy nepíše.
Closure dostane v Gap token na okraji mezery, hodnotu slotu nebo položku, pro kterou se nárok dělá, její
index v seznamu a styl souboru. Z toho se rozhoduje podle kódu, třeba jinak pro . než pro ostatní
operátory:
public function getClaims(): array
{
$operator = fn(Gap $gap): ?Claim => $gap->token->text === '.' ? null : $this->claim;
return [
BinaryNode::class => ['operator' => [$operator, $operator]],
AssignNode::class => ['operator' => [$this->claim, $this->claim]],
];
}
Pravidlo s volbou si nárok postaví jednou v configure() a v closure ho jen vrací, ne staví u každé
mezery.
Rozhodnutí, které musí být stejné
Nárok, který závisí na tvaru kódu (seznam už je rozlomený, řádek je moc dlouhý), musí dát každé mezeře téže konstrukce stejnou odpověď, ať už engine mezitím s předchozími mezerami udělal cokoli. Jinak by první čárka seznam rozlomila a druhá ho zase slepila. Proto se takové rozhodnutí dělá jednou:
fn(Gap $gap) => $gap->once($list, fn() => $this->isBroken($list)) ? Claim::nextLine() : null
once() vyhodnotí closure při první mezeře uzlu a stejnou odpověď vrací u všech dalších do konce
průchodu. Seznam se přitom počítá za rozlomený, jakmile některá položka nebo zavírací závorka začíná řádek, aby
rozhodnutí rozlomit platilo pro celý zbytek.
Kdo vyhraje
Engine skládá nároky obou stran mezery složku po složce podle pevných pravidel, ne podle pořadí v konfiguraci:
- nárok konkrétní třídy před nárokem
*, - nárok vnitřního slotu před nárokem předka,
- u mezery přísnější před volnějším (
No whitespacevyhraje nadAt least one spaceureturn;), - u řádku
Line::NextnadLine::Same, - u prázdných řádků průnik toho, co obě strany dovolí, nebo užší z rozsahů, které se vylučují.
Dva holé nároky na tutéž složku téže strany téhož slotu jsou ConfigurationException už při skládání
pravidel; dvě closures smějí slot sdílet, protože každá může tam, kde rozhoduje druhá, vrátit null.
Vestavěná pravidla se nekříží; plugin, který chce jednu mezeru jinak než vestavěné pravidlo, ho pro tu mezeru dnes musí
vypnout a nárok převzít, nebo požádat o volbu.
Požadované zalomení engine udělá hned a prázdné řádky doladí v dalším průchodu; zakázané zalomení odstraní
jen tehdy, když v mezeře není nic než bílé znaky. Odsazení řádku, který zalomením vznikl, není věcí nároku: engine
mu dá obvyklé odsazení a pravidlo indentation ho pak umístí přesně.
Testování
Pravidlo o mezerách se testuje stejně jako každé jiné, RuleTesterem nad fixturami; vypsané zprávy jsou ty
složené enginem. Pravidlo může zároveň navštěvovat uzly a dělat něco navíc; pak getVisitedTypes() není
prázdné a platí pro něj všechno z Pravidel hry.