Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add no-callback-literal rule #179

Merged
merged 2 commits into from Aug 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
45 changes: 45 additions & 0 deletions docs/rules/no-callback-literal.md
@@ -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
@@ -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
@@ -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.",
},
],
},
],
})