diff --git a/packages/eslint-plugin/docs/rules/explicit-module-boundary-types.md b/packages/eslint-plugin/docs/rules/explicit-module-boundary-types.md index a5327a1e547..3c077a75c88 100644 --- a/packages/eslint-plugin/docs/rules/explicit-module-boundary-types.md +++ b/packages/eslint-plugin/docs/rules/explicit-module-boundary-types.md @@ -86,12 +86,17 @@ type Options = { * An array of function/method names that will not have their arguments or their return values checked. */ allowedNames?: string[]; + /** + * If true, track references to exported variables as well as direct exports. + */ + shouldTrackReferences?: boolean; }; const defaults = { allowTypedFunctionExpressions: true, allowHigherOrderFunctions: true, allowedNames: [], + shouldTrackReferences: true, }; ``` @@ -238,6 +243,28 @@ You may pass function/method names you would like this rule to ignore, like so: } ``` +### `shouldTrackReferences` + +Examples of **incorrect** code for this rule with `{ shouldTrackReferences: true }`: + +```ts +function foo(bar) { + return bar; +} + +export default foo; +``` + +Examples of **correct** code for this rule with `{ shouldTrackReferences: true }`: + +```ts +function foo(bar: string): string { + return bar; +} + +export default foo; +``` + ## When Not To Use It If you wish to make sure all functions have explicit return types, as opposed to only the module boundaries, you can use [explicit-function-return-type](https://github.com/eslint/eslint/blob/master/docs/rules/explicit-function-return-type.md) diff --git a/packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts b/packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts index 55c6bd29f99..b00d8eb1087 100644 --- a/packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts +++ b/packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts @@ -1,6 +1,7 @@ import { TSESTree, AST_NODE_TYPES, + TSESLint, } from '@typescript-eslint/experimental-utils'; import * as util from '../util'; import { @@ -15,6 +16,7 @@ type Options = [ allowHigherOrderFunctions?: boolean; allowDirectConstAssertionInArrowFunctions?: boolean; allowedNames?: string[]; + shouldTrackReferences?: boolean; }, ]; type MessageIds = 'missingReturnType' | 'missingArgType'; @@ -52,6 +54,9 @@ export default util.createRule({ type: 'string', }, }, + shouldTrackReferences: { + type: 'boolean', + }, }, additionalProperties: false, }, @@ -63,6 +68,7 @@ export default util.createRule({ allowHigherOrderFunctions: true, allowDirectConstAssertionInArrowFunctions: true, allowedNames: [], + shouldTrackReferences: true, }, ], create(context, [options]) { @@ -171,50 +177,201 @@ export default util.createRule({ return false; } + /** + * Finds an array of a function expression node referred by a variable passed from parameters + */ + function findFunctionExpressionsInScope( + variable: TSESLint.Scope.Variable, + ): + | (TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression)[] + | undefined { + const writeExprs = variable.references + .map(ref => ref.writeExpr) + .filter( + ( + expr, + ): expr is + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionExpression => + expr?.type === AST_NODE_TYPES.FunctionExpression || + expr?.type === AST_NODE_TYPES.ArrowFunctionExpression, + ); + + return writeExprs; + } + + /** + * Finds a function node referred by a variable passed from parameters + */ + function findFunctionInScope( + variable: TSESLint.Scope.Variable, + ): TSESTree.FunctionDeclaration | undefined { + if (variable.defs[0].type !== 'FunctionName') { + return; + } + + const functionNode = variable.defs[0].node; + + if (functionNode?.type !== AST_NODE_TYPES.FunctionDeclaration) { + return; + } + + return functionNode; + } + + /** + * Checks if a function referred by the identifier passed from parameters follow the rule + */ + function checkWithTrackingReferences(node: TSESTree.Identifier): void { + const scope = context.getScope(); + const variable = scope.set.get(node.name); + + if (!variable) { + return; + } + + if (variable.defs[0].type === 'ClassName') { + const classNode = variable.defs[0].node; + for (const classElement of classNode.body.body) { + if ( + classElement.type === AST_NODE_TYPES.MethodDefinition && + classElement.value.type === AST_NODE_TYPES.FunctionExpression + ) { + checkFunctionExpression(classElement.value); + } + + if ( + classElement.type === AST_NODE_TYPES.ClassProperty && + (classElement.value?.type === AST_NODE_TYPES.FunctionExpression || + classElement.value?.type === + AST_NODE_TYPES.ArrowFunctionExpression) + ) { + checkFunctionExpression(classElement.value); + } + } + } + + const functionNode = findFunctionInScope(variable); + if (functionNode) { + checkFunction(functionNode); + } + + const functionExpressions = findFunctionExpressionsInScope(variable); + if (functionExpressions && functionExpressions.length > 0) { + for (const functionExpression of functionExpressions) { + checkFunctionExpression(functionExpression); + } + } + } + + /** + * Checks if a function expression follow the rule + */ + function checkFunctionExpression( + node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression, + ): void { + if ( + node.parent?.type === AST_NODE_TYPES.MethodDefinition && + node.parent.accessibility === 'private' + ) { + // don't check private methods as they aren't part of the public signature + return; + } + + if ( + isAllowedName(node.parent) || + isTypedFunctionExpression(node, options) + ) { + return; + } + + checkFunctionExpressionReturnType(node, options, sourceCode, loc => + context.report({ + node, + loc, + messageId: 'missingReturnType', + }), + ); + + checkArguments(node); + } + + /** + * Checks if a function follow the rule + */ + function checkFunction(node: TSESTree.FunctionDeclaration): void { + if (isAllowedName(node.parent)) { + return; + } + + checkFunctionReturnType(node, options, sourceCode, loc => + context.report({ + node, + loc, + messageId: 'missingReturnType', + }), + ); + + checkArguments(node); + } + return { 'ArrowFunctionExpression, FunctionExpression'( node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression, ): void { - if ( - node.parent?.type === AST_NODE_TYPES.MethodDefinition && - node.parent.accessibility === 'private' - ) { - // don't check private methods as they aren't part of the public signature + if (isUnexported(node)) { return; } - if ( - isAllowedName(node.parent) || - isUnexported(node) || - isTypedFunctionExpression(node, options) - ) { + checkFunctionExpression(node); + }, + FunctionDeclaration(node): void { + if (isUnexported(node)) { return; } - checkFunctionExpressionReturnType(node, options, sourceCode, loc => - context.report({ - node, - loc, - messageId: 'missingReturnType', - }), - ); - - checkArguments(node); + checkFunction(node); }, - FunctionDeclaration(node): void { - if (isAllowedName(node.parent) || isUnexported(node)) { + 'ExportDefaultDeclaration, TSExportAssignment'( + node: TSESTree.ExportDefaultDeclaration | TSESTree.TSExportAssignment, + ): void { + if (!options.shouldTrackReferences) { return; } - checkFunctionReturnType(node, options, sourceCode, loc => - context.report({ - node, - loc, - messageId: 'missingReturnType', - }), - ); + let exported: TSESTree.Node; + + if (node.type === AST_NODE_TYPES.ExportDefaultDeclaration) { + exported = node.declaration; + } else { + exported = node.expression; + } - checkArguments(node); + switch (exported.type) { + case AST_NODE_TYPES.Identifier: { + checkWithTrackingReferences(exported); + break; + } + case AST_NODE_TYPES.ArrayExpression: { + for (const element of exported.elements) { + if (element.type === AST_NODE_TYPES.Identifier) { + checkWithTrackingReferences(element); + } + } + break; + } + case AST_NODE_TYPES.ObjectExpression: { + for (const property of exported.properties) { + if ( + property.type === AST_NODE_TYPES.Property && + property.value.type === AST_NODE_TYPES.Identifier + ) { + checkWithTrackingReferences(property.value); + } + } + break; + } + } }, }; }, diff --git a/packages/eslint-plugin/tests/rules/explicit-module-boundary-types.test.ts b/packages/eslint-plugin/tests/rules/explicit-module-boundary-types.test.ts index cdff53533b7..5d63e8e1036 100644 --- a/packages/eslint-plugin/tests/rules/explicit-module-boundary-types.test.ts +++ b/packages/eslint-plugin/tests/rules/explicit-module-boundary-types.test.ts @@ -348,6 +348,95 @@ export const Foo: JSX.Element = ( ecmaFeatures: { jsx: true }, }, }, + { + code: ` +const test = (): void => { + return; +}; +export default test; + `, + options: [{ shouldTrackReferences: true }], + }, + { + code: ` +function test(): void { + return; +} +export default test; + `, + options: [{ shouldTrackReferences: true }], + }, + { + code: ` +const test = (): void => { + return; +}; +export default [test]; + `, + options: [{ shouldTrackReferences: true }], + }, + { + code: ` +function test(): void { + return; +} +export default [test]; + `, + options: [{ shouldTrackReferences: true }], + }, + { + code: ` +const test = (): void => { + return; +}; +export default { test }; + `, + options: [{ shouldTrackReferences: true }], + }, + { + code: ` +function test(): void { + return; +} +export default { test }; + `, + options: [{ shouldTrackReferences: true }], + }, + { + code: ` +const foo = (arg => arg) as Foo; +export default foo; + `, + options: [{ shouldTrackReferences: true }], + }, + { + code: ` +let foo = (arg => arg) as Foo; +foo = 3; +export default foo; + `, + options: [{ shouldTrackReferences: true }], + }, + { + code: ` +class Foo { + bar = (arg: string): string => arg; +} +export default { Foo }; + `, + options: [{ shouldTrackReferences: true }], + }, + { + code: ` +class Foo { + bar(): void { + return; + } +} +export default { Foo }; + `, + options: [{ shouldTrackReferences: true }], + }, ], invalid: [ { @@ -925,5 +1014,271 @@ export function fn(test): string { }, ], }, + { + code: ` +const foo = arg => arg; +export default foo; + `, + options: [{ shouldTrackReferences: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 2, + }, + { + messageId: 'missingArgType', + line: 2, + }, + ], + }, + { + code: ` +const foo = arg => arg; +export = foo; + `, + options: [{ shouldTrackReferences: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 2, + }, + { + messageId: 'missingArgType', + line: 2, + }, + ], + }, + { + code: ` +let foo = (arg: number): number => arg; +foo = arg => arg; +export default foo; + `, + options: [{ shouldTrackReferences: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 3, + }, + { + messageId: 'missingArgType', + line: 3, + }, + ], + }, + { + code: ` +const foo = arg => arg; +export default [foo]; + `, + options: [{ shouldTrackReferences: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 2, + }, + { + messageId: 'missingArgType', + line: 2, + }, + ], + }, + { + code: ` +const foo = arg => arg; +export default { foo }; + `, + options: [{ shouldTrackReferences: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 2, + }, + { + messageId: 'missingArgType', + line: 2, + }, + ], + }, + { + code: ` +function foo(arg) { + return arg; +} +export default foo; + `, + options: [{ shouldTrackReferences: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 2, + }, + { + messageId: 'missingArgType', + line: 2, + }, + ], + }, + { + code: ` +function foo(arg) { + return arg; +} +export default [foo]; + `, + options: [{ shouldTrackReferences: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 2, + }, + { + messageId: 'missingArgType', + line: 2, + }, + ], + }, + { + code: ` +function foo(arg) { + return arg; +} +export default { foo }; + `, + options: [{ shouldTrackReferences: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 2, + }, + { + messageId: 'missingArgType', + line: 2, + }, + ], + }, + { + code: ` +const bar = function foo(arg) { + return arg; +}; +export default { bar }; + `, + options: [{ shouldTrackReferences: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 2, + }, + { + messageId: 'missingArgType', + line: 2, + }, + ], + }, + { + code: ` +class Foo { + bool(arg) { + return arg; + } +} +export default Foo; + `, + options: [{ shouldTrackReferences: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 3, + }, + { + messageId: 'missingArgType', + line: 3, + }, + ], + }, + { + code: ` +class Foo { + bool = arg => { + return arg; + }; +} +export default Foo; + `, + options: [{ shouldTrackReferences: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 3, + }, + { + messageId: 'missingArgType', + line: 3, + }, + ], + }, + { + code: ` +class Foo { + bool = function(arg) { + return arg; + }; +} +export default Foo; + `, + options: [{ shouldTrackReferences: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 3, + }, + { + messageId: 'missingArgType', + line: 3, + }, + ], + }, + { + code: ` +class Foo { + bool = function(arg) { + return arg; + }; +} +export default [Foo]; + `, + options: [{ shouldTrackReferences: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 3, + }, + { + messageId: 'missingArgType', + line: 3, + }, + ], + }, + { + code: ` +let test = arg => argl; +test = (): void => { + return; +}; +export default test; + `, + options: [{ shouldTrackReferences: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 2, + }, + { + messageId: 'missingArgType', + line: 2, + }, + ], + }, ], });