Writing custom TypeScript Eslint rules with unit tests for Angular project

Michal Szpak
BigPicture by Appfire
14 min readNov 11, 2021

--

After reading this article you will know how to create a custom Eslint rule in Typescript for Angular project and how to test it using Jest.

We are going to work on a clean Angular project so all you need to have is node and Angular CLI installed on your device and I’m using yarn to add dependencies and run scripts but you can, of course, use a different tool like npm.

First steps

First, let's start with creating a new project. Let’s use Angular CLI to generate a clean project:

ng new custom-eslint-rules --style=scss --skip-tests=true --routing=false --minimal=true

then we go on to add Eslint:

yarn add eslint --dev

and initialize it:

yarn run eslint --init

After the configuration steps:

.eslintrc.json should appear in the root directory with the following configuration:

Now in package.json, we are going to add a custom script to run eslint with predefined rules for typescript files. The eslint script will be invoked for the “src” directory and all its subdirectories:

to run this script we use:

yarn lint

after running this command, you should see fixable errors in the console. This is because by default eslint adds some predefined rules in the .eslintrs.json file. One of the rules is indent: tab, which causes the errors shown below:

Speaking of fixable rules. Eslint provides a flag to fix broken rules. We’ll take a closer look at this later in this article, but for now, just to try it out, you can run the script below, but keep in mind that eslint will change your files as a result. I suggest you run this script on a repository without any local changes so if you have any problems later, you can easily roll back the changes made by the script.

yarn lint --fix

AST Tree

Before we start writing our first rule we need to understand how ESLint works. Under the hood, Eslint searches AST (Abstract Syntax Tree) which is an abstract tree that represents your code.

To imagine how it works and what AST exactly is we will use one of the online tools: https://astexplorer.net/.

I’ve changed our AppComponent a little bit to look like this:

and copied it to https://astexplorer.net/, then selected JavaScript language and @typescript-elsint/parser which is also selected in our .eslintrc.json file. It should look like this:

You can play around and discover how your code is represented in AST and if that’s not enough for you and you want to know more about what AST is and how it works, I recommend this article: https://www.twilio.com/blog/abstract-syntax-trees

Custom Rule

Let’s jump back to the code and clean our .eslintrc.json, by removing default rules, plugins and extends, fixing the parserOptions: emcaVersion to 2021 and setting root to true. After all those operations .eslintrs.json should look like this:

I left plugins and rules empty on purpose because it’s the place where we are going to put our custom plugin and rules.

Now the tricky part comes in. To add a custom rule, you need to add a plugin that has this rule and the plugin needs to be set as a dependency of your project in package.json and should be a part of the node_modules.

…at least that’s what the official Eslint documentation says as seen below:

But maybe you don't want to share your custom plugin and you want to keep it locally as part of your application for better maintenance and accessibility. (as I did at my company) In the next chapter, we will go through the whole process of setting up the local plugin.

Local plugin

First, we need to create a directory and then initialize the npm project. To do it we use

npm init

and after the configuration steps:

the file should appear in custom-eslint-rules/eslint/package.json and it should include the following text:

now we need to add some necessary dependencies:

  • typescript for typescript, obviously
  • jest with types for testing purposes
  • and finally eslint parser with experimental utils

eslint/package.json should look like this:

let's run the install script inside the eslint directory:

yarn install

and then generate tsconfig.json file by running this script:

yarn tsc --init

Now add some directories and index.js to keep our plugin’s structure clean:

notice that I've added src/rules for keeping custom rules and tests/rules directory for keeping unit tests.

The next step is to clean up tsconfig.json. You can choose your own typescript settings although I recommend you set outDir to dist. Here is how my tsconifg.json look’s like:

last but not least we configure jest and to do that simply run this script:

yarn jest --init

and after the configuration steps:

jest.config.js should appear in the eslint directory. Now you can also configure it on your own but remember to set transform property to ts-jest. Here is how my jest.config.js look’s like:

At the end of this chapter, let's jump into eslint/src/index.js and export all the rules from the src/rules that we’ll be adding there later:

Let me explain what happened: we took all the rules from src/rules iterated through them and exported them as their file names. So notice that your rule file name is actually your rule name that will appear later in .eslintrc.json.

Writing First Rule

In Angular, we have the possibility to use a technique called dependency injection. We all use it to inject needed dependencies into components, pipes, directives, modules and services.

The injected dependency is usually created as a singleton and overwriting it breaks the whole concept of dependency injection pattern.

So knowing this we want to write a rule that forces the programmer to precede each injected dependency with the word readonly, as they should not be overwritten. For this reason, let’s assume that this is a wrong injection of AppService

constructor(private appService: AppService) {
}

and this is correct:

constructor(private readonly appService: AppService) {
}

