How ESLint Can Enforce Your Design System Best Practices
Tags: javascript, webdev, tooling
Published: 2022-01-19 17:34:31

If you're creating a design system component library for your company or the open-source community, there's a good chance that you have strong opinions on how end-users should consume your design system.

To ensure that your design system is used in its intended way, and to reduce the number of possible bugs, you might want your users to adhere to your best practices. The following are two examples of possible best practices:

- Avoiding inline styles in your elements
- Ensuring that tooltips don't contain interactive content.

If you're the only person designing, developing, and consuming your design system, then you can sleep comfortably knowing that your design system is being used exactly as intended.

Chances are you're not the only person developing the design system and you'll certainly not be present when someone consumes it. How can you feel sure that everyone abides by your design system's best practices? You could cross your fingers and trust that your end-users read the documentation, heed your warnings, and never fail to abide by your rules.

Unfortunately, this is often not the case and it's very easy to miss warnings or misunderstand how to properly use a tool. I've been there!

Fortunately, a great way to encourage your consumers to follow your best practices is through the use of [ESLint](https://eslint.org/), a static analysis tool to find problems in your code.

By default, ESLint ships with a handful of general best practices, called _rules_ and will display red squigglys in your IDE if the rules have been violated. Some of these rules include:

- No duplicate keys in objects
- No unreachable code
- No unused variables

However, the rules you enable in your project don't need to come directly from ESLint. Popular libraries like [Cypress](https://www.cypress.io/), [Lodash](https://lodash.com/), and [React](https://reactjs.org/) have ESLint configurations that anyone can use in their own projects to ensure users adhere to best practices. If you're an intrepid explorer of the JavaScript language you can go a step further and create custom rules specific to your design system which you can export for other people to use in their projects. That's exactly what we'll do in these articles.

---

In this article, we'll spend a bit of time understanding how tools like ESLint parse JavaScript down into a data structure called an _abstract syntax tree_ (AST). We'll then touch on how ESLint rules work and how to parse our Lit templates into HTML. Finally we'll start creating our rules. We'll even use ESLint's built-in testing tool to make sure our rules work under a variety of conditions.

The pre-requisite for this article is some JavaScript + HTML knowledge. A little experience using ESLint and Lit may come in handy but isn't necessary.

## What’s an Abstract Syntax Tree?

For those, like me, who haven't gotten their hands dirty with compilers before, conceptualising how the human-readable language we write in our IDE gets understood (and transformed) by tools like [Webpack](https://webpack.js.org/), [Prettier](https://prettier.io/), and [Babel](https://babeljs.io/) can feel like magic.

Under the hood, when a tool like ESLint wants to start performing actions against your JavaScript it _parses_ your code. Parsing is the process of taking the JavaScript you've written and turning it into a tree representation of the code, an _abstract syntax tree_ (AST).

This process of parsing is split into two parts, _tokenization_ and _tree construction_.

Tokenization takes the code and splits it into things called tokens which describe isolated parts of the syntax.

Tokens for a JavaScript program like:

```jsx
const helloWorld = 'hello world';
```

will look something like this:

```jsx
[
  { type: 'IdentifierName', value: 'const' },
  { type: 'WhiteSpace', value: ' ' },
  { type: 'IdentifierName', value: 'helloWorld' },
  { type: 'WhiteSpace', value: ' ' },
  { type: 'Punctuator', value: '=' },
  { type: 'WhiteSpace', value: ' ' },
  { type: 'StringLiteral', value: "'hello world'", closed: true },
];
```

{% collapsible JS Tokens %}

I used [js-tokens](https://github.com/lydell/js-tokens) as a quick way of tokenizing my JS for this example but we won't be dealing with tokenization directly ourselves in this article.

{% endcollapsible %}

The second step in the parsing process is _tree construction_, which reformats the tokens into an AST. The AST describes each part of the syntax and its relationship to the others.

We can visualise this relationship by parsing the following JavaScript statement:

```jsx
const component = html`<h1>Creating custom ESLint rules</h1>`;
```

It would get transformed into an AST, with the following structure:

![AST representation of the previous code sample](/img/blog/best-practices-w-eslint/parser.drawio.png)

Tools like Babel and Prettier turn your written JavaScript into an AST to analyse and transform the code we've written. Babel uses the AST to transpile our code into a browser-friendly version of JavaScript, while Prettier uses the AST to reformat your code.

## Getting Curious with the AST Explorer

To really explore what an AST looks like, have a play with the [AST explorer](https://astexplorer.net/). Get familiar with the AST explorer as we're gonna be using it loads later in the article.

Write a simple statement, like the following:

```jsx
const helloWorld = 'hello world';
```

You'll see that the top level of the tree describes the entire program and we can look into the _body_ array to see the individual constituents of our above statement represented in the AST.

If you hover over the `VariableDeclaration` you can see that the entire statement on the left gets highlighted. If we go a level deeper into the `declarations` array you'll see an additional node `VariableDeclarator`. If we keep going we'll eventually reach rock bottom. In the case of our hello world statement, it's with the variable's `Identifier` and the variable's `Literal` value.

Let's revisit our component from earlier:

```jsx
const component = html`<h1>Creating custom ESLint rules</h1>`;
```

If you step through the tree in the AST explorer you can see that the structure matches our image from earlier. Pay particular attention to the `TaggedTemplateExpression` node and the `TemplateLiteral` node. These are the ones that will come in handy when we write our ESLint rules.

Our call to the `html` function is an expression, but it looks a little different from other function definitions. Let's see how the AST differs with an expression like the following:

```jsx
function heyThere() {
  return 'hey';
}

heyThere();
```

If we hover over the `heyThere()` `ExpressionStatement`, we see that the properties match our `html` ExpressionStatement. The main difference is that the value in the `expression` property looks different. The expression this time is a `CallExpression`, which has a set of properties different to that of our `TaggedTemplateExpression`.

If we look back at our `TaggedTemplateExpression`, we can see that we have properties like tag and quasi.

The tag gives us some details about the function name. Which in this case is `html`.

This means when writing our ESlint rule we'll be able to do something like this:

```jsx
// Some ESLint psuedo-code
function createRule() {
  return {
    TaggedTemplateExpression(node) {
      const isLitExpression = node.tag.name === 'html';

      if (isLitExpression) {
        // rest of the rule
      }

      // do nothing
    },
  };
}
```

Finally, if you look into the `TaggedTemplateExpression` object, you'll see a property named `quasi`. This property contains our two noteworthy properties `expressions` and `quasis`. Take the following expression:

![Tagged Template Expression Highlights](/img/blog/best-practices-w-eslint/taggedtemplateexpression.png)

The blue underlines, the first and third respectively, will live in the `quasis` array and they'll be in the order that they're written in your template literal.

The green underline, the second one, will live in the `expressions` array, and provides a reference to the name of the variable. Like the `quasis`, the items in the array are in the order that they're defined. This makes it very easy to reconcile your template literal later on.

{% collapsible Quasis %}

When accessing the value in your quasis, you'll see the string available as either _raw_ or _cooked_. These values determine whether escape sequences are ignored or interpreted. Axel Rauschmayer covers this in a little more detail in [this article](https://2ality.com/2016/09/template-literal-revision.html).

{% endcollapsible %}

Here's a question for you, what happens if the first character of our template literal is an expression? How is this represented in our AST? Try the following snippet in the AST explorer:

```jsx
const helloWorld = `${name}, how you doin'?`;
```

Take a little more time exploring quasis and expressions if they still feel unfamiliar to you.

Fortunately, we won't need to directly deal with the parsing process when writing our ESLint rules. We've covered a lot of ground because having a high-level understanding of how the tooling works, makes for a much more intuitive development experience later on.

{% collapsible Super Tiny Compiler %}

  If you're interested in learning a little more about the whole compilation process, the [Super Tiny Compiler](https://github.com/jamiebuilds/the-super-tiny-compiler/blob/master/the-super-tiny-compiler.js) is a really fun way of building your own JavaScript compiler using only a couple of hundred lines of code.

{% endcollapsible %}

## How Do Eslint Rules Work?

### The Visitor Pattern

Fortunately, we don't need to make any transformation when writing ESLint rules and instead we write our checks against specific node types in our code. These nodes are slices from our code's AST.

Once ESLint has parsed your code into an AST, it then traverses your tree, _visiting_ each node along the way. For those familiar with programming design patterns, you might recognise this pattern as the _visitor_ pattern.

The visitor pattern is a way of running some new logic against an object without modifying the object. ESLint uses the visitor pattern to separate the code used to run checks against your code from the AST.

Let's take a look at the [visitor pattern in action](https://codesandbox.io/s/mystifying-morning-lk3xh?file=/src/index.js).

You can see, that I've implemented the visitor using 3 blocks of code:

1. `ast.js`: The AST for `const name = 'andrico'`
2. `traverser.js`: An algorithm that traverses the nodes of our AST.
3. `visitors.js`: An object of methods where a given method fires once the traverser reaches its corresponding node. In our case, when the traverser reaches a `VariableDeclarator` node, it fires off our visitor function.

Let's break down the `traverser` a little more:

We begin in `index.js` by creating an instance of our `Traverser` class and passing through our AST and our visitors. Under the hood, our `Traverser` class stores our AST and visitors as instance variables for us to use later.

We then invoke the instance's `traverse` method. If you move to the `traverser.js` file, you can see that when we invoke `traverse` 5 things can happen:

1. The node is `null`, which will happen when we manually invoke the `traverse` method without any arguments. When this happens, we kick off the traversal function using the AST we stored during the class's initialisation.
2. The _node_ has a type of `Program`, which will happen for the top-level nodes in our AST. When this happens we recursively call the traversal method on the child nodes.
3. The _node_ has a type that matches a visitor function. When this happens, we fire our visitor function and pass through the node as an argument.
4. The _node_ has additional declarations, so we continue calling our traversal function on those child declarations.
5. Our _node_ satisfies none of these conditions, which will cause our traversal method to exit.

In the context of our `const name = 'andrico'` example, our traversal function will continue making its way through the AST until it reaches the `VariableDeclarator`, where it will invoke the visitor we defined in `visitors.js`. In this visitor we check to see if the value is `Andrico` and if it is, we log a message saying that it's an invalid name (though I kinda like it).

Open the console in the CodeSandbox and see what it outputs. Try changing the check in your visitor and see what happens, if anything.

The good news is that ESLint handles the traversal logic for our JavaScript. The other good news is that we'll need to implement the traversal logic for our parsed HTML. 😄

{% collapsible Open WC's eslint-plugin-lit-a11y %}

  This section was heavily informed by my recent involvement with Open WC's [eslint-plugin-lit-a11y](https://github.com/open-wc/open-wc/tree/master/packages/eslint-plugin-lit-a11y) and [eslint-plugin-lint](https://github.com/43081j/eslint-plugin-lit). If you'd like to learn more about (or try a handful of) ESLint rules focused on web components, these are your go-to repos.

{% endcollapsible %}

### What Does an Eslint Rule Look Like?

Writing an ESLint rule doesn't require anything fancy, it's just a plain ol' JavaScript object. The object's top-level can receive two properties: `meta` and `create`.

`meta` provides the metadata for the rule.

The `create` property is a function that returns an object of visitors that ESLint calls when it visits each node. This follows the same principle as the snippets in the codesandbox. And much like the demo in our codesandbox, the name of each visitor function is the name of the node that we want to visit.

In fact, we can even repurpose the pseudo-code from earlier, and decorate it with the ESLint specific boilerplate:

```jsx
module.exports = {
  create: function create() {
    return {
      TaggedTemplateExpression(node) {
        const isLitExpression = node.tag.name === 'html';

        if (isLitExpression) {
          // rest of the rule
        }

        // do nothing
      },
    };
  },
};
```

The `create` function also provides a context object, which provides some additional helpers and information about the current rule. The helper we're most concerned with right now is the `report()` method. We can call `report` whenever we want an ESLint error to display in the console or the IDE.

[Context.report](http://Context.report) takes an object with a handful of properties, but we're most interested in the following:

- message: the description of the problem
- node: the AST node related to the problem

{% collapsible Additional information %}

  We can pass through additional information, like the line of code we want to report the error on, but that's outside the scope of this tutorial.

{% endcollapsible %}

Before continuing, why not think about adjusting the pseudocode above to display an ESLint error when a tagged template is invoked, and the template literal has no content, like this:

```jsx
const expression = html``;
```

With a basic understanding of JavaScript's AST, the visitor pattern, and the anatomy of an ESLint rule, the only thing left to cover is how to parse our template string into HTML before we can start creating our rules.

For a more in-depth read into the anatomy of an ESLint rule, there's no better place to look than the [official docs](https://eslint.org/docs/developer-guide/working-with-rules).

## How Can We Transform Our Templates into HTML?

When using ESLint, we have the luxury of ESLint providing us with our parsed JavaScript AST. And while ESLint can't parse our HTML, we can use a library like `[parse5](https://github.com/inikulin/parse5)` to parse a valid HTML string into a data structure, not unlike our JavaScript AST.

The AST explorer we've spent so much time exploring even has settings for displaying [HTML ASTs](https://astexplorer.net/#/1CHlCXc4n4).

Since one of our rules is going to prevent us from passing through inline styles, let's see how the following gets represented as an AST:

```html
<div style="display:inline;">Main content</div>
```

If we dive into the AST and look for our div, we can see we're presented with some useful information. The most notable are:

**tagName**: Which is the name of the html element. (in this case `div`).

**attrs**: Which is an array of attributes, represented as a key-value pair. Our div's `attrs` property holds a single item. The item has a `name` of `style` and a `value` of `display:inline;`.

Using this information we can already start seeing how to piece together everything we've learned to create our first lint rule.

Here's how we can parse our JavaScript templates using the `parse5` library:

```jsx
import parse5 from 'parse5';

// We're defining out HTML templates
const htmlString = `<div style="display:inline;">Main content</div>`;

// We're passing through an HTML snippet to parseFragment, which returns our HTML AST
const parsedFragment = parse5.parseFragment(htmlString);

// We access the first child because the top-level contains metadata we don't need right now.
const div = parsedFragment.childNodes[0];

// We check to see if there are any style attributes in our div
const hasStyleAttr = div.attrs.some((attr) => attr.name === 'style');

// If there are, we report an error
if (hasStyleAttr) console.log('FAIL');
```

Thanks to tools like parse 5 and ESLint, we can offload a lot of the complex processing, and focus on writing the code for our specific rules. This is what we'll start doing from the next article onwards.

## Vibe Check

{% collapsible Further Reading %}

- [Babel Parser](https://babeljs.io/docs/en/babel-parser)
- [js-tokens](https://github.com/lydell/js-tokens)
- [AST Explorer](https://astexplorer.net/)
- [Abstract Syntax Trees Kent Dodds](https://egghead.io/lessons/javascript-introduction-to-abstract-syntax-trees)
- [Parsing and ASTs in JS](https://blog.sessionstack.com/how-javascript-works-parsing-abstract-syntax-trees-asts-5-tips-on-how-to-minimize-parse-time-abfcf7e8a0c8)
- [Super Tiny Compiler](https://github.com/jamiebuilds/the-super-tiny-compiler/blob/master/the-super-tiny-compiler.js)
- [You Don't Know JS Yet](https://github.com/getify/You-Dont-Know-JS/blob/2nd-ed/get-started/ch1.md) - Chapter 1
- [Working with rules - ESLint](https://eslint.org/docs/developer-guide/working-with-rules)
- [ESLint RuleTester](https://eslint.org/docs/developer-guide/nodejs-api#ruletester)
- [Interactive content](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_categories#interactive_content)
- [Babel plugin handbook](https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md)
- [ESLint Plugin Lit A11y](https://github.com/open-wc/open-wc/tree/master/packages/eslint-plugin-lit-a11y)
- [ESLint Plugin Lit](https://github.com/43081j/eslint-plugin-lit)

{% endcollapsible %}

We've covered a lot of theory so far, and a lot of separate ideas. We'll bring everything together in the next article.

Let's have a vibe check, if something doesn't quite make sense at this point, it's worth giving it a quick re-review. And if things still aren't clear, that's probably on me, so feel free to [reach out and let me know how I can make things even clearer](https://twitter.com/andricokaroulla?lang=en).

Before we move on, let's go through the key points one last time:

1. The JavaScript we write gets parsed into an AST, which tools can use to validate or transform our code
2. Each part of our JavaScript code is represented as a node, as seen in the AST explorer.
3. ESLint then traverses our AST, and invokes our visitor functions whenever it _visits_ a node we're interested in.
4. Once ESLint invokes our visitor function, we can start running checks against the node.
5. We can then check to see if the node that gets passed to our function is a `lit` _TaggedTemplateExpression_, and if it is, we can grab its HTML fragment, which we can build by consolidating the expressions and quasis.
6. We'll use `parse5` to parse the fragment and give us our HTML's AST.
7. We now have everything we need to run our checks, like seeing if a certain attribute is present when it shouldn't be.
8. We can then invoke ESLint's report function if the rule has been violated.

We've learned a lot of theory, and the best thing to do with all that theory is to put it into practice. In the next two articles, we're going to be creating a couple of ESLint rules and taking everything we've learned into something you can use for your own design systems.