Здравейте отново. На линия! . Днес ще споделя с вас една от моите главоболия, а именно — какво да правим, когато проектът се пише от много разноуровневи програмисти на примера на приложение на angular.

Така се случи, че дълго време работех само с моя екип, където отдавна бяхме се съгласили по правилата за оформление, коментиране, отстъпи и т.н. Притеряхме се към тях и живеехме дружно и щастливо. От радост дори публикувах статия в Хабр по . Затова магически използвахме само tslint на пре-комит.
И тук се разраснахме. Появи се нов проект с наследен код, а в допълнение нови разработчици в размер на 4 добри момчета. И нещо тук не тръгна по план.

Мисля, че много знаят, че работата с наследен код не е приятна. Спомням си един проект, от който бях възхитен, а останалото... Та за какво говорех?) Ах да.
Откровено казано, архитектурата в проекта оставяше да се желае, а коментарите и типизацията само ни се сънуваха. В някакъв момент се приуных, че нашата документация по правилата за оформление не работи, коментарите не се пишат, тип — какво е това?). Е, с това нещо трябваше нещо да се направи.
За тези, които не търпят да разберат всички стъпки веднага:
Разделихме tslint на меки правила (за pre-commit) и строги правила (за ide, за да напомня на разработчиците какво са забравили да направят)
Поставихме автофиксация на възможни правила от строгия tslint на pre-commit
Написахме правила за prettier
Танцувахме с бубна, за да стартираме ng lint с lint-staged
Първа стъпка — разделяй и властвай
Когато ми дойде идеята да затегна правилата на линтера, помислих, че ще се обесим. Кодът е наследен. Трябва да се разбира, а в такъв обем може да се затисне. Беше взето решение да се създаде 2-ри линтер за ide, който да дразни очите и да принуждава да се пише jsdoc за методи и свойства, да се пишат интерфейси или злочестивия onPush и т.н.
И така в корена ни имаше 2 tslin файла:
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": "Използвайте cloneDeep (lodash) за дублиране на обект"
}
],
"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
}
}В файла src/tslint Заменихме стандартния tslint с 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`След това започнахме да се улавяме от подчертаните неща, които трябва да се коригират.
Стъпка втора — коригирайте няколко неща

Този tslint има правила с има коригиране. Така че нека го използваме.
tslint --project tslint.ide_only.json --fix --forceТук стартираме правилата на стриктния линтер с автоматично коригиране на наличните параметри и казваме, че тази команда да не връща грешки (в крайна сметка целта ни е да направим автоизправяне).
Стъпка трета — пишете красиво
Когато всеки пише по своя начин, накрая става изтощително. Кодът трябва да се пише така, че да изглежда, че го прави един човек. Затова добавих prettier с следните настройки:
.prettierr.yaml
printWidth: 200 # Максимален брой символи на ред
tabWidth: 2 # Пробели в таба
singleQuote: true # Използвайте единични кавички
trailingComma: all # Използвайте запетаи, където е възможно
arrowParens: always # Стрелковите функции изглеждат (x) => x
overrides:
- files: "*.ts" # Проверка на файлове *.ts
options:
parser: typescript # Език в файлове *.tsИ добавих команда: prettier --write --config .prettierr.yaml
Стъпка четвърта — Как да задействам всичко това?
Нека сега по-подробно разгледаме как да задействаме всичко това. За да работи всичко, трябва да изтеглим следните библиотеки:
npm i -D prettier lint-staged huskyС помощта на husky ще свържем стартирането на нашите команди с git hook — pre-commit. lint-staged ще стартира командите ни в зависимост от променените файлове (така ще подставя тези файлове в командите).
Искам също така да очертая проблема, с който се сблъсках. В нашия проект използваме ng lint. Когато го използваме в комбинация с lint-staged, в нашата команда се добавят променените файлове. ng lint има за това ключ --files, но както разбрах, той не вижда пакет от файлове и му е нужно да добавя този ключ за всеки файл. Затова ми се наложи да създам файл:
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 $DESTINATIONSЗа да стартираме този файл, трябва да предадем името на проекта. То се намира в файла angular.json в свойството project. В моя случай това е partner-account и partner-account-e2e. Нужен ми е 1-ят.
Връщам се към настройката. Нашият package.json сега изглежда така:
"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"
]
},Обърнете внимание на lint-staged --relative. Параметърът --relative е задължително. Сега при комит ние стартираме lint-staged. Той от своя страна избира файловете и стартира списъка с команди в зависимост от тях.
За съжаление това не отменя ревюто на кода, но стана много по-чисто. Ще отбележа, че по-рядко напомням на разработчиците за модификаторите на достъп, описанието на методите и свойствата, а тяхната креативност бе написана в единен стил (ну почти 😀 ).
P.S. — Благодаря за снимките на нашия PM.
Източник: habr.com
