From 0dcb9d79dfee03ad17befbd80a148ab877f429b5 Mon Sep 17 00:00:00 2001 From: auvred Date: Fri, 19 Apr 2024 09:57:13 +0300 Subject: [PATCH 1/5] fix(eslint-plugin): [unbound-method] report on destructuring in function parameters --- .../eslint-plugin/src/rules/unbound-method.ts | 69 ++++-- .../tests/rules/unbound-method.test.ts | 224 ++++++++++++++++++ 2 files changed, 279 insertions(+), 14 deletions(-) diff --git a/packages/eslint-plugin/src/rules/unbound-method.ts b/packages/eslint-plugin/src/rules/unbound-method.ts index 9b416ea4a57..0d254ab87eb 100644 --- a/packages/eslint-plugin/src/rules/unbound-method.ts +++ b/packages/eslint-plugin/src/rules/unbound-method.ts @@ -135,9 +135,9 @@ export default createRule({ function checkIfMethodAndReport( node: TSESTree.Node, symbol: ts.Symbol | undefined, - ): void { + ): boolean { if (!symbol) { - return; + return false; } const { dangerous, firstParamIsThis } = checkIfMethod( @@ -152,7 +152,9 @@ export default createRule({ : 'unbound', node, }); + return true; } + return false; } return { @@ -173,22 +175,25 @@ export default createRule({ checkIfMethodAndReport(node, services.getSymbolAtLocation(node)); }, - 'VariableDeclarator, AssignmentExpression'( - node: TSESTree.AssignmentExpression | TSESTree.VariableDeclarator, - ): void { - const [idNode, initNode] = - node.type === AST_NODE_TYPES.VariableDeclarator - ? [node.id, node.init] - : [node.left, node.right]; - - if (initNode && idNode.type === AST_NODE_TYPES.ObjectPattern) { - const rightSymbol = services.getSymbolAtLocation(initNode); + ObjectPattern(node): void { + let initNode: TSESTree.Node | null = null; + if (node.parent.type === AST_NODE_TYPES.VariableDeclarator) { + initNode = node.parent.init; + } else if ( + node.parent.type === AST_NODE_TYPES.AssignmentPattern || + node.parent.type === AST_NODE_TYPES.AssignmentExpression + ) { + initNode = node.parent.right; + } + + if (initNode) { + const initSymbol = services.getSymbolAtLocation(initNode); const initTypes = services.getTypeAtLocation(initNode); const notImported = - rightSymbol && isNotImported(rightSymbol, currentSourceFile); + initSymbol && isNotImported(initSymbol, currentSourceFile); - idNode.properties.forEach(property => { + node.properties.forEach(property => { if ( property.type === AST_NODE_TYPES.Property && property.key.type === AST_NODE_TYPES.Identifier @@ -209,6 +214,42 @@ export default createRule({ ); } }); + return; + } + + if (node.typeAnnotation) { + const subtypes = tsutils.unionTypeParts( + services.getTypeAtLocation(node.typeAnnotation.typeAnnotation), + ); + for (const property of node.properties) { + for (const subtype of subtypes) { + if ( + property.type !== AST_NODE_TYPES.Property || + property.key.type !== AST_NODE_TYPES.Identifier + ) { + continue; + } + + const propertyKey = property.key; + if ( + checkIfMethodAndReport( + propertyKey, + subtype.getProperty(propertyKey.name), + ) || + (tsutils.isIntersectionType(subtype) && + tsutils + .intersectionTypeParts(subtype) + .some(intersectionSubtype => + checkIfMethodAndReport( + propertyKey, + intersectionSubtype.getProperty(propertyKey.name), + ), + )) + ) { + break; + } + } + } } }, }; diff --git a/packages/eslint-plugin/tests/rules/unbound-method.test.ts b/packages/eslint-plugin/tests/rules/unbound-method.test.ts index 81f4f126fd3..90819a863e1 100644 --- a/packages/eslint-plugin/tests/rules/unbound-method.test.ts +++ b/packages/eslint-plugin/tests/rules/unbound-method.test.ts @@ -283,6 +283,43 @@ class Foo { bound = () => 'foo'; } const { bound } = new Foo(); + `, + ` +class Foo { + bound = () => 'foo'; +} +function foo({ bound } = new Foo()) {} + `, + ` +class Foo { + bound = () => 'foo'; +} +declare const bar: Foo; +function foo({ bound }: Foo) {} + `, + ` +class Foo { + bound = () => 'foo'; +} +class Bar { + bound = () => 'bar'; +} +function foo({ bound }: Foo | Bar) {} + `, + ` +class Foo { + bound = () => 'foo'; +} +type foo = ({ bound }: Foo) => void; + `, + ` +class Foo { + bound = () => 'foo'; +} +class Bar { + bound = () => 'bar'; +} +function foo({ bound }: Foo & Bar) {} `, // https://github.com/typescript-eslint/typescript-eslint/issues/1866 ` @@ -516,6 +553,193 @@ let unbound; }, { code: ` +class Foo { + unbound = function () {}; +} +function foo({ unbound }: Foo = new Foo()) {} + `, + errors: [ + { + line: 5, + messageId: 'unbound', + }, + ], + }, + { + code: ` +class Foo { + unbound = function () {}; +} +declare const bar: Foo; +function foo({ unbound }: Foo = bar) {} + `, + errors: [ + { + line: 6, + messageId: 'unbound', + }, + ], + }, + { + code: ` +class Foo { + unbound = function () {}; +} +function foo({ unbound }: Foo) {} + `, + errors: [ + { + line: 5, + messageId: 'unbound', + }, + ], + }, + { + code: ` +class Foo { + unbound = function () {}; + + foo({ unbound }: Foo) {} +} + `, + errors: [ + { + line: 5, + messageId: 'unbound', + }, + ], + }, + { + code: ` +class Foo { + unbound = function () {}; +} +type foo = ({ unbound }: Foo) => void; + `, + errors: [ + { + line: 5, + messageId: 'unbound', + }, + ], + }, + { + code: ` +class Foo { + unbound = function () {}; +} +class Bar { + unbound = function () {}; +} +function foo({ unbound }: Foo | Bar) {} + `, + errors: [ + { + line: 8, + messageId: 'unbound', + }, + ], + }, + { + code: ` +class Foo { + unbound = function () {}; +} +function foo({ unbound }: { unbound: () => string } | Foo) {} + `, + errors: [ + { + line: 5, + messageId: 'unbound', + }, + ], + }, + { + code: ` +class Foo { + unbound = function () {}; +} +class Bar { + unbound = () => {}; +} +function foo({ unbound }: Foo | Bar) {} + `, + errors: [ + { + line: 8, + messageId: 'unbound', + }, + ], + }, + { + code: ` +class Foo { + unbound = function () {}; +} +type foo = ({ unbound }: Foo & { foo: () => 'bar' }) => void; + `, + errors: [ + { + line: 5, + messageId: 'unbound', + }, + ], + }, + { + code: ` +class Foo { + unbound = function () {}; +} +class Bar { + unbound = () => {}; +} +type foo = ({ unbound }: (Foo & { foo: () => 'bar' }) | Bar) => void; + `, + errors: [ + { + line: 8, + messageId: 'unbound', + }, + ], + }, + { + code: ` +class Foo { + unbound = function () {}; +} +class Bar { + unbound = () => {}; +} +type foo = ({ unbound }: Foo & Bar) => void; + `, + errors: [ + { + line: 8, + messageId: 'unbound', + }, + ], + }, + { + code: ` +class Foo { + unbound = function () {}; + + other = function() {} +} +class Bar { + unbound = () => {}; +} +type foo = ({ unbound, ...rest }: Foo & Bar) => void; + `, + errors: [ + { + line: 10, + messageId: 'unbound', + }, + ], + }, + { + code: ` class CommunicationError { foo() {} } From 0031ebf0280ac9692c7dc03af95d1a10491f6875 Mon Sep 17 00:00:00 2001 From: auvred Date: Fri, 19 Apr 2024 18:26:04 +0300 Subject: [PATCH 2/5] chore: lint --fix --- packages/eslint-plugin/tests/rules/unbound-method.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/tests/rules/unbound-method.test.ts b/packages/eslint-plugin/tests/rules/unbound-method.test.ts index 90819a863e1..be606af2b8a 100644 --- a/packages/eslint-plugin/tests/rules/unbound-method.test.ts +++ b/packages/eslint-plugin/tests/rules/unbound-method.test.ts @@ -724,7 +724,7 @@ type foo = ({ unbound }: Foo & Bar) => void; class Foo { unbound = function () {}; - other = function() {} + other = function () {}; } class Bar { unbound = () => {}; From 9f57b64505a2d65e5bab8b4ca4229e874505e108 Mon Sep 17 00:00:00 2001 From: auvred Date: Tue, 23 Apr 2024 19:04:54 +0300 Subject: [PATCH 3/5] --- .../eslint-plugin/src/rules/unbound-method.ts | 189 +++++++----- .../tests/rules/unbound-method.test.ts | 284 +++++++++++++++++- packages/type-utils/src/builtinSymbolLikes.ts | 8 +- 3 files changed, 401 insertions(+), 80 deletions(-) diff --git a/packages/eslint-plugin/src/rules/unbound-method.ts b/packages/eslint-plugin/src/rules/unbound-method.ts index 0d254ab87eb..9836664522c 100644 --- a/packages/eslint-plugin/src/rules/unbound-method.ts +++ b/packages/eslint-plugin/src/rules/unbound-method.ts @@ -7,7 +7,8 @@ import { createRule, getModifiers, getParserServices, - isIdentifier, + isBuiltinSymbolLike, + isSymbolFromDefaultLibrary, } from '../util'; //------------------------------------------------------------------------------ @@ -83,12 +84,6 @@ const isNotImported = ( ); }; -const getNodeName = (node: TSESTree.Node): string | null => - node.type === AST_NODE_TYPES.Identifier ? node.name : null; - -const getMemberFullName = (node: TSESTree.MemberExpression): string => - `${getNodeName(node.object)}.${getNodeName(node.property)}`; - const BASE_MESSAGE = 'Avoid referencing unbound methods which may cause unintentional scoping of `this`.'; @@ -157,25 +152,67 @@ export default createRule({ return false; } - return { - MemberExpression(node: TSESTree.MemberExpression): void { - if (isSafeUse(node)) { - return; - } - - const objectSymbol = services.getSymbolAtLocation(node.object); + function isNativelyBound( + object: TSESTree.Node, + property: TSESTree.Node, + ): boolean { + if ( + object.type === AST_NODE_TYPES.Identifier && + property.type === AST_NODE_TYPES.Identifier + ) { + const objectSymbol = services.getSymbolAtLocation(object); + const notImported = + objectSymbol && isNotImported(objectSymbol, currentSourceFile); if ( - objectSymbol && - nativelyBoundMembers.has(getMemberFullName(node)) && - isNotImported(objectSymbol, currentSourceFile) + notImported && + nativelyBoundMembers.has(`${object.name}.${property.name}`) ) { + return true; + } + } + + return ( + isBuiltinSymbolLike( + services.program, + services.getTypeAtLocation(object), + [ + 'NumberConstructor', + 'Number', + 'ObjectConstructor', + 'Object', + 'StringConstructor', + 'String', // eslint-disable-line @typescript-eslint/internal/prefer-ast-types-enum + 'SymbolConstructor', + 'ArrayConstructor', + 'Array', + 'ProxyConstructor', + 'DateConstructor', + 'Date', + 'Atomics', + 'Math', + 'JSON', + ], + ) && + isSymbolFromDefaultLibrary( + services.program, + services.getTypeAtLocation(property).getSymbol(), + ) + ); + } + + return { + MemberExpression(node: TSESTree.MemberExpression): void { + if (isSafeUse(node) || isNativelyBound(node.object, node.property)) { return; } checkIfMethodAndReport(node, services.getSymbolAtLocation(node)); }, ObjectPattern(node): void { + if (isNodeInsideTypeDeclaration(node)) { + return; + } let initNode: TSESTree.Node | null = null; if (node.parent.type === AST_NODE_TYPES.VariableDeclarator) { initNode = node.parent.init; @@ -186,66 +223,58 @@ export default createRule({ initNode = node.parent.right; } - if (initNode) { - const initSymbol = services.getSymbolAtLocation(initNode); - const initTypes = services.getTypeAtLocation(initNode); - - const notImported = - initSymbol && isNotImported(initSymbol, currentSourceFile); - - node.properties.forEach(property => { - if ( - property.type === AST_NODE_TYPES.Property && - property.key.type === AST_NODE_TYPES.Identifier - ) { - if ( - notImported && - isIdentifier(initNode) && - nativelyBoundMembers.has( - `${initNode.name}.${property.key.name}`, - ) - ) { - return; - } + for (const property of node.properties) { + if ( + property.type !== AST_NODE_TYPES.Property || + property.key.type !== AST_NODE_TYPES.Identifier + ) { + continue; + } - checkIfMethodAndReport( + if (initNode) { + if (!isNativelyBound(initNode, property.key)) { + const reported = checkIfMethodAndReport( property.key, - initTypes.getProperty(property.key.name), + services + .getTypeAtLocation(initNode) + .getProperty(property.key.name), ); - } - }); - return; - } - - if (node.typeAnnotation) { - const subtypes = tsutils.unionTypeParts( - services.getTypeAtLocation(node.typeAnnotation.typeAnnotation), - ); - for (const property of node.properties) { - for (const subtype of subtypes) { - if ( - property.type !== AST_NODE_TYPES.Property || - property.key.type !== AST_NODE_TYPES.Identifier - ) { + if (reported) { continue; } + // In assignment patterns, we should also check the type of + // Foo's nativelyBound method because initNode might be used as + // default value: + // function ({ nativelyBound }: Foo = NativeObject) {} + } else if (node.parent.type !== AST_NODE_TYPES.AssignmentPattern) { + continue; + } + } + + for (const unionPart of tsutils.unionTypeParts( + services.getTypeAtLocation(node), + )) { + const propertyKey = property.key; + const reported = checkIfMethodAndReport( + propertyKey, + unionPart.getProperty(propertyKey.name), + ); + if (reported) { + break; + } - const propertyKey = property.key; - if ( - checkIfMethodAndReport( - propertyKey, - subtype.getProperty(propertyKey.name), - ) || - (tsutils.isIntersectionType(subtype) && - tsutils - .intersectionTypeParts(subtype) - .some(intersectionSubtype => - checkIfMethodAndReport( - propertyKey, - intersectionSubtype.getProperty(propertyKey.name), - ), - )) - ) { + if (!tsutils.isIntersectionType(unionPart)) { + continue; + } + + for (const intersectionPart of tsutils.intersectionTypeParts( + unionPart, + )) { + const reported = checkIfMethodAndReport( + propertyKey, + intersectionPart.getProperty(propertyKey.name), + ); + if (reported) { break; } } @@ -256,6 +285,24 @@ export default createRule({ }, }); +function isNodeInsideTypeDeclaration(node: TSESTree.Node): boolean { + let parent: TSESTree.Node | undefined = node; + while ((parent = parent.parent)) { + if ( + (parent.type === AST_NODE_TYPES.ClassDeclaration && parent.declare) || + parent.type === AST_NODE_TYPES.TSAbstractMethodDefinition || + parent.type === AST_NODE_TYPES.TSDeclareFunction || + parent.type === AST_NODE_TYPES.TSFunctionType || + parent.type === AST_NODE_TYPES.TSInterfaceDeclaration || + parent.type === AST_NODE_TYPES.TSTypeAliasDeclaration || + (parent.type === AST_NODE_TYPES.VariableDeclaration && parent.declare) + ) { + return true; + } + } + return false; +} + interface CheckMethodResult { dangerous: boolean; firstParamIsThis?: boolean; diff --git a/packages/eslint-plugin/tests/rules/unbound-method.test.ts b/packages/eslint-plugin/tests/rules/unbound-method.test.ts index be606af2b8a..fc10aa2a58c 100644 --- a/packages/eslint-plugin/tests/rules/unbound-method.test.ts +++ b/packages/eslint-plugin/tests/rules/unbound-method.test.ts @@ -56,8 +56,78 @@ ruleTester.run('unbound-method', rule, { 'Promise.resolve().then(console.log);', "['1', '2', '3'].map(Number.parseInt);", '[5.2, 7.1, 3.6].map(Math.floor);', + ` + const foo = Number; + ['1', '2', '3'].map(foo.parseInt); + `, + ` + const foo = Math; + [5.2, 7.1, 3.6].map(foo.floor); + `, + "['1', '2', '3'].map(Number['floor']);", + ` + class Foo extends Number {} + const x = Foo.parseInt; + `, 'const x = console.log;', 'const x = Object.defineProperty;', + ` + const foo = Object; + const x = foo.defineProperty; + `, + ` + class Foo extends Object {} + const x = Foo.defineProperty; + `, + 'const x = String.fromCharCode;', + ` + const foo = String; + const x = foo.fromCharCode; + `, + ` + class Foo extends String {} + const x = Foo.fromCharCode; + `, + 'const x = RegExp.prototype;', + 'const x = Symbol.keyFor;', + ` + const foo = Symbol; + const x = foo.keyFor; + `, + 'const x = Array.isArray;', + ` + const foo = Array; + const x = foo.isArray; + `, + ` + class Foo extends Array {} + const x = Foo.isArray; + `, + 'const x = Proxy.revocable;', + ` + const foo = Proxy; + const x = foo.revocable; + `, + 'const x = Date.parse;', + ` + const foo = Date; + const x = foo.parse; + `, + ` + class Foo extends Date {} + const x = Foo.parse; + `, + 'const x = Atomics.load;', + ` + const foo = Atomics; + const x = foo.load; + `, + 'const x = Reflect.deleteProperty;', + 'const x = JSON.stringify;', + ` + const foo = JSON; + const x = foo.stringify; + `, ` const o = { f: function (this: void) {}, @@ -313,6 +383,12 @@ class Foo { type foo = ({ bound }: Foo) => void; `, ` +class Foo { + unbound = function () {}; +} +type foo = ({ unbound }: Foo) => void; + `, + ` class Foo { bound = () => 'foo'; } @@ -321,6 +397,87 @@ class Bar { } function foo({ bound }: Foo & Bar) {} `, + ` +class Foo { + unbound = function () {}; +} +declare const { unbound }: Foo; + `, + "declare const { unbound } = '***';", + ` +class Foo { + unbound = function () {}; +} +type foo = (a: (b: (c: ({ unbound }: Foo) => void) => void) => void) => void; + `, + ` +class Foo { + unbound = function () {}; +} +class Bar { + property: ({ unbound }: Foo) => void; +} + `, + ` +class Foo { + unbound = function () {}; +} +function foo void>() {} + `, + ` +class Foo { + unbound = function () {}; +} +abstract class Bar { + abstract foo({ unbound }: Foo); +} + `, + ` +class Foo { + unbound = function () {}; +} +declare class Bar { + foo({ unbound }: Foo); +} + `, + ` +class Foo { + unbound = function () {}; +} +declare function foo({ unbound }: Foo); + `, + ` +class Foo { + unbound = function () {}; +} +interface Bar { + foo: ({ unbound }: Foo) => void; +} + `, + ` +class Foo { + unbound = function () {}; +} +interface Bar { + foo({ unbound }: Foo): void; +} + `, + ` +class Foo { + unbound = function () {}; +} +interface Bar { + new ({ unbound }: Foo): Foo; +} + `, + ` +class Foo { + unbound = function () {}; +} +type foo = new ({ unbound }: Foo) => void; + `, + 'const { unbound } = { unbound: () => {} };', + 'function foo({ unbound }: { unbound: () => void } = { unbound: () => {} }) {}', // https://github.com/typescript-eslint/typescript-eslint/issues/1866 ` class BaseClass { @@ -481,6 +638,21 @@ instance.unbound = x; // THIS SHOULD NOT }, { code: ` +class Foo extends Number { + static parseInt = function (string: string, radix?: number): number {}; +} +const foo = Foo; +['1', '2', '3'].map(foo.parseInt); + `, + errors: [ + { + line: 6, + messageId: 'unbound', + }, + ], + }, + { + code: ` class Foo { unbound = function () {}; } @@ -585,6 +757,36 @@ function foo({ unbound }: Foo = bar) {} class Foo { unbound = function () {}; } +declare const bar: Foo; +function foo({ unbound }: Foo = { unbound: () => {} }) {} + `, + errors: [ + { + line: 6, + messageId: 'unbound', + }, + ], + }, + { + code: ` +class Foo { + unbound = function () {}; +} +declare const bar: Foo; +function foo({ unbound }: Foo = { unbound: function () {} }) {} + `, + errors: [ + { + line: 6, + messageId: 'unboundWithoutThisAnnotation', + }, + ], + }, + { + code: ` +class Foo { + unbound = function () {}; +} function foo({ unbound }: Foo) {} `, errors: [ @@ -598,8 +800,38 @@ function foo({ unbound }: Foo) {} code: ` class Foo { unbound = function () {}; - - foo({ unbound }: Foo) {} +} +function bar(cb: (arg: Foo) => void) {} +bar(({ unbound }) => {}); + `, + errors: [ + { + line: 6, + messageId: 'unbound', + }, + ], + }, + { + code: ` +class Foo { + unbound = function () {}; +} +function bar(cb: (arg: { unbound: () => void }) => void) {} +bar(({ unbound } = new Foo()) => {}); + `, + errors: [ + { + line: 6, + messageId: 'unbound', + }, + ], + }, + { + code: ` +class Foo { + unbound = function () {}; +} +for (const { unbound } of [new Foo(), new Foo()]) { } `, errors: [ @@ -613,8 +845,9 @@ class Foo { code: ` class Foo { unbound = function () {}; + + foo({ unbound }: Foo) {} } -type foo = ({ unbound }: Foo) => void; `, errors: [ { @@ -676,7 +909,7 @@ function foo({ unbound }: Foo | Bar) {} class Foo { unbound = function () {}; } -type foo = ({ unbound }: Foo & { foo: () => 'bar' }) => void; +const foo = ({ unbound }: Foo & { foo: () => 'bar' }) => {}; `, errors: [ { @@ -693,7 +926,7 @@ class Foo { class Bar { unbound = () => {}; } -type foo = ({ unbound }: (Foo & { foo: () => 'bar' }) | Bar) => void; +const foo = ({ unbound }: (Foo & { foo: () => 'bar' }) | Bar) => {}; `, errors: [ { @@ -710,7 +943,7 @@ class Foo { class Bar { unbound = () => {}; } -type foo = ({ unbound }: Foo & Bar) => void; +const foo = ({ unbound }: Foo & Bar) => {}; `, errors: [ { @@ -729,7 +962,7 @@ class Foo { class Bar { unbound = () => {}; } -type foo = ({ unbound, ...rest }: Foo & Bar) => void; +const foo = ({ unbound, ...rest }: Foo & Bar) => {}; `, errors: [ { @@ -738,6 +971,43 @@ type foo = ({ unbound, ...rest }: Foo & Bar) => void; }, ], }, + { + code: 'const { unbound } = { unbound: function () {} };', + errors: [ + { + line: 1, + messageId: 'unboundWithoutThisAnnotation', + }, + ], + }, + { + code: ` +function foo( + { unbound }: { unbound: () => void } = { unbound: function () {} }, +) {} + `, + errors: [ + { + line: 3, + messageId: 'unboundWithoutThisAnnotation', + }, + ], + }, + { + code: ` +class Foo { + floor = function () {}; +} + +const { floor } = Math.random() > 0.5 ? new Foo() : Math; + `, + errors: [ + { + line: 6, + messageId: 'unboundWithoutThisAnnotation', + }, + ], + }, { code: ` class CommunicationError { diff --git a/packages/type-utils/src/builtinSymbolLikes.ts b/packages/type-utils/src/builtinSymbolLikes.ts index 3443a0d0382..307d02e3494 100644 --- a/packages/type-utils/src/builtinSymbolLikes.ts +++ b/packages/type-utils/src/builtinSymbolLikes.ts @@ -105,7 +105,7 @@ export function isBuiltinTypeAliasLike( export function isBuiltinSymbolLike( program: ts.Program, type: ts.Type, - symbolName: string, + symbolName: string | string[], ): boolean { return isBuiltinSymbolLikeRecurser(program, type, subType => { const symbol = subType.getSymbol(); @@ -113,8 +113,12 @@ export function isBuiltinSymbolLike( return false; } + const actualSymbolName = symbol.getName(); + if ( - symbol.getName() === symbolName && + (Array.isArray(symbolName) + ? symbolName.some(name => actualSymbolName === name) + : actualSymbolName === symbolName) && isSymbolFromDefaultLibrary(program, symbol) ) { return true; From 7dafb3ab3ae3224d92931111e8b0754b47851794 Mon Sep 17 00:00:00 2001 From: auvred Date: Fri, 3 May 2024 21:25:21 +0300 Subject: [PATCH 4/5] refactor: improve iteration over union/intersection parts --- .../eslint-plugin/src/rules/unbound-method.ts | 27 ++++--------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/packages/eslint-plugin/src/rules/unbound-method.ts b/packages/eslint-plugin/src/rules/unbound-method.ts index 9836664522c..0d0b5eb6f88 100644 --- a/packages/eslint-plugin/src/rules/unbound-method.ts +++ b/packages/eslint-plugin/src/rules/unbound-method.ts @@ -251,33 +251,16 @@ export default createRule({ } } - for (const unionPart of tsutils.unionTypeParts( - services.getTypeAtLocation(node), - )) { - const propertyKey = property.key; + for (const intersectionPart of tsutils + .unionTypeParts(services.getTypeAtLocation(node)) + .flatMap(unionPart => tsutils.intersectionTypeParts(unionPart))) { const reported = checkIfMethodAndReport( - propertyKey, - unionPart.getProperty(propertyKey.name), + property.key, + intersectionPart.getProperty(property.key.name), ); if (reported) { break; } - - if (!tsutils.isIntersectionType(unionPart)) { - continue; - } - - for (const intersectionPart of tsutils.intersectionTypeParts( - unionPart, - )) { - const reported = checkIfMethodAndReport( - propertyKey, - intersectionPart.getProperty(propertyKey.name), - ); - if (reported) { - break; - } - } } } }, From 71074a68b54319512b69c343cb8e22fbd11d9a40 Mon Sep 17 00:00:00 2001 From: auvred <61150013+auvred@users.noreply.github.com> Date: Sun, 12 May 2024 17:17:51 +0300 Subject: [PATCH 5/5] Update packages/eslint-plugin/src/rules/unbound-method.ts Co-authored-by: Kirk Waiblinger --- packages/eslint-plugin/src/rules/unbound-method.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/unbound-method.ts b/packages/eslint-plugin/src/rules/unbound-method.ts index 0d0b5eb6f88..222e5115d39 100644 --- a/packages/eslint-plugin/src/rules/unbound-method.ts +++ b/packages/eslint-plugin/src/rules/unbound-method.ts @@ -162,7 +162,7 @@ export default createRule({ ) { const objectSymbol = services.getSymbolAtLocation(object); const notImported = - objectSymbol && isNotImported(objectSymbol, currentSourceFile); + objectSymbol != null && isNotImported(objectSymbol, currentSourceFile); if ( notImported &&