Repository: sindresorhus/eslint-plugin-unicorn (JavaScript)

Issue:
`prefer-includes` rule
<!-- Issuehunt Badges -->

[<img alt="Issuehunt badges" src="https://issuehunt.io/r/sindresorhus/eslint-plugin-unicorn/issues/8/badge.svg" />](https://issuehunt.io/r/sindresorhus/eslint-plugin-unicorn/issues/8)

<!-- /Issuehunt Badges -->

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. This is luckily true for all builtins: `String#includes`, `Array#includes`, `TypedArray#includes`, `Buffer#includes`.

See https://github.com/eslint/eslint/issues/4209 for more.

`.includes()` cases:
- `'foobar'.indexOf('foo') !== -1`
- `'foobar'.indexOf('foo') != -1`
- `'foobar'.indexOf('foo') > -1`
- `'foobar'.indexOf('foo') >= 0`
## 

New to making ESLint rules? Check out this quick guide on [how to get started](https://gist.github.com/sindresorhus/1656c46f23545deff8cc713649dcff26).
<!-- Issuehunt content -->



> [prbart](https://issuehunt.io/u/prbart) earned $42.00 by resolving this issue!
> - Checkout the [Issuehunt explorer](https://issuehunt.io/r/sindresorhus/eslint-plugin-unicorn/) to discover more funded issues.
> - Need some help from other developers? [Add your repositories](https://issuehunt.io/r/new) on Issuehunt to raise funds.


<!-- /Issuehunt content-->


Discussion:
Also the negation for `!arr.includes`

`== 0`

`=== 0`

`< 0`

I assume you meant:

```
== -1
=== -1
< 0
```

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

> I assume you meant:

Indeed

I found the same rule in [`eslint-plugin-lodash`](https://github.com/wix/eslint-plugin-lodash/blob/master/docs%2Frules%2Fprefer-includes.md), though it's not necessarily all that portable.

I think I'll probably add the same rule to [eslint-plugin-lodash-fp](https://github.com/jfmengels/eslint-plugin-lodash-fp), because there is a lot of overlap but it does not always fit the bill for both... :/
In the case of the rule implemented in Lodash, they check whether it's Lodash's `indexOf` or the native one (though they don't check whether it's really from a native builtin type, which is fine IMO).

I guess I could rewrite the rule for the needs of the `fp` plugin, and in the same process add it to this repo too. I'm not fond of having the same rule written in three places, but don't see a much better way, as each one has its specific needs. What do you think?

I have hard time thinking of a separate package that checks just this ESLint case.

Sure, sounds good. Although, if any logic could be reused it would be nice to have it in some kind of eslint-utils module of some kind (hint hint).

ESLint closed the rule suggestion (https://github.com/eslint/eslint/issues/4209), so this can be implemented here now.

I can attempt to tackle this in the name of Hacktoberfest. Has anyone else started on it?

Also there is a hack to check:
```js
!!~'foobar'.indexOf('foo') // true
!!~'foobar'.indexOf('foo1') // false
```

as

```js
~n === -(n + 1)
```

it can be added as additional case
@Beraliv No one should write unreadable code like that. There should rather be a separate rule to disallow it.
Hi guys, I am a beginner and would like to implement this. Am I good to pick this one up?
@akhilpanchal There's already #70, but it was never finished. You could continue that one, by addressing the feedback on the PR and try to improve it.
Thank you. I see there is #115 as well which is built on top of #70. Will take a look at all the conversation and put out a PR.
Oh yeah, I meant #115.
This was almost finished in https://github.com/sindresorhus/eslint-plugin-unicorn/pull/115, but then got stale. Hopefully someone can help move it past the finish line.
@issuehuntfest has funded $40.00 to this issue. [See it on IssueHunt](https://issuehunt.io/repos/55832243/issues/8)
@rororofff has funded $2.00 to this issue.

---
- Submit pull request via [IssueHunt](https://issuehunt.io/repos/55832243/issues/8) to receive this reward.
- Want to contribute? Chip in to this issue via [IssueHunt](https://issuehunt.io/repos/55832243/issues/8).
- Checkout the [IssueHunt Issue Explorer](https://issuehunt.io/issues) to see more funded issues.
- Need help from developers? [Add your repository](https://issuehunt.io/users/47737115/repositories) on IssueHunt to raise funds.
@sindresorhus has rewarded $37.80 to @prbart. [See it on IssueHunt](https://issuehunt.io/repos/55832243/issues/8)

- :moneybag: Total deposit: $42.00
- :tada: Repository reward(0%): $0.00
- :wrench: Service fee(10%): $4.20



Patch:
diff --git a/docs/rules/prefer-includes.md b/docs/rules/prefer-includes.md
new file mode 100644
index 0000000000..15893af128
--- /dev/null
+++ b/docs/rules/prefer-includes.md
@@ -0,0 +1,27 @@
+# Prefer `.includes()` over `.indexOf()` when checking for existence
+
+All built-ins have `.includes()` in addition to `.indexOf()`. Prefer `.includes()` over comparing the value of `.indexOf()`
+
+This rule is fixable.
+
+## Fail
+
+```js
+[].indexOf('foo') !== -1;
+x.indexOf('foo') != -1;
+str.indexOf('foo') > -1;
+'foobar'.indexOf('foo') >= 0;
+x.indexOf('foo') === -1
+```
+
+## Pass
+
+```js
+const x = 'foobar';
+str.indexOf('foo') !== -n;
+str.indexOf('foo') !== 1;
+!str.indexOf('foo') === 1;
+!str.indexOf('foo') === -n;
+str.includes('foo');
+[1,2,3].includes(4);
+```
diff --git a/index.js b/index.js
index 83c7080592..822316ab22 100644
--- a/index.js
+++ b/index.js
@@ -48,7 +48,8 @@ module.exports = {
 				'unicorn/prefer-node-remove': 'error',
 				'unicorn/prefer-text-content': 'error',
 				'unicorn/no-for-loop': 'error',
-				'unicorn/prevent-abbreviations': 'error'
+				'unicorn/prevent-abbreviations': 'error',
+				'unicorn/prefer-includes': 'error'
 			}
 		}
 	}
diff --git a/readme.md b/readme.md
index 3827b3fbd2..6ea6b45c97 100644
--- a/readme.md
+++ b/readme.md
@@ -65,7 +65,8 @@ Configure it in `package.json`.
 			"unicorn/prefer-node-remove": "error",
 			"unicorn/prefer-text-content": "error",
 			"unicorn/no-for-loop": "error",
-			"unicorn/prevent-abbreviations": "error"
+			"unicorn/prevent-abbreviations": "error",
+			"unicorn/prefer-includes": "error"
 		}
 	}
 }
@@ -106,6 +107,7 @@ Configure it in `package.json`.
 - [prefer-text-content](docs/rules/prefer-text-content.md) - Prefer `textContent` over `innerText`. *(fixable)*
 - [no-for-loop](docs/rules/no-for-loop.md) - Do not use a `for` loop that can be replaced with a `for-of` loop. *(fixable)*
 - [prevent-abbreviations](docs/rules/prevent-abbreviations.md) - Prevent abbreviations *(partly fixable)*
+- [prefer-includes](docs/rules/prefer-includes.md) - Prefer `includes` over `indexOf` when checking for existence or non existence. *(fixable)*
 
 
 ## Recommended config
diff --git a/rules/prefer-includes.js b/rules/prefer-includes.js
new file mode 100644
index 0000000000..994cb6bbb8
--- /dev/null
+++ b/rules/prefer-includes.js
@@ -0,0 +1,73 @@
+'use strict';
+const getDocsUrl = require('./utils/get-docs-url');
+
+const isIndexOf = node => {
+	return (
+		node.type === 'CallExpression' &&
+		node.callee.type === 'MemberExpression' &&
+		node.callee.property.type === 'Identifier' &&
+		node.callee.property.name === 'indexOf'
+	);
+};
+
+const isNegativeOne = (operator, value) => operator === '-' && value === 1;
+
+const getSourceCode = (context, node) => (
+	context.getSourceCode().getText(node)
+);
+
+const report = (context, node, target, pattern) => {
+	const targetSource = getSourceCode(context, target);
+	const patternSource = getSourceCode(context, pattern);
+	context.report({
+		node,
+		message: 'Use `.includes()`, rather than `.indexOf()`, when checking for existence.',
+		fix: fixer => {
+			const isNot = node => ['===', '==', '<'].includes(node.operator) ? '!' : '';
+			const replacement = `${isNot(node)}${targetSource}.includes(${patternSource})`;
+			return fixer.replaceText(node, replacement);
+		}
+	});
+};
+
+const create = context => ({
+	BinaryExpression: node => {
+		const {left, right} = node;
+
+		if (isIndexOf(left)) {
+			const target = left.callee.object;
+			const pattern = left.arguments[0];
+
+			if (right.type === 'UnaryExpression') {
+				const {argument} = right;
+
+				if (argument.type !== 'Literal') {
+					return false;
+				}
+
+				const {value} = argument;
+
+				if (['!==', '!=', '>', '===', '=='].includes(node.operator) && isNegativeOne(right.operator, value)) {
+					report(context, node, target, pattern);
+				}
+			}
+
+			if (right.type === 'Literal' && ['>=', '<'].includes(node.operator) && right.value === 0) {
+				report(context, node, target, pattern);
+			}
+
+			return false;
+		}
+	}
+});
+
+module.exports = {
+	create,
+	meta: {
+		type: 'suggestion',
+		docs: {
+			url: getDocsUrl(__filename)
+		},
+		fixable: 'code'
+	}
+};


Test patch:
diff --git a/test/prefer-includes.js b/test/prefer-includes.js
new file mode 100644
index 0000000000..a06cac3f6e
--- /dev/null
+++ b/test/prefer-includes.js
@@ -0,0 +1,65 @@
+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()`, rather than `.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)',
+		'null.indexOf(\'foo\') !== 1',
+		'f(0) < 0'
+	],
+	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\')',
+			errors
+		},
+		{
+			code: '[1,2,3].indexOf(4) !== -1',
+			output: '[1,2,3].includes(4)',
+			errors
+		},
+		{
+			code: 'str.indexOf(\'foo\') < 0',
+			output: '!str.includes(\'foo\')',
+			errors
+		},
+		{
+			code: '\'\'.indexOf(\'foo\') < 0',
+			output: '!\'\'.includes(\'foo\')',
+			errors
+		}
+	]
+});