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

feat(rule): add no-identity-handlers rule #108

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ Then configure the rules you want to use under the rules section.
"promise/catch-or-return": "error",
"promise/no-native": "off",
"promise/no-nesting": "warn",
"promise/no-identity-handlers": "warn",
"promise/no-promise-in-callback": "warn",
"promise/no-callback-in-promise": "warn",
"promise/avoid-new": "warn",
Expand Down Expand Up @@ -86,6 +87,7 @@ or start with the recommended rule set
| [`always-return`][always-return] | Return inside each `then()` to create readable and reusable Promise chains. | :bangbang: | |
| [`no-native`][no-native] | In an ES5 environment, make sure to create a `Promise` constructor before using. | | |
| [`no-nesting`][no-nesting] | Avoid nested `then()` or `catch()` statements | :warning: | |
| [`no-identity-handlers`][no-identity-handlers] | Avoid unnecessary identity functions in `then()` or `catch()` | :warning: | |
| [`no-promise-in-callback`][no-promise-in-callback] | Avoid using promises inside of callbacks | :warning: | |
| [`no-callback-in-promise`][no-callback-in-promise] | Avoid calling `cb()` inside of a `then()` (use [nodeify][] instead) | :warning: | |
| [`avoid-new`][avoid-new] | Avoid creating `new` promises outside of utility libs (use [pify][] instead) | | |
Expand Down Expand Up @@ -120,6 +122,7 @@ or start with the recommended rule set
[always-return]: docs/rules/always-return.md
[no-native]: docs/rules/no-native.md
[no-nesting]: docs/rules/no-nesting.md
[no-identity-handlers]: docs/rules/no-identity-handlers.md
[no-promise-in-callback]: docs/rules/no-promise-in-callback.md
[no-callback-in-promise]: docs/rules/no-callback-in-promise.md
[avoid-new]: docs/rules/avoid-new.md
Expand Down
81 changes: 81 additions & 0 deletions __tests__/no-identity-handlers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
'use strict'

const rule = require('../rules/no-identity-handlers')
const RuleTester = require('eslint').RuleTester
const ruleTester = new RuleTester({
parserOptions: {
ecmaVersion: 6
}
})

