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
Conversation
prefer-includes
rule - fixes #8
Indeed. See #64 (comment) for relevant discussion. Add your thoughts there if any. |
@@ -0,0 +1,48 @@ | |||
# Prefer .includes() over .indexOf() when checking for existence. |
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.
@@ -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 comment
The reason will be displayed to describe this comment to others. Learn more.
I would form it a bit different:
The rule assume all properties named
.indexOf()
have a.includes()
counterpart, as ESLint doesn't do type analysis. This is luckily true for all built-ins:String#includes
,Array#includes
,TypedArray#includes
,Buffer#includes
.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Semicolons
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Use
.includes()
rather than.indexOf()
when checking for existence.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Would be better to use context.getSourceCode().getText(node)
Very good work @jeremyjs! :) Have you worked with AST's before? // @jfmengels Mind doing a review too? |
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.
This looks like a pretty good start @jeremyjs, thanks a lot!
I think we can do quite a few more fixes too, like handling -1 !== foo.indexOf(bar)
for instance.
FYI, some good practice to help you along the way:
- Add as many valid tests as that makes you feel safe, especially for code that uses similar constructs to the code you're targeting, but slightly off.
- Write one or several failing test cases before making any new changes.
|
||
const property = node.callee.property; | ||
|
||
return property.name === 'indexOf'; |
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.
This will crash, because property
is not always there. Here, you assume that callee
will be a MemberExpression
(something like a.b()
), but it could be anything, like simply a Identifier
(a()
). Please add indexOf(foo)
as a valid test and fix to make sure it won't break.
Also, property
may not always be a Identifier
, so it may not always have a name. It could be a Literal (where the "name" is found under the value
property), or it could be something else. The absence of the name
field may not crash with this implementation or cause false positives, but I'd suggest checking that property
is an Identifier (or a Literal and compare that accordingly, with new tests :) )
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 comment
The 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 indexOf(foo)
wouldn't be a MemberExpression
?
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.
function indexOf(input) {
// ...
}
indexOf(foo);
But in this implementation, you assume that any CallExpression's callee is a MemberExpression
, which will cause a crash before you even check whether the property is indexOf
. Add indexOf(foo)
and a()
in your valid tests and the problems should be made clear right awaya IIRC.
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 comment
The 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);
}
|
||
```js | ||
foo.includes('\r\n'); | ||
``` |
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.
IIRC, includes
is not available in Node v4 (nor in some browsers), so it might be a good thing to add a When not to use it
section like here.
const left = node.left; | ||
const right = node.right; | ||
|
||
if (isIndexOfCallExpression(left)) { |
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.
I find these conditions to be really big, and I would extract them into their own function (you can pass them the context
arg so as not to keep it under create
).
Another benefit would be that you could make the same comparisons but with inversed expressions -1 !== foo.indexOf(bar)
. Example: (all names hereafter to be improved 😅)
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 >
, and for the second one, you'll need to check against <
. Write tests first to make sure you don't get odd results 😅
report(context, node, target, pattern); | ||
} | ||
|
||
return false; |
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.
if (right.type === 'Literal' && node.operator === '>=' && right.value === 0) {
report(context, node, target, pattern);
}
return;
errors | ||
}, | ||
{ | ||
code: '!str.indexOf(\'foo\') === -1', |
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.
Could you add another example with two !!
: !str.indexOf(\'foo\') === -1
. I want to make sure the result is correct.
}, | ||
{ | ||
code: '\'foobar\'.indexOf(\'foo\') >= 0', | ||
output: '\'foobar\'.includes(\'foo\')', |
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.
Instead of escaping \'
everywhere, you can use backticks around the whole string.
output: `'foobar'.includes('foo')`
'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 comment
The 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
Hey @jeremyjs! Are you still up to continue working on this? |
ping @jeremyjs |
Merry Christmas! 🎅 Sorry for the neglect. 😅 I'll see if I can iron out these kinks in my spare time before the new year. 😄 |
Merccy Christmas (or rather, happy new year now). No problem for the delay, I'm not the quick to review these days anyway ^^' |
Hey @jeremyjs. Still interested in finishing this? :) |
Closing for lack of activity. Happy to reopen if you ever have time to finish this :) |
Implements #8 except for the regex part which is tricky due to needing to distinguish a simple string from a regex with special characters.