Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat(eslint-plugin): [unbound-method] support bound builtins (#1526)
Co-authored-by: Brad Zacher <brad.zacher@gmail.com>
  • Loading branch information
G-Rath and bradzacher committed Feb 3, 2020
1 parent 9e0f6dd commit 0a110eb
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 0 deletions.
68 changes: 68 additions & 0 deletions packages/eslint-plugin/src/rules/unbound-method.ts
Expand Up @@ -18,6 +18,59 @@ export type Options = [Config];

export type MessageIds = 'unbound';

const nativelyBoundMembers = ([
'Promise',
'Number',
'Object',
'String', // eslint-disable-line @typescript-eslint/internal/prefer-ast-types-enum
'RegExp',
'Symbol',
'Array',
'Proxy',
'Date',
'Infinity',
'Atomics',
'Reflect',
'console',
'Math',
'JSON',
'Intl',
] as const)
.map(namespace => {
const object = global[namespace];
return Object.getOwnPropertyNames(object)
.filter(
name =>
!name.startsWith('_') &&
typeof (object as Record<string, unknown>)[name] === 'function',
)
.map(name => `${namespace}.${name}`);
})
.reduce((arr, names) => arr.concat(names), []);

const isMemberNotImported = (
symbol: ts.Symbol,
currentSourceFile: ts.SourceFile | undefined,
): boolean => {
const { valueDeclaration } = symbol;
if (!valueDeclaration) {
// working around https://github.com/microsoft/TypeScript/issues/31294
return false;
}

return (
!!currentSourceFile &&
currentSourceFile !== valueDeclaration.getSourceFile()
);
};

const getNodeName = (node: TSESTree.Node): string | null =>
node.type === AST_NODE_TYPES.Identifier ? node.name : null;

const getMemberFullName = (
node: TSESTree.MemberExpression | TSESTree.OptionalMemberExpression,
): string => `${getNodeName(node.object)}.${getNodeName(node.property)}`;

export default util.createRule<Options, MessageIds>({
name: 'unbound-method',
meta: {
Expand Down Expand Up @@ -53,6 +106,9 @@ export default util.createRule<Options, MessageIds>({
create(context, [{ ignoreStatic }]) {
const parserServices = util.getParserServices(context);
const checker = parserServices.program.getTypeChecker();
const currentSourceFile = parserServices.program.getSourceFile(
context.getFilename(),
);

return {
'MemberExpression, OptionalMemberExpression'(
Expand All @@ -62,6 +118,18 @@ export default util.createRule<Options, MessageIds>({
return;
}

const objectSymbol = checker.getSymbolAtLocation(
parserServices.esTreeNodeToTSNodeMap.get(node.object),
);

if (
objectSymbol &&
nativelyBoundMembers.includes(getMemberFullName(node)) &&
isMemberNotImported(objectSymbol, currentSourceFile)
) {
return;
}

const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node);
const symbol = checker.getSymbolAtLocation(originalNode);

Expand Down
3 changes: 3 additions & 0 deletions packages/eslint-plugin/tests/fixtures/class.ts
@@ -1,2 +1,5 @@
// used by no-throw-literal test case to validate custom error
export class Error {}

// used by unbound-method test case to test imports
export const console = { log() {} };
36 changes: 36 additions & 0 deletions packages/eslint-plugin/tests/rules/unbound-method.test.ts
Expand Up @@ -7,6 +7,7 @@ const rootPath = getFixturesRootDir();
const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
tsconfigRootDir: rootPath,
project: './tsconfig.json',
},
Expand Down Expand Up @@ -43,6 +44,10 @@ function addContainsMethodsClassInvalid(

ruleTester.run('unbound-method', rule, {
valid: [
'Promise.resolve().then(console.log)',
'["1", "2", "3"].map(Number.parseInt)',
'[5.2, 7.1, 3.6].map(Math.floor);',
'const x = console.log',
...[
'instance.bound();',
'instance.unbound();',
Expand Down Expand Up @@ -208,6 +213,37 @@ if(myCondition || x.mightBeDefined) {
`,
],
invalid: [
{
code: `
class Console {
log(str) {
process.stdout.write(str);
}
}
const console = new Console();
Promise.resolve().then(console.log);
`,
errors: [
{
line: 10,
messageId: 'unbound',
},
],
},
{
code: `
import { console } from './class';
const x = console.log;
`,
errors: [
{
line: 3,
messageId: 'unbound',
},
],
},
{
code: addContainsMethodsClass(`
function foo(arg: ContainsMethods | null) {
Expand Down
8 changes: 8 additions & 0 deletions packages/eslint-plugin/typings/node.d.ts
@@ -0,0 +1,8 @@
// augment nodejs global with ES2015+ things
declare namespace NodeJS {
interface Global {
Atomics: typeof Atomics;
Proxy: typeof Proxy;
Reflect: typeof Reflect;
}
}

0 comments on commit 0a110eb

Please sign in to comment.