Writing a custom TSLint rule

October 7, 2019

When working on Angular projects with TypeScript, TSLint is a neat tool for keeping the code base tidy and consistent. It has many rules available and allows you to benefit from many predefined and configurable code styles.

If you have a look at the core rules list, you see how many rules exist, and they should cover already most use cases. But still, enforcing coding guidelines is hard if you don't use the tooling available, and manual checks just don't catch every case. In this post, I'd like to show how easy it is to create your own lint rule!

There are already good tutorials out there (have a look here, here or here), but most of these use the now deprecated RuleWalker, so we're gonna use a walk function instead.

So let's work out how to create a new custom lint rule!

Checklist

Steps for setting up a custom TSLint rule:

  1. Add a directory for your rules to your project, e.g. tslint-rules
  2. Add new file for rule (e.g. noTruthyFalsyRule.ts)
  3. Compile rule: tsc noTruthyFalsyRule.ts
  4. Configure directory: ?
  5. Add rule to tslint.json: "no-truthy-falsy": true

Instead of first implementing the rule I recommend to copy an existing rule (for example the custom rule below) to make sure the setup works. You can check the Pitfalls section at the end if you've got an error integrating the custom rule.

Writing a custom rule

At work, we have a code convention not to use Jasmine's toBeTruthy() (or toBeFalsy()) in Karma tests. The reason is pretty simple, many things evaluate to true or false in JavaScript / TypeScript, so the tests are unnecessary vague and can always be stated more precisely. For example:

  • any kind of object will evaluate to true - better use toBeDefined()
  • true or false can be directly checked - use toBe(true) or toBe(false)
  • any number will evaluate to true - except for zero (what?)

There's a blog post with the funny title toBeTruthy() will bite you which I recommend if you're interested why not to use those methods. For now, we'll focus on creating our custom rule.

If we generate a new Angular component with ng generate component my-component, a test file with the following test is generated:

generated test

Not what we want, so let's make a rule that reports this as an error!

If everything from the checklist above is wired up, we can start actually implementing the rule. As already said, I would recommend doing so as above so you know your setup works, nothing is more frustrating if it's not the rule that doesn't work, but some faulty configuration in your setup.

Abstract syntax tree (AST) and checking nodes

As applyWithWalker seems to be deprecated, I'm gonna use the applyWithFunction, see Walker Design, which works slightly different from the RuleWalker.

Writing a TSLint rule boils down to the following:

  • We provide a code that will get called as TypeScript "walks" through the source code
  • For each Node, we can decide whether it is erroneous or okay.

A Node can be pretty much everything: A type declaration, a method call, a string literal. I found Parser API to be come in handy, as it provides interface declarations which are easily browsable. For example the interface CallExpression for nodes which are function calls:

Parser API

But how do we know what exactly to check for in our example? The AST explorer comes in handy, so we can copy-paste the generated test and explore how to find our node:

AST explorer

At first, I tried working with CallExpression (because we're dealing with a method call), but we can go deeper and work directly with ts.SyntaxKind.Identifier (the name of the called method) and check whether it matches what we search for:

if (node.kind === ts.SyntaxKind.Identifier) {
  if (node.getText() === 'toBeTruthy' || 
      node.getText() === 'toBeFalsy') {
    return ctx.addFailureAtNode(node, Rule.FAILURE_STRING);
  }
}

We can provide a failure string which will be shown if our rule has a match. The rest is pretty much straightforward - In the end, our rule looks like this:

import * as Lint from 'tslint';
import * as ts from 'typescript';

export class Rule extends Lint.Rules.AbstractRule {
  public static FAILURE_STRING = 'Methods toBeTruthy/toBeFalsy not allowed, use more specific checks instead.';

  public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
    return this.applyWithFunction(sourceFile, walk);
  }
}

const walk = (ctx: Lint.WalkContext<void>) => {
  return ts.forEachChild(ctx.sourceFile, checkNode);

  function checkNode(node: ts.Node): void {
    if (node.kind === ts.SyntaxKind.Identifier) {
      if (node.getText() === 'toBeTruthy' || node.getText() === 'toBeFalsy') {
        return ctx.addFailureAtNode(node, Rule.FAILURE_STRING);
      }
    }
    return ts.forEachChild(node, checkNode);
  }
};

That's it! About 20 lines of code and we implemented our own lint rule. After transpiling to JavaScript we should be up and ready.

As soon as the IDE updates we see that our rule is applied and reports an error in the generated test:

success

Success! So now we will be warned if someone tries to use toBeTruthy() or `toBeFalsy(). Next steps would be to write a test for it. You can view the source in the Github repo if you're interested. For some cases it even makes sense to write a fixer that automatically fixes the rule on autoformatting, see the second part in the official docs for more details.

Pitfalls

If you see an error message like the following:

custom rule not found

Make sure you thought of the following:

  • Did you name your rule file in camel case, e.g. noTruthyFalsyRule.ts?
  • Does the file name end with ...Rule?
  • Did you compile your rule to JavaScript? (tsc noTruthyFalsyRule.ts)
  • Did you add the correct directory in tslint.json under "rulesDirectory": [...]?
  • Did you add the rule in kebab case (without "rule") in the "rules"-array in the tslint.json-File, e.g. "no-truthy-falsy": true?

Thanks for reading and happy linting!