Vladimir Klepov as a Coder

Become the master of your eslint with no-restricted-syntax

The other day I was doing my normal thing trying to force import '*.css' to be the last import in a file, which ensures a predicatbale CSS order. I spent hours looking for a eslint plugin to do that, but with little luck. Without getting into too much details:

I got entrenched in an argument about alphabetizing imports, so the issue was swept under the rug for a while. Just as I was about to write a custom plugin, the help arrived in the form of no-restricted-syntax — an amazing eslint rule that allows you to enforce almost anything, including my CSS import ordering. It lets you describe the code you don't want using ESQuery, a CSS-selector-like query language for ES AST. Sounds fancy and complicated, but if you know CSS (as a front-end developer, you probably do), you quickly get the hang of it.

Let's walk through an example, forcing CSS imports to be last:

  1. ImportDeclaration matches the AST node for import ...;. A good start, but too loose.
  2. To be more specific, we match only imports of files with .css extension using the amazing regex attribute selector: ImportDeclaration[source.value=/\\.css$/] Much better, but we don't want to ban all CSS imports.
  3. Finally, we can find (watch closely) imports following a CSS import with general sibling selector ImportDeclaration[source.value=/\\.css$/i] ~ ImportDeclaration[source.value!=/\\.css$/i] and ban them!

All in all, we end up with

"no-restricted-syntax": ["error", [{
"selector": "ImportDeclaration[source.value=/\\.css$/i] ~ ImportDeclaration[source.value!=/\\.css$/i]",
"message": "CSS import must be last"
}]

The warning is shown on imports following the CSS import, not the CSS import itself. Not ideal, but it's is a tradeoff you have to make since ESQuery selectors can't look ahead in the tree, just like CSS. On a bright note, you can use the dope CSS4 :has selector, which is not supported in any browser yet.

Two resources I find helpful when working with no-restricted-syntax are:

  • Esprima demo prints the AST for a JS snippet you provide. Very handy to see what node types and attributes the AST has.
  • ESQuery docs describe the supported AST selectors with links to their CSS counterparts. There's also a live playground that lets you try the selectors in a browser, but it doesn't work that great.

More and more often I find that writing a quick query for no-restricted-syntax is faster and simpler than looking for a plugin that does what you want, and then configuring it. Hell, it's even easier than trying to recall the name of a built-in rule that you know exists.

Even when a rule is available and you remember the name, no-restricted-syntax may offer some benefits:

  • More maintainable, as in MemberExpression[property.name=/^(add|remove)EventListener$/] vs no-restricted-properties with several copies of the rule to ban explicit addEventListener and removeEventListener.
  • More flexible, as in MemberExpression[property.name=/^(add|remove)EventListener$/][object.name!=/^(document|window)$/] that only allows explicit listeners on document and window.

As with anything, there are some weaker points:

  • ESQuery (and regexes) do have a learning curve, and other team members may struggle with editing the rules.
  • Autofix is clearly not available.
  • You can't disable a specific restriction per line with /* eslint-disable */, only the whole rule.
  • A minor inconvenience (or my stupidity), but I could not get slashes in regex attribute matchers to work no matter how much I escaped them (and I went all the way from / to \\\\/).

Overall, no-restricted-syntax is a very cool rule. It covers probably 95% of the cases where you might want a custom eslint plugin. If you ever wanted to ban some pretty specific thing, but abandoned that idea after a fruitless search for a fitting eslint plugin, it might be your turn to give it a shot!

ESQuery is not almighty — you still can't match across multiple modules or maintain a complex context. Is it time to write a plugin for that? Probably not — stay tuned for my next post on working around eslint limitations!

Hello, friend! My name is Vladimir, and I love writing about web development. If you got down here, you probably enjoyed this article. My goal is to become an independent content creator, and you'll help me get there by buying me a coffee!
More? All articles ever
Older? How to increase test coverage FAST Newer? Go beyond eslint limits with these 3 tricks