Now let’s create some files:

  • readonly-injectables.ts inside eslint/src/rules
  • readonly-injectables.test.ts inside eslint/src/tests/rules.
  • create-eslint-rule.ts inside eslint/src/utils
  • utils.ts inside eslint/src/utils
  • selectors.ts inside eslint/src/utils

It should look like this:

In create-eslint-rule.ts we will write a util function that will help us create custom eslint rules and for this, we will use RuleCreator from @typescript-eslint/experimental-utils

Then we should export and define the following variables in readonly-injectables.ts:

  • RULE_NAME — the name of our rule, that will be also used in tests
  • MessageIds — this field will hold our error message in the future configuration
  • Options — this field is to further configure our rule in .eslintrc.json but in our case, we will not use it

Now let’s jump into selectors.ts and write a selector that will take all Angular classes in which we can inject services.

For that let’s go to the https://astexplorer.net/ and pass our component here. As you can see on the screenshot below we are looking for a class that has one of these decorators: @Component, @Directive, @Pipe, @Injectable, @NgModule.

So the correct selector for this should look like this:

Now when we have a selector, we can go back to our readonly-injectables.ts file and configure our rule and to do that we need to use the previously written createEslintRule method.

You can configure it according to your own needs and it should not be difficult because in WebStorm you can always command + right-click inside any field and check the documentation and if your IDE does not provide such a possibility I recommend you take a look here.

So here is my readonly-injectables.ts after configuration:

please notice that the utils are imported using relative paths.

Unit Tests

It is very difficult to write an eslint rule without first writing a unit test for it because without a test you would have to deploy not finished rule and test your application code with it which you don’t want to do 🙂. It is also good practice according to TTD to write a test before a rule so let’s do so.

We are going to import RuleTester from @typescript-eslint/experimental-utils/dist/ts-eslint into readonly-injectables.test.ts and provide the correct parser:

then we will write some valid and invalid statements for which we want to test our rule. So here is an array of valid statements:

Note that the last two are also valid because there is no decorator for the class and there is no accessibility property (public/protected/private).

We also declared three invalid statements:

Now, all we need to do is import rule, RULE_NAME and MessageIds from readonly-injectables.ts as shown below:

and write our test using ruleTester:

The run method requires 3 parameters as it is well described in the documentation:

  1. ruleName — the name of the rule to run
  2. rule — the rule to test
  3. tests — the collection of tests to run

The whole file should look like this: link

To run the test we simply need to execute the script below:

yarn test

and as expected we got 3 failed tests and 5 passed because our rule body is empty and if the rule doesn’t report anything then the testing code is considered correct.

Now that we have the unit tests, we can start implementing the rule and we will know that it works when all the tests passed.

Utility functions

All we have to do now is fix the tests that aren’t working. To do this, let’s figure out how we should write our rule and https://astexplorer.net/ will help us with that.

Let’s paste one of the rules from valid statements in there as shown in the screenshot below:

Now let’s prepare a plan of action, remembering that the selector we have prepared will select the class decorator and we want to take all the constructor parameters from this class to check if they are preceded by the readonly keyword.

So first we want to get the class declaration from the class decorator because it is where the constructor is. Then we want to get the constructor of this class and at the end, we want to get all constructor parameters and check if they are preceded by the readonly keyword.

Sounds good, now we will write utility functions that will help us to achieve this goal and to do so let's go to the utils.ts.

First, we will write some functions that will help us keep good typings:

  • isClassDeclaration — check if node is ClassDeclaration
  • isMethodDefinition — check if node is MethodDefinition
  • isFunctionExpression — check if node is FunctionExpression
  • isParameterProperty — check if node is ParameterProperty

then we add functions that actually help us achieve the goal:

  • getClassDeclarationFromDecorator — for getting ClassDeclaration from Decorator
  • getConstructorFromClassDeclaration — for getting constructor which is typed as MethodDefinition in TSESTree from ClassDeclaration
  • getParameterPropertiesFromMethodDefinition — for getting MethodDefinition ParameterProperties
  • isParameterPropertyAccessibilityNamed— for checking the accessibility of our dependencies
  • isParameterPropertyReadonly — for checking if our dependency isn’t already preceded by keyword readonly

the whole file should look like this: link

Does it look difficult? Maybe at first, but if you slowly analyze the code and the AST you have at https://astexplorer.net/ you should quickly catch on and see that it is not that hard 🙂

Back to the rule

Everything has been prepared, now all we have to do is finish writing the rule. Let’s jump into readonly-injectables.ts and import all of the functions from our util.ts:

according to our action plan let’s get ClassDeclaration from Decorator and then constructor from ClassDeclaration:

Remember that when searching an AST you always have to be careful that you may get undefined and the same in this case, if the class does not contain a constructor then we report nothing and stop the rule from executing. And according to what I wrote above, if the rule does not report anything then it is considered valid. So we add something like this:

