Witajcie ponownie. Z tej strony . Dziś podzielę się z Wami jednym z moich problemów, a mianowicie – co zrobić, gdy projekt pisze wielu programistów o różnym poziomie, na przykład aplikacja Angular.

Tak się jakoś złożyło, że przez długi czas pracowałem tylko z moim zespołem, gdzie od dawna ustalaliśmy zasady formatowania, komentowania, wcięć itd. Przywykliśmy do nich i żyliśmy zgodnie i szczęśliwie. Z okazji radości nawet opublikowałem artykuł na Habra o . Dlatego z jakiegoś magicznego powodu używaliśmy tylko tslint w pre-commit.
I wtedy się rozrośliśmy. Pojawił się nowy projekt z odziedziczonym kodem, a w dodatku nowych programistów w liczbie 4 dobrych facetów. I coś tu poszło nie tak.

Myślę, że wielu wie, że praca z odziedziczonym kodem to nie przyjemność. W mojej pamięci miałem tylko jeden projekt, z którego byłem zachwycony, a reszta… O czym to ja?) Ach tak.
Szczerze mówiąc, architektura w projekcie pozostawiała wiele do życzenia, a komentarze i typowanie tylko nam się śniły. W pewnym momencie zasmuciłem się, że nasza dokumentacja dotycząca zasad formatowania nie działa, komentarze nie są pisane, typ – co to jest?). Trzeba było coś z tym zrobić.
Dla tych, którzy nie mogą się doczekać, aby poznać wszystkie kroki od razu:
Podzieliliśmy tslint na miękkie zasady (dla pre-commit) i twarde zasady (dla ide, aby przypominała o tym, co programiści zapomnieli zrobić)
Wprowadziliśmy automatyczną korekcję możliwych zasad z twardego tslint na pre-commit
Napisaliśmy zasady dla prettier
Tańczyliśmy z bębnem, aby uruchomić ng lint z lint-staged
Krok pierwszy – dziel i rządź
Kiedy przyszła mi do głowy myśl o zaostrzeniu zasad lintera, pomyślałem, że się powiesimy. Kod jest odziedziczony. Trzeba się w nim zorientować, a w takim objętości można się zakopać. Podjęto decyzję o stworzeniu 2. lintera dla ide, który by drażnił oczy i zmuszał do pisania jsdoc dla metod i właściwości, pisania interfejsów czy feralnego onPush itd.
Zatem w katalogu głównym zaczęły leżeć 2 pliki tslina:
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": "Używaj cloneDeep (lodash) do kopiowania obiektów"
}
],
"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
}
}W pliku src/tslint Zamieniliśmy standardowy tslint na 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`Po tym zaczęliśmy mieć problemy z podkreślonymi fragmentami, które trzeba poprawić.
Krok drugi — popraw kilka rzeczy

Z tslint są zasady z ma naprawiacz. Więc skorzystajmy z tego.
tslint --project tslint.ide_only.json --fix --forceTutaj uruchamiamy reguły surowego linera z automatycznym naprawianiem dostępnych parametrów i mówimy, aby ta komenda nie zwracała błędów (naszym celem jest w końcu wykonać automatyczne poprawki).
Krok trzeci — pisz pięknie
Kiedy każdy pisze w swoim stylu, ostatecznie staje się to męczące. Kod należy pisać tak, aby sprawiało wrażenie, że robi to jedna osoba. Dlatego dodałem prettier z następującymi ustawieniami:
.prettierr.yaml
printWidth: 200 # Maksymalna liczba znaków w wierszu
tabWidth: 2 # Liczba spacji w Tabie
singleQuote: true # Używaj pojedynczych cudzysłowów
trailingComma: all # Używaj przecinków tam, gdzie to możliwe
arrowParens: always # Funkcje strzałkowe wyglądają (x) => x
overrides:
- files: "*.ts" # Sprawdź pliki *.ts
options:
parser: typescript # Język w plikach *.tsI dodałem komendę: prettier --write --config .prettierr.yaml
Krok czwarty — Jak zamierzasz to wszystko uruchomić?
Przyjrzyjmy się teraz bardziej szczegółowo, jak to wszystko uruchomić. Aby to działało, musimy pobrać następujące biblioteki:
npm i -D prettier lint-staged huskyDzięki husky podłączymy uruchamianie naszych poleceń do haka git — pre-commit. lint-staged uruchamia nasze polecenia w zależności od zmienionych plików (dołącza również te pliki do naszych poleceń).
Chciałbym także od razu wskazać problem, z którym się spotkałem. W naszym projekcie używamy ng lint. Kiedy używamy go w połączeniu z lint-staged, pliki zmienione są dodawane do naszego polecenia. ng lint ma w tym celu flagę --files, ale jak zrozumiałem, nie widzi partii plików i potrzebuje, aby ta flaga została dodana do każdego pliku. W tym celu musiałem stworzyć plik:
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 $DESTINATIONSAby uruchomić ten plik, musimy przekazać nazwę projektu. Znajduje się ona w pliku angular.json w właściwości project. W moim przypadku to partner-account i partner-account-e2e. Potrzebuję pierwszego.
Wracam do konfiguracji. Nasz package.json wygląda teraz tak:
"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"
]
},Zwróć uwagę na lint-staged --relative. Parametr --relative jest konieczny. Teraz przy commicie uruchamiamy lint-staged. On z kolei wybiera pliki i uruchamia w zależności od nich listę poleceń.
Niestety nie zwalnia to od przeglądu kodu, ale stał się on znacznie czystszy. Zauważę, że rzadziej przypominam programistom o modyfikatorach dostępu, opisie metod i właściwości, a ich twórczość jest pisana w jednolitym stylu (no prawie 😀 ).
P.S. — Dziękuję za zdjęcia naszemu PM.
Źródło: habr.com
