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 prefer-includes rule - fixes #8 #70

Closed
wants to merge 3 commits into from

Conversation

jeremyjs
Copy link

@jeremyjs jeremyjs commented Oct 30, 2016

Implements #8 except for the regex part which is tricky due to needing to distinguish a simple string from a regex with special characters.

@jeremyjs jeremyjs changed the title Prefer includes (fixes #8) Prefer includes - fixes #8 Oct 30, 2016
@jeremyjs jeremyjs changed the title Prefer includes - fixes #8 Add prefer-includes rule - fixes #8 Oct 30, 2016
@sindresorhus
Copy link
Owner

except for the regex part which is tricky due to needing to distinguish a simple string from a regex with special characters.

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.
Copy link
Owner

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.
Copy link
Owner

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
Copy link
Owner

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.',
Copy link
Owner

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])
Copy link
Owner

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)

@sindresorhus
Copy link
Owner

Very good work @jeremyjs! :)

Have you worked with AST's before?


// @jfmengels Mind doing a review too?

Copy link
Contributor

@jfmengels jfmengels left a 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';
Copy link
Contributor

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';
};

Copy link
Author

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?

Copy link
Contributor

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)) {
Copy link
Contributor

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');
```
Copy link
Contributor

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)) {
Copy link
Contributor

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;
Copy link
Contributor

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',
Copy link
Contributor

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\')',
Copy link
Contributor

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"'
Copy link
Contributor

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

@jfmengels
Copy link
Contributor

Hey @jeremyjs! Are you still up to continue working on this?

@sindresorhus
Copy link
Owner

ping @jeremyjs

@jeremyjs
Copy link
Author

Merry Christmas! 🎅 Sorry for the neglect. 😅 I'll see if I can iron out these kinks in my spare time before the new year. 😄

@jfmengels
Copy link
Contributor

Merccy Christmas (or rather, happy new year now). No problem for the delay, I'm not the quick to review these days anyway ^^'

@sindresorhus
Copy link
Owner

Hey @jeremyjs. Still interested in finishing this? :)

@sindresorhus
Copy link
Owner

Closing for lack of activity. Happy to reopen if you ever have time to finish this :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants