Jak DressCode přemýšlí

Strom místo tokenů, pravidlo jako věta, presety místo vlastního stylu, průchody do ustálení místo priorit a mezera s jedním vlastníkem. Mentální model, bez kterého konfigurace nedává smysl.

Strom, ve kterém je všechno

Nástroje na styl kódu se v PHP dvacet let stavěly nad token_get_all(): nad plochým seznamem, kde je if, závorka, proměnná, mezera, a nic neříká, kde if končí ani jestli [ otvírá pole nebo index. Každé pravidlo si pak strukturu domýšlí samo a většina jeho kódu je obrana proti tomu, aby se nespletlo.

DressCode parsuje soubor do bezztrátového konkrétního syntaktického stromu. Každý uzel ví, co je: IfNode má podmínku a tělo, TernaryNodecond, if a else. A každý token zdrojáku v tom stromu je, včetně mezer a komentářů, které visí na tokenech jako takzvaná trivia. Když strom vytisknete, dostanete původní soubor bajt po bajtu. Tohle je základní smlouva: pravidlo mění jen to, na co sáhne, a zbytek souboru zůstane, jak byl. Žádné přetištění podle vlastních představ, žádný ztracený komentář, diff přesně tak velký jako změna.

Pro vás jako uživatele z toho plyne jedna praktická věc: hlášení a opravy jsou přesné. Když pravidlo říká, že u ternáru chybí mezera, myslí tenhle ternár, ne „něco kolem otazníku na řádku 12“.

Pravidlo je věta

Pravidlo v DressCode je malá třída, která řekne, které uzly ji zajímají, a pro každý z nich položí otázku. Tady je jádro pravidla, které přepisuje $a ? $a : $b na $a ?: $b:

public function enter(Node|Token $node, RuleContext $context): void
{
	if (
		$node instanceof TernaryNode
		&& $node->if !== null
		&& $node->cond->isRepeatableRead()
		&& $node->cond->matches($node->if)
		&& $node->question->getLine() === $node->colon->getLine()
		&& !$node->question->hasCommentUpTo($node->colon)
		&& $context->report($node, "A ternary repeating its condition must be written '?:'")
	) {
		$node->setIf(null);
		$node->question->setTrailingTrivia([]);
	}
}

Dá se to přečíst nahlas: je to ternár, má prostřední část, podmínku lze bezpečně vyhodnotit dvakrát, prostřední část je stejná jako podmínka, je to na jednom řádku, není uvnitř komentář, a nikdo to nepotlačil. Otázky jako „je tenhle výraz bezpečné číst podruhé“ zodpovídá strom; pravidlo si je nemusí odvozovat. Proto má většina pravidel v DressCode pod sto řádků a proto si vlastní pravidlo napíšete za odpoledne.

Oprava jen po ohlášení

V ukázce výše stojí report() v podmínce. Není to náhoda, je to smlouva: pravidlo smí změnit strom jen poté, co porušení ohlásilo a hlášení nebylo potlačeno. Engine to hlídá: každou změnu stromu spáruje s hlášením, a pravidlo, které změní něco tiše, poruší kontrakt a dozvíte se to.

Díky tomu má potlačení skutečnou váhu. Když napíšete // dresscode:ignore nebo pravidlo pro danou cestu vypnete, nezmizí jen hláška: oprava se opravdu neprovede, a nehlídá to dobrá vůle autora pravidla, ale engine. Pravidlo, které by opravovalo i potlačené místo, neprojde ani vlastním testem.

Presety místo vlastního názoru

DressCode nemá vlastní styl. Má pravidla a jejich volby, a preset je pojmenovaný seznam pravidel s volbami, který dává dohromady jeden styl. Vestavěné jsou tři: dresscode/psr12 sleduje PSR-12 oddíl po oddílu, dresscode/per je jeho potomek podle PER Coding Style a je výchozí, dresscode/nette je Nette Coding Standard, tedy PER s tabulátory a několika odchylkami. Kde se pravidlo se specifikací rozchází, dostane volbu, ne výjimku v presetu, takže tytéž volby jsou k dispozici i vám.

Presety se skládají: potomek přebírá rodiče a přepisuje celé položky. Vaše konfigurace je poslední vrstva, která přepisuje presety. O tom, jak přesně se vrstvy skládají, je stránka Konfigurace.

Průchody do ustálení místo priorit

Když dvě pravidla sahají na totéž místo, záleží na pořadí. PHP CS Fixer to řeší ručně udržovanými prioritami: většina jeho fixerů nese číslo od −100 do 100 a v komentáři prózu o tom, po kom musí běžet. Když si píšete vlastní fixer, číslo hádáte.

DressCode priority nemá. Pustí všechna pravidla ve třech fázích (napřed strukturní změny, pak formátování, nakonec úklid bílých znaků), a pokud některé strom změnilo, pustí je znovu, dokud se strom nepřestane měnit. Od pravidel to chce dvě vlastnosti: musí být idempotentní (nad vlastním výstupem už nic nemění) a lokální (nezávisí na pořadí průchodu). Obojí se dá testovat a RuleTester to testuje za vás. A kdyby se dvě pravidla přetahovala, engine to pozná podle toho, že se strom vrátil do stavu, ve kterém už byl, a vypíše, která to byla a v čem se neshodla.

Důsledek, který stojí za zapamatování: co běh ohlásí, závisí na tom, která pravidla jsou zapnutá. Pravidlo, které tvar kódu opraví dřív, sebere hlášení pravidlu, které by na něj narazilo později. Není to chyba, je to definice: hlásí se, co zbylo.

Mezera má jednoho vlastníka

Bílé znaky mezi dvěma tokeny jsou v každém nástroji na styl zdrojem sporů: jedno pravidlo chce mezeru za čárkou, druhé zarovnává sloupce, třetí láme dlouhý řádek. V DressCode žádné pravidlo mezeru nepřepisuje. Pravidlo o mezerách vysloví nárok: mezi tímhle a tamtím tokenem má být jedna mezera, nebo zalomení řádku, nebo dva prázdné řádky. Engine nároky posbírá, rozhodne, opraví a hlášení vypíše pod jménem pravidla, jehož nárok vyhrál.

Pro vás z toho plyne:

  • Hlášení o mezeře nese vždy jméno jednoho konkrétního pravidla, i když se místa dotýká víc pravidel. To jméno vypnete nebo nastavíte.
  • Dvě pravidla, která by nárokovala tutéž složku téže mezery, jsou chyba konfigurace, ne tichá loterie. Nastane to jen u pluginů; vestavěná pravidla se nekříží.
  • Odsazení řádků má jediného vlastníka, pravidlo indentation, a určuje ho ze stromu, ne z toho, jak byl odsazený řádek nad ním. Špatně odsazený řádek proto nestrhne řádky pod sebou.

Kdo píše vlastní pravidlo o mezerách, najde podrobnosti na stránce Pravidla o mezerách.

Verze PHP je vlastnost projektu

Pravidlo, které zapisuje syntaxi novějšího PHP (třeba 0o755 z PHP 8.1), se nesmí zapnout v projektu, který na té verzi ještě neběží. DressCode proto cílovou verzi bere z projektu, ne z interpretu, který ho spouští: z phpVersion v konfiguraci, jinak z require.php v composer.json, jinak PHP 8.0 jako nejnižší podporované. Stejný soubor tak dostane stejný verdikt z jakéhokoli počítače. Pravidlo pro novější syntaxi se pod svou verzí samo vynechá; preset ho hlídat nemusí.

Co z toho plyne pro konfiguraci

  • Zapínáte a nastavujete pravidla jménem, které vidíte ve výpisu. Nic jiného než jméno a volby k nastavení není.
  • Pořadí pravidel v konfiguraci nehraje roli.
  • Když vypnete pravidlo, může se objevit hlášení jiného pravidla, které dosud nedostalo šanci. Je to totéž místo v kódu, jen ho teď hlásí někdo jiný.
  • Výjimka pro cestu nebo řádek opravdu vypne i opravu.
verze: 1.0