How I put things in order in a project where there is a forest of direct hands (tslint, prettier, etc settings)

Hello again. In touch Omelnitsky Sergey . Today I will share with you one of my headaches, namely, what to do when a lot of multilevel programmers write a project using the example of an angular application.













It so happened that for a long time I worked only with my team, where we had long agreed on the rules for formatting, commenting, indentation, etc. I got used to them and lived together happily. To celebrate, I even published an article on Habr on our code style . Therefore, from something magical, we used only tslint on the pre-commit.







And then we grew. A new project with an inherited code appeared, and to it, in addition to new developers, in the amount of 4 good fellows. And even here it didn’t go according to plan.













I think many people know that working with legacy code is not high. In my memory, I received only one project from which I was delighted, and the rest ... So what am I talking about?) Oh yes.







Frankly, the architecture in the project left much to be desired, and we only dreamed of commenting and typing. At some point, I was saddened by the fact that our document didn’t work according to the design rules, comments were not written, type - what is this?). Here it was necessary to do something with this.







For those who can’t wait to find out all the steps at once:
  • We divided tslint into soft rules (for pre-commit) and hard rules (for ide, so that it reminds us of what the developers forgot to do)







  • Hang on pre-commit autofixation of possible rules from hard tslint







  • Wrote rules for prettier







  • Danced with a tambourine to run ng lint with lint-staged









Step One - Divide and Conquer



When I got the idea to tighten the rules of the linter, I thought that we would hang ourselves. The code is inherited. You need to understand it, and in such a volume you can dig. It was decided to create a 2nd linter for ide, which would call my eyes and force me to write jsdoc for methods and sv, write interfaces or unhappy onPush, etc.







So, in the root, we started to lie 2 tslin files:







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 } }
      
      





In the src/tslint



we replaced the standard tslint with ide







src / tslint.json
 { "extends": "../tslint.ide_only.json", "rules": { "directive-selector": [ true, "attribute", "app", "camelCase" ], "component-selector": [ true, "element", "app", "kebab-case" ] } }
      
      





And fixed the launch of our linter in the scripts of package.json







 ng lint --tslint-config ./tslint.json --fix`
      
      





Then we began to hang ourselves from the underlined things that need to be corrected.







Step two - fix a couple of points









Tslint has rules with has fixer



. So let's use it.







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





Here we run the rules of the hard linter with autofixation of the available parameters and say that this command does not return errors (here our goal is still to do auto-correction).







Step Three - write beautifully



When everyone writes in his own way, it ultimately tires. The code needs to be written so that it seems that this is done by one person. For this, I screwed prettier, with the following settings:







.prettierr.yaml
 printWidth: 200 #  -    tabWidth: 2 #    singleQuote: true #    trailingComma: all #     arrowParens: always #  -  (x) => x overrides: - files: "*.ts" #   *.ts options: parser: typescript #    *.ts
      
      





And he added the command: prettier --write --config .prettierr.yaml









Step Four - And how do you command all this to run?



Let's now take a closer look at how to start it all. In order for this to work, we need to download the following:







 npm i -D prettier lint-staged husky
      
      





With husky, we will hang the launch of our commands on a git hook - pre-commit. lint-staged will run commands for us depending on the changed files (also substitute these files for us in commands).







I would also like to immediately outline the problem that I encountered. In our project we use ng lint. When we use it in conjunction with lint-staged, modified files are added to our command. Ng lint has the --files



key for this, but as I understand it, it does not see a bunch of files, and it needs to add this key to each file. To do this, I had to create a file:







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
      
      





To run this file, we must pass the name of the project. It is located in the angular.json



file in the project property. In my case, this is partner-account



and partner-account-e2e



. I need a 1st.







Back to the setup. Our package.json now looks like this:







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





Check out lint-staged --relative



. The --relative



parameter is --relative



there. Now when we commit, lint-staged



launched. He, in turn, selects files and starts a list of commands depending on them.







Unfortunately this does not cancel the code review, but it has become much cleaner. I note that I less often began to remind developers about access modifiers, a description of methods and sv, and their work began to be written in the same style (well, almost: D).







PS - Thanks for the pictures to our PM.








All Articles