BAM

4 lessons I learned implementing my first ESLint rule

ESLint is a very useful and powerful tool that analyzes JavaScript code and allows to set rules up. It is vastly used to define good coding practices shared throughout the entire project.

I recently had to implement my first ESLint rule ever, and I’d like to share some pitfalls I encountered as an eslint-beginner and the lessons I learned out of this first implementation.

As a mobile developer, my issue was the following: I realized that on iOS devices, some of my app components had a shadow effect that was somehow missing on Android devices.

It came from the fact that my components styles contained the CSS property box-shadow but not the CSS property elevation. However, box-shadow is only supported on iOS. For Android shadow support, the elevation property would be needed instead.

As nothing enforced that in the code, that gave me the idea of writing an ESLint rule that would prevent developers from using box-shadow without elevation and vice versa.

Lesson #1: simplify the ESLint rule

I needed a rule that would parse all the CSS properties. If one of my two shadow properties existed without the other, then an error should be raised.

The problem is that this idea requires complex logic that would be very heavy in terms of computation. Indeed once the parser has found the target node corresponding to a shadow property, it still needs to check all its siblings in the syntax tree.

There was actually a simpler step that would help improve the code first and make the writing of the ESLint rule easier later.

In fact, instead of implementing a rule to force developers to provide both shadow properties every time, I could write a util function getShadowStyle that would generate the pair of shadow properties first. Then I would make the developer go through that util function thanks to the ESLint rule.

++pre>++code class="language-typescript">
import React, { FunctionComponent } from 'react';
import styled from '@core/styled/styled';

export const MyComponent: FunctionComponent = () => <Container />;

const Container = styled.View`
 height: 100px;
 background-color: blue;
 box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.25);
 elevation: 2;
`;
++/pre>++/code>Bad implementation with both shadow properties needed for iOS and Android support

++pre>++code class="language-typescript">
import React, { FunctionComponent } from 'react';
import { Platform } from 'react-native';
import styled, { css } from '@core/styled/styled';

// Declare pairs of shadow properties for iOS and Android
const getShadowStyleFromType = (type: 'small' | 'medium') => {
 switch (shadowType) {
   case 'small':
     return { iOS: '0px -4px 4px rgba(0, 0, 0, 0.04)', android: 1 };
   case 'medium':
     return { iOS: '0px 3px 6px rgba(0, 0, 0, 0.25)', android: 2 };
 }
};

// Util function to be called in my styled-components template literals
const getShadowStyle = (type: 'small' | 'medium') => {
 const shadowStyle = getShadowStyleFromType(type);
 return Platform.select({
   ios: css`
     box-shadow: ${shadowStyle.iOS};
   `,
   android: css`
     elevation: ${shadowStyle.android};
   `,
 });
};

export const MyComponent: FunctionComponent = () => <Container />;

const Container = styled.View`
 height: 100px;
 background-color: blue;
 ${() => getShadowStyle('medium')}
`;
++/pre>++/code> Safe and consistent implementation using a util function to get both properties at once


From Never use a shadow property without the other, that’s how my ESLint rule evolved to Never use a shadow property directly (but call getShadowStyle instead). That simplified the rule implementation a lot: I just needed to check if any of box-shadow or elevation properties were used in the CSS and if so, raise an error.

Lesson #2: use AST selectors

Understanding abstract syntax trees

An Abstract Syntax Tree (AST) is a tree representing the structure of program code. Each node in the tree represents a construct occurring in the source code.

You can visualize the AST of any bit of JavaScript code thanks to this JointJS online tool.

For instance, here is the AST for the instruction console.log("Hello world!"):

AST visualization for console.log("Hello world"), generated with JointJS

As a matter of fact, it is important to know what the code AST looks like to understand how ESLint will evaluate patterns.

I went to astexplorer.net to figure out how to manipulate my syntax tree to target the proper nodes in my rule implementation. AST Explorer is a web tool that allows to generate and explore abstract syntax trees. It provides several code parsers among which an ESLint parser for TypeScript, which I used.

Naive implementation

I provided a minimal code to AST Explorer and could easily find the CSS properties nodes thanks to the syntax tree.

AST Explorer interface with the code to be tested (top left), the corresponding abstract syntax tree (top right), my rule code (bottom left), and the output of the rule applied to my code (bottom right)

I could then raise an error if a CSS property matched one of my shadow properties. The rule was somehow quick to write, and was as follows (pardon the lack of intelligibility):

++pre>++code class="language-javascript">

export default function (context) {
 // Define the targeted properties
 const shadowProperties = ["box-shadow", "elevation"];

 return {
     // Check all nodes corresponding to variable declarations
   VariableDeclaration(node) {
      // Extract the value assigned to the node
     const source = node.declarations[0].init;
      // Target tagged template expressions
     if (source.type === "TaggedTemplateExpression") {
// If a shadow property is present in raw value, raise an error
       const hasShadowProperty = shadowProperties.some((property) => source.quasi.quasis[0].value.raw.includes(property));
       if (hasShadowProperty) {
         context.report({
           node,
           message: "Do not use box-shadow / elevation in css"
         });
       }
     }
   }
 };
}

++/pre>++/code>

Naive ESLint rule code, tested in AST Explorer


However, there were several problems with this approach:

  • the rule was wordy and difficult to read
  • it seemed hard to maintain
  • I wrote it naively exploring the AST, without understanding exactly the role of each attribute I was accessing

Needless to say, I was very unsure about the correctness of my rule, even if it was working well with the example. Moreover, I didn’t pay attention to the fact that with this code, the error would not point to the relevant node: in the AST Explorer example, the error is raised at the ++code>const++/code> declaration and not at the CSS shadow property.

Implementation with AST selectors

A much simpler way to target the right node was actually to use selectors to query my AST.

AST selectors are strings that can match nodes in an AST. Their syntax is very close to CSS selectors syntax.

For instance, they allow you to select nodes:

  • by type (example: ++code>VariableDeclarator++/code>)
  • by attribute existence (example: ++code>[attr]++/code>)
  • by attribute value (example: ++code>[attr="foo"]++/code>)
  • by descendant (example: ++code>FunctionExpression ReturnStatement++/code>)
  • by first child (example: ++code>:first-child++/code>)

Using selectors, my rule implementation then became:

++pre>++code class="language-javascript">

export default function (context) {
 return {
   'TaggedTemplateExpression:has(Identifier[name="styled"]) TemplateElement[value.raw=/.*(elevation|box-shadow).*/]': function reportShadowProperties(
     node
   ) {
     context.report({
       node,
       message: "Do not use box-shadow / elevation in css"
     });
   }
 };
}

++/pre>++/code>

ESLint rule code with AST selectors

That rule can be read as:

  • Find me a node that is a TaggedTemplateExpression (here it is styled.View...`)
    - that has an Identifier
    - whose name is “styled”
  • Once you’ve found it, find all its children
    - that are template elements
    - whose value contains either the word "elevation" or the word "box-shadow"

It requires you to be a bit familiar with selectors, but once you know the basics, that rule is easy to understand and modify.

Lesson #3: use built-in ESLint rules

Now that I had a simple and functional rule tested in AST Explorer, I intended to add it as a custom rule to my project. In order to do this, I first needed to install and configure the plugin eslint-plugin-local-rules.

However, I wanted to avoid installing a plugin for this one rule, without even knowing how tedious the configuration would be. What if there were a simpler way?

Looking at other ESLint rules in my project, I could see that the built-in rule no-restricted-syntax would actually help me achieve exactly what I wanted to do. This rule takes a list of AST selectors as input and disallows the provided syntax in your project.

In the end, I simply had to add the previously written AST selector with my custom error message next to the no-restricted-syntax rule declaration in my eslintrc file:
++pre>++code class="language-javascript">

'no-restricted-syntax': [
     'error',
     {
       ... // Existing rules in the project
     },
     {
       selector:
     'TaggedTemplateExpression:has(Identifier[name="styled"]) TemplateElement[value.raw=/.*(elevation|box-shadow).*/]',
       message: 'Always use getShadowStyle instead of box-shadow / elevation',
     },
   ],

++/pre>++/code>

no-restricted-syntax rule declaration in the eslintrc file

Thanks to no-restricted-syntax, not only did I avoid installing a new plugin, but I could also define my rule in a very condensed and understandable way.

Lesson #4: refine your ESLint rule application scope

Though applying an ESLint rule to the entire code repository is quite frequent, sometimes you’ll want to restrain a rule application to some specific files or folders. That was actually my case: my project is a monorepo that gathers React web code and React Native app code, both in TypeScript. And while I wanted to prevent developers from using box-shadow directly in the app code, that property should still be available in the web code as it is fully supported. That is why my rule had to be applied to the app source code folder only.

To do so, I declared an array commonNoRestrictedSyntax with common (meaning app and web) rules and extended it to declare another array appNoRestrictedSyntax for app-only rules. Thanks to the overrides key, I could then specify which set of rules I wanted to apply to the src/app files.

Git diff for eslinrc file: common rules can be overriden with app rules based on glob specification

Thanks to this configuration, using box-shadow in the app code raises an error, while using it in the web code is permitted.

Conclusion

Creating that first ESLint rule and enforcing it in my project was very enlightening. While writing a rule can be pretty straightforward, that rule should be as easy to read, maintainable, and efficient as possible.

To write ESLint rules cleverly, remember to ask yourself these questions:

  • Can the rule be simpler?
  • Can I use selectors to implement it?
  • Do I really need to write a custom rule or is there already a built-in rule that I can use?
  • Is the perimeter of my rule correct, and are there exceptions to the rule to take into account?

And keep in mind that ESLint is a fantastic tool for enforcing coding standards in your project. It really is worth learning how to implement rules!

Développeur mobile ?

Rejoins nos équipes