И снова здравствуйте. На связи Омельницкий Сергей. Сегодня я поделюсь с Вами одной из своих головных болей, а именно — что делать, когда проект пишут много разноуровневых программистов на примере angular приложения.
Так повелось, что я долгое время работал только со своей командой, где мы уже давно согласовывали правила оформления, комментирования, отступы и т.п. Притерлись к ним и жили дружно и счастливо. На радостях я даже опубликовал статью на Хабр по нашему кодстайлу. Поэтому из чего-то магического мы использовали только tslint на пре-коммит.
И тут мы разрослись. Появился новый проект с унаследованным кодом, а к нему в придачу новые разработчики в размере 4-х добрых молодцев. И чет тут пошло не по плану.
Я думаю многие знают, что работа с унаследованным кодом не кайф. На моей памяти я получил только один проект от которого был в восторге, а остальное… Так о чем я?) Ах да.
Откровенно говоря архитектура в проекте оставляла желать лучшего, а комментарии и типизация нам только снилась. В какой-то момент я приуныл от того, что наша документашка по правилам оформления не работает, комментарии не пишутся, тип — что это?). Вот с этим нужно было что-то делать.
Мы разделили tslint на мягкие правила ( для pre-commit ) и жесткие правила ( для ide, чтоб напоминала о том, что разработчики забыли сделать )
Повесили на pre-commit автофиксацию возможных правил от жесткого tslint
Написали правила для prettier
Танцевали с бубном чтоб запустить ng lint с lint-staged
Шаг первый — разделяй и властвуй
Когда мне пришла идея ужесточить правила линтера я подумал, что мы повесимся. Код-то унаследованный. В нем нужно разбираться, а в таком объеме можно закопаться. Было принято решения создать 2-й линтер для ide, которое бы мозолил глаза и заставлял писать jsdoc для методов и св-в, писать интерфейсы или зласчастный onPush и т.п.
Итак в корне у нас начало лежать 2 tslin файла:
{
"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
}
}
{
"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
{
"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 есть правила с has fixer
. Так давай воспользуемся.
tslint --project tslint.ide_only.json --fix --force
Здесь мы запускаем правила жесткого линтера с автофиксацией доступных параметров и говорим, чтобы эта команда не возвращала ошибок ( тут наша цель все-таки делать автоисправление ).
Шаг третий — пиши красиво
Когда каждый пишет в своей манере это в конечном счете утомляет. Код нужно писать так, чтоб казалось, что это делает один человек. Для этого я прикрутил prettier, со следующими настройками:
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 хук — pre-commit. lint-staged будет запускать нам команды в зависимости от измененных файлов ( так же подставлять эти файлы к нам в команды).
Хотелось бы еще сразу обрисовать проблему, с которой столкнулся я. У нас в проекте мы используем ng lint. Когда мы используем его в связке с lint-staged, то в нашу команду добавляются измененные файлы. У ng lint есть для этого ключ --files
, но, как я понял, он не видит пачку файлов, и ему нужно на каждый файл добавлять этот ключ. Для этого мне пришлось создать файл:
#!/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
. Он в свою очередь отбирает файлы и запускает в зависимости он них список команд.
К сожалению это не отменяет ревью кода, но он стал гораздо чище. Замечу, что я реже стал напоминать разработчикам про модификаторы доступа, описание методов и св-в, а их творчество стало написано в едином стиле ( ну почти :D ).
P.S. — Спасибо за картинки нашему PM.
Комментарии (5)
Valery4
23.08.2019 22:24+1Параметр --relative там обязателен
Можете пояснить почему?
У нас тоже исользуютеся Husky, только «pre-push» хук. И всё работает без ключа relative.
Возможно дело в том что у нас не Angular CLI проект и вообще не Angular?Sergamers Автор
23.08.2019 23:01+1Ага. Этот параметр необходим, чтоб путь был не от корня, а от папки проекта. Это важно только для команды `ng lint ...`, т.к. он начнет ругаться, что файлы не являются частью проекта.
ilnuribat
eslint.org/blog/2019/01/future-typescript-eslint
Есть смысл не использовать tslint, а сразу прикрутить eslint
KyKyPy3uK
Безусловно есть github.com/palantir/tslint/issues/3865#issuecomment-519302911