Skip to content

Commit 57e9b2c

Browse files
Josh-CenaJoshuaKGoldberg
authored andcommitted
fix(eslint-plugin): [unbound-method] exempt all non-Promise built-in statics (typescript-eslint#8096)
* fix(eslint-plugin): [unbound-method] exempt all non-Promise built-in statics * yarn format --write --------- Co-authored-by: Josh Goldberg <[email protected]>
1 parent 905f3a3 commit 57e9b2c

File tree

3 files changed

+30
-64
lines changed

3 files changed

+30
-64
lines changed

Diff for: .cspell.json

+1
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@
122122
"stringification",
123123
"stringifying",
124124
"stringly",
125+
"subclassing",
125126
"superset",
126127
"thenables",
127128
"transpiled",

Diff for: packages/eslint-plugin/src/rules/unbound-method.ts

+28-64
Original file line numberDiff line numberDiff line change
@@ -24,51 +24,16 @@ export type Options = [Config];
2424
export type MessageIds = 'unbound' | 'unboundWithoutThisAnnotation';
2525

2626
/**
27-
* The following is a list of exceptions to the rule
28-
* Generated via the following script.
29-
* This is statically defined to save making purposely invalid calls every lint run
30-
* ```
31-
SUPPORTED_GLOBALS.flatMap(namespace => {
32-
const object = window[namespace];
33-
return Object.getOwnPropertyNames(object)
34-
.filter(
35-
name =>
36-
!name.startsWith('_') &&
37-
typeof object[name] === 'function',
38-
)
39-
.map(name => {
40-
try {
41-
const x = object[name];
42-
x();
43-
} catch (e) {
44-
if (e.message.includes("called on non-object")) {
45-
return `${namespace}.${name}`;
46-
}
47-
}
48-
});
49-
}).filter(Boolean);
50-
* ```
27+
* Static methods on these globals are either not `this`-aware or supported being
28+
* called without `this`.
29+
*
30+
* - `Promise` is not in the list because it supports subclassing by using `this`
31+
* - `Array` is in the list because although it supports subclassing, the `this`
32+
* value defaults to `Array` when unbound
33+
*
34+
* This is now a language-design invariant: static methods are never `this`-aware
35+
* because TC39 wants to make `array.map(Class.method)` work!
5136
*/
52-
const nativelyNotBoundMembers = new Set([
53-
'Promise.all',
54-
'Promise.race',
55-
'Promise.resolve',
56-
'Promise.reject',
57-
'Promise.allSettled',
58-
'Object.defineProperties',
59-
'Object.defineProperty',
60-
'Reflect.defineProperty',
61-
'Reflect.deleteProperty',
62-
'Reflect.get',
63-
'Reflect.getOwnPropertyDescriptor',
64-
'Reflect.getPrototypeOf',
65-
'Reflect.has',
66-
'Reflect.isExtensible',
67-
'Reflect.ownKeys',
68-
'Reflect.preventExtensions',
69-
'Reflect.set',
70-
'Reflect.setPrototypeOf',
71-
]);
7237
const SUPPORTED_GLOBALS = [
7338
'Number',
7439
'Object',
@@ -78,31 +43,30 @@ const SUPPORTED_GLOBALS = [
7843
'Array',
7944
'Proxy',
8045
'Date',
81-
'Infinity',
8246
'Atomics',
8347
'Reflect',
8448
'console',
8549
'Math',
8650
'JSON',
8751
'Intl',
8852
] as const;
89-
const nativelyBoundMembers = SUPPORTED_GLOBALS.map(namespace => {
90-
if (!(namespace in global)) {
91-
// node.js might not have namespaces like Intl depending on compilation options
92-
// https://nodejs.org/api/intl.html#intl_options_for_building_node_js
93-
return [];
94-
}
95-
const object = global[namespace];
96-
return Object.getOwnPropertyNames(object)
97-
.filter(
98-
name =>
99-
!name.startsWith('_') &&
100-
typeof (object as Record<string, unknown>)[name] === 'function',
101-
)
102-
.map(name => `${namespace}.${name}`);
103-
})
104-
.reduce((arr, names) => arr.concat(names), [])
105-
.filter(name => !nativelyNotBoundMembers.has(name));
53+
const nativelyBoundMembers = new Set(
54+
SUPPORTED_GLOBALS.flatMap(namespace => {
55+
if (!(namespace in global)) {
56+
// node.js might not have namespaces like Intl depending on compilation options
57+
// https://nodejs.org/api/intl.html#intl_options_for_building_node_js
58+
return [];
59+
}
60+
const object = global[namespace];
61+
return Object.getOwnPropertyNames(object)
62+
.filter(
63+
name =>
64+
!name.startsWith('_') &&
65+
typeof (object as Record<string, unknown>)[name] === 'function',
66+
)
67+
.map(name => `${namespace}.${name}`);
68+
}),
69+
);
10670

10771
const isNotImported = (
10872
symbol: ts.Symbol,
@@ -201,7 +165,7 @@ export default createRule<Options, MessageIds>({
201165

202166
if (
203167
objectSymbol &&
204-
nativelyBoundMembers.includes(getMemberFullName(node)) &&
168+
nativelyBoundMembers.has(getMemberFullName(node)) &&
205169
isNotImported(objectSymbol, currentSourceFile)
206170
) {
207171
return;
@@ -232,7 +196,7 @@ export default createRule<Options, MessageIds>({
232196
if (
233197
notImported &&
234198
isIdentifier(initNode) &&
235-
nativelyBoundMembers.includes(
199+
nativelyBoundMembers.has(
236200
`${initNode.name}.${property.key.name}`,
237201
)
238202
) {

Diff for: packages/eslint-plugin/tests/rules/unbound-method.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ ruleTester.run('unbound-method', rule, {
5757
"['1', '2', '3'].map(Number.parseInt);",
5858
'[5.2, 7.1, 3.6].map(Math.floor);',
5959
'const x = console.log;',
60+
'const x = Object.defineProperty;',
6061
...[
6162
'instance.bound();',
6263
'instance.unbound();',

0 commit comments

Comments
 (0)