diff --git a/packages/eslint-plugin/docs/rules/prefer-includes.md b/packages/eslint-plugin/docs/rules/prefer-includes.md index 07cc9ea44e1..018780a657d 100644 --- a/packages/eslint-plugin/docs/rules/prefer-includes.md +++ b/packages/eslint-plugin/docs/rules/prefer-includes.md @@ -22,6 +22,7 @@ let str: string; let array: any[]; let readonlyArray: ReadonlyArray; let typedArray: UInt8Array; +let maybe: string; let userDefined: { indexOf(x: any): number; includes(x: any): boolean; @@ -31,6 +32,7 @@ str.indexOf(value) !== -1; array.indexOf(value) !== -1; readonlyArray.indexOf(value) === -1; typedArray.indexOf(value) > -1; +maybe?.indexOf('') !== -1; userDefined.indexOf(value) >= 0; // simple RegExp test diff --git a/packages/eslint-plugin/src/rules/prefer-includes.ts b/packages/eslint-plugin/src/rules/prefer-includes.ts index d21b088d4aa..13a6021c806 100644 --- a/packages/eslint-plugin/src/rules/prefer-includes.ts +++ b/packages/eslint-plugin/src/rules/prefer-includes.ts @@ -1,5 +1,6 @@ import { AST_NODE_TYPES, + TSESLint, TSESTree, } from '@typescript-eslint/experimental-utils'; import { AST as RegExpAST, parseRegExpLiteral } from 'regexpp'; @@ -125,67 +126,81 @@ export default createRule({ ); } - return { - [[ - // a.indexOf(b) !== 1 - "BinaryExpression > CallExpression.left > MemberExpression.callee[property.name='indexOf'][computed=false]", - // a?.indexOf(b) !== 1 - "BinaryExpression > ChainExpression.left > CallExpression > MemberExpression.callee[property.name='indexOf'][computed=false]", - ].join(', ')](node: TSESTree.MemberExpression): void { - // Check if the comparison is equivalent to `includes()`. - const callNode = node.parent as TSESTree.CallExpression; - const compareNode = ( - callNode.parent?.type === AST_NODE_TYPES.ChainExpression - ? callNode.parent.parent - : callNode.parent - ) as TSESTree.BinaryExpression; - const negative = isNegativeCheck(compareNode); - if (!negative && !isPositiveCheck(compareNode)) { - return; - } + function checkArrayIndexOf( + node: TSESTree.MemberExpression, + allowFixing: boolean, + ): void { + // Check if the comparison is equivalent to `includes()`. + const callNode = node.parent as TSESTree.CallExpression; + const compareNode = ( + callNode.parent?.type === AST_NODE_TYPES.ChainExpression + ? callNode.parent.parent + : callNode.parent + ) as TSESTree.BinaryExpression; + const negative = isNegativeCheck(compareNode); + if (!negative && !isPositiveCheck(compareNode)) { + return; + } - // Get the symbol of `indexOf` method. - const tsNode = services.esTreeNodeToTSNodeMap.get(node.property); - const indexofMethodDeclarations = types - .getSymbolAtLocation(tsNode) + // Get the symbol of `indexOf` method. + const tsNode = services.esTreeNodeToTSNodeMap.get(node.property); + const indexofMethodDeclarations = types + .getSymbolAtLocation(tsNode) + ?.getDeclarations(); + if ( + indexofMethodDeclarations == null || + indexofMethodDeclarations.length === 0 + ) { + return; + } + + // Check if every declaration of `indexOf` method has `includes` method + // and the two methods have the same parameters. + for (const instanceofMethodDecl of indexofMethodDeclarations) { + const typeDecl = instanceofMethodDecl.parent; + const type = types.getTypeAtLocation(typeDecl); + const includesMethodDecl = type + .getProperty('includes') ?.getDeclarations(); if ( - indexofMethodDeclarations == null || - indexofMethodDeclarations.length === 0 + includesMethodDecl == null || + !includesMethodDecl.some(includesMethodDecl => + hasSameParameters(includesMethodDecl, instanceofMethodDecl), + ) ) { return; } + } - // Check if every declaration of `indexOf` method has `includes` method - // and the two methods have the same parameters. - for (const instanceofMethodDecl of indexofMethodDeclarations) { - const typeDecl = instanceofMethodDecl.parent; - const type = types.getTypeAtLocation(typeDecl); - const includesMethodDecl = type - .getProperty('includes') - ?.getDeclarations(); - if ( - includesMethodDecl == null || - !includesMethodDecl.some(includesMethodDecl => - hasSameParameters(includesMethodDecl, instanceofMethodDecl), - ) - ) { - return; - } - } - - // Report it. - context.report({ - node: compareNode, - messageId: 'preferIncludes', - *fix(fixer) { + // Report it. + context.report({ + node: compareNode, + messageId: 'preferIncludes', + ...(allowFixing && { + *fix(fixer): Generator { if (negative) { yield fixer.insertTextBefore(callNode, '!'); } yield fixer.replaceText(node.property, 'includes'); yield fixer.removeRange([callNode.range[1], compareNode.range[1]]); }, - }); + }), + }); + } + + return { + // a.indexOf(b) !== 1 + "BinaryExpression > CallExpression.left > MemberExpression.callee[property.name='indexOf'][computed=false]"( + node: TSESTree.MemberExpression, + ): void { + checkArrayIndexOf(node, /* allowFixing */ true); + }, + + // a?.indexOf(b) !== 1 + "BinaryExpression > ChainExpression.left > CallExpression > MemberExpression.callee[property.name='indexOf'][computed=false]"( + node: TSESTree.MemberExpression, + ): void { + checkArrayIndexOf(node, /* allowFixing */ false); }, // /bar/.test(foo) @@ -230,7 +245,7 @@ export default createRule({ } yield fixer.insertTextAfter( argNode, - `${node.optional ? '?.' : '.'}includes(${JSON.stringify(text)}`, + `${node.optional ? '?.' : '.'}includes('${text}'`, ); }, }); diff --git a/packages/eslint-plugin/tests/rules/prefer-includes.test.ts b/packages/eslint-plugin/tests/rules/prefer-includes.test.ts index c72488a4937..ceda607df49 100644 --- a/packages/eslint-plugin/tests/rules/prefer-includes.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-includes.test.ts @@ -1,6 +1,4 @@ -import { TSESLint } from '@typescript-eslint/experimental-utils'; import rule from '../../src/rules/prefer-includes'; -import * as util from '../../src/util'; import { RuleTester, getFixturesRootDir } from '../RuleTester'; const rootPath = getFixturesRootDir(); @@ -13,126 +11,101 @@ const ruleTester = new RuleTester({ }, }); -type MessageIds = util.InferMessageIdsTypeFromRule; - -type InvalidTestCase = TSESLint.InvalidTestCase; -type ValidTestCase = TSESLint.ValidTestCase | string; -function addOptional(cases: ValidTestCase[]): ValidTestCase[]; -function addOptional(cases: InvalidTestCase[]): InvalidTestCase[]; -function addOptional( - cases: (ValidTestCase | InvalidTestCase)[], -): (ValidTestCase | InvalidTestCase)[] { - return cases.reduce<(ValidTestCase | InvalidTestCase)[]>((acc, c) => { - acc.push(c); - if (typeof c === 'string') { - acc.push(c.replace('.', '?.')); - } else { - acc.push({ - ...c, - code: c.code.replace('.', '?.'), - output: 'output' in c ? c.output?.replace('.', '?.') : null, - }); - } - - return acc; - }, []); -} - ruleTester.run('prefer-includes', rule, { - valid: addOptional([ + valid: [ ` function f(a: string): void { - a.indexOf(b) + a.indexOf(b); } `, ` function f(a: string): void { - a.indexOf(b) + 0 + a.indexOf(b) + 0; } `, ` - function f(a: string | {value: string}): void { - a.indexOf(b) !== -1 + function f(a: string | { value: string }): void { + a.indexOf(b) !== -1; } `, ` type UserDefined = { - indexOf(x: any): number // don't have 'includes' - } + indexOf(x: any): number; // don't have 'includes' + }; function f(a: UserDefined): void { - a.indexOf(b) !== -1 + a.indexOf(b) !== -1; } `, ` type UserDefined = { - indexOf(x: any, fromIndex?: number): number - includes(x: any): boolean // different parameters - } + indexOf(x: any, fromIndex?: number): number; + includes(x: any): boolean; // different parameters + }; function f(a: UserDefined): void { - a.indexOf(b) !== -1 + a.indexOf(b) !== -1; } `, ` type UserDefined = { - indexOf(x: any, fromIndex?: number): number - includes(x: any, fromIndex: number): boolean // different parameters - } + indexOf(x: any, fromIndex?: number): number; + includes(x: any, fromIndex: number): boolean; // different parameters + }; function f(a: UserDefined): void { - a.indexOf(b) !== -1 + a.indexOf(b) !== -1; } `, ` type UserDefined = { - indexOf(x: any, fromIndex?: number): number - includes: boolean // different type - } + indexOf(x: any, fromIndex?: number): number; + includes: boolean; // different type + }; function f(a: UserDefined): void { - a.indexOf(b) !== -1 + a.indexOf(b) !== -1; } `, ` function f(a: string): void { - /bar/i.test(a) + /bar/i.test(a); } `, ` function f(a: string): void { - /ba[rz]/.test(a) + /ba[rz]/.test(a); } `, ` function f(a: string): void { - /foo|bar/.test(a) + /foo|bar/.test(a); } `, ` function f(a: string): void { - /bar/.test() + /bar/.test(); } `, ` function f(a: string): void { - something.test(a) + something.test(a); } `, ` - const pattern = new RegExp("bar") + const pattern = new RegExp('bar'); function f(a) { - return pattern.test(a) + return pattern.test(a); } `, - ]), - invalid: addOptional([ + ], + invalid: [ // positive { code: ` function f(a: string): void { - a.indexOf(b) !== -1 + a.indexOf(b) !== -1; } `, output: ` function f(a: string): void { - a.includes(b) + a.includes(b); } `, errors: [{ messageId: 'preferIncludes' }], @@ -140,12 +113,12 @@ ruleTester.run('prefer-includes', rule, { { code: ` function f(a: string): void { - a.indexOf(b) != -1 + a.indexOf(b) != -1; } `, output: ` function f(a: string): void { - a.includes(b) + a.includes(b); } `, errors: [{ messageId: 'preferIncludes' }], @@ -153,12 +126,12 @@ ruleTester.run('prefer-includes', rule, { { code: ` function f(a: string): void { - a.indexOf(b) > -1 + a.indexOf(b) > -1; } `, output: ` function f(a: string): void { - a.includes(b) + a.includes(b); } `, errors: [{ messageId: 'preferIncludes' }], @@ -166,12 +139,12 @@ ruleTester.run('prefer-includes', rule, { { code: ` function f(a: string): void { - a.indexOf(b) >= 0 + a.indexOf(b) >= 0; } `, output: ` function f(a: string): void { - a.includes(b) + a.includes(b); } `, errors: [{ messageId: 'preferIncludes' }], @@ -181,12 +154,12 @@ ruleTester.run('prefer-includes', rule, { { code: ` function f(a: string): void { - a.indexOf(b) === -1 + a.indexOf(b) === -1; } `, output: ` function f(a: string): void { - !a.includes(b) + !a.includes(b); } `, errors: [{ messageId: 'preferIncludes' }], @@ -194,12 +167,12 @@ ruleTester.run('prefer-includes', rule, { { code: ` function f(a: string): void { - a.indexOf(b) == -1 + a.indexOf(b) == -1; } `, output: ` function f(a: string): void { - !a.includes(b) + !a.includes(b); } `, errors: [{ messageId: 'preferIncludes' }], @@ -207,12 +180,12 @@ ruleTester.run('prefer-includes', rule, { { code: ` function f(a: string): void { - a.indexOf(b) <= -1 + a.indexOf(b) <= -1; } `, output: ` function f(a: string): void { - !a.includes(b) + !a.includes(b); } `, errors: [{ messageId: 'preferIncludes' }], @@ -220,12 +193,28 @@ ruleTester.run('prefer-includes', rule, { { code: ` function f(a: string): void { - a.indexOf(b) < 0 + a.indexOf(b) < 0; } `, output: ` function f(a: string): void { - !a.includes(b) + !a.includes(b); + } + `, + errors: [{ messageId: 'preferIncludes' }], + }, + { + code: ` + function f(a?: string): void { + a?.indexOf(b) === -1; + } + `, + errors: [{ messageId: 'preferIncludes' }], + }, + { + code: ` + function f(a?: string): void { + a?.indexOf(b) !== -1; } `, errors: [{ messageId: 'preferIncludes' }], @@ -235,42 +224,42 @@ ruleTester.run('prefer-includes', rule, { { code: ` function f(a: string): void { - /bar/.test(a) + /bar/.test(a); } `, output: ` function f(a: string): void { - a.includes("bar") + a.includes('bar'); } `, errors: [{ messageId: 'preferStringIncludes' }], }, { code: ` - const pattern = new RegExp("bar") + const pattern = new RegExp('bar'); function f(a: string): void { - pattern.test(a) + pattern.test(a); } `, output: ` - const pattern = new RegExp("bar") + const pattern = new RegExp('bar'); function f(a: string): void { - a.includes("bar") + a.includes('bar'); } `, errors: [{ messageId: 'preferStringIncludes' }], }, { code: ` - const pattern = /bar/ + const pattern = /bar/; function f(a: string, b: string): void { - pattern.test(a + b) + pattern.test(a + b); } `, output: ` - const pattern = /bar/ + const pattern = /bar/; function f(a: string, b: string): void { - (a + b).includes("bar") + (a + b).includes('bar'); } `, errors: [{ messageId: 'preferStringIncludes' }], @@ -280,12 +269,12 @@ ruleTester.run('prefer-includes', rule, { { code: ` function f(a: any[]): void { - a.indexOf(b) !== -1 + a.indexOf(b) !== -1; } `, output: ` function f(a: any[]): void { - a.includes(b) + a.includes(b); } `, errors: [{ messageId: 'preferIncludes' }], @@ -293,12 +282,12 @@ ruleTester.run('prefer-includes', rule, { { code: ` function f(a: ReadonlyArray): void { - a.indexOf(b) !== -1 + a.indexOf(b) !== -1; } `, output: ` function f(a: ReadonlyArray): void { - a.includes(b) + a.includes(b); } `, errors: [{ messageId: 'preferIncludes' }], @@ -306,12 +295,12 @@ ruleTester.run('prefer-includes', rule, { { code: ` function f(a: Int8Array): void { - a.indexOf(b) !== -1 + a.indexOf(b) !== -1; } `, output: ` function f(a: Int8Array): void { - a.includes(b) + a.includes(b); } `, errors: [{ messageId: 'preferIncludes' }], @@ -319,12 +308,12 @@ ruleTester.run('prefer-includes', rule, { { code: ` function f(a: Int16Array): void { - a.indexOf(b) !== -1 + a.indexOf(b) !== -1; } `, output: ` function f(a: Int16Array): void { - a.includes(b) + a.includes(b); } `, errors: [{ messageId: 'preferIncludes' }], @@ -332,12 +321,12 @@ ruleTester.run('prefer-includes', rule, { { code: ` function f(a: Int32Array): void { - a.indexOf(b) !== -1 + a.indexOf(b) !== -1; } `, output: ` function f(a: Int32Array): void { - a.includes(b) + a.includes(b); } `, errors: [{ messageId: 'preferIncludes' }], @@ -345,12 +334,12 @@ ruleTester.run('prefer-includes', rule, { { code: ` function f(a: Uint8Array): void { - a.indexOf(b) !== -1 + a.indexOf(b) !== -1; } `, output: ` function f(a: Uint8Array): void { - a.includes(b) + a.includes(b); } `, errors: [{ messageId: 'preferIncludes' }], @@ -358,12 +347,12 @@ ruleTester.run('prefer-includes', rule, { { code: ` function f(a: Uint16Array): void { - a.indexOf(b) !== -1 + a.indexOf(b) !== -1; } `, output: ` function f(a: Uint16Array): void { - a.includes(b) + a.includes(b); } `, errors: [{ messageId: 'preferIncludes' }], @@ -371,12 +360,12 @@ ruleTester.run('prefer-includes', rule, { { code: ` function f(a: Uint32Array): void { - a.indexOf(b) !== -1 + a.indexOf(b) !== -1; } `, output: ` function f(a: Uint32Array): void { - a.includes(b) + a.includes(b); } `, errors: [{ messageId: 'preferIncludes' }], @@ -384,12 +373,12 @@ ruleTester.run('prefer-includes', rule, { { code: ` function f(a: Float32Array): void { - a.indexOf(b) !== -1 + a.indexOf(b) !== -1; } `, output: ` function f(a: Float32Array): void { - a.includes(b) + a.includes(b); } `, errors: [{ messageId: 'preferIncludes' }], @@ -397,12 +386,12 @@ ruleTester.run('prefer-includes', rule, { { code: ` function f(a: Float64Array): void { - a.indexOf(b) !== -1 + a.indexOf(b) !== -1; } `, output: ` function f(a: Float64Array): void { - a.includes(b) + a.includes(b); } `, errors: [{ messageId: 'preferIncludes' }], @@ -410,25 +399,51 @@ ruleTester.run('prefer-includes', rule, { { code: ` function f(a: T[] | ReadonlyArray): void { - a.indexOf(b) !== -1 + a.indexOf(b) !== -1; } `, output: ` function f(a: T[] | ReadonlyArray): void { - a.includes(b) + a.includes(b); } `, errors: [{ messageId: 'preferIncludes' }], }, { code: ` - function f | Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array>(a: U): void { - a.indexOf(b) !== -1 + function f< + T, + U extends + | T[] + | ReadonlyArray + | Int8Array + | Uint8Array + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | Float32Array + | Float64Array, + >(a: U): void { + a.indexOf(b) !== -1; } `, output: ` - function f | Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array>(a: U): void { - a.includes(b) + function f< + T, + U extends + | T[] + | ReadonlyArray + | Int8Array + | Uint8Array + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | Float32Array + | Float64Array, + >(a: U): void { + a.includes(b); } `, errors: [{ messageId: 'preferIncludes' }], @@ -436,20 +451,20 @@ ruleTester.run('prefer-includes', rule, { { code: ` type UserDefined = { - indexOf(x: any): number - includes(x: any): boolean - } + indexOf(x: any): number; + includes(x: any): boolean; + }; function f(a: UserDefined): void { - a.indexOf(b) !== -1 + a.indexOf(b) !== -1; } `, output: ` type UserDefined = { - indexOf(x: any): number - includes(x: any): boolean - } + indexOf(x: any): number; + includes(x: any): boolean; + }; function f(a: UserDefined): void { - a.includes(b) + a.includes(b); } `, errors: [{ messageId: 'preferIncludes' }], @@ -457,15 +472,15 @@ ruleTester.run('prefer-includes', rule, { { code: ` function f(a: Readonly): void { - a.indexOf(b) !== -1 + a.indexOf(b) !== -1; } `, output: ` function f(a: Readonly): void { - a.includes(b) + a.includes(b); } `, errors: [{ messageId: 'preferIncludes' }], }, - ]), + ], });