Skip to content

Commit 64dfda9

Browse files
authored
no-array-callback-reference: Ignore non-array TypeScript receivers (#3130)
1 parent 3c7c767 commit 64dfda9

3 files changed

Lines changed: 191 additions & 1 deletion

File tree

docs/rules/no-array-callback-reference.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ This rule intentionally reports locally declared callbacks too. Use an inline wr
1515

1616
Type predicate callbacks are allowed for `.every()`, `.filter()`, `.find()`, and `.findLast()` because wrapping them can fail to preserve TypeScript's predicate overload narrowing.
1717

18+
When TypeScript type information is available, this rule ignores receivers that are known not to be arrays or typed arrays. Untyped JavaScript and unknown TypeScript receivers are still checked heuristically, so use the `ignore` option or an inline disable for unsupported non-array APIs that intentionally share array method names.
19+
1820
Suppose you have a `unicorn` module:
1921

2022
```js

rules/no-array-callback-reference.js

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import helperValidatorIdentifier from '@babel/helper-validator-identifier';
22
import {findVariable} from '@eslint-community/eslint-utils';
3+
import typedArray from './shared/typed-array.js';
34
import {isMethodCall} from './ast/index.js';
45
import {
56
isNodeMatches,
@@ -168,6 +169,89 @@ const defaultIgnoredCallees = [
168169
'jQuery',
169170
];
170171

172+
const typedArrayTypes = new Set(typedArray);
173+
const arrayTypeNames = new Set(['Array', 'ReadonlyArray']);
174+
const unknownTypeNames = new Set(['any', 'error', 'unknown']);
175+
const nullishTypeNames = new Set(['null', 'undefined']);
176+
177+
const isDefaultLibrarySymbol = (symbol, program) =>
178+
symbol?.declarations?.some(declaration => program.isSourceFileDefaultLibrary(declaration.getSourceFile())) ?? false;
179+
180+
const isNullishType = type => nullishTypeNames.has(type.intrinsicName);
181+
182+
function getBaseTypes(type, checker) {
183+
try {
184+
return checker.getBaseTypes(type) ?? type.getBaseTypes?.() ?? [];
185+
} catch {
186+
return [];
187+
}
188+
}
189+
190+
function shouldReportReceiverType(type, checker, program, allowNullish) {
191+
if (unknownTypeNames.has(type.intrinsicName)) {
192+
return true;
193+
}
194+
195+
if (type.isUnion()) {
196+
const types = allowNullish
197+
? type.types.filter(type => !isNullishType(type))
198+
: type.types;
199+
200+
return types.length > 0 && types.every(type => shouldReportReceiverType(type, checker, program, allowNullish));
201+
}
202+
203+
const constraint = checker.getBaseConstraintOfType(type);
204+
if (constraint && constraint !== type) {
205+
return shouldReportReceiverType(constraint, checker, program, allowNullish);
206+
}
207+
208+
if (isNullishType(type)) {
209+
return false;
210+
}
211+
212+
const types = type.isIntersection() ? type.types : [type];
213+
return types.some(type => {
214+
if (unknownTypeNames.has(type.intrinsicName)) {
215+
return true;
216+
}
217+
218+
if (checker.isArrayType(type) || checker.isTupleType(type)) {
219+
return true;
220+
}
221+
222+
if (getBaseTypes(type, checker).some(baseType => shouldReportReceiverType(baseType, checker, program, allowNullish))) {
223+
return true;
224+
}
225+
226+
const symbol = type.getSymbol() ?? type.aliasSymbol;
227+
if (!isDefaultLibrarySymbol(symbol, program)) {
228+
return false;
229+
}
230+
231+
const typeName = symbol.getName();
232+
return arrayTypeNames.has(typeName) || typedArrayTypes.has(typeName);
233+
});
234+
}
235+
236+
function shouldReportReceiver(callExpression, context) {
237+
const {parserServices} = context.sourceCode;
238+
if (!parserServices?.program) {
239+
return true;
240+
}
241+
242+
try {
243+
const {program} = parserServices;
244+
return shouldReportReceiverType(
245+
parserServices.getTypeAtLocation(callExpression.callee.object),
246+
program.getTypeChecker(),
247+
program,
248+
callExpression.callee.optional,
249+
);
250+
} catch {
251+
return true;
252+
}
253+
}
254+
171255
const isValidParameterName = name =>
172256
isIdentifierName(name)
173257
&& !isKeyword(name)
@@ -363,7 +447,10 @@ const create = context => {
363447
}
364448

365449
const options = iteratorMethods.get(methodName);
366-
if (options.shouldIgnoreCallExpression(callExpression, ignoredCallees)) {
450+
if (
451+
options.shouldIgnoreCallExpression(callExpression, ignoredCallees)
452+
|| !shouldReportReceiver(callExpression, context)
453+
) {
367454
return;
368455
}
369456

test/no-array-callback-reference.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import outdent from 'outdent';
2+
import {typescriptEslintParser} from '../scripts/parsers.js';
23
import notFunctionTypes from './utils/not-function-types.js';
34
import {getTester} from './utils/test.js';
45

@@ -53,6 +54,26 @@ const invalidTestCase = (({code, options, method, name, suggestions}) => ({
5354
],
5455
}));
5556

57+
const typeAware = testCase => ({
58+
...(typeof testCase === 'string' ? {code: testCase} : testCase),
59+
filename: 'file.ts',
60+
languageOptions: {
61+
parser: typescriptEslintParser,
62+
parserOptions: {projectService: {allowDefaultProject: ['*.ts']}},
63+
},
64+
});
65+
66+
const invalidTypeAwareMapCallbackTestCase = code => typeAware(invalidTestCase({
67+
code,
68+
method: 'map',
69+
name: 'callback',
70+
suggestions: [
71+
code.replace('map(callback)', 'map((element) => callback(element))'),
72+
code.replace('map(callback)', 'map((element, index) => callback(element, index))'),
73+
code.replace('map(callback)', 'map((element, index, array) => callback(element, index, array))'),
74+
],
75+
}));
76+
5677
test({
5778
valid: [
5879
...simpleMethods.map(method => `foo.${method}(element => fn(element))`),
@@ -756,6 +777,86 @@ test.snapshot({
756777
],
757778
});
758779

780+
test({
781+
valid: [
782+
typeAware(outdent`
783+
interface SearchService {
784+
find(callback: Function): unknown;
785+
}
786+
declare const callback: Function;
787+
declare const service: SearchService;
788+
service.find(callback);
789+
`),
790+
typeAware(outdent`
791+
declare const callback: Function;
792+
class Collection {
793+
map(callback: Function) {}
794+
}
795+
const collection = new Collection();
796+
collection.map(callback);
797+
`),
798+
typeAware(outdent`
799+
interface Model {
800+
find(query: object): unknown;
801+
}
802+
declare const AccountModel: Model;
803+
const query = {};
804+
AccountModel.find(query);
805+
`),
806+
typeAware(outdent`
807+
interface NgMocks {
808+
find(component: unknown): unknown;
809+
}
810+
declare const ngMocks: NgMocks;
811+
declare const MyComponent: unknown;
812+
ngMocks.find(MyComponent);
813+
`),
814+
typeAware(outdent`
815+
declare const callback: Function;
816+
declare const collection: string[] | {map(callback: Function): unknown};
817+
collection.map(callback);
818+
`),
819+
typeAware(outdent`
820+
export {};
821+
type Array<T> = {map(callback: Function): unknown};
822+
declare const callback: Function;
823+
declare const collection: Array<string>;
824+
collection.map(callback);
825+
`),
826+
typeAware(outdent`
827+
export {};
828+
class Uint8Array {
829+
map(callback: Function) {}
830+
}
831+
declare const callback: Function;
832+
const collection = new Uint8Array();
833+
collection.map(callback);
834+
`),
835+
],
836+
invalid: [
837+
...[
838+
'declare const callback: Function; declare const array: string[]; array.map(callback);',
839+
'declare const callback: Function; declare const array: readonly string[]; array.map(callback);',
840+
'declare const callback: Function; declare const array: [string, string]; array.map(callback);',
841+
'declare const callback: Function; declare const array: Array<string>; array.map(callback);',
842+
'declare const callback: Function; declare const array: ReadonlyArray<string>; array.map(callback);',
843+
'declare const callback: Function; declare const array: Uint8Array; array.map(callback);',
844+
'declare const callback: Function; declare const array: string[] | readonly number[]; array.map(callback);',
845+
'declare const callback: Function; declare const array: string[] | Uint8Array; array.map(callback);',
846+
'declare const callback: Function; declare const array: string[] & {foo: string}; array.map(callback);',
847+
'declare const callback: Function; declare const array: string[] | undefined; array?.map(callback);',
848+
'declare const callback: Function; function run<T extends string[]>(array: T) { array.map(callback); }',
849+
'declare const callback: Function; function run<T extends readonly string[]>(array: T) { array.map(callback); }',
850+
'declare const callback: Function; class Strings extends Array<string> {} const array = new Strings(); array.map(callback);',
851+
'declare const callback: Function; interface S extends ReadonlyArray<string> {} declare const array: S; array.map(callback);',
852+
'declare const callback: Function; function run<T extends Uint8Array>(array: T) { array.map(callback); }',
853+
'declare const callback: Function; declare const array: any; array.map(callback);',
854+
'declare const callback: Function; declare const array: unknown; array.map(callback);',
855+
'declare const callback: Function; declare const array: MissingType; array.map(callback);',
856+
].map(code => invalidTypeAwareMapCallbackTestCase(code)),
857+
],
858+
});
859+
759860
test.typescript({
760861
valid: [
761862
outdent`

0 commit comments

Comments
 (0)