From c3753c21768d355ecdb9e7ae8e0bfdfbbc1d3bbe Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Sun, 21 Jun 2020 04:54:34 +0900 Subject: [PATCH] fix(eslint-plugin): [unbound-method] handling destructuring (#2228) --- .../eslint-plugin/src/rules/unbound-method.ts | 46 +++++- .../tests/rules/unbound-method.test.ts | 141 +++++++++++++++++- 2 files changed, 183 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/unbound-method.ts b/packages/eslint-plugin/src/rules/unbound-method.ts index ade04229887..e8217348a4a 100644 --- a/packages/eslint-plugin/src/rules/unbound-method.ts +++ b/packages/eslint-plugin/src/rules/unbound-method.ts @@ -99,7 +99,7 @@ const nativelyBoundMembers = SUPPORTED_GLOBALS.map(namespace => { .reduce((arr, names) => arr.concat(names), []) .filter(name => !nativelyNotBoundMembers.has(name)); -const isMemberNotImported = ( +const isNotImported = ( symbol: ts.Symbol, currentSourceFile: ts.SourceFile | undefined, ): boolean => { @@ -176,7 +176,7 @@ export default util.createRule({ if ( objectSymbol && nativelyBoundMembers.includes(getMemberFullName(node)) && - isMemberNotImported(objectSymbol, currentSourceFile) + isNotImported(objectSymbol, currentSourceFile) ) { return; } @@ -191,6 +191,48 @@ export default util.createRule({ }); } }, + 'VariableDeclarator, AssignmentExpression'( + node: TSESTree.VariableDeclarator | TSESTree.AssignmentExpression, + ): 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 tsNode = parserServices.esTreeNodeToTSNodeMap.get(initNode); + const rightSymbol = checker.getSymbolAtLocation(tsNode); + const initTypes = checker.getTypeAtLocation(tsNode); + + const notImported = + rightSymbol && isNotImported(rightSymbol, currentSourceFile); + + idNode.properties.forEach(property => { + if ( + property.type === AST_NODE_TYPES.Property && + property.key.type === AST_NODE_TYPES.Identifier + ) { + if ( + notImported && + util.isIdentifier(initNode) && + nativelyBoundMembers.includes( + `${initNode.name}.${property.key.name}`, + ) + ) { + return; + } + + const symbol = initTypes.getProperty(property.key.name); + if (symbol && isDangerousMethod(symbol, ignoreStatic)) { + context.report({ + messageId: 'unbound', + node, + }); + } + } + }); + } + }, }; }, }); diff --git a/packages/eslint-plugin/tests/rules/unbound-method.test.ts b/packages/eslint-plugin/tests/rules/unbound-method.test.ts index 12d6486e09f..4c0cb8f58d3 100644 --- a/packages/eslint-plugin/tests/rules/unbound-method.test.ts +++ b/packages/eslint-plugin/tests/rules/unbound-method.test.ts @@ -228,6 +228,35 @@ class A { } } `, + 'const { parseInt } = Number;', + 'const { log } = console;', + ` +let parseInt; +({ parseInt } = Number); + `, + ` +let log; +({ log } = console); + `, + ` +const foo = { + bar: 'bar', +}; +const { bar } = foo; + `, + ` +class Foo { + unbnound() {} + bar = 4; +} +const { bar } = new Foo(); + `, + ` +class Foo { + bound = () => 'foo'; +} +const { bound } = new Foo(); + `, ], invalid: [ { @@ -288,8 +317,8 @@ function foo(arg: ContainsMethods | null) { 'const unbound = instance.unbound;', 'const unboundStatic = ContainsMethods.unboundStatic;', - 'const { unbound } = instance.unbound;', - 'const { unboundStatic } = ContainsMethods.unboundStatic;', + 'const { unbound } = instance;', + 'const { unboundStatic } = ContainsMethods;', 'instance.unbound;', 'instance.unbound as any;', @@ -384,5 +413,113 @@ const unbound = new Foo().unbound; }, ], }, + { + code: ` +class Foo { + unbound() {} +} +const { unbound } = new Foo(); + `, + errors: [ + { + line: 5, + messageId: 'unbound', + }, + ], + }, + { + code: ` +class Foo { + unbound = function () {}; +} +const { unbound } = new Foo(); + `, + errors: [ + { + line: 5, + messageId: 'unbound', + }, + ], + }, + { + code: ` +class Foo { + unbound() {} +} +let unbound; +({ unbound } = new Foo()); + `, + errors: [ + { + line: 6, + messageId: 'unbound', + }, + ], + }, + { + code: ` +class Foo { + unbound = function () {}; +} +let unbound; +({ unbound } = new Foo()); + `, + errors: [ + { + line: 6, + messageId: 'unbound', + }, + ], + }, + { + code: ` +class CommunicationError { + foo() {} +} +const { foo } = CommunicationError.prototype; + `, + errors: [ + { + line: 5, + messageId: 'unbound', + }, + ], + }, + { + code: ` +class CommunicationError { + foo() {} +} +let foo; +({ foo } = CommunicationError.prototype); + `, + errors: [ + { + line: 6, + messageId: 'unbound', + }, + ], + }, + { + code: ` +import { console } from './class'; +const { log } = console; + `, + errors: [ + { + line: 3, + messageId: 'unbound', + }, + ], + }, + { + code: 'const { all } = Promise;', + errors: [ + { + line: 1, + messageId: 'unbound', + }, + ], + }, ], });