DEV Community

Vladimir Klepov
Vladimir Klepov

Posted on • Originally published at blog.thoughtspile.tech on

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"
  }]
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)