Introduction
In software development, when developers work on a project for a period of time, they tend to add code smell into the code that Eslint rules identify as problems. When architect adds a new Eslint rule to fix these errors, npm run lint
reports error messages in terminal that require immediate attention.
I have two approaches to handle Eslint errors at work.
When npm run lint
returns few errors, I fix all of them at once, verify on my machine and push the codes to remote repository.
When the number of errors is significant, I choose a few to fix, turn off the Eslint rule and git push the code to remote repository, Then, I enable the rule, re-run npm run lint
and repeat the process. Therefore, the process continues until the Eslint rule passes and I can enable the rule permanently in the project.
Approach two is tedious and it disrupts the flow of developer by enabling and disabling the rule to fix issues. Fortunately, Craig Spence created betterer to improve the code base incrementally and I can throw approach two out of the window.
Add code smell to Angular
I add code smell to the project to demonstrate betterer. The example calls async/await statements in a for loop to sum some numbers in ngOnit function.
sum = 0
async ngOnInit(): Promise<void> {
for (let i = 1; i <= 4; i++) {
this.sum += await this.square(i)
this.sum += await this.cube(i)
}
}
private async square(num: number): Promise<number> {
return Promise.resolve(num * num)
}
private async cube(num: number): Promise<number> {
return Promise.resolve(num * num * num)
}
Executing await in a loop is code smell and Eslint even has a no-await-in-loop rule to check for its existence.
Next, I will show the readers how to write betterer test to look for await in a loop and to improve the test values by refactoring the code.
Install dependencies
npx @betterer/cli init
npm i --save-dev @betterer/angular @betterer/eslint
The npx command generates betterer script in package.json and creates a blank betterer.ts for the tests as a result.
package.json
{
"betterer": "betterer"
}
Write betterer tests for Eslint and Angular
Writing tests for Eslint and Angular is very simple because betterer library provides eslint and angular functions that accept the name of the rule and its options.
// betterer.ts
import { angular } from '@betterer/angular'
import { eslint } from '@betterer/eslint'
export default {
'no more await in loop': () => eslint({ 'no-await-in-loop': 'error' }).include('src/**/*.ts'),
'stricter template compilation': () =>
angular('./tsconfig.json', {
strictTemplates: true,
}).include('src/**/*.ts', 'src/**/*.html'),
}
eslint({ 'no-await-in-loop': 'error' }).include('src/**/*.ts')
test looks for await in any loop in TypeScript files of src folder and outputs error messages.
Then, we execute npm run betterer
to run the tests and store the initial values in betterer.results
“no more await in loop test” fails and finds two await calls in the for loop. Therefore, our task is to refactor ngOnInit to get rid of them.
(Optional) Run betterer in pre-commit hook
If your project has husky installed, you can run betterer precommit
in pre-commit hook to update the values in betterer.result file
Precommit requires configurations in package.json, gitignore file and eventually pre-commit husky file
// package.json
{
....
"betterer": "betterer --cache",
"betterer:precommit": "betterer precommit --cache"
}
Betterer normal and betterer precommit tests cache ids to .betterer.cache to bypass checking on unmodified files. Therefore, the tests complete faster and developers do not have to wait forever.
We add betterer.cache to gitignore to ensure we don’t not accidentally commit it to our repo
// .gitignore
.betterer.cache
// pre-commit husky file
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
npm run betterer:precommit
Whenever we commit changes, betterer re-run the tests and update the values in .betterer.results file
Refactor code to improve the values of betterer
// food-shell.component.ts
const sumAndCubePromises = await Promise.all(
[1, 2, 3, 4].map(async (i) => (await this.square(i)) + (await this.cube(i))),
)
this.sum = sumAndCubePromises.reduce((acc, value) => acc + value)
I refactor the codes to create all promises and obtains the results in await Promise.all(). The result is an array of numbers and we compute the total by Array.reduce().
Code rewrite eliminates both for loop and await calls; thus, the betterer values get better.
The “no more await in loop” test has met its goal while “stricter template compilation” test stays the same.
Since the code base does not have await in loop, I can delete the test and add the eslint rule in the configuration file
After I commit the latest changes, betterer is run again to update betterer.results file.
The terminal displays the result of the only test and the test value stays the same.
Bonus: Resolve conflicts in betterer.results
We have a large team at work and we constantly deal with merge conflicts in .betterer.results file. Fortunately, the library provides merge functionality to resolve them automatically.
{
....
"betterer:merge": "betterer merge"
}
Type npm run merge
and the conflicts should disappear. As precaution, we run npm run betterer
to recalculate the values before code push.
Final thought
Betterer allows developers to add tests to improve codes in stages. These tests stay on until the issues are resolved. When one issue is fully resolved, we can remove the corresponding test and include the rule to eslintrc configuration file. One benefit is to make better codes without widespread changes. The other benefit is prevent massive code refactoring that breaks existing functionality.
This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular, architecture and good engineering practice.
Resources:
- Repo: https://github.com/railsstudent/ng-spanish-menu
- Betterer: https://phenomnomnominal.github.io/betterer/docs/introduction
- Angular Nation and Betterer w/Craig Spence: https://www.youtube.com/watch?v=BCdDEhNWpUU
- Craig Spence’s twitter: https://twitter.com/phenomnominal