diff --git a/packages/eslint-plugin/src/rules/no-for-in-array.ts b/packages/eslint-plugin/src/rules/no-for-in-array.ts index ec8ebe9125fb..45986948abe8 100644 --- a/packages/eslint-plugin/src/rules/no-for-in-array.ts +++ b/packages/eslint-plugin/src/rules/no-for-in-array.ts @@ -1,10 +1,10 @@ +import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; import { createRule, getConstrainedTypeAtLocation, getParserServices, - isTypeArrayTypeOrUnionOfArrayTypes, } from '../util'; import { getForStatementHeadLoc } from '../util/getForStatementHeadLoc'; @@ -32,10 +32,7 @@ export default createRule({ const type = getConstrainedTypeAtLocation(services, node.right); - if ( - isTypeArrayTypeOrUnionOfArrayTypes(type, checker) || - (type.flags & ts.TypeFlags.StringLike) !== 0 - ) { + if (isArrayLike(checker, type)) { context.report({ loc: getForStatementHeadLoc(context.sourceCode, node), messageId: 'forInViolation', @@ -45,3 +42,34 @@ export default createRule({ }; }, }); + +function isArrayLike(checker: ts.TypeChecker, type: ts.Type): boolean { + return isTypeRecurser( + type, + t => t.getNumberIndexType() != null && hasArrayishLength(checker, t), + ); +} + +function hasArrayishLength(checker: ts.TypeChecker, type: ts.Type): boolean { + const lengthProperty = type.getProperty('length'); + + if (lengthProperty == null) { + return false; + } + + return tsutils.isTypeFlagSet( + checker.getTypeOfSymbol(lengthProperty), + ts.TypeFlags.NumberLike, + ); +} + +function isTypeRecurser( + type: ts.Type, + predicate: (t: ts.Type) => boolean, +): boolean { + if (type.isUnionOrIntersection()) { + return type.types.some(t => isTypeRecurser(t, predicate)); + } + + return predicate(type); +} diff --git a/packages/eslint-plugin/tests/fixtures/tsconfig.lib-dom.json b/packages/eslint-plugin/tests/fixtures/tsconfig.lib-dom.json new file mode 100644 index 000000000000..56c8433f6094 --- /dev/null +++ b/packages/eslint-plugin/tests/fixtures/tsconfig.lib-dom.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "lib": ["es2015", "es2017", "esnext", "dom"] + } +} diff --git a/packages/eslint-plugin/tests/rules/no-for-in-array.test.ts b/packages/eslint-plugin/tests/rules/no-for-in-array.test.ts index 47b3d1ab3ea2..7721006b1d75 100644 --- a/packages/eslint-plugin/tests/rules/no-for-in-array.test.ts +++ b/packages/eslint-plugin/tests/rules/no-for-in-array.test.ts @@ -23,6 +23,23 @@ for (const x of [3, 4, 5]) { ` for (const x in { a: 1, b: 2, c: 3 }) { console.log(x); +} + `, + // this is normally a type error, this test is here to make sure the rule + // doesn't include an "extra" report for it + ` +declare const nullish: null | undefined; +// @ts-expect-error +for (const k in nullish) { +} + `, + ` +declare const obj: { + [key: number]: number; +}; + +for (const key in obj) { + console.log(key); } `, ], @@ -177,5 +194,260 @@ for (const x }, ], }, + { + code: ` +declare const array: string[] | null; + +for (const key in array) { + console.log(key); +} + `, + errors: [ + { + column: 1, + endColumn: 25, + endLine: 4, + line: 4, + messageId: 'forInViolation', + }, + ], + }, + { + code: ` +declare const array: number[] | undefined; + +for (const key in array) { + console.log(key); +} + `, + errors: [ + { + column: 1, + endColumn: 25, + endLine: 4, + line: 4, + messageId: 'forInViolation', + }, + ], + }, + { + code: ` +declare const array: boolean[] | { a: 1; b: 2; c: 3 }; + +for (const key in array) { + console.log(key); +} + `, + errors: [ + { + column: 1, + endColumn: 25, + endLine: 4, + line: 4, + messageId: 'forInViolation', + }, + ], + }, + { + code: ` +declare const array: [number, string]; + +for (const key in array) { + console.log(key); +} + `, + errors: [ + { + column: 1, + endColumn: 25, + endLine: 4, + line: 4, + messageId: 'forInViolation', + }, + ], + }, + { + code: ` +declare const array: [number, string] | { a: 1; b: 2; c: 3 }; + +for (const key in array) { + console.log(key); +} + `, + errors: [ + { + column: 1, + endColumn: 25, + endLine: 4, + line: 4, + messageId: 'forInViolation', + }, + ], + }, + { + code: ` +declare const array: string[] | Record; + +for (const key in array) { + console.log(key); +} + `, + errors: [ + { + column: 1, + endColumn: 25, + endLine: 4, + line: 4, + messageId: 'forInViolation', + }, + ], + }, + { + code: ` +const arrayLike = /fe/.exec('foo'); + +for (const x in arrayLike) { + console.log(x); +} + `, + errors: [ + { + column: 1, + endColumn: 27, + endLine: 4, + line: 4, + messageId: 'forInViolation', + }, + ], + }, + { + code: ` +declare const arrayLike: HTMLCollection; + +for (const x in arrayLike) { + console.log(x); +} + `, + errors: [ + { + column: 1, + endColumn: 27, + endLine: 4, + line: 4, + messageId: 'forInViolation', + }, + ], + languageOptions: { + parserOptions: { + project: './tsconfig.lib-dom.json', + projectService: false, + tsconfigRootDir: rootDir, + }, + }, + }, + { + code: ` +declare const arrayLike: NodeList; + +for (const x in arrayLike) { + console.log(x); +} + `, + errors: [ + { + column: 1, + endColumn: 27, + endLine: 4, + line: 4, + messageId: 'forInViolation', + }, + ], + languageOptions: { + parserOptions: { + project: './tsconfig.lib-dom.json', + projectService: false, + tsconfigRootDir: rootDir, + }, + }, + }, + { + code: ` +function foo() { + for (const a in arguments) { + console.log(a); + } +} + `, + errors: [ + { + column: 3, + endColumn: 29, + endLine: 3, + line: 3, + messageId: 'forInViolation', + }, + ], + }, + { + code: ` +declare const array: + | (({ a: string } & string[]) | Record) + | Record; + +for (const key in array) { + console.log(key); +} + `, + errors: [ + { + column: 1, + endColumn: 25, + endLine: 6, + line: 6, + messageId: 'forInViolation', + }, + ], + }, + { + code: ` +declare const array: + | (({ a: string } & RegExpExecArray) | Record) + | Record; + +for (const key in array) { + console.log(k); +} + `, + errors: [ + { + column: 1, + endColumn: 25, + endLine: 6, + line: 6, + messageId: 'forInViolation', + }, + ], + }, + { + code: ` +declare const obj: { + [key: number]: number; + length: 1; +}; + +for (const key in obj) { + console.log(key); +} + `, + errors: [ + { + column: 1, + endColumn: 23, + endLine: 7, + line: 7, + messageId: 'forInViolation', + }, + ], + }, ], });