И снова здравствуйте. На связи Омельницкий Сергей. Сегодня я поделюсь с Вами одной из своих головных болей, а именно — что делать, когда проект пишут много разноуровневых программистов на примере 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. Он в свою очередь отбирает файлы и запускает в зависимости он них список команд.
К сожалению это не отменяет ревью кода, но он стал гораздо чище. Замечу, что я реже стал напоминать разработчикам про модификаторы доступа, описание методов и св-в, а их творчество стало написано в едином стиле ( ну почти 😀 ).
P.S. — Спасибо за картинки нашему PM.
ссылка на оригинал статьи https://habr.com/ru/post/464383/
Добавить комментарий