Using ESLint's no-restricted-syntax rule
There’s a gotcha with Javascript’s Array.reduce
. If you don’t pass an initial value to Array.reduce
, you’ll receive a TypeError
when the array is empty.
In this post we’ll demonstrate how to use ESLint’s no-restricted-syntax
rule to build a lint for Array.reduce
.
Preventing errors with Array.reduce
The following code raises a TypeError
because we don’t provide an initial value to Array.reduce
.
[].reduce((a, b) => a + b);
// Uncaught TypeError: Reduce of empty array with no initial value
// at Array.reduce (<anonymous>)
We can prevent this TypeError
by passing an initial value, 0
.
[].reduce((a, b) => a + b, 0);
// Success!
To prevent the TypeError
, our lint should require us to pass an initial value to all Array.reduce
calls.
To create our lint with no-restricted-syntax
, we need an ESLint AST Selector that matches our problem code.
Building an AST Selector
Our AST Selector should match only the syntax we’d like to ban. Let’s go step-by-step and build a Selector.
-
Find a sample of the syntax you’d like to ban, like the code above.
// ban [].reduce((a, b) => a + b); // allow [].reduce((a, b) => a + b, 0);
-
Visit astexplorer.net and select the “espree” parser. This is the parser used by ESLint.
Paste in your sample code.
Looking at the AST, we see that our
Array.reduce
expression is represented as aCallExpression
.[].reduce((a, b) => a + b); // AST, truncated for readability { "type": "CallExpression", "callee": { "type": "MemberExpression", "property": { "type": "Identifier", // we should match CallExpression's with a callee.property.name of // "reduce". "name": "reduce" } }, // we should match CallExpression's with one argument. "arguments": [ { "type": "ArrowFunctionExpression" // truncated } ] }
-
Find a feature of the AST node that differentiates the good syntax from the bad.
A bad
Array.reduce
call will only have one argument (the callback), while a good call will have two arguments. So we want to match allArray.reduce
calls with one argument.We also only care about calls for
.reduce()
. Looking at our AST, we want to matchCallExpression
s with acallee
MemberExpression
having anIdentifier
with aname
"reduce"
.Using the Selector syntax, we can match a
CallExpression
with a single argument as:CallExpression[arguments.length=1]
To match the
"reduce"
callee
, we can write:CallExpression[callee.property.name='reduce']
Combining our two selectors together, we have:
CallExpression[arguments.length=1][callee.property.name='reduce']
-
Test your selector on the
esquery
website.Here we can test how our selector matches on our sample code.
Paste your sample code and enter your selector. You’ll see “1 nodes found” towards the bottom of the page if your selector matches.
Testing our selector against our sample code from above, everything looks good.
-
Add your selector to ESLint’s config (
.eslintrc.js
)This will configure ESLint’s
no-restricted-syntax
rule to use our new selector.module.exports = { rules: { "no-restricted-syntax": [ "error", { selector: "CallExpression[arguments.length=1][callee.property.name='reduce']", message: "Provide initialValue to .reduce().", }, ], }, };
If we write,
[].reduce((a, b) => a + b)
, we’ll get an error: “Provide initialValue to .reduce().” -
Success!
ESLint should now warn about incorrect
Array.reduce
calls.
alternative solutions
We could have written our Selector using nesting like the following:
CallExpression[arguments.length=1] > MemberExpression.callee > Identifier.property[name=reduce]
By nesting our selector, we can tell ESLint to only highlight the final Identifier
node when reporting the error.
Which looks like:
Check out the “Selectors” page on the ESLint website for more information about ESLint AST Selectors.