Skip to content

Commit c3753c2

Browse files
authored
fix(eslint-plugin): [unbound-method] handling destructuring (typescript-eslint#2228)
1 parent f5c271f commit c3753c2

File tree

2 files changed

+183
-4
lines changed

2 files changed

+183
-4
lines changed

packages/eslint-plugin/src/rules/unbound-method.ts

+44-2
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ const nativelyBoundMembers = SUPPORTED_GLOBALS.map(namespace => {
9999
.reduce((arr, names) => arr.concat(names), [])
100100
.filter(name => !nativelyNotBoundMembers.has(name));
101101

102-
const isMemberNotImported = (
102+
const isNotImported = (
103103
symbol: ts.Symbol,
104104
currentSourceFile: ts.SourceFile | undefined,
105105
): boolean => {
@@ -176,7 +176,7 @@ export default util.createRule<Options, MessageIds>({
176176
if (
177177
objectSymbol &&
178178
nativelyBoundMembers.includes(getMemberFullName(node)) &&
179-
isMemberNotImported(objectSymbol, currentSourceFile)
179+
isNotImported(objectSymbol, currentSourceFile)
180180
) {
181181
return;
182182
}
@@ -191,6 +191,48 @@ export default util.createRule<Options, MessageIds>({
191191
});
192192
}
193193
},
194+
'VariableDeclarator, AssignmentExpression'(
195+
node: TSESTree.VariableDeclarator | TSESTree.AssignmentExpression,
196+
): void {
197+
const [idNode, initNode] =
198+
node.type === AST_NODE_TYPES.VariableDeclarator
199+
? [node.id, node.init]
200+
: [node.left, node.right];
201+
202+
if (initNode && idNode.type === AST_NODE_TYPES.ObjectPattern) {
203+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(initNode);
204+
const rightSymbol = checker.getSymbolAtLocation(tsNode);
205+
const initTypes = checker.getTypeAtLocation(tsNode);
206+
207+
const notImported =
208+
rightSymbol && isNotImported(rightSymbol, currentSourceFile);
209+
210+
idNode.properties.forEach(property => {
211+
if (
212+
property.type === AST_NODE_TYPES.Property &&
213+
property.key.type === AST_NODE_TYPES.Identifier
214+
) {
215+
if (
216+
notImported &&
217+
util.isIdentifier(initNode) &&
218+
nativelyBoundMembers.includes(
219+
`${initNode.name}.${property.key.name}`,
220+
)
221+
) {
222+
return;
223+
}
224+
225+
const symbol = initTypes.getProperty(property.key.name);
226+
if (symbol && isDangerousMethod(symbol, ignoreStatic)) {
227+
context.report({
228+
messageId: 'unbound',
229+
node,
230+
});
231+
}
232+
}
233+
});
234+
}
235+
},
194236
};
195237
},
196238
});

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

+139-2
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,35 @@ class A {
228228
}
229229
}
230230
`,
231+
'const { parseInt } = Number;',
232+
'const { log } = console;',
233+
`
234+
let parseInt;
235+
({ parseInt } = Number);
236+
`,
237+
`
238+
let log;
239+
({ log } = console);
240+
`,
241+
`
242+
const foo = {
243+
bar: 'bar',
244+
};
245+
const { bar } = foo;
246+
`,
247+
`
248+
class Foo {
249+
unbnound() {}
250+
bar = 4;
251+
}
252+
const { bar } = new Foo();
253+
`,
254+
`
255+
class Foo {
256+
bound = () => 'foo';
257+
}
258+
const { bound } = new Foo();
259+
`,
231260
],
232261
invalid: [
233262
{
@@ -288,8 +317,8 @@ function foo(arg: ContainsMethods | null) {
288317
'const unbound = instance.unbound;',
289318
'const unboundStatic = ContainsMethods.unboundStatic;',
290319

291-
'const { unbound } = instance.unbound;',
292-
'const { unboundStatic } = ContainsMethods.unboundStatic;',
320+
'const { unbound } = instance;',
321+
'const { unboundStatic } = ContainsMethods;',
293322

294323
'<any>instance.unbound;',
295324
'instance.unbound as any;',
@@ -384,5 +413,113 @@ const unbound = new Foo().unbound;
384413
},
385414
],
386415
},
416+
{
417+
code: `
418+
class Foo {
419+
unbound() {}
420+
}
421+
const { unbound } = new Foo();
422+
`,
423+
errors: [
424+
{
425+
line: 5,
426+
messageId: 'unbound',
427+
},
428+
],
429+
},
430+
{
431+
code: `
432+
class Foo {
433+
unbound = function () {};
434+
}
435+
const { unbound } = new Foo();
436+
`,
437+
errors: [
438+
{
439+
line: 5,
440+
messageId: 'unbound',
441+
},
442+
],
443+
},
444+
{
445+
code: `
446+
class Foo {
447+
unbound() {}
448+
}
449+
let unbound;
450+
({ unbound } = new Foo());
451+
`,
452+
errors: [
453+
{
454+
line: 6,
455+
messageId: 'unbound',
456+
},
457+
],
458+
},
459+
{
460+
code: `
461+
class Foo {
462+
unbound = function () {};
463+
}
464+
let unbound;
465+
({ unbound } = new Foo());
466+
`,
467+
errors: [
468+
{
469+
line: 6,
470+
messageId: 'unbound',
471+
},
472+
],
473+
},
474+
{
475+
code: `
476+
class CommunicationError {
477+
foo() {}
478+
}
479+
const { foo } = CommunicationError.prototype;
480+
`,
481+
errors: [
482+
{
483+
line: 5,
484+
messageId: 'unbound',
485+
},
486+
],
487+
},
488+
{
489+
code: `
490+
class CommunicationError {
491+
foo() {}
492+
}
493+
let foo;
494+
({ foo } = CommunicationError.prototype);
495+
`,
496+
errors: [
497+
{
498+
line: 6,
499+
messageId: 'unbound',
500+
},
501+
],
502+
},
503+
{
504+
code: `
505+
import { console } from './class';
506+
const { log } = console;
507+
`,
508+
errors: [
509+
{
510+
line: 3,
511+
messageId: 'unbound',
512+
},
513+
],
514+
},
515+
{
516+
code: 'const { all } = Promise;',
517+
errors: [
518+
{
519+
line: 1,
520+
messageId: 'unbound',
521+
},
522+
],
523+
},
387524
],
388525
});

0 commit comments

Comments
 (0)