Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat(eslint-plugin): [explicit-module-boundary-types] add optio… (#1778)
  • Loading branch information
sosukesuzuki committed Apr 13, 2020
1 parent 87b7dbb commit 3eee804
Show file tree
Hide file tree
Showing 3 changed files with 568 additions and 29 deletions.
Expand Up @@ -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,
};
```

Expand Down Expand Up @@ -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)
Expand Down
215 changes: 186 additions & 29 deletions 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 {
Expand All @@ -15,6 +16,7 @@ type Options = [
allowHigherOrderFunctions?: boolean;
allowDirectConstAssertionInArrowFunctions?: boolean;
allowedNames?: string[];
shouldTrackReferences?: boolean;
},
];
type MessageIds = 'missingReturnType' | 'missingArgType';
Expand Down Expand Up @@ -52,6 +54,9 @@ export default util.createRule<Options, MessageIds>({
type: 'string',
},
},
shouldTrackReferences: {
type: 'boolean',
},
},
additionalProperties: false,
},
Expand All @@ -63,6 +68,7 @@ export default util.createRule<Options, MessageIds>({
allowHigherOrderFunctions: true,
allowDirectConstAssertionInArrowFunctions: true,
allowedNames: [],
shouldTrackReferences: true,
},
],
create(context, [options]) {
Expand Down Expand Up @@ -171,50 +177,201 @@ export default util.createRule<Options, MessageIds>({
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;
}
}
},
};
},
Expand Down

0 comments on commit 3eee804

Please sign in to comment.