ruleTester.run('no-identity-handlers', rule, {
valid: [
'Promise.resolve(2).then(value => value * 2)',
'Promise.resolve(2).then(value => { return value + 1 })',
'Promise.resolve(2).then(() => null)',
'Promise.resolve(2).then(() => doSomethingTotallyDifferent())',
'somePromise().then(value => doSomethingWith(value))',
'somePromise().then(handler)',
'Promise.reject(Error()).catch(err => { console.error(err); throw err; })',
'Promise.reject(Error()).catch(err => { throw doSomethingTo(err) })',
'somePromise().catch(handler)',
'somePromise().catch(createHandler())',
'somePromise().then(value => value * 2, err => { console.error(err); throw err; })',
'somePromise().then(func, func)',
'getObject().then(({ a, b, c }) => ({ a, b, c: c, d: calculate(a, b, c) }))',
'getObject().then(({ a: rename, b, c }) => ({ rename, b, c }))',
'getObject().then(({ a, b, c }) => { return { a, b: c, c: b } })',
'getArray().then(([a, b]) => [a, b, ...getSomeOtherArray()])',
'getArray().then(([a, b]) => [b, a])',

// edge cases that aren't really valid but shouldn't throw or report
'Promise.resolve(2).then()',
'Promise.reject(Error()).catch()'
],
invalid: [
{
code: 'Promise.resolve(2).then(_ => _)',
errors: [{ message: 'No identity handlers' }]
},
{
code: 'Promise.resolve(2).then(val => { return val })',
errors: [{ message: 'No identity handlers' }]
},
{
code: 'Promise.resolve(2).then(function (value) { return value })',
errors: [{ message: 'No identity handlers' }]
},
{
code: 'getObject().then(({ a, b, c }) => ({ a, b, c }))',
errors: [{ message: 'No identity handlers' }]
},
{
code: 'getObject().then(({ a, b, c }) => { return { a, b, c } })',
errors: [{ message: 'No identity handlers' }]
},
{
code: 'getArray().then(([a, b]) => [a, b])',
errors: [{ message: 'No identity handlers' }]
},
{
code: 'getArray().then(([a, b]) => { return [a, b] })',
errors: [{ message: 'No identity handlers' }]
},
{
code: 'Promise.reject(Error()).catch(err => { throw err })',
errors: [{ message: 'No identity handlers' }]
},
{
code: 'Promise.reject(Error()).catch(function (e) { throw e })',
errors: [{ message: 'No identity handlers' }]
},
{
code: 'Promise.reject(Error()).then(null, error => { throw error })',
errors: [{ message: 'No identity handlers' }]
},
{
code: 'Promise.reject(Error()).then(null, function (e) { throw e })',
errors: [{ message: 'No identity handlers' }]
}
]
})
1 change: 1 addition & 0 deletions docs/rules/no-identity-handlers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Avoid unnecessary identity functions in `then()` or `catch()` (no-identity-handlers)
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module.exports = {
'no-callback-in-promise': require('./rules/no-callback-in-promise'),
'no-promise-in-callback': require('./rules/no-promise-in-callback'),
'no-nesting': require('./rules/no-nesting'),
'no-identity-handlers': require('./rules/no-identity-handlers'),
'avoid-new': require('./rules/avoid-new'),
'no-new-statics': require('./rules/no-new-statics'),
'no-return-in-finally': require('./rules/no-return-in-finally'),
Expand All @@ -33,6 +34,7 @@ module.exports = {
'promise/catch-or-return': 'error',
'promise/no-native': 'off',
'promise/no-nesting': 'warn',
'promise/no-identity-handlers': 'warn',
'promise/no-promise-in-callback': 'warn',
'promise/no-callback-in-promise': 'warn',
'promise/avoid-new': 'off',
Expand Down
143 changes: 143 additions & 0 deletions rules/no-identity-handlers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
'use strict'

const getDocsUrl = require('./lib/get-docs-url')
const isPromise = require('./lib/is-promise')

const checkObjectPatternIdentity = createIdentityCheck({
type: 'ObjectExpression',
areAllIdentifiers: expression => {
return expression.properties.every(
property => property.key.type === 'Identifier'
)
},
toSetOfNames: node => {
return new Set(node.properties.map(property => property.key.name))
}
})

const checkArrayPatternIdentity = createIdentityCheck({
type: 'ArrayExpression',
areAllIdentifiers: expression => {
return expression.elements.every(element => element.type === 'Identifier')
},
toSetOfNames: node => {
return new Set(node.elements.map(element => element.name))
}
})

function createIdentityCheck(config) {
function difference(setA, setB) {
return new Set(Array.from(setA).filter(x => !setB.has(x)))
}

function containsSameIdentifierNames(expressionA, expressionB) {
const setA = config.toSetOfNames(expressionA)
const setB = config.toSetOfNames(expressionB)
return setA.size === setB.size && difference(setA, setB).size === 0
}

function checkIdentity(firstParam, body) {
return (
config.areAllIdentifiers(body) &&
containsSameIdentifierNames(firstParam, body)
)
}

return (firstParam, body) => {
switch (body.type) {
case config.type:
return checkIdentity(firstParam, body)
case 'BlockStatement':
const firstBodyStatement = body.body[0]
return firstBodyStatement.type === 'ReturnStatement' &&
firstBodyStatement.argument.type === config.type
? checkIdentity(firstParam, firstBodyStatement.argument)
: false
default:
return false
}
}
}

function isFunctionExpression(node) {
return (
node.type === 'FunctionExpression' ||
node.type === 'ArrowFunctionExpression'
)
}

function getFirstParamName(node) {
const firstParam = node.params[0]
return firstParam && firstParam.type === 'Identifier' ? firstParam.name : null
}

function getBodyValueName(node) {
const body = node.body || {}
if (body.type === 'Identifier') {
return body.name
}
if (body.type === 'BlockStatement') {
const firstBodyStatement = body.body[0] || { type: '', argument: {} }
return (firstBodyStatement.type === 'ReturnStatement' ||
firstBodyStatement.type === 'ThrowStatement') &&
firstBodyStatement.argument.type === 'Identifier'
? firstBodyStatement.argument.name
: null
}
return null
}

function isIdentityFunction(node) {
if (node.params.length === 1) {
const firstParam = node.params[0]
switch (firstParam.type) {
case 'Identifier':
return getFirstParamName(node) === getBodyValueName(node)
case 'ObjectPattern':
return checkObjectPatternIdentity(firstParam, node.body)
case 'ArrayPattern':
return checkArrayPatternIdentity(firstParam, node.body)
default:
return false
}
}
return false
}

module.exports = {
meta: {
docs: {
url: getDocsUrl('no-identity-handlers')
}
},
create(context) {
function checkIdentity(node) {
if (node && isFunctionExpression(node) && isIdentityFunction(node)) {
context.report({
node,
message: 'No identity handlers'
})
}
}

return {
CallExpression(node) {
if (!isPromise(node)) {
return
}

switch (node.callee.property.name) {
case 'then':
checkIdentity(node.arguments[0])
checkIdentity(node.arguments[1])
break
case 'catch':
checkIdentity(node.arguments[0])
break
default:
break
}
}
}
}
}