Skip to content

Commit

Permalink
✨ add no-callback-literal rule (#179)
Browse files Browse the repository at this point in the history
  • Loading branch information
feross authored and mysticatea committed Aug 29, 2019
1 parent 72de3a3 commit c1d5dbf
Show file tree
Hide file tree
Showing 3 changed files with 240 additions and 0 deletions.
45 changes: 45 additions & 0 deletions docs/rules/no-callback-literal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# node/no-callback-literal

> Ensures the Node.js error-first callback pattern is followed
When invoking a callback function which uses the Node.js error-first callback pattern, all of your errors should either use the `Error` class or a subclass of it. It is also acceptable to use `undefined` or `null` if there is no error.

## 📖 Rule Details

When a function is named `cb` or `callback`, then it must be invoked with a first argument that is `undefined`, `null`, an `Error` class, or a subclass or `Error`.

Examples of :-1: **incorrect** code for this rule:

```js
/*eslint node/no-callback-literal: "error" */

cb('this is an error string');
cb({ a: 1 });
callback(0);
```

Examples of :+1: **correct** code for this rule:

```js
/*eslint node/no-callback-literal: "error" */

cb(undefined);
cb(null, 5);
callback(new Error('some error'));
callback(someVariable);
```

### Options

```json
{
"rules": {
"node/no-callback-literal": "error"
}
}
```

## 🔎 Implementation

- [Rule source](../../lib/rules/no-callback-literal.js)
- [Test source](../../tests/lib/rules/no-callback-literal.js)
82 changes: 82 additions & 0 deletions lib/rules/no-callback-literal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* @author Jamund Ferguson
* See LICENSE file in root directory for full license.
*/
"use strict"

module.exports = {
meta: {
docs: {
description:
"ensure Node.js-style error-first callback pattern is followed",
category: "Possible Errors",
recommended: false,
url:
"https://github.com/mysticatea/eslint-plugin-node/blob/v9.1.0/docs/rules/no-callback-literal.md",
},
type: "problem",
fixable: null,
schema: [],
},

create(context) {
const callbackNames = ["callback", "cb"]

function isCallback(name) {
return callbackNames.indexOf(name) > -1
}

return {
CallExpression(node) {
const errorArg = node.arguments[0]
const calleeName = node.callee.name

if (
errorArg &&
!couldBeError(errorArg) &&
isCallback(calleeName)
) {
context.report({
node,
message:
"Unexpected literal in error position of callback.",
})
}
},
}
},
}

/**
* Determine if a node has a possiblity to be an Error object
* @param {ASTNode} node ASTNode to check
* @returns {boolean} True if there is a chance it contains an Error obj
*/
function couldBeError(node) {
switch (node.type) {
case "Identifier":
case "CallExpression":
case "NewExpression":
case "MemberExpression":
case "TaggedTemplateExpression":
case "YieldExpression":
return true // possibly an error object.

case "AssignmentExpression":
return couldBeError(node.right)

case "SequenceExpression": {
const exprs = node.expressions
return exprs.length !== 0 && couldBeError(exprs[exprs.length - 1])
}

case "LogicalExpression":
return couldBeError(node.left) || couldBeError(node.right)

case "ConditionalExpression":
return couldBeError(node.consequent) || couldBeError(node.alternate)

default:
return node.value === null
}
}
113 changes: 113 additions & 0 deletions tests/lib/rules/no-callback-literal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* @author Jamund Ferguson
* See LICENSE file in root directory for full license.
*/
"use strict"

const RuleTester = require("eslint").RuleTester
const rule = require("../../../lib/rules/no-callback-literal")

const ruleTester = new RuleTester()
ruleTester.run("no-callback-literal", rule, {
valid: [
// random stuff
"horse()",
"sort(null)",
'require("zyx")',
'require("zyx", data)',

// callback()
"callback()",
"callback(undefined)",
"callback(null)",
"callback(x)",
'callback(new Error("error"))',
"callback(friendly, data)",
"callback(undefined, data)",
"callback(null, data)",
"callback(x, data)",
'callback(new Error("error"), data)',
"callback(x = obj, data)",
"callback((1, a), data)",
"callback(a || b, data)",
"callback(a ? b : c, data)",
"callback(a ? 1 : c, data)",
"callback(a ? b : 1, data)",

// cb()
"cb()",
"cb(undefined)",
"cb(null)",
'cb(undefined, "super")',
'cb(null, "super")',
],

invalid: [
// callback
{
code: 'callback(false, "snork")',
errors: [
{
message:
"Unexpected literal in error position of callback.",
},
],
},
{
code: 'callback("help")',
errors: [
{
message:
"Unexpected literal in error position of callback.",
},
],
},
{
code: 'callback("help", data)',
errors: [
{
message:
"Unexpected literal in error position of callback.",
},
],
},

// cb
{
code: "cb(false)",
errors: [
{
message:
"Unexpected literal in error position of callback.",
},
],
},
{
code: 'cb("help")',
errors: [
{
message:
"Unexpected literal in error position of callback.",
},
],
},
{
code: 'cb("help", data)',
errors: [
{
message:
"Unexpected literal in error position of callback.",
},
],
},
{
code: "callback((a, 1), data)",
errors: [
{
message:
"Unexpected literal in error position of callback.",
},
],
},
],
})

0 comments on commit c1d5dbf

Please sign in to comment.