Как наводих ред в проекта, където е гора от директни ръце (настройки tslint, prettier и т.н.)

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

Как наводих ред в проекта, където е гора от директни ръце (настройки tslint, prettier и т.н.)

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

И тук се разраснахме. Появи се нов проект с наследен код, а в допълнение нови разработчици в размер на 4 добри момчета. И нещо тук не тръгна по план.

Как наводих ред в проекта, където е гора от директни ръце (настройки tslint, prettier и т.н.)

Мисля, че много знаят, че работата с наследен код не е приятна. Спомням си един проект, от който бях възхитен, а останалото... Та за какво говорех?) Ах да.

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

За тези, които не търпят да разберат всички стъпки веднага:

  • Разделихме 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, prettier и т.н.)

Този 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

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster