Jak uporządkowałem projekt, w którym panował chaos (ustawienia tslint, prettier itd.)

Witajcie ponownie. Z tej strony Sergiej Omielnicki. Dziś podzielę się z Wami jednym z moich problemów, a mianowicie – co zrobić, gdy projekt pisze wielu programistów o różnym poziomie, na przykład aplikacja Angular.

Jak uporządkowałem projekt, w którym panował chaos (ustawienia tslint, prettier itd.)

Tak się jakoś złożyło, że przez długi czas pracowałem tylko z moim zespołem, gdzie od dawna ustalaliśmy zasady formatowania, komentowania, wcięć itd. Przywykliśmy do nich i żyliśmy zgodnie i szczęśliwie. Z okazji radości nawet opublikowałem artykuł na Habra o naszym stylu kodowania. Dlatego z jakiegoś magicznego powodu używaliśmy tylko tslint w pre-commit.

I wtedy się rozrośliśmy. Pojawił się nowy projekt z odziedziczonym kodem, a w dodatku nowych programistów w liczbie 4 dobrych facetów. I coś tu poszło nie tak.

Jak uporządkowałem projekt, w którym panował chaos (ustawienia tslint, prettier itd.)

Myślę, że wielu wie, że praca z odziedziczonym kodem to nie przyjemność. W mojej pamięci miałem tylko jeden projekt, z którego byłem zachwycony, a reszta… O czym to ja?) Ach tak.

Szczerze mówiąc, architektura w projekcie pozostawiała wiele do życzenia, a komentarze i typowanie tylko nam się śniły. W pewnym momencie zasmuciłem się, że nasza dokumentacja dotycząca zasad formatowania nie działa, komentarze nie są pisane, typ – co to jest?). Trzeba było coś z tym zrobić.

Dla tych, którzy nie mogą się doczekać, aby poznać wszystkie kroki od razu:

  • Podzieliliśmy tslint na miękkie zasady (dla pre-commit) i twarde zasady (dla ide, aby przypominała o tym, co programiści zapomnieli zrobić)

  • Wprowadziliśmy automatyczną korekcję możliwych zasad z twardego tslint na pre-commit

  • Napisaliśmy zasady dla prettier

  • Tańczyliśmy z bębnem, aby uruchomić ng lint z lint-staged

Krok pierwszy – dziel i rządź

Kiedy przyszła mi do głowy myśl o zaostrzeniu zasad lintera, pomyślałem, że się powiesimy. Kod jest odziedziczony. Trzeba się w nim zorientować, a w takim objętości można się zakopać. Podjęto decyzję o stworzeniu 2. lintera dla ide, który by drażnił oczy i zmuszał do pisania jsdoc dla metod i właściwości, pisania interfejsów czy feralnego onPush itd.

Zatem w katalogu głównym zaczęły leżeć 2 pliki tslina:

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": "Używaj cloneDeep (lodash) do kopiowania obiektów"
      }
    ],
    "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
  }
}

W pliku src/tslint Zamieniliśmy standardowy tslint na 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`

Po tym zaczęliśmy mieć problemy z podkreślonymi fragmentami, które trzeba poprawić.

Krok drugi — popraw kilka rzeczy

Jak uporządkowałem projekt, w którym panował chaos (ustawienia tslint, prettier itd.)

Z tslint są zasady z ma naprawiacz. Więc skorzystajmy z tego.

tslint --project tslint.ide_only.json --fix --force

Tutaj uruchamiamy reguły surowego linera z automatycznym naprawianiem dostępnych parametrów i mówimy, aby ta komenda nie zwracała błędów (naszym celem jest w końcu wykonać automatyczne poprawki).

Krok trzeci — pisz pięknie

Kiedy każdy pisze w swoim stylu, ostatecznie staje się to męczące. Kod należy pisać tak, aby sprawiało wrażenie, że robi to jedna osoba. Dlatego dodałem prettier z następującymi ustawieniami:

.prettierr.yaml

printWidth: 200     # Maksymalna liczba znaków w wierszu
tabWidth: 2         # Liczba spacji w Tabie
singleQuote: true   # Używaj pojedynczych cudzysłowów
trailingComma: all  # Używaj przecinków tam, gdzie to możliwe
arrowParens: always # Funkcje strzałkowe wyglądają (x) => x
overrides:
  - files: "*.ts"   # Sprawdź pliki *.ts
    options:
      parser: typescript  # Język w plikach *.ts

I dodałem komendę: prettier --write --config .prettierr.yaml

Krok czwarty — Jak zamierzasz to wszystko uruchomić?

Przyjrzyjmy się teraz bardziej szczegółowo, jak to wszystko uruchomić. Aby to działało, musimy pobrać następujące biblioteki:

npm i -D prettier lint-staged husky

Dzięki husky podłączymy uruchamianie naszych poleceń do haka git — pre-commit. lint-staged uruchamia nasze polecenia w zależności od zmienionych plików (dołącza również te pliki do naszych poleceń).

Chciałbym także od razu wskazać problem, z którym się spotkałem. W naszym projekcie używamy ng lint. Kiedy używamy go w połączeniu z lint-staged, pliki zmienione są dodawane do naszego polecenia. ng lint ma w tym celu flagę --files, ale jak zrozumiałem, nie widzi partii plików i potrzebuje, aby ta flaga została dodana do każdego pliku. W tym celu musiałem stworzyć plik:

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

Aby uruchomić ten plik, musimy przekazać nazwę projektu. Znajduje się ona w pliku angular.json w właściwości project. W moim przypadku to partner-account i partner-account-e2e. Potrzebuję pierwszego.

Wracam do konfiguracji. Nasz package.json wygląda teraz tak:

  "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"
    ]
  },

Zwróć uwagę na lint-staged --relative. Parametr --relative jest konieczny. Teraz przy commicie uruchamiamy lint-staged. On z kolei wybiera pliki i uruchamia w zależności od nich listę poleceń.

Niestety nie zwalnia to od przeglądu kodu, ale stał się on znacznie czystszy. Zauważę, że rzadziej przypominam programistom o modyfikatorach dostępu, opisie metod i właściwości, a ich twórczość jest pisana w jednolitym stylu (no prawie 😀 ).

P.S. — Dziękuję za zdjęcia naszemu PM.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster