---
title: "Why did we build a custom ESLint plugin?"
description: "The story behind eslint-plugin-neeto"
canonical_url: "https://www.bigbinary.com/blog/eslint-plugin-neeto"
markdown_url: "https://www.bigbinary.com/blog/eslint-plugin-neeto.md"
---

# Why did we build a custom ESLint plugin?

The story behind eslint-plugin-neeto

- Author: Amaljith K
- Published: August 22, 2023
- Categories: JavaScript

At [neeto](https://neeto.com) we are working on
[20+ products](https://blog.neeto.com/p/neeto-products-and-people)
simultaneously. While we were developing multiple products, we aimed to make all
the products look like they were built by a single developer. They should have a
consistent coding style and should follow our standards & best practices.

The backend code was fairly clean because of the convention-over-configuration
philosophy of the Ruby on Rails framework. But, since React is just a JS
library, developers have the complete freedom to write code in all possible ways
JavaScript would allow.

In the initial stages, when we were building only 4-5 products, the only
enforcement mechanism we had was pull request reviews, apart from basic linting
using [ESLint](https://eslint.org/) and code formatting using
[Prettier](https://prettier.io/). PR reviews quickly turned inefficient as the
number of products grew. The code reviewers started missing some of the new
changes to the coding standards and continued suggesting the outdated standard.
This led to inconsistent code and behavior between products.

At that time, we thought of building our custom ESLint plugin. Initially, we
expected it to be very hard because of the need to deal with the language AST to
identify the code that didn't follow our standards. However, after some
research, we discovered a FOSS tool, [astexplorer](https://astexplorer.net),
which shows a visual representation of JavaScript AST. With that, writing ESLint
plugins for non-standard code became easy.

The custom plugin was a huge success during the POC itself. It helped us
significantly in improving the quality of the front-end code. At the time of
writing this blog, we have 50 custom ESLint rules enforced by the plugin.

The plugin was named `eslint-plugin-neeto`, adhering to
[ESLint's plugin naming conventions](https://eslint.org/docs/latest/extend/plugins#name-a-plugin).
Later, we namespaced it under `@bigbinary`, to be consistent with our other
frontend packages. Now the plugin is available as
[`@bigbinary/eslint-plugin-neeto`](https://www.npmjs.com/package/@bigbinary/eslint-plugin-neeto).

## Enforcing ESLint at neeto

In the overall development flow, ESLint will run on three levels.

1.  #### IDE extension (optional)

    Even though BigBinary doesn't have any IDE preference, the majority of
    developers use [Visual Studio Code](https://code.visualstudio.com) here. The
    popular
    [ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
    on VS Code runs ESLint while the developer is writing code and gives
    immediate visual feedback with squiggly lines.

    ![ESLint extension on VS Code](https://www.bigbinary.com/blog/images/images_used_in_blog/2023/eslint-plugin-neeto/ide-extension.gif)

    The use of IDE extension is completely optional. We have enforcement
    mechanisms at other levels to ensure the code quality & standards.

2.  #### Pre-commit hook

    There are some ESLint rules like
    [sort-imports](https://eslint.org/docs/latest/rules/sort-imports) that are
    only meant to keep code consistency. Even though functionality-wise, there
    is nothing wrong with keeping the imports in any order, this rule would
    throw an error if the imports are not correctly sorted.

    Secondly, developers will see this error frequently because most IDEs add
    the new import statement to the bottom of the imports list during
    auto-import.

    ![Auto import and import-order errors](https://www.bigbinary.com/blog/images/images_used_in_blog/2023/eslint-plugin-neeto/import-order.gif)

    But, we believe that developers should not be concerned about errors like
    these. They should be focusing on the business logic rather than spending
    time on things that are auto-correctable. At the same time, we need this
    rule to be auto-fixed at some point to keep consistency.

    So, we chose Git's pre-commit hooks for automatically fixing such errors. We
    used [husky](https://www.npmjs.com/package/husky) to add `eslint --fix`
    command to the pre-commit hook. Also, we used
    [lint-staged](https://www.npmjs.com/package/lint-staged) to run the commands
    only on the files that are included in the current commit. Thus, we
    automated lint-fixing and formatting of the files that we are going to
    commit.

    If any rule violations aren't auto-fixable, the commit would fail with an
    error. Developers can get the details of the rule violation from the error
    message, make corrections themselves, and commit again.

    ![Pre-commit hook](https://www.bigbinary.com/blog/images/images_used_in_blog/2023/eslint-plugin-neeto/precommit-hook.gif)

3.  #### CI checks

    VS Code extension and pre-commit hooks are great tools to warn developers
    early and it saves time. However, VS Code extensions and pre-commit hooks
    are not something we can rely on fully. That's because the developer can
    skip these if they prefer to.

    Here are some cases where things might not work out:

    - Lint checks from both the pre-commit hook and VS Code extension won't run
      if the developer forgets to run `yarn install` while setting up the
      repository. This is because the installation and setting up of all the npm
      packages in a repository is done by `yarn install` command. So, the
      packages [eslint](https://www.npmjs.com/package/eslint),
      [husky](https://www.npmjs.com/package/husky), and
      [lint-staged](https://www.npmjs.com/package/lint-staged) won't be
      available to run until `yarn install` is run.
    - Developers can manually skip Git's pre-commit hooks by running
      `git commit --no-verify`. The `--no-verify` flag indicates that I want to
      commit the changes, but I don't want the hooks to run and block me.

    To ensure that the code that goes into the repository is lint-free, we need
    to implement continuous integration (CI) checks. With CI checks in place,
    the PR (Pull Request) reviewer can know whether a PR meets the quality
    standards just by looking into this section of the PR:

    Passing CI checks
    ![CI checks on github](https://www.bigbinary.com/blog/images/images_used_in_blog/2023/eslint-plugin-neeto/ci-checks.png)

    Failing CI checks
    ![Failed CI checks on github](https://www.bigbinary.com/blog/images/images_used_in_blog/2023/eslint-plugin-neeto/failed-ci-checks.png)

    From the CI checks, we will run ESLint without the `--fix` flag. The goal of
    CI checks is to detect and report lints and not correct them.

    For CI, currently, we are using an in-house developed tool named
    [NeetoCI](https://www.neeto.com/neetoci).

## Challenges we faced

[neeto](https://neeto.com) is building a lot of products. All these products had
a lot of code written in a non-standard way. It meant that when we got started
with the code standardization task, we had to deal with a large amount of code
to fix.

Some of the non-standard codes were auto-correctable. For such cases, we
published ESLint rules withan auto-fixer. Whenever anyone makes any changes in a
file containing that non-standard code pattern, it would get auto-corrected
during the pre-commit hook execution. It was easy.

But some cases weren't auto-fixable. For such patterns, we published the ESLint
rule and then asked a few engineers to go through all projects and run the
command `eslint app/**/*.{js,jsx,json}`. It would reveal all pieces of code that
violate the new rule. Those errors had to be fixed manually. We named this
process **rollout**.

While doing this, we realized that we were not thinking much about false
positives and true negatives. We started encountering them a lot during the
rollout. It forced us to make fixes, publish the updates again to npm, and redo
the rollout. This was inefficient.

After facing such incidents, we decided to clone all repos locally and test the
changes in all products before raising PR. Even though it was hard at the
beginning, it proved to be quite effective in detecting all possible edge cases
of a rule.

Later, we began developing rules for more complex, abstract standards. It was
impossible to detect and flag all non-standard code patterns for them. If we
stressed covering more cases, we would start getting several false positives
along with it.

A good example would be the `hard-coded-strings-should-be-localized` rule. The
aim was to force people to use `i18next` based localization instead of
hardcoding strings in English. In other words, all the strings that are supposed
to be rendered on the DOM should come from the translation files.

If we were to flag all string literals as errors in the code, we would have more
false positives than real errors. There were several strings like `enum` keys
that were used only in the application logic. They will never be rendered on UI.
There is no point in applying localization to them.

As you might guess, there is no way an ESLint plugin could tell if a string is
going to be rendered in the UI or if it is used only in the application logic.
To circumvent this, we decided to raise an error if the string contains a space
character in it. Since enum keys didn't contain spaces, it eliminated a lot of
false positives. But, it created a lot of true negatives. We were missing all
one-word strings that were rendered in the UI.

Also, even with that change, we didn't fully eliminate false positives. There
were several cases where we used space-separated strings in the application
logic. An example is `classNames` prop. It usually contains a space-separated
string of CSS classes (like `classNames="flex justify-center"`).

To minimize such edge cases, we added a whitelist for property names like
`classNames` which should always be ignored, and a blacklist for properties like
`label` which should always be flagged even if it is a single word. With that
change, we were able to lower the false positives and true negatives to a great
extent.

Even with all this logic in place, we weren't confident that we have eliminated
false positives fully. So we decided to publish this rule as a warning. ESLint
warnings are less strict than errors. VS Code extension would still show yellow
squiggly lines and we would still get warning statements while running
pre-commit hooks. But the pre-commit hook won't fail when a warning is
encountered. The same applies to the CI checks.

![ESLint warnings in VS Code](https://www.bigbinary.com/blog/images/images_used_in_blog/2023/eslint-plugin-neeto/eslint-warning.png)

## Lessons learned

In the case of ESLint rules, false positives are more harmful than true
negatives. If a false positive error is raised, it would confuse the developers.
Even though they are writing code in the right way, the plugin would complain
that it is an error. Some developers might even write some tricky code to
circumvent the ESLint error, thinking that it is genuine.

We always try to minimize false positives by testing the rule with the existing
code in several repos before publishing it. In some cases, we can't avoid very
rare false positives to favor a large number of true negatives. If a developer
encounters false positives for such rules, they are advised to disable the rule
for that specific line of code.

Building ESLint rules not only helped us improve the code quality but also gave
us exposure to AST parsing and manipulation. With the help of that new
knowledge, we were able to accomplish several other achievements. As an example,
we have built a custom Babel plugin that could generate the boilerplate code of
[fetching values from a Zustand store](https://github.com/pmndrs/zustand#selecting-multiple-state-slices)
at compile time. You can read about how you can build your own ESLint and Babel
plugins on our upcoming blogs. Stay tuned.

## Links

- [Human page](https://www.bigbinary.com/blog/eslint-plugin-neeto)
