Skip to content

fix(eslint-plugin): [no-for-in-array] report on any type which may be an array or array-like #10535

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

Merged
merged 10 commits into from
Jan 19, 2025
38 changes: 33 additions & 5 deletions packages/eslint-plugin/src/rules/no-for-in-array.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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',
Expand All @@ -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);
}
6 changes: 6 additions & 0 deletions packages/eslint-plugin/tests/fixtures/tsconfig.lib-dom.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"lib": ["es2015", "es2017", "esnext", "dom"]
}
}
Comment on lines +1 to +6
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a better way to do this? Specifically dom is necessary for the two dom-array-likes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, this is good!

272 changes: 272 additions & 0 deletions packages/eslint-plugin/tests/rules/no-for-in-array.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
`,
],
Expand Down Expand Up @@ -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<number, string>;

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<string, boolean>)
| Record<number, string>;

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<string, boolean>)
| Record<number, string>;

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',
},
],
},
],
});
Loading