now we have a class constructor and we know it exists, we extract the constructor’s parameters and in the same way, if the constructor has no parameters, we are stopping the execution of the rule and it is considered valid:

at this point, we know that our class has a constructor and at least one parameter in it so let’s now prepare a local function that will check if a parameter meets the requirements we specified earlier.

First, we need to check the accessibility of the parameter and if it is not one of the following: public/protected/private then we stop the rule execution. Then, we check if it is not preceded by the readonly keyword and if it is, we also stop the rule execution:

We have considered all cases where the injected dependency is valid, so now we can finally report and mark the injected dependency as invalid and to do that we use context.report:

Now when our local function resolveParameter is finished we execute it for each parameter:

At the end your readonly-injectables.ts should look like this: link

Time to run the tests and see if it works:

and as you can see, it works 😄

Rule Fixer*

This chapter will focus on creating a fix for a previously written rule.

It’s not mandatory, but it’s worth knowing that eslint also provides mechanisms to automatically fix a rule, just like we did at the beginning with the predefined tabs rule.

When you make a new rule in your team and plug it into CI it will mean that any commit that breaks this rule will be blocked, which is pretty cool.

But what about the code which is already in your repository and which breaks the rule? This is where Rule Fixer comes to the rescue. It’s a great refactoring tool, but it should be used consciously and in my opinion not automatically, i.e. you should call the fix command by yourself and check later if it didn’t do any harm to your code and always on a clean repository, because later if something goes wrong you can easily rollback all the changes.

Okay, enough of this theoretical talk, let’s jump into the code. In readonly-injectables.ts in rule configuration we should add fixable: ‘code’ as we can see in the screenshot below:

Then in context.report we need to provide the fix function which gives us access to RuleFixer object which has the following capabilities:

  • insertTextAfter — used for inserting text after the given node
  • insertTextAfterRange — used for inserting text after the given range
  • insertTextBefore used for inserting text before the given node
  • insertTextBeforeRange used for inserting text before the given range
  • remove used for removing the given node
  • removeRange used for removing from the given range
  • replaceText used for replacing the whole text of the given node
  • ReplaceTextRange used for replacing whole text in the given range

we’ll use insertTextBefore so all we need to do now is find a node in the AST before which we can put the readonly keyword to do this let’s take a look at https://astexplorer.net/:

now we can see that we are looking for the parameter of our ParameterProperty which is the name of our service, so in our context.report we simple do as follows:

Finally, we need to fix the tests, because if the rule has a fix, the test will automatically apply the fix and expect us to indicate what it should look like after the fix so in readonly.injectables.test.ts we will add output property in ruleTester:

let’s see if it works by running a test

everything ended as expected, let’s jump into the next chapter 🙂

Adding the rule

We got this far, so now all we have to do is add the custom rule to our main project and see how it works. To do this we first need to build it, so let’s go to eslint/package.json and add the following script:

After running the script you should see the following directory structure:

Now let’s add our local plugin to the main project, to do this go to package.json and add new devDependencies:

and after running npm install your custom plugin should appear in node_moduls in the main project:

then go to the .eslintrc.json and add your rule:

To test if everything works, let’s go into app.component.ts and incorrectly inject a dependency like this:

next go to the console and run yarn lint:

you can also run the script with — fix flag to automatically fix errors, but as I warned you before, it’s best to do it on a clean repository 🙂

At the end of the job, let’s clean up after ourselves so we can freely commit our changes. Let’s go to the .gitignore for a moment and add following:

Then to make it easy for our teammates to add a local rule to their environment after your changes have been merged, let’s add some scripts in the main package.json:

That way after merging your commit, all they have to do is yarn lint:build-rules and npm install.

IDE

To get the most benefit from eslint, it is recommended to configure it with your IDE. Most popular IDEs provide the possibility to configure eslint. It is very convenient and recommended to have the IDE show you the errors in realtime and give you the fixes.

If you are using WebStorm like me, I will show you how to configure it, but if you are using another IDE, I recommend you to google how to configure eslint in it.

So in WebStorm go to the preferences and type “eslint” then set Automatic ESLint configuration, apply changes and click ok as in the screenshot below:

and that's it 🙂.

Now let’s go to app.component.ts and see how it works:

Conclusion

Eslint is a very powerful tool. When the project grows and we introduce new conventions on how to write code it is always good to write eslint rule for it. We are all human beings and we don’t like to adapt to new things and eslint is a tool that helps us adapt to them. And even more, if we set a new rule on error, we are forced to follow it.

Now, you know how to write a custom rule, so nothing stands in the way of implementing new conventions in your project 🙂

In the end, I give you a link to the repo with what we did and I encourage you to write comments because this is my first article and I am curious about feedback 🙂

--

--