From 75d1ecbad517fbf7f2561c7b28d7d7cdf2e64f2f Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Sat, 21 Dec 2024 17:57:52 +0200 Subject: [PATCH 1/9] report on for-in on 'Array | undefined' --- .../src/rules/no-for-in-array.ts | 19 ++++++- .../tests/rules/no-for-in-array.test.ts | 54 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) 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..9d7391211f2c 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'; @@ -45,3 +45,20 @@ export default createRule({ }; }, }); + +function isTypeArrayTypeOrUnionOfArrayTypes( + type: ts.Type, + checker: ts.TypeChecker, +): boolean { + for (const t of tsutils.unionTypeParts(type)) { + if (tsutils.isTypeFlagSet(t, ts.TypeFlags.Undefined | ts.TypeFlags.Null)) { + continue; + } + + if (!checker.isArrayType(t)) { + return false; + } + } + + return true; +} 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..341f2a73f233 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 @@ -177,5 +177,59 @@ for (const x }, ], }, + { + code: ` +declare const arr: string[] | null; + +for (const x in arr) { + console.log(x); +} + `, + errors: [ + { + column: 1, + endColumn: 21, + endLine: 4, + line: 4, + messageId: 'forInViolation', + }, + ], + }, + { + code: ` +declare const arr: number[] | undefined; + +for (const x in arr) { + console.log(x); +} + `, + errors: [ + { + column: 1, + endColumn: 21, + endLine: 4, + line: 4, + messageId: 'forInViolation', + }, + ], + }, + { + code: ` +declare const arr: boolean[] | undefined | null; + +for (const x in arr) { + console.log(x); +} + `, + errors: [ + { + column: 1, + endColumn: 21, + endLine: 4, + line: 4, + messageId: 'forInViolation', + }, + ], + }, ], }); From 4ded1c5b542b73d4dee44ff68378c0a43b33fbba Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Fri, 27 Dec 2024 23:22:50 +0200 Subject: [PATCH 2/9] update the rule to report on anything that may be an array or array-like --- .../src/rules/no-for-in-array.ts | 42 +++++--- .../tests/rules/no-for-in-array.test.ts | 100 +++++++++++++++++- 2 files changed, 127 insertions(+), 15 deletions(-) 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 9d7391211f2c..b357a2e5d062 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, + isBuiltinSymbolLike, } from '../util'; import { getForStatementHeadLoc } from '../util/getForStatementHeadLoc'; @@ -33,8 +33,9 @@ export default createRule({ const type = getConstrainedTypeAtLocation(services, node.right); if ( - isTypeArrayTypeOrUnionOfArrayTypes(type, checker) || - (type.flags & ts.TypeFlags.StringLike) !== 0 + isArray(checker, type) || + isRegExpExecArrayLike(services.program, type) || + isArgumentsObjectType(type) ) { context.report({ loc: getForStatementHeadLoc(context.sourceCode, node), @@ -46,19 +47,32 @@ export default createRule({ }, }); -function isTypeArrayTypeOrUnionOfArrayTypes( +function isArgumentsObjectType(type: ts.Type): boolean { + return ( + type.getSymbol()?.escapedName === ts.escapeLeadingUnderscores('IArguments') + ); +} + +function isRegExpExecArrayLike(program: ts.Program, type: ts.Type): boolean { + return isTypeRecurser(type, t => + isBuiltinSymbolLike(program, t, 'RegExpExecArray'), + ); +} + +function isArray(checker: ts.TypeChecker, type: ts.Type): boolean { + return isTypeRecurser( + type, + t => checker.isArrayType(t) || checker.isTupleType(t), + ); +} + +function isTypeRecurser( type: ts.Type, - checker: ts.TypeChecker, + predicate: (t: ts.Type) => boolean, ): boolean { - for (const t of tsutils.unionTypeParts(type)) { - if (tsutils.isTypeFlagSet(t, ts.TypeFlags.Undefined | ts.TypeFlags.Null)) { - continue; - } - - if (!checker.isArrayType(t)) { - return false; - } + if (type.isUnionOrIntersection()) { + return type.types.some(t => isTypeRecurser(t, predicate)); } - return true; + return predicate(type); } 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 341f2a73f233..4f6845618195 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,14 @@ 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) { } `, ], @@ -215,7 +223,7 @@ for (const x in arr) { }, { code: ` -declare const arr: boolean[] | undefined | null; +declare const arr: boolean[] | { a: 1; b: 2; c: 3 }; for (const x in arr) { console.log(x); @@ -231,5 +239,95 @@ for (const x in arr) { }, ], }, + { + code: ` +declare const arr: [number, string]; + +for (const x in arr) { + console.log(x); +} + `, + errors: [ + { + column: 1, + endColumn: 21, + endLine: 4, + line: 4, + messageId: 'forInViolation', + }, + ], + }, + { + code: ` +declare const arr: [number, string] | { a: 1; b: 2; c: 3 }; + +for (const x in arr) { + console.log(x); +} + `, + errors: [ + { + column: 1, + endColumn: 21, + endLine: 4, + line: 4, + messageId: 'forInViolation', + }, + ], + }, + { + code: ` +declare const x: string[] | Record; + +for (const k in x) { + console.log(k); +} + `, + errors: [ + { + column: 1, + endColumn: 19, + endLine: 4, + line: 4, + messageId: 'forInViolation', + }, + ], + }, + { + code: ` +const reArray = /fe/.exec('foo'); + +for (const x in reArray) { + console.log(x); +} + `, + errors: [ + { + column: 1, + endColumn: 25, + endLine: 4, + line: 4, + messageId: 'forInViolation', + }, + ], + }, + { + code: ` +function foo() { + for (const a in arguments) { + console.log(a); + } +} + `, + errors: [ + { + column: 3, + endColumn: 29, + endLine: 3, + line: 3, + messageId: 'forInViolation', + }, + ], + }, ], }); From 7efca69b1a73b7ca2894b090c6452aa4cbb5e854 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Sat, 28 Dec 2024 00:15:35 +0200 Subject: [PATCH 3/9] nested test cases --- .../tests/rules/no-for-in-array.test.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) 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 4f6845618195..767465e8649c 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 @@ -329,5 +329,45 @@ function foo() { }, ], }, + { + code: ` +declare const x: + | (({ a: string } & string[]) | Record) + | Record; + +for (const k in x) { + console.log(k); +} + `, + errors: [ + { + column: 1, + endColumn: 19, + endLine: 6, + line: 6, + messageId: 'forInViolation', + }, + ], + }, + { + code: ` +declare const x: + | (({ a: string } & RegExpExecArray) | Record) + | Record; + +for (const k in x) { + console.log(k); +} + `, + errors: [ + { + column: 1, + endColumn: 19, + endLine: 6, + line: 6, + messageId: 'forInViolation', + }, + ], + }, ], }); From e126daba7e8a301ab74ae24e52e8f1da00a617e1 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Sat, 28 Dec 2024 14:14:07 +0200 Subject: [PATCH 4/9] refactor code to use 'isBuiltinSymbolLike' for checking the arguments object --- packages/eslint-plugin/src/rules/no-for-in-array.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) 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 b357a2e5d062..65bd0f95985a 100644 --- a/packages/eslint-plugin/src/rules/no-for-in-array.ts +++ b/packages/eslint-plugin/src/rules/no-for-in-array.ts @@ -1,4 +1,4 @@ -import * as ts from 'typescript'; +import type * as ts from 'typescript'; import { createRule, @@ -35,7 +35,7 @@ export default createRule({ if ( isArray(checker, type) || isRegExpExecArrayLike(services.program, type) || - isArgumentsObjectType(type) + isArgumentsObjectType(services.program, type) ) { context.report({ loc: getForStatementHeadLoc(context.sourceCode, node), @@ -47,10 +47,8 @@ export default createRule({ }, }); -function isArgumentsObjectType(type: ts.Type): boolean { - return ( - type.getSymbol()?.escapedName === ts.escapeLeadingUnderscores('IArguments') - ); +function isArgumentsObjectType(program: ts.Program, type: ts.Type): boolean { + return isBuiltinSymbolLike(program, type, 'IArguments'); } function isRegExpExecArrayLike(program: ts.Program, type: ts.Type): boolean { From b612eef7fd4352752dd5efa28afc6b9687e09fe6 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Sat, 28 Dec 2024 14:34:36 +0200 Subject: [PATCH 5/9] refactor, add some more array-likes --- .../src/rules/no-for-in-array.ts | 19 ++-- .../tests/eslint-rules/no-undef.test.ts | 2 +- .../tests/fixtures/tsconfig.lib-dom.json | 6 + .../tests/rules/no-for-in-array.test.ts | 104 ++++++++++++------ 4 files changed, 85 insertions(+), 46 deletions(-) create mode 100644 packages/eslint-plugin/tests/fixtures/tsconfig.lib-dom.json 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 65bd0f95985a..00946cbe5c93 100644 --- a/packages/eslint-plugin/src/rules/no-for-in-array.ts +++ b/packages/eslint-plugin/src/rules/no-for-in-array.ts @@ -32,11 +32,7 @@ export default createRule({ const type = getConstrainedTypeAtLocation(services, node.right); - if ( - isArray(checker, type) || - isRegExpExecArrayLike(services.program, type) || - isArgumentsObjectType(services.program, type) - ) { + if (isArray(checker, type) || isArrayLike(services.program, type)) { context.report({ loc: getForStatementHeadLoc(context.sourceCode, node), messageId: 'forInViolation', @@ -47,13 +43,14 @@ export default createRule({ }, }); -function isArgumentsObjectType(program: ts.Program, type: ts.Type): boolean { - return isBuiltinSymbolLike(program, type, 'IArguments'); -} - -function isRegExpExecArrayLike(program: ts.Program, type: ts.Type): boolean { +function isArrayLike(program: ts.Program, type: ts.Type): boolean { return isTypeRecurser(type, t => - isBuiltinSymbolLike(program, t, 'RegExpExecArray'), + isBuiltinSymbolLike(program, t, [ + 'IArguments', + 'HTMLCollection', + 'RegExpExecArray', + 'NodeList', + ]), ); } diff --git a/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts b/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts index 3668c6c98135..377a40a00140 100644 --- a/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts +++ b/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts @@ -219,7 +219,7 @@ const links = document.querySelectorAll(selector) as NodeListOf; `, languageOptions: { parserOptions: { - lib: ['dom'], + lib: ['es2023'], }, }, }, 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 767465e8649c..1b03217fdce2 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 @@ -7,7 +7,7 @@ const rootDir = getFixturesRootDir(); const ruleTester = new RuleTester({ languageOptions: { parserOptions: { - project: './tsconfig.json', + project: './tsconfig.lib-dom.json', tsconfigRootDir: rootDir, }, }, @@ -187,16 +187,16 @@ for (const x }, { code: ` -declare const arr: string[] | null; +declare const array: string[] | null; -for (const x in arr) { - console.log(x); +for (const key in array) { + console.log(key); } `, errors: [ { column: 1, - endColumn: 21, + endColumn: 25, endLine: 4, line: 4, messageId: 'forInViolation', @@ -205,16 +205,16 @@ for (const x in arr) { }, { code: ` -declare const arr: number[] | undefined; +declare const array: number[] | undefined; -for (const x in arr) { - console.log(x); +for (const key in array) { + console.log(key); } `, errors: [ { column: 1, - endColumn: 21, + endColumn: 25, endLine: 4, line: 4, messageId: 'forInViolation', @@ -223,16 +223,16 @@ for (const x in arr) { }, { code: ` -declare const arr: boolean[] | { a: 1; b: 2; c: 3 }; +declare const array: boolean[] | { a: 1; b: 2; c: 3 }; -for (const x in arr) { - console.log(x); +for (const key in array) { + console.log(key); } `, errors: [ { column: 1, - endColumn: 21, + endColumn: 25, endLine: 4, line: 4, messageId: 'forInViolation', @@ -241,16 +241,52 @@ for (const x in arr) { }, { code: ` -declare const arr: [number, string]; +declare const array: [number, string]; -for (const x in arr) { - console.log(x); +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: 21, + endColumn: 25, endLine: 4, line: 4, messageId: 'forInViolation', @@ -259,16 +295,16 @@ for (const x in arr) { }, { code: ` -declare const arr: [number, string] | { a: 1; b: 2; c: 3 }; +const arrayLike = /fe/.exec('foo'); -for (const x in arr) { +for (const x in arrayLike) { console.log(x); } `, errors: [ { column: 1, - endColumn: 21, + endColumn: 27, endLine: 4, line: 4, messageId: 'forInViolation', @@ -277,16 +313,16 @@ for (const x in arr) { }, { code: ` -declare const x: string[] | Record; +declare const arrayLike: HTMLCollection; -for (const k in x) { - console.log(k); +for (const x in arrayLike) { + console.log(x); } `, errors: [ { column: 1, - endColumn: 19, + endColumn: 27, endLine: 4, line: 4, messageId: 'forInViolation', @@ -295,16 +331,16 @@ for (const k in x) { }, { code: ` -const reArray = /fe/.exec('foo'); +declare const arrayLike: NodeList; -for (const x in reArray) { +for (const x in arrayLike) { console.log(x); } `, errors: [ { column: 1, - endColumn: 25, + endColumn: 27, endLine: 4, line: 4, messageId: 'forInViolation', @@ -331,18 +367,18 @@ function foo() { }, { code: ` -declare const x: +declare const array: | (({ a: string } & string[]) | Record) | Record; -for (const k in x) { - console.log(k); +for (const key in array) { + console.log(key); } `, errors: [ { column: 1, - endColumn: 19, + endColumn: 25, endLine: 6, line: 6, messageId: 'forInViolation', @@ -351,18 +387,18 @@ for (const k in x) { }, { code: ` -declare const x: +declare const array: | (({ a: string } & RegExpExecArray) | Record) | Record; -for (const k in x) { +for (const key in array) { console.log(k); } `, errors: [ { column: 1, - endColumn: 19, + endColumn: 25, endLine: 6, line: 6, messageId: 'forInViolation', From 6466fb5f5be969a4c9de920c54d10973df902fdf Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Sat, 28 Dec 2024 14:39:33 +0200 Subject: [PATCH 6/9] undo unrelated change --- packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts b/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts index 377a40a00140..3668c6c98135 100644 --- a/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts +++ b/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts @@ -219,7 +219,7 @@ const links = document.querySelectorAll(selector) as NodeListOf; `, languageOptions: { parserOptions: { - lib: ['es2023'], + lib: ['dom'], }, }, }, From 2ce3ceaa14707a9b74e06d74e61ca3612d0d108c Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Sat, 28 Dec 2024 14:43:50 +0200 Subject: [PATCH 7/9] use lib-dom tsconfig only for tests that need it --- .../tests/rules/no-for-in-array.test.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) 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 1b03217fdce2..66e7296dea8d 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 @@ -7,7 +7,7 @@ const rootDir = getFixturesRootDir(); const ruleTester = new RuleTester({ languageOptions: { parserOptions: { - project: './tsconfig.lib-dom.json', + project: './tsconfig.json', tsconfigRootDir: rootDir, }, }, @@ -328,6 +328,12 @@ for (const x in arrayLike) { messageId: 'forInViolation', }, ], + languageOptions: { + parserOptions: { + project: './tsconfig.lib-dom.json', + tsconfigRootDir: rootDir, + }, + }, }, { code: ` @@ -346,6 +352,12 @@ for (const x in arrayLike) { messageId: 'forInViolation', }, ], + languageOptions: { + parserOptions: { + project: './tsconfig.lib-dom.json', + tsconfigRootDir: rootDir, + }, + }, }, { code: ` From 762934d690ca5cfdda750467d47f43e0b84621cb Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Sat, 28 Dec 2024 15:53:39 +0200 Subject: [PATCH 8/9] fix failing tests --- packages/eslint-plugin/tests/rules/no-for-in-array.test.ts | 2 ++ 1 file changed, 2 insertions(+) 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 66e7296dea8d..fc0bc8d526fa 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 @@ -331,6 +331,7 @@ for (const x in arrayLike) { languageOptions: { parserOptions: { project: './tsconfig.lib-dom.json', + projectService: false, tsconfigRootDir: rootDir, }, }, @@ -355,6 +356,7 @@ for (const x in arrayLike) { languageOptions: { parserOptions: { project: './tsconfig.lib-dom.json', + projectService: false, tsconfigRootDir: rootDir, }, }, From 599ac75a373e4b48f95bc4fcb7b5e5f7e03a8d2d Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Sat, 11 Jan 2025 15:47:49 +0200 Subject: [PATCH 9/9] check array-like based on it having a length and a number-index-signature --- .../src/rules/no-for-in-array.ts | 32 ++++++++++--------- .../tests/rules/no-for-in-array.test.ts | 30 +++++++++++++++++ 2 files changed, 47 insertions(+), 15 deletions(-) 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 00946cbe5c93..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 type * as ts from 'typescript'; +import * as tsutils from 'ts-api-utils'; +import * as ts from 'typescript'; import { createRule, getConstrainedTypeAtLocation, getParserServices, - isBuiltinSymbolLike, } from '../util'; import { getForStatementHeadLoc } from '../util/getForStatementHeadLoc'; @@ -32,7 +32,7 @@ export default createRule({ const type = getConstrainedTypeAtLocation(services, node.right); - if (isArray(checker, type) || isArrayLike(services.program, type)) { + if (isArrayLike(checker, type)) { context.report({ loc: getForStatementHeadLoc(context.sourceCode, node), messageId: 'forInViolation', @@ -43,21 +43,23 @@ export default createRule({ }, }); -function isArrayLike(program: ts.Program, type: ts.Type): boolean { - return isTypeRecurser(type, t => - isBuiltinSymbolLike(program, t, [ - 'IArguments', - 'HTMLCollection', - 'RegExpExecArray', - 'NodeList', - ]), +function isArrayLike(checker: ts.TypeChecker, type: ts.Type): boolean { + return isTypeRecurser( + type, + t => t.getNumberIndexType() != null && hasArrayishLength(checker, t), ); } -function isArray(checker: ts.TypeChecker, type: ts.Type): boolean { - return isTypeRecurser( - type, - t => checker.isArrayType(t) || checker.isTupleType(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, ); } 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 fc0bc8d526fa..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 @@ -31,6 +31,15 @@ for (const x in { a: 1, b: 2, c: 3 }) { 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); } `, ], @@ -419,5 +428,26 @@ for (const key in array) { }, ], }, + { + 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', + }, + ], + }, ], });