Custom TSLint rules with TSQuery 😍
Tags: javascript, typescript, linting, tsquery
Published: 2019-10-27 17:27:31

(originally posted on [Medium](https://medium.com/@phenomnominal/custom-typescript-lint-rules-with-tsquery-and-tslint-144184b6ff2d))

# TL;DR

[TSLint](https://palantir.github.io/tslint/) already has a fairly straightforward API for adding custom [lint](https://en.wikipedia.org/wiki/Lint_(software)) rules, but writing the actual logic of the rules can be quite tricky! We can make that a lot easier by using powerful AST node selectors with [TSQuery](https://github.com/phenomnomnominal/tsquery):

{% gist https://gist.github.com/phenomnomnominal/54d90ab84f69b9edd102f663d204576a %}

> ### If you want to learn more about the “how?” and “why?” of TSQuery, [check out this introduction here](https://medium.com/@phenomnominal/easier-typescript-tooling-with-tsquery-d74f04f2b29d)!

___

## What is a lint rule?

A lint rule is a little piece of code that uses [static analysis](https://en.wikipedia.org/wiki/Static_program_analysis) to automatically detect things that may be wrong with other bits of code. They can be as simple as checking for the presence (or lack of) whitespace, or as complex as avoiding certain functions or patterns in specific circumstances. Lint rules can be used to enforce coding standards throughout a codebase, and to prevent bad code from being released to the world, such as a stray **`debugger;`** or **`console.log();`**.

There are a number of existing tools for linting your code, such as [ESLint](https://eslint.org/), [TSLint](https://palantir.github.io/tslint/), [HTMLLint](https://github.com/htmllint/htmllint), and [SASSLint](https://github.com/sasstools/sass-lint). These tools are each designed for a specific language, come with their own rules, and can be configured to fit your specific needs. ESLint and TSLint even allow you to create custom rules, giving you fine-grained control over the standards of your codebase. We’re going to focus specifically on writing custom rules with TSLint.

> ### If you want to read a bit more about linting (specifically ESLint), [check this awesome post](https://medium.com/the-node-js-collection/why-and-how-to-use-eslint-in-your-project-742d0bc61ed7) by Sam Roberts.

## Building a custom lint rule

TSLint comes with a [huge set of lint rules](https://palantir.github.io/tslint/rules/) out of the box, but sometimes you may want to write a custom lint rule that is specific to your codebase. If you’re frequently seeing the same issue in code reviews, or you’d like to pre-emptively discourage something that could cause future issues, writing a custom lint rule is a great option.

As an example, let’s consider [**`fdescribe`**](https://jestjs.io/docs/en/api#describeonlyname-fn) and [**`fit`**](https://jestjs.io/docs/en/api#testonlyname-fn-timeout), both of which are common utility functions in JavaScript testing libraries. **`fdescribe`** gives us a way to run specific groups of tests, and **`fit`** gives us a way to run individual tests— very useful! There’s certainly nothing wrong with using either of these functions as part of normal development. However, if they get committed to source control, you could unintentionally end up running only a few tests in your test suite! That wouldn’t be good, so we should write a custom lint rule to prevent it from happening. We’re going to start with a “normal” TSLint rule and then modify it to use [TSQuery](https://github.com/phenomnomnominal/tsquery). To get started, we need to talk about **ASTs**.

### Getting an AST

An **Abstract Syntax Tree (AST)** is a data structure that contains all the structural meaning of the source code, without using any formal syntax. That means it is perfect for static analysis like linting. Within the TypeScript parser and compiler, the AST is described by an object called a **`SourceFile`**. If we want to do any serious analysis of TypeScript code, we need to get our hands on one of them. Thankfully, TSLint makes it very easy! Let’s look at the basic shell of a new TSLint **`Rule`**:

{% gist https://gist.github.com/phenomnomnominal/8227319af9e6d1a032fe2c86836a91f9 %}

A TSLint rule has a **`Rule`** class, which extends **`Rules.AbstractRule`** from TSLint. It has a single public function called **`apply`**, which takes a **`SourceFile`**, and returns an array of **`RuleFailures`**.

The **`apply`** function is the link between the TSLint runner and our custom rule. It is where we get access to the AST via the **`SourceFile`**. The **`apply`** function is where you set up any configuration that your rule has, and then (typically) call through to **`applyWithWalker`** like this:

{% gist https://gist.github.com/phenomnomnominal/0057ea707b12e5d0a857c6522c3957f6 %}

> ### If you’re still not sure what an AST is, or just want to brush up a bit, [check out this ⚡magical⚡ talk I gave at JS Conf AU 2018](https://www.youtube.com/watch?v=UKCoRKzRqN4) for a refresher!

### Walking an AST

TSLint’s Walker APIs allow you to walk through each node in the TypeScript AST and define rules that inspect the properties of individual nodes, and the relationships between them.

{% gist https://gist.github.com/phenomnomnominal/8b1e486dd6491c3ef077c8fa3d16623c %}

We start with the **`SourceFile`** (the root of the tree) and recursively visit each child of each node that we come across. As we walk through the tree, we can inspect each node and check for structures that break our rule.

In this case we’re looking for any occurrences of code like **`fdescribe()`** or **`fit()`**. We don’t want our rule to fire if we see a comment (e.g. **`// fdescribe();`**) or a standalone variable (e.g. *``fit`**). This is why AST traversal is preferred over Regular Expressions — we can be very specific about what constitutes a failure.

### Exploring an AST

To help understand the structure of our code, we can use something like [ASTExplorer](https://astexplorer.net/) to see the different nodes of the AST. Check out [this example](https://astexplorer.net/#/gist/93f64a8746e9fa5b1f7837f7a3449c6c/42c4825dcff6b2b60fb47a4a89d3333c14559e80), and click on the functions in the code. The individual AST nodes should be highlighted:

![Using ASTExplorer we can see the Identifier node for the fdescribe function](https://thepracticaldev.s3.amazonaws.com/i/olcwz69xinpgi5cz29xt.png)

We can see that when we write something like **`fdescribe()`**, TypeScript converts that for us into an AST, with three nodes:

  1. An Expression Statement, which represents a single expression of code.
  2. A Call Expression, which represents a call to a function
  3. And an Identifier, which in this case represents the name of the function that is being called.

> ### ASTExplorer shows you a very rich, detailed representation of the structure of the code, which is very useful but can also be overwhelming. As an alternative, you may want to also check out [Uri Shaked](https://dev.to/urishaked)’s [TSQuery Playground](https://tsquery-playground.firebaseapp.com/).

### Inspecting an AST

Just looking for an Identifier with the value **`fdescribe`** on its own would mean we may get false positives. We specifically care about the case where we have an Identifier *inside* a Call Expression. In code, that looks like something like this:

{% gist https://gist.github.com/phenomnomnominal/2d3f63c2eea5fcf48aa353dc56e2710e %}

With the correct structure in place, we now need to test what the **`text`** of the Identifier is, and if it matches **`'fdescribe'`**, or **`'fit’`**. If we get the right structure and the right Identifier name, we should raise a lint failure:

{% gist https://gist.github.com/phenomnomnominal/69a685048fdcba627b36a1b535f08403 %}

And boom 💥, we have a working lint rule!

### Querying an AST

Our lint rule is pretty cool, and it works great! But the code for the rule isn’t particularly readable, and it could get a lot worse if we had to write a more complex rule. Thankfully, we can use [TSQuery](https://github.com/phenomnomnominal/tsquery), which gives us a powerful way to express these AST traversals, using something like CSS selectors.

We know that we care about a Call Expression if it contains an Identifier, and that Identifier’s name is either **`fdescribe`** or **`fit`**. This can be expressed with TSQuery with the following query:

    CallExpression > Identifier[name=/^f(describe|it)$/]

This query is a bit like CSS with some extra tricks. We can use a child combinator (**`>`**) to check for the Identifier inside the Call Expression, and then use an attribute selector (**`[]`**) to check for the specific name values with a Regular Expression. We can hook it into our rule like so:

{% gist https://gist.github.com/phenomnomnominal/5110392042c5bda05537f2f644471ab8 %}

By adding in our TSQuery selector, and running that over the **`SourceFile`**, we can just map from our matches to an array of **`RuleFailures`** 😎! We no longer need to worry about using **`applyWithWalker`** or traversing the tree 🎉!

We can finish off our rule by adding an automatic fix, and an option to only run this rule on files that match specific extensions (no point running the rule on a non-spec file!), and we end up with something like this:

{% gist https://gist.github.com/phenomnomnominal/54d90ab84f69b9edd102f663d204576a %}

Pretty neat eh?! 🚀

## Testing a custom lint rule

While we’re at it, let’s add some tests for our rule. We start by defining what a “failing” bit of code looks like, parsing that code into AST, and then passing the AST to our rule. TSQuery is again useful, as it provides a helper function to turn a string of code into an AST:

{% gist https://gist.github.com/phenomnomnominal/001e38500d01effe3911f880cdd493ea %}

We can do the same thing for a “passing” bit of code:

{% gist https://gist.github.com/phenomnomnominal/28fb1f8a54ad14ed784f3bc1c6d768e5 %}

We can even add a test to make sure our “fix” works:

{% gist https://gist.github.com/phenomnomnominal/85b73d248f847ffb50a018da6f8d49b5 %}

Check out [the full **`.spec.ts`** file here](https://gist.github.com/phenomnomnominal/5db4cf146e93eb023bd61064056b0922). There’s no reason why we couldn’t have written these tests first and had a great spec against which to write our custom rule. Let’s write another, more complex rule, and do just that!

### Building another custom lint rule

This time, we’re going to kick things up a notch and write a pretty niche rule, based on [this great post](https://medium.com/city-pantry/handling-errors-in-ngrx-effects-a95d918490d9) by [Paul Lessing](https://medium.com/u/3c6acfb1e50d). It turns out that error-handling with [@ngrx/effects](https://github.com/ngrx/platform) can be a bit tricky, and there’s a particular case where it can break your whole app:

{% gist https://gist.github.com/phenomnomnominal/1add61d77d97cc4302b7cf1061ea7245 %}

If you use **`catchError`** in an observable chain in an effect, one error can stop the whole chain from running, which is almost definitely what you don’t want. We use **`@ngrx/effects`** in our Angular app, and we don’t want this to happen by accident! This basic example becomes the “failing” test case.

{% gist https://gist.github.com/phenomnomnominal/e891764be89ba8006332e5473c5dd81c %}

Our “passing” test is just some code that *doesn’t* use **`catchError`**:

{% gist https://gist.github.com/phenomnomnominal/ebf3c44f12a7f8454a625e8e73e8984e %}

### Creating our query

We need to write a query that only selects calls to **`catchError()`** when they occur on the outermost chain in an **`@Effect`**. We can start with our failing code and build up from there. Using [TSQuery playground](https://tsquery-playground.firebaseapp.com/) to see the structure, we get something like this:

![We can use TSQuery playground to inspect the AST of our code and build up a query to select very specific structures.](https://thepracticaldev.s3.amazonaws.com/i/ltrzpi4dpigzfqpgip9o.png)

We care about any **`CallExpression`** which has the name **`"catchError"`**, but specifically only at the *first* level of an Effect chain. An Effect chain is created with a **`PropertyDeclaration`** that has a **`Decorator`** with the name **`Effect`**. We can describe that relationship with the following query:

    ClassDeclaration 
    > PropertyDeclaration:has(Decorator Identifier[name="Effect"]) 
    > CallExpression 
    > CallExpression
    > Identifier[name="catchError"])

Whew. That looks quite a bit more complex, but it fairly concisely describes the specific structure that we care about. We can now come up with a useful error message, and plug it into a new rule:

{% gist https://gist.github.com/phenomnomnominal/592bdfab89738927352be6d1bf56d00f %}

Et voilà! Our tests pass, and we have a brand new, super specific lint rule fresh out the oven 🥖! Our codebase is safer than ever before, and we don’t have to remember to look for this in code reviews! This rule could’ve been pretty tricky to write with an AST Walker, but with TSQuery it’s easy.

### Wrapping up

There you have it!

TSQuery is particularly suited for this kind of code inspection, so why not have a go at writing your own rule using it. You can see some more examples of lint rules written using TSQuery [here](https://github.com/cartant/rxjs-tslint-rules/blob/208650cfe32ee4987400d66afaf57fb4486068bc/source/rules/rxjsJustRule.ts) and [here](https://github.com/cartant/rxjs-tslint-rules/blob/251a075a78bd9b2be6b14849144336adec6b7a9d/source/rules/rxjsNoUnboundMethodsRule.ts) from [Nicholas Jamieson](https://dev.to/cartant). Please hit me up if you have any comments/questions, and thanks for reading 👋!