Přepis pravidla z jiného nástroje

Jak přepsat sniff z PHP_CodeSniffer nebo fixer z PHP CS Fixeru: specifikaci vzít z testů a ne ze zdrojáku, přeložit otázky z tokenů na strom a zahodit obranný kód, který nad stromem nemá co dělat.

Pro koho to je

Máte vlastní sniff (pravidlo PHP_CodeSniffer) nebo fixer (pravidlo PHP CS Fixeru), který léta hlídá něco, co žádný standard neumí, a chcete ho mít i tady. Tenhle postup je napsaný tak, aby se podle něj dalo pracovat krok za krokem, a stejně dobře ho zvládne agent, kterému cizí pravidlo předložíte. Většinu vestavěných pravidel DressCode ostatně takhle přepsali agenti.

Nejdřív si ale ověřte, jestli přepis vůbec potřebujete: dresscode rules u každého pravidla vypíše jména z PHP CS Fixeru, PHP_CodeSniffer a Slevomatu, která pokrývá. Přepisujte jen to, co v tom seznamu není.

1. Specifikaci vezměte z testů, ne ze zdrojáku

Zdrojový kód cizího pravidla popisuje, jak se ta věc obchází nad plochým polem tokenů: kde se couvá, co se počítá, kdy to pravidlo vzdá. Přesně tahle informace je tady k ničemu. Co cenu má, jsou testovací případy: dvojice kódu před opravou a po ní a hraniční případy, které autor za roky nasbíral. Ty přeneste skoro doslova do fixtur. Fixtura je vaše zadání a zároveň důkaz, že jste cestou nic neztratili.

2. Přeložte otázky z tokenů na strom

Každý cyklus přes tokeny je v originále ve skutečnosti otázka na strukturu kódu. Tenhle překlad se opakuje pořád dokola:

v originále v DressCode
getPrevMeaningfulToken() v cyklu, dokud se nenajde začátek výrazu slot uzlu: $node->condition, $node->arguments, nebo $node->parent
ruční počítání závorek, aby se našel konec bloku sloty openParen a closeParen, openBrace a closeBrace
T_STRING a hádání z kontextu, co to vlastně je konkrétní třída uzlu: NameNode, IdentifierNode, FunctionCallNode
vlastní pomocník na „je to globální funkce“ a „v jakém jsme jmenném prostoru“ NameResolver::isGlobalFunctionCall(), getUnqualifiedResolution(), getNamespace(), resolveClass()
vlastní pomocník na „jsme uvnitř metody“ a „je tu $this Scope::getFunction(), getClass(), hasThis()
porovnání dvou úseků tokenů Node::matches()
kontrola, že v úseku není ++, volání a podobně Node::isRepeatableRead()
hledání komentáře mezi tokeny Token::hasCommentUpTo(), Node::hasComment()
$phpcsFile->addFixableError() a pak $fixer->replaceToken() report() v podmínce a teprve za ní zápis do slotu nebo replaceWith()
Tokens::insertAt() s ručně sestavenými tokeny Parser::parseExpression() nebo parseStatement() a insert() do seznamu

Kde originál pracoval s indexy tokenů, pracujte s uzly; kde četl text, ptejte se stromu. Které uzly a sloty existují, říká přehled uzlů.

3. Zahoďte obranný kód

Velká část cizího pravidla existuje jen proto, aby se nespletlo: aby [ nebylo přístupem k prvku místo pole, aby se nepočítala závorka uvnitř řetězce, aby komentář uprostřed volání nerozbil hledání. Nad stromem taková možnost nevzniká, takže ten kód nemá co přenášet. Pokušení překládat originál řádek po řádku je silné, zvlášť pro agenta, a proti němu stojí jednoduchá zkouška: každý řádek nového pravidla musí odpovídat na otázku o kódu, ne o tokenech. Řádek, který řeší, co všechno může stát mezi dvěma tokeny, jde ven.

Výsledek bývá několikanásobně kratší. Pravidlo, které má po přepisu přes sto řádků, je podezřelé: buď dělá víc věcí najednou a má se rozdělit, nebo v něm zůstal překlad místo přepisu.

4. Bezpečnost řešte jinak než originál

Originál často hlídal vedlejší účinky výčtem: T_INC, T_DEC, možná volání. Tady je na to isRepeatableRead(), které je přísnější i přesnější. Pravidlo, které bylo v PHP CS Fixeru označené jako risky (rizikové), po přepisu risky být nemusí, pokud se ptá stromu. Kde bezpečný případ z kódu poznat nejde, hlaste opravu jako rizikovou: report(..., risky: true) u jednoho výskytu, nebo risky: true v RuleInfo, když je riziková každá oprava; udělá se pak jen se svolením projektu. Pravidlo, které mění význam programu ze své podstaty, zůstane rozhodnutím uživatele a jeho stránka to má říct.

5. Jméno a stará jména

Nové pravidlo dostane jméno podle konvence (vendor/slug, stav, ne krok). Jméno původního sniffu nebo fixeru si ale uživatelé ponesou v komentářích phpcs:ignore. Cizí jména překládá DressCode jen pro vestavěná pravidla, takže u vlastního pravidla je v kódu nahraďte: migrate-suppressions vypíše, která jména nezná, a to je přesně ten seznam.

6. Ověřte na cizích fixturách i na vlastních

Prošly cizí testovací případy? Pak přidejte to, co v nich chybělo, protože strom vidí víc: komentář uvnitř konstrukce, konstrukci přes několik řádků, alternativní syntaxi, kód prokládaný kusy HTML. Nakonec pusťte fix nad větším cizím kódem a podívejte se na diff.

Licence

Přenášíte chování a testovací případy, ne kód. PHP_CodeSniffer je pod licencí BSD-3-Clause, PHP CS Fixer a Slevomat pod MIT; obojí dovoluje odvozenou práci s uvedením autorství. Fixtury převzaté z cizího projektu označte v hlavičce souboru původem a licencí. Pravidlo napsané podle tohoto postupu cizí kód neobsahuje, protože z něj nakonec nezbylo co přenášet.