Building an overly specific lint rule with ESLint, AST and React

Tom Parsons
Dec 17, 2019 · 6 min read

Do you live in a world where some developers, maybe even you, have specific things you’d like to enforce in a code review? Outside of the realms of the current ESLint rules, Prettier rules, or anything else that can’t be caught via CI? Well, hopefully, this will help you take that extra step forward to a world of overly specific, probably unnecessary linting rules.

A couple of notes before we start:

  1. You don’t need to know the absolute underlying fundamentals of javascript to do this, it might help, but it won’t hinder.
  2. AST and typescript aren’t super great friends yet, so this will just focus on linting ES6 code
  3. You can do this without tests, but you’ll be there for ages, this project is a great example of why TTD is a good approach
  4. We’re not compiling our lint rule, so we’re using module.exports etc.

The code for this project is available here

For this, I cloned the Create React App and added ESLint as per the ESLint docs.

Getting started

I suppose the first thing needed is to decide on a specific rule, for this, we are going to enforce this oddly specific rule:

React Class (Component) names, must contain at least two words.

According to Wikipedia, there is actually a reason for this rule

How are we going to do this?

First, let’s look at a component that should fail:

import React, { Component } from "react"class Hello extends Component {
render() {
return (
<h1>hello world!</h1>
)
}
}
export default Hello

And a component that should pass:

import React, { Component } from "react"class HelloWorld extends Component {
render() {
return (
<h1>hello world!</h1>
)
}
}
export default HelloWorld

We can see from the above that our Component name, is declared in the class instantiation. In this instance, it’s written in PascalCase, and this may become something else we can look to enforce also.

As the rule is to specify the number of words in a phrase, for now, we’ll add one parameter to our rule: MinNoOfWords (Int).

The next thing we’ll do is start playing with the AST explorer. By copying one of the above examples, we can see that we have a couple of useful properties that will help us build our function:

  • ClassDeclaration — if you wish to know more about this stuff click here
  • Identifier (and name)

These are pretty much all we’re going to need for this bit, so let’s get building!

Building this rule:

At this point, it might be worth reading over some of the docs from ESLint on this subject: https://eslint.org/docs/developer-guide/working-with-rules

Essentially we’re going to create four files, a package file, a rule file, a test file, and an index file, for the said rule (If you’re confused at this point, it’s all in the repo), and remember, TDD — tests first! We’re using the ESLint “Rule Tester” framework, built for this specific reason. Thanks, ESLint!

We’ll need to create those files and then we can start testing.

valid: [
{
code: "class HelloWorld extends Component { render() { return ( <h1>hello world!</h1> ) }}",
options: [],
},
],
invalid: [
{
code: "class Hello extends Component { render() { return ( <h1>hello world!</h1> ) }}",
options: [],
errors: [{
message: "class name is too short",
type: "ClassDeclaration",
}],
},
]

Here we have our first test, this will have both, a valid, and an invalid test, (same code just with Hello instead of HelloWorld ). We’ll see our first test passes, and the second fails because we haven't written our rule yet, and so it doesn’t know to fail.

As we build out our rule, we’ll add many more tests, allowing us to check default variables for options, weird or extreme instances (i.e. is AbCd a valid component name?) etc.

A bit more about what is happening here:

In our index.js file we export the rule as componentNaming: (context) => componentNamingRule(context) you’ll see that we pass context into the rule.

In this instance, context contains loads of useful information and will allow us to output a rule failure in the correct place using the report function, read more here.

and then in the actual rule file, we have:

const componentNaming = (context) => {
const options = context.options.length ? context.options[0] : {}
return {
ClassDeclaration(node) {
testClassName(context, node, options)
},
}
}

where testClassName is our actual function for verifying the validity of our node — it will run logic on the code being passed in, which can be broken down in the same way the AST explorer shows you.

Our node is the source code we want to check, i.e. the actual component. More on this here.

And finally, options, these are (as the name might suggest), optional configuration params, so we’ll be able to access variables such as our defined minNoOfWords value, and we could add more in as the rule grows. More info on options, here.

We can start small with the testClassName function and have it always return true, knowing that our tests will fail, and we can continue to make this function as complex as it needs to be.

const testClassName = (context, node, options) => {
return true
}

Applying the rule to our settings

Once we have a roughly working rule, we need to add it into our eslintrc.

First, we need to add the lint package (note the package.json in the lint folder), which we can do by adding it as a dev dependency in our root like so:

"eslint-plugin-my-custom-rules": "file:path/to/dir/lint"

You can name this whatever you want, as long as it matches the plugins in the .eslintrc file.

plugins: ["react", "eslint-plugin-my-custom-rules"],

And finally the rule, with its options (NB: you don’t use the eslint-plugin bit when using the rule) — more on this here.

"my-custom-rules/componentNaming": ["error", {"minNoOfWords": 2}]

You could also add this rule to an override, allowing you to only add this rule to specific files, i.e. any files called index.js should have class names with a minimum of 4 words in. Get as creative as you like.

At this point, in the source package, if you change the rule to have a minimum number of 4 words, you’ll see the project gets an error: “class name is too short” as our component is called HelloWorld and that’s only 2 words…

And that’s pretty much it, as far as the most simple application of this lint rule goes. These rules can be as creative, as niche, and as opinionated as you might like them to be, just remember your colleagues might not enjoy a rule that doesn’t let them use vowels…

Thadius Vent from Oscars Orchestra banned music cos he’s a grinch. A bit like you banning underscores in your codebase.

Further additions to this rule:

This is still a very, very specific rule, so extending it may be useful, here are some thoughts:

  • Better messages (as I just hardcoded them for now)
  • Class names must be PascalCase (optional param to enforce different casings?)
  • You can’t use the same word twice, no <CardCard/> Components
  • You can’t use the word “Component”, no <CardComponent /> Components
  • Adding in the functionality for functional components, not just classes

The Startup

Get smarter at building your thing. Join The Startup’s +725K followers.

Tom Parsons

Written by

Front End Tech Lead, you can find me on github @thomasparsons

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +725K followers.

Tom Parsons

Written by

Front End Tech Lead, you can find me on github @thomasparsons

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +725K followers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store