Ahoj znovu. Tady je . Dnes se s vámi podělím o jednu ze svých bolestí hlavy, a to – co dělat, když na projektu pracuje mnoho různě úrovňových programátorů na příkladu aplikace Angular.

Jak se stalo, dlouho jsem pracoval jen se svým týmem, kde jsme si už dávno ujasnili pravidla formátování, komentování, odsazení atd. Užívali jsme si jich a žili jsme v harmonii a štěstí. Z radosti jsem dokonce publikoval článek na Habru o . Tak jsme nějak magicky používali pouze tslint na pre-commit.
A pak jsme se rozrostli. Objevil se nový projekt se zděděným kódem a k tomu noví vývojáři v počtu 4 hodných hochů. A tady to šlo podle plánu.

Myslím, že mnozí ví, že práce se zděděným kódem není zrovna radost. Z mé zkušenosti jsem dostal pouze jeden projekt, který mě ohromil, a ostatní… O čem to mluvím?) Ach ano.
Otevřeně řečeno, architektura v projektu zanechávala mnoho přání, a komentáře a typování nám jen snila. V nějakém okamžiku jsem byl znechucen tím, že naše dokumentace k pravidlům formátování nefunguje, komentáře se nepíšou, typ – co to je?). S tím bylo potřeba něco dělat.
Pro ty, kdo se nemohou dočkat, že se dozvědí všechny kroky najednou:
Rozdělili jsme tslint na měkká pravidla (pro pre-commit) a tvrdá pravidla (pro IDE, aby připomínala, co vývojáři zapomněli udělat)
Pověsili jsme na pre-commit automatickou opravu možných pravidel od tvrdého tslintu
Napsali jsme pravidla pro prettier
Tančili jsme s bubnem, abychom spustili ng lint s lint-staged
Krok první – rozděl a panuj
Když mi přišel nápad zpřísnit pravidla lintrů, pomyslel jsem si, že se oběsíme. Kód je přece zděděný. Je potřeba se v něm orientovat, a v takovém objemu se člověk může zahrabat. Bylo rozhodnuto vytvořit 2. linter pro IDE, který by zraňoval oči a donutil psát jsdoc pro metody a vlastnosti, psát rozhraní nebo prokletý onPush atd.
A tak máme v kořenovém adresáři 2 tslint soubory:
tsconfig.json
{
"rulesDirectory": [
"node_modules/codelyzer"
],
"rules": {
"arrow-return-shorthand": true,
"callable-types": true,
"class-name": true,
"comment-format": [
true,
"check-space"
],
"curly": true,
"deprecation": {
"severity": "warn"
},
"eofline": true,
"forin": true,
"import-blacklist": [
true,
"rxjs/Rx"
],
"import-spacing": true,
"indent": [
true,
"spaces"
],
"interface-over-type-literal": true,
"label-position": true,
"max-line-length": [
true,
200
],
"member-access": false,
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-arg": true,
"no-bitwise": true,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-construct": true,
"no-debugger": true,
"no-duplicate-super": true,
"no-empty": false,
"no-empty-interface": true,
"no-eval": true,
"no-inferrable-types": [
false,
"ignore-params"
],
"no-duplicate-imports": true,
"no-misused-new": true,
"no-non-null-assertion": true,
"no-redundant-jsdoc": true,
"no-shadowed-variable": false,
"no-string-literal": false,
"no-string-throw": true,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": [
true,
"ignore-comments",
"ignore-jsdoc"
],
"no-unnecessary-initializer": true,
"no-unused-expression": true,
"no-use-before-declare": false,
"no-var-keyword": true,
"object-literal-sort-keys": false,
"one-line": [
true,
"check-open-brace",
"check-catch",
"check-else",
"check-whitespace"
],
"prefer-const": true,
"quotemark": [
true,
"single"
],
"radix": false,
"semicolon": [
true,
"always"
],
"triple-equals": [
true,
"allow-null-check"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"unified-signatures": true,
"variable-name": false,
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
],
"directive-selector": [
true,
"attribute",
"app",
"camelCase"
],
"component-selector": [
true,
"element",
"app",
"kebab-case"
],
"no-output-on-prefix": false,
"no-inputs-metadata-property": true,
"no-outputs-metadata-property": true,
"no-host-metadata-property": true,
"no-input-rename": false,
"no-output-rename": true,
"use-lifecycle-interface": true,
"use-pipe-transform-interface": true,
"component-class-suffix": true,
"directive-class-suffix": true,
"no-consecutive-blank-lines": true
}
}tslint.ide_only.json
{
"rulesDirectory": [
"node_modules/codelyzer"
],
"rules": {
"completed-docs": [
true,
{
"properties": true,
"methods": true
}
],
"no-angle-bracket-type-assertion": true,
"no-any": true,
"prefer-output-readonly": true,
"prefer-on-push-component-change-detection": true,
"array-type": [
true,
"array"
],
"typedef": [
true,
"call-signature",
"arrow-call-signature"
],
"arrow-return-shorthand": true,
"callable-types": true,
"class-name": true,
"comment-format": [
true,
"check-space"
],
"curly": true,
"deprecation": {
"severity": "warn"
},
"eofline": true,
"forin": true,
"import-blacklist": [
true,
"rxjs/Rx"
],
"import-spacing": true,
"indent": [
true,
"spaces"
],
"interface-over-type-literal": true,
"label-position": true,
"max-line-length": [
true,
200
],
"member-access": [
true,
"check-parameter-property",
"check-accessor"
],
"member-ordering": [
true,
{
"order": [
"public-static-field",
"protected-static-field",
"private-static-field",
"public-instance-field",
"protected-instance-field",
"private-instance-field",
"constructor",
"public-static-method",
"protected-static-method",
"private-static-method",
"public-instance-method",
"protected-instance-method",
"private-instance-method"
]
}
],
"no-arg": true,
"no-bitwise": true,
"no-console": true,
"no-construct": true,
"no-debugger": true,
"no-duplicate-super": true,
"no-empty": false,
"no-empty-interface": true,
"no-duplicate-switch-case": true,
"no-eval": true,
"no-inferrable-types": [
false,
"ignore-params"
],
"no-duplicate-imports": true,
"one-variable-per-declaration": true,
"no-misused-new": true,
"no-non-null-assertion": true,
"prefer-template": [
true,
"allow-single-concat"
],
"ordered-imports": true,
"no-redundant-jsdoc": true,
"no-shadowed-variable": false,
"no-string-literal": false,
"no-string-throw": true,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": [
true,
"ignore-comments",
"ignore-jsdoc"
],
"ban": [
true,
{
"name": [
"Object",
"assign"
],
"message": "Používejte cloneDeep (lodash) k kopírování objektu"
}
],
"max-classes-per-file": [
true,
1
],
"cyclomatic-complexity": [
true,
6
],
"static-this": true,
"no-unnecessary-initializer": true,
"no-unused-expression": true,
"no-var-keyword": true,
"object-literal-sort-keys": false,
"one-line": [
true,
"check-open-brace",
"check-catch",
"check-else",
"check-whitespace"
],
"prefer-const": true,
"quotemark": [
true,
"single"
],
"radix": false,
"semicolon": [
true,
"always"
],
"triple-equals": [
true,
"allow-null-check"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"unified-signatures": true,
"variable-name": false,
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
],
"directive-selector": [
true,
"attribute",
"app",
"camelCase"
],
"component-selector": [
true,
"element",
"app",
"kebab-case"
],
"no-output-on-prefix": false,
"no-inputs-metadata-property": true,
"no-outputs-metadata-property": true,
"no-host-metadata-property": true,
"no-input-rename": false,
"no-output-rename": true,
"use-lifecycle-interface": true,
"use-pipe-transform-interface": true,
"component-class-suffix": true,
"directive-class-suffix": true,
"no-consecutive-blank-lines": true
}
}V souboru src/tslint nahradili jsme standardní tslint za ide
src/tslint.json
{
"extends": "../tslint.ide_only.json",
"rules": {
"directive-selector": [
true,
"attribute",
"app",
"camelCase"
],
"component-selector": [
true,
"element",
"app",
"kebab-case"
]
}
}И поправил запуск нашего линтера в скритах package.json
ng lint --tslint-config ./tslint.json --fix`Poté jsme začali narážet na zdůrazněné záležitosti, které je třeba opravit.
Druhý krok — opravit pár detailů

Tslint má pravidla s má opraváře. Tak se pojďme využít.
tslint --project tslint.ide_only.json --fix --forceZde spouštíme pravidla přísného lintru s automatickým opravováním dostupných parametrů a říkáme, aby tento příkaz nevracel chyby (naším cílem je přece dělat automatické opravy).
Třetí krok — piš hezky
Když každý píše svým způsobem, na konci to vyčerpává. Kód by měl být psán tak, aby vypadalo, že ho píše jedna osoba. K tomu jsem připojil prettier s následujícími nastaveními:
.prettierr.yaml
printWidth: 200 # Maximální počet znaků v řádku
tabWidth: 2 # Mezera v tabulátoru
singleQuote: true # Používat jednoduché uvozovky
trailingComma: all # Používat čárky, kde je to možné
arrowParens: always # Šipkové funkce vypadají (x) => x
overrides:
- files: "*.ts" # Kontrola souborů *.ts
options:
parser: typescript # Jazyk v souborech *.tsA přidal jsem příkaz: prettier --write --config .prettierr.yaml
Čtvrtý krok — A jak to všechno spustíš?
Pojďme nyní podrobněji prozkoumat, jak to všechno spustit. Aby to všechno fungovalo, potřebujeme stáhnout následující knihovny:
npm i -D prettier lint-staged huskyPomocí husky připojíme spouštění našich příkazů na git hook — pre-commit. lint-staged nám bude spouštět příkazy v závislosti na změněných souborech (také tyto soubory vložíme do našich příkazů).
Chtěl bych také hned nastínit problém, na který jsem narazil. V našem projektu používáme ng lint. Když jej používáme v kombinaci s lint-staged, přidávají se nám do příkazu změněné soubory. Ng lint má pro to klíč --files, ale jak jsem pochopil, nevidí balík souborů, a je třeba pro každý soubor tento klíč přidávat. K tomu jsem musel vytvořit soubor:
lint.sh
#!/bin/bash
PROJECT=$1
shift
SOURCES=$@
DESTINATIONS=""
DELIMITER=""
for src in $SOURCES
do
DELIMITER=" --files "
DESTINATIONS="$DESTINATIONS$DELIMITER${src}"
done
ng lint $PROJECT --tslint-config ./tslint.json $DESTINATIONSPro spuštění tohoto souboru musíme předat název projektu. Ten se nachází v souboru angular.json ve vlastnosti project. V mém případě je to partner-account a partner-account-e2e. Potřebuji první.
Vrátím se k nastavení. Náš package.json nyní vypadá takto:
"husky": {
"hooks": {
"pre-commit": "lint-staged --relative"
}
},
"lint-staged": {
"*.{ts,js}": [
"prettier --write --config .prettierr.yaml",
"tslint --project tslint.ide_only.json --fix --force",
"sh lint.sh partner-account",
"git add"
],
"*.{html,scss,css}": [
"prettier --write --config .prettierr.yaml",
"git add"
]
},Vezměte na vědomí lint-staged --relative. Parametr --relative je povinný. Nyní se při commitu spouští lint-staged. Ten na oplátku vybírá soubory a spouští seznam příkazů v závislosti na nich.
Bohužel to nezrušuje kontrolu kódu, ale výrazně to zlepšilo čistotu. Všiml jsem si, že jsem méně často připomínal vývojářům modifikátory přístupu, popisy metod a vlastností, a jejich kreativita začala být napsaná v jednotném stylu (no téměř 😀 ).
P.S. — Děkuji našemu PM za obrázky.
Zdroj: habr.com
