Hello again. This is . Today I will share one of my headaches with you, specifically what to do when a project is being developed by multiple programmers of varying levels, using an Angular application as an example.

It has so happened that for a long time I worked only with my own team, where we had long agreed on formatting rules, commenting, indentation, etc. We got used to them and lived happily and harmoniously. Out of joy, I even published an article on Habr about . So, magically, we only used tslint during pre-commit.
And then we expanded. A new project appeared with inherited code, and along with it, four new developers joined. Suddenly, things went off track.

I think many know that working with inherited code is not fun. To my memory, I've only received one project that I was thrilled with; the rest... So, what was I saying?) Oh yes.
Frankly speaking, the architecture of the project left much to be desired, and we were only dreaming of comments and typing. At some point, I became disheartened because our documentation on formatting rules was not being followed, comments were not written, and the type—what is that?). We had to do something about it.
For those who are eager to know all the steps at once:
We divided tslint into soft rules (for pre-commit) and strict rules (for IDEs, to remind developers about things they forgot to do)
We set up auto-fixing of possible strict tslint rules on pre-commit
We wrote rules for prettier
We jumped through hoops to run ng lint with lint-staged
Step one — divide and conquer
When the idea to tighten the linter rules came to me, I thought we were going to hang ourselves. The code is inherited. It needs to be understood, and at such a scale, one can get buried in it. A decision was made to create a second linter for the IDE that would catch the eye and force developers to write jsdoc for methods and properties, write interfaces, or the infamous onPush, etc.
So at the root, we had two tslint 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": "Use cloneDeep (lodash) for object copying"
}
],
"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 file src/tslint We replaced the default 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"
]
}
}И поправил запуск нашего линтера в скритах package.json
ng lint --tslint-config ./tslint.json --fix`After which we started getting frustrated with the highlighted items that need fixing.
Step two — fix a couple of points

tslint has rules with has fixer. So let's make use of that.
tslint --project tslint.ide_only.json --fix --forceHere we run the strict linter rules with auto-fixing available parameters and instruct it to not return errors (our goal is still to do auto-correction).
Step three — write beautifully
When everyone writes in their own style, it ultimately gets tiring. Code should be written in such a way that it seems like it's done by one person. For this, I attached prettier with the following settings:
.prettierr.yaml
printWidth: 200 # Maximum number of characters in a line
tabWidth: 2 # Spaces in the Tab
singleQuote: true # Use single quotes
trailingComma: all # Use commas where possible
arrowParens: always # Arrow functions look like (x) => x
overrides:
- files: "*.ts" # Checking *.ts files
options:
parser: typescript # Language in *.ts filesAnd added the command: prettier --write --config .prettierr.yaml
Step four — And how do you command all this to run?
Now let's take a closer look at how to run all of this. To make this work, we need to install the following libraries:
npm i -D prettier lint-staged huskyWith husky, we will hook our commands to a git hook — pre-commit. lint-staged will execute our commands depending on the changed files (also substituting those files into our commands).
I would also like to outline a problem I faced. In our project, we use ng lint. When we use it in conjunction with lint-staged, the changed files are added to our command. ng lint has a key for this --files, but as I understand, it does not see a batch of files and needs to add this key for each file. For 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 $DESTINATIONSTo run this file, we need to pass the project name. It is located in the file angular.json in the project property. In my case, it is partner-account and partner-account-e2e. I need the first one.
Returning to the settings. 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"
]
},Note the lint-staged --relative. The parameter --relative is required. Now on commit, we run lint-staged. It selects files and runs the command list based on them.
Unfortunately, this doesn't eliminate code reviews, but they have become much cleaner. I’ve noticed that I remind developers less often about access modifiers, method descriptions, and properties, and their creativity is now written in a unified style (well, almost 😀).
P.S. — Thanks for the pictures to our PM.
Source: habr.com
