Как я наводил порядок в проекте, где лес прямых рук (настройки tslint, prettier, etc)

от автора

И снова здравствуйте. На связи Омельницкий Сергей. Сегодня я поделюсь с Вами одной из своих головных болей, а именно — что делать, когда проект пишут много разноуровневых программистов на примере angular приложения.

Так повелось, что я долгое время работал только со своей командой, где мы уже давно согласовывали правила оформления, комментирования, отступы и т.п. Притерлись к ним и жили дружно и счастливо. На радостях я даже опубликовал статью на Хабр по нашему кодстайлу. Поэтому из чего-то магического мы использовали только tslint на пре-коммит.

И тут мы разрослись. Появился новый проект с унаследованным кодом, а к нему в придачу новые разработчики в размере 4-х добрых молодцев. И чет тут пошло не по плану.

Я думаю многие знают, что работа с унаследованным кодом не кайф. На моей памяти я получил только один проект от которого был в восторге, а остальное… Так о чем я?) Ах да.

Откровенно говоря архитектура в проекте оставляла желать лучшего, а комментарии и типизация нам только снилась. В какой-то момент я приуныл от того, что наша документашка по правилам оформления не работает, комментарии не пишутся, тип — что это?). Вот с этим нужно было что-то делать.

Для тех кому не терпится узнать все шаги сразу:

  • Мы разделили tslint на мягкие правила ( для pre-commit ) и жесткие правила ( для ide, чтоб напоминала о том, что разработчики забыли сделать )

  • Повесили на pre-commit автофиксацию возможных правил от жесткого tslint

  • Написали правила для 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 есть правила с has fixer. Так давай воспользуемся.

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 хук — 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.


ссылка на оригинал статьи https://habr.com/ru/post/464383/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *