Skip to main content

Library design - Inversion of Control

· 5 min read
Andrea Pontrandolfo
Sheriff author, Tech Lead @ Velasca

I wanted to give a glimpse into the architectural design decisions going on behind the scenes in Sheriff, so I'm sharing a deep dive into the Inversion of Control principle and how it helps Sheriff achieve a more flexible and accessible API.

info

I know that Inversion of Control has many meanings in computer programming, but I'm using here it to describe the process of giving the user more control over the library's behavior. Sorry, Java bros.

Why

One of the coolest things about Sheriff is the way it encapsulates the complexity of ESLint and its ecosystem, mostly hiding the ugly details behind a curtain. The problem is, sometimes the level of abstraction is too intrusive and can interfere with users' ability to customize the configuration to suit their needs. It's a delicate balance to strike.

In this article, we will explore how Sheriff uses the Inversion of Control principle as the key to unlocking the best APIs.

The noRestrictedSyntaxOverride option

The problem

ESLint offers a rule called no-restricted-syntax that allows you to disallow specific JavaScript syntax features.

Sheriff comes with a preconfigured no-restricted-syntax rule that disallows the most harmful and confusing syntax features.

The problem here is that the rule accepts an array of features, meaning that if the user dislike any one of these lints, they had only 2 options:

  1. disable the rule entirely
    noRestrictedSyntax: 0
  2. override the rule with a new one
    noRestrictedSyntax: [
    2,
    {
    selector: "...",
    message: "...",
    },
    {
    selector: "...",
    message: "...",
    },
    ]

In Sheriff, we came up with a solution that allows users to both disable specific restrictions they disliked and append new ones.

The API looked like this:

noRestrictedSyntaxOverride: {
adjuncts: [
{
selector: "LabeledStatement",
message: "...",
},
{
selector: "ForInStatement",
message: "...",
},
],
allows: [
"LabeledStatement",
"ForInStatement",
],
},

In the adjuncts array, users can add syntax features to disallow on top of the defaults, while they can use the allows array to disable specific defaults.

As you can see, the offered API looked rather complex and unintuitive. Both adjuncts and allows suggest "adding" something, so newcomers found understanding "what did what" difficult. In short: it was not user-friendly at all.

The solution

Inversion of control to the rescue!

Instead of wrapping no-restricted-syntax options into Sheriff’s options, we let users define and configure the no-restricted-syntax rule directly in their own config and then offer composables around it. This way, our users are back in the driver’s seat and have full control once more.

Sheriff now exposes a variable called baseNoRestrictedSyntaxRules that contains the contents of the Sheriff-configured no-restricted-syntax rule. With this simple API, adding new restrictions on top of the defaults is as simple as:

no-restricted-syntax: [
2,
...baseNoRestrictedSyntaxRules,
// your custom rules here...
],

Admittedly, disabling the defaults is a little more involved.

Each entry prints its index at the end of the message, so users can use the indices to identify which entries they wish to remove.

In the docs we suggest using the native JavaScript Array#toSpliced() method to remove an entry from the baseNoRestrictedSyntaxRules array:

no-restricted-syntax: [
2,
...baseNoRestrictedSyntaxRules.toSpliced(2, 1)
// your custom rules here...
],

🠪 Learn more about this API in the docs.

Going forward

Recently, Brad Zacher released an interesting alternative to no-restricted-syntax called eslint-no-restricted.

Sheriff will likely adopt this library in the future and abandon the current, homegrown solution, as Brad’s solution seems to provide superior DX.

To keep up with the latest developments, check out issue #375.

Typescript-eslint project API

Another case where Sheriff swallowed up complexity at the cost of taking away control from users was with typescript-eslint’s parserOptions.project API.

The problem

Sheriff used to hardcode the path to the tsconfig.json it passed to typescript-eslint.

project: './tsconfig.json'

Naturally, users with multiple tsconfig.jsons felt limited because it wasn't possible to specify different paths for tsconfig.jsons.

The solution

The solution was to let the user define the project path through the Sheriff options:

pathsOverrides: {
tsconfigLocation: "./tsconfig.sample.json",
},

and fall back to the default if not specified:

parserOptions: {
project: userChosenTSConfig || true,
},

Going forward

Since then, the typescript-eslint team has moved to a new system called the project service.

This option is meant to get rid of the need for custom tsconfig.jsons like tsconfig.eslint.json, etc., that some users had made for advanced use cases. The project service also has other benefits, and is often more performant. Learn more.

Sheriff is currently exploring on adopting this new API (issue | PR).

FlatConfig VS ESLint wrappers

info

This topic is also briefly covered in the prior-art section.

While Sheriff has many unique features, it strives to remain nothing more than a standard ESLint config.

This means, again, that the user has all the power in their hands over the linting experience. Sheriff takes nothing out of ESLint, it only enhances it.

On the contrary, ESLint wrappers hinder the power of ESLint by removing control from the hands of the users.

Conclusion

By simplifying APIs and empowering users with more granular controls, Sheriff seeks to strike a balance between ease of use and developer freedom. As the library evolves, we remain committed to enhancing developer experience with modern, user-focused solutions.