-
-
Notifications
You must be signed in to change notification settings - Fork 353
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 prefer-includes
rule - fixes #8
#70
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
# Prefer .includes() over .indexOf() when checking for existence. | ||
|
||
Since ESLint has no type analysis we'll have to assume all properties named .indexOf() have a .includes() counterpart. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would form it a bit different:
|
||
|
||
This is luckily true for all builtins: String#includes, Array#includes, TypedArray#includes, Buffer#includes. | ||
|
||
|
||
## Fail | ||
|
||
```js | ||
const str = 'foobar'; | ||
|
||
str.indexOf('foo') !== -1 | ||
str.indexOf('foo') != -1 | ||
str.indexOf('foo') > -1 | ||
str.indexOf('foo') >= 0 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Semicolons |
||
``` | ||
|
||
```js | ||
const str = 'foobar'; | ||
|
||
!str.includes === -1 | ||
!str.includes == -1 | ||
!str.includes < 0 | ||
``` | ||
|
||
|
||
## Pass | ||
|
||
```js | ||
const str = 'foobar'; | ||
|
||
str.includes('foo') | ||
``` | ||
|
||
TODO: | ||
|
||
Would also be useful to catch cases where .includes() would be better than a regex: | ||
|
||
```js | ||
/\r\n/.test(foo); | ||
``` | ||
|
||
Could be: | ||
|
||
```js | ||
foo.includes('\r\n'); | ||
``` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IIRC, |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
'use strict'; | ||
|
||
const isIndexOfCallExpression = node => { | ||
if (node.type !== 'CallExpression') { | ||
return false; | ||
} | ||
|
||
const property = node.callee.property; | ||
|
||
return property.name === 'indexOf'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will crash, because Also, PS : Not that it will apply here because there are a few changes to make anyway, but you could have written this in a more concise way like this const isIndexOfCallExpression = node => {
return node.type === 'CallExpression' &&
node.callee.property.name === 'indexOf';
}; There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I totally understand. Could you provide an example in context where There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. function indexOf(input) {
// ...
}
indexOf(foo); But in this implementation, you assume that any CallExpression's callee is a |
||
}; | ||
|
||
const isUnaryNotExpression = node => ( | ||
node.type === 'UnaryExpression' && node.operator === '!' | ||
); | ||
|
||
const isNegativeOne = (operator, value) => operator === '-' && value === 1; | ||
|
||
const getSourceCode = (context, node) => ( | ||
context.getSourceCode().text.slice(node.range[0], node.range[1]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be better to use |
||
); | ||
|
||
const report = (context, node, target, pattern) => { | ||
const targetSource = getSourceCode(context, target); | ||
const patternSource = getSourceCode(context, pattern); | ||
context.report({ | ||
node, | ||
message: 'Use `.includes()`, not .indexOf(), when checking for existence.', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
fix: fixer => fixer.replaceText(node, `${targetSource}.includes(${patternSource})`) | ||
}); | ||
}; | ||
|
||
const create = context => ({ | ||
BinaryExpression: node => { | ||
const left = node.left; | ||
const right = node.right; | ||
|
||
if (isIndexOfCallExpression(left)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I find these conditions to be really big, and I would extract them into their own function (you can pass them the Another benefit would be that you could make the same comparisons but with inversed expressions function checkIndexOf(context, nodeA, nodeB) {
if (isIndexOfCallExpression(nodeA)) {
if (nodeB.type === 'UnaryExpression') {
// ..
}
}
}
const create = context => {
BinaryExpression: node => {
checkIndexOf(context, node.left, node.right);
checkIndexOf(context, node.right, node.left);
}
}; Should you do this (and I would love to see this), you may need to pass the operators you will compare to too, as in the first call, you'll need to check |
||
const target = left.callee.object; | ||
const pattern = left.arguments[0]; | ||
|
||
if (right.type === 'UnaryExpression') { | ||
const argument = right.argument; | ||
|
||
if (argument.type !== 'Literal') { | ||
return false; | ||
} | ||
|
||
const value = argument.value; | ||
|
||
if (node.operator === '!==' && isNegativeOne(right.operator, value)) { | ||
report(context, node, target, pattern); | ||
} | ||
if (node.operator === '!=' && isNegativeOne(right.operator, value)) { | ||
report(context, node, target, pattern); | ||
} | ||
if (node.operator === '>' && isNegativeOne(right.operator, value)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shorter: if (['!==', '!=', '>'].indexOf(node.operator) !== -1 && isNegativeOne(right.operator, value)) {
report(context, node, target, pattern);
} |
||
report(context, node, target, pattern); | ||
} | ||
} | ||
|
||
if (right.type !== 'Literal') { | ||
return false; | ||
} | ||
|
||
if (node.operator === '>=' && right.value === 0) { | ||
report(context, node, target, pattern); | ||
} | ||
|
||
return false; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if (right.type === 'Literal' && node.operator === '>=' && right.value === 0) {
report(context, node, target, pattern);
}
return; |
||
} | ||
|
||
if (isUnaryNotExpression(left)) { | ||
const argument = left.argument; | ||
|
||
if (isIndexOfCallExpression(argument)) { | ||
const target = argument.callee.object; | ||
const pattern = argument.arguments[0]; | ||
|
||
if (right.type === 'UnaryExpression') { | ||
const argument = right.argument; | ||
|
||
if (argument.type !== 'Literal') { | ||
return false; | ||
} | ||
|
||
const value = argument.value; | ||
|
||
if (node.operator === '===' && isNegativeOne(right.operator, value)) { | ||
report(context, node, target, pattern); | ||
} | ||
if (node.operator === '==' && isNegativeOne(right.operator, value)) { | ||
report(context, node, target, pattern); | ||
} | ||
} | ||
|
||
if (right.type !== 'Literal') { | ||
return false; | ||
} | ||
|
||
if (node.operator === '<' && right.value === 0) { | ||
report(context, node, target, pattern); | ||
} | ||
|
||
return false; | ||
} | ||
} | ||
} | ||
}); | ||
|
||
module.exports = { | ||
create, | ||
meta: { | ||
fixable: 'code' | ||
} | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import test from 'ava'; | ||
import avaRuleTester from 'eslint-ava-rule-tester'; | ||
import rule from '../rules/prefer-includes'; | ||
|
||
const ruleTester = avaRuleTester(test, { | ||
env: { | ||
es6: true | ||
} | ||
}); | ||
|
||
const errors = [{ | ||
ruleId: 'prefer-includes', | ||
message: 'Use `.includes()`, not .indexOf(), when checking for existence.' | ||
}]; | ||
|
||
ruleTester.run('prefer-includes', rule, { | ||
valid: [ | ||
'str.indexOf(\'foo\') !== -n', | ||
'str.indexOf(\'foo\') !== 1', | ||
'!str.indexOf(\'foo\') === 1', | ||
'!str.indexOf(\'foo\') === -n', | ||
'str.includes(\'foo\')', | ||
'\'foobar\'.includes(\'foo\')', | ||
'[1,2,3].includes(4)', | ||
'"str.indexOf(\'foo\') !== -1"' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you add, just to be sure that nothing odds is happening, the following tests: `a + b` // you target binary expressions. If your checks have gaps, code like this could crash the plugin or create false positives
`indexOf !== -1`
// And whatever else you feel like adding :D |
||
], | ||
invalid: [ | ||
{ | ||
code: '\'foobar\'.indexOf(\'foo\') !== -1', | ||
output: '\'foobar\'.includes(\'foo\')', | ||
errors | ||
}, | ||
{ | ||
code: 'str.indexOf(\'foo\') != -1', | ||
output: 'str.includes(\'foo\')', | ||
errors | ||
}, | ||
{ | ||
code: 'str.indexOf(\'foo\') > -1', | ||
output: 'str.includes(\'foo\')', | ||
errors | ||
}, | ||
{ | ||
code: '\'foobar\'.indexOf(\'foo\') >= 0', | ||
output: '\'foobar\'.includes(\'foo\')', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of escaping output: `'foobar'.includes('foo')` |
||
errors | ||
}, | ||
{ | ||
code: '!str.indexOf(\'foo\') === -1', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you add another example with two |
||
output: 'str.includes(\'foo\')', | ||
errors | ||
}, | ||
{ | ||
code: '!str.indexOf(\'foo\') == -1', | ||
output: 'str.includes(\'foo\')', | ||
errors | ||
}, | ||
{ | ||
code: '!\'foobar\'.indexOf(\'foo\') < 0', | ||
output: '\'foobar\'.includes(\'foo\')', | ||
errors | ||
}, | ||
{ | ||
code: '[1,2,3].indexOf(4) !== -1', | ||
output: '[1,2,3].includes(4)', | ||
errors | ||
} | ||
] | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrap the methods in backticks and drop the
.
at the end.