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.