Pravidla pro bílé znaky
Pravidla, která hlídají mezery, zalomení řádků a prázdné řádky, se píšou jinak než ostatní: do kódu vůbec nesahají. Jen řeknou, co má být mezi dvěma tokeny, a o zbytek se postará jádro nástroje.
Co je mezera mezi tokeny
Nejdřív pojem, na kterém celá stránka stojí. Mezerou mezi tokeny se tu myslí všechno, co ve zdrojáku stojí
mezi dvěma sousedními tokeny: buď nic, nebo jedna či víc mezer, nebo zalomení řádku, prázdné řádky a komentáře.
V API se takové místo jmenuje Gap.
$sum = $a + $b;
Tenhle řádek má pět mezer v tomhle smyslu: mezi $sum a =, mezi = a $a,
mezi $a a +, mezi + a $b a nakonec mezi $b a ;. Ta
poslední je taky mezera, jen v ní nic nestojí, a i o ní se dá něco prohlásit: že tam nemá být nic.
Pravidlo pro bílé znaky (třída GapRule) o takovém místě řekne, jak má vypadat. Nepíše do něj, nemaže
z něj a nic v něm nehledá.
Proč pravidla bílé znaky nepřepisují
Kdyby si pravidlo o mezeře za čárkou psalo mezeru samo a pravidlo o zalomení dlouhého seznamu si samo psalo konec řádku, potkala by se na jednom místě dvě pravidla a vyhrálo by to, které náhodou běželo později. Podruhé by to mohlo dopadnout obráceně.
Proto pravidla pro bílé znaky do kódu nesahají. Každé jen vysloví požadavek (v API Claim), co má
na daném místě být, a jádro nástroje, které vidí všechny požadavky najednou, rozhodne, opraví a hlášení vypíše pod
jménem toho pravidla, jehož požadavek vyhrál.
Z toho plyne to, co vidí uživatel: každá mezera má právě jednoho vlastníka a dvě pravidla, která by si nárokovala totéž místo, jsou chyba konfigurace, na kterou nástroj upozorní hned při startu.
Požadavek
Požadavek Claim má čtyři složky a pravidlo vyplní jen ty, na kterých mu záleží:
| složka | co říká | hodnoty |
|---|---|---|
space |
vodorovná mezera, když jsou oba tokeny na jednom řádku | Space::None, Single, AtLeastSingle a varianty SingleOrTabs,
AtLeastSingleOrTabs, které nechají projít tabulátor zarovnávající sloupce |
line |
jestli druhý token stojí 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ší požadavky mají továrny, které vracejí sdílené instance: Claim::none(),
single(), atLeastSingle(), sameLine(), nextLine() a blank(1).
Požadavek na víc složek najednou nebo požadavek s odůvodněním se staví konstruktorem:
new Claim(Space::None, line: Line::Same) nebo
new Claim(line: Line::Next, because: 'the line is 135 characters long'). Odůvodnění jádro připojí za čárku
k hlášce.
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 sebou. Výjimkou je komentář před zavírací závorkou a před koncem souboru, který patří
k tomu, co je nad ním. Řádky mezi posledním komentářem a tokenem tvoří vlastní složku blankBelowComment;
tu si nárokuje třeba pravidlo o mezeře mezi dokumentačním komentářem a deklarací.
Kde požadavek platí
Pravidlo dědí od GapRule a v metodě getClaims() řekne, u kterých slotů kterých uzlů má
požadavek, a to zvlášť před hodnotou slotu a za ní:
final class BlankLineBeforeReturnRule extends GapRule
{
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ý takový slot má. Klíč druhé úrovně
je jméno slotu ('body'), případně slot:item pro každou položku seznamu ('stmts:item',
'items:item') nebo slot:separator pro jeho oddělovače ('items:separator'). Hodnotou je
dvojice požadavků, před a za: buď Claim, nebo null, když pravidlo nic nechce, nebo anonymní
funkce, která dostane Gap a vrátí požadavek či null. Požadavek u slotu, ve kterém sedí uzel,
platí pro jeho první token (před) nebo poslední token (za).
Jméno slotu je řetězec, takže ho refaktoring nepřejmenuje spolu se slotem. Přejmenovaný slot udělá z požadavku tichý omyl, který odhalí až fixtura pravidla; jména slotů najdete v přehledu uzlů.
Ukázka výše říká: před každým příkazem seznamu, který je return a není první v pořadí, má být
jeden prázdný řádek. Pravidlo nenavštěvuje žádné uzly, protože GapRule návštěvy vůbec nemá; všechnu
práci odvede jádro z požadavků. Hlášení zní Expected 1 blank line before the return, 0 found a skládá ho
jádro z toho, u čeho požadavek stojí. Stejně vznikají hlášky A single space after the comma nebo
A line break before the opening brace. Autor pravidla texty hlášek nepíše.
Anonymní funkce dostane v objektu Gap token na okraji mezery, hodnotu slotu nebo položku, které se požadavek
týká, její index v seznamu a styl souboru. Podle toho se rozhodne, například jinak pro tečku 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 požadavek postaví jednou v configure() a v anonymní funkci ho jen vrací; nestaví ho
u každé mezery znovu.
Rozhodnutí, které musí být pokaždé stejné
Požadavek, který závisí na tvaru kódu (seznam už je rozlomený na řádky, řádek je moc dlouhý), musí dát každé mezeře téže konstrukce stejnou odpověď, ať už jádro s předchozími mezerami mezitím udělalo cokoli. Jinak by první čárka seznam rozlomila a druhá ho zase slepila. Takové rozhodnutí se proto dělá jen jednou:
fn(Gap $gap) => $gap->once($list, fn() => $this->isBroken($list)) ? Claim::nextLine() : null
Metoda once() vyhodnotí funkci u první mezery daného uzlu a stejnou odpověď pak vrací u všech dalších
až 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
Jádro skládá požadavky obou stran mezery složku po složce, a to podle pevných pravidel, ne podle pořadí v konfiguraci:
- požadavek konkrétní třídy má přednost před požadavkem
*, - požadavek vnitřního slotu před požadavkem předka,
- u vodorovné mezery přísnější před volnějším, takže u
return;vyhrajeNo whitespacenadAt least one space, - u zalomení řádku má
Line::Nextpřednost předLine::Same, - u prázdných řádků platí průnik toho, co obě strany dovolí, a když se rozsahy vylučují, ten užší z nich.
Dva pevné požadavky na tutéž složku téže strany téhož slotu jsou ConfigurationException už při
skládání pravidel. Dvě anonymní funkce si slot rozdělit smějí, protože každá z nich může tam, kde rozhoduje ta
druhá, vrátit null. Vestavěná pravidla se nekříží; rozšíření, které chce jednu mezeru řešit jinak než
vestavěné pravidlo, ho pro to místo dnes musí vypnout a požadavek převzít, nebo požádat o novou volbu.
Vynucené zalomení řádku jádro udělá hned a prázdné řádky doladí až v dalším průchodu. Zakázané zalomení
naopak odstraní jen tehdy, když v mezeře není nic než bílé znaky. Odsazení řádku, který zalomením vznikl, není
věcí požadavku: jádro mu dá obvyklé odsazení a pravidlo indentation ho pak umístí přesně.
Testování
Pravidlo pro bílé znaky se testuje stejně jako každé jiné, RuleTesterem nad fixturami; vypsané hlášky jsou ty, které složilo jádro.
Co GapRule neumí, je sáhnout na tokeny: skládá se z požadavků a nic jiného nedělá. Když je u jedné
konstrukce potřeba obojí, jsou z toho dvě pravidla se dvěma jmény, jedno GapRule a jedno NodeRule. Vestavěná dvojice attribute-spacing a
useless-attribute-parentheses je přesně tenhle případ: první si nárokuje bílé znaky uvnitř
#[...], druhé odstraňuje prázdné závorky za jménem atributu. Vypadá to jako zbytečná komplikace, dokud
někdo nechce vypnout jen jednu z těch dvou věcí.