diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 2e4c9c5ae46..0f81cf64216 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -106,6 +106,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/default-param-last`](./docs/rules/default-param-last.md) | Enforce default parameters to be last | | | | | [`@typescript-eslint/explicit-function-return-type`](./docs/rules/explicit-function-return-type.md) | Require explicit return types on functions and class methods | :heavy_check_mark: | | | | [`@typescript-eslint/explicit-member-accessibility`](./docs/rules/explicit-member-accessibility.md) | Require explicit accessibility modifiers on class properties and methods | | | | +| [`@typescript-eslint/explicit-module-boundary-types`](./docs/rules/explicit-module-boundary-types.md) | Require explicit return and argument types on exported functions' and classes' public class methods | | | | | [`@typescript-eslint/func-call-spacing`](./docs/rules/func-call-spacing.md) | Require or disallow spacing between function identifiers and their invocations | | :wrench: | | | [`@typescript-eslint/indent`](./docs/rules/indent.md) | Enforce consistent indentation | | :wrench: | | | [`@typescript-eslint/member-delimiter-style`](./docs/rules/member-delimiter-style.md) | Require a specific member delimiter style for interfaces and type literals | :heavy_check_mark: | :wrench: | | @@ -138,7 +139,6 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/no-unnecessary-qualifier`](./docs/rules/no-unnecessary-qualifier.md) | Warns when a namespace qualifier is unnecessary | | :wrench: | :thought_balloon: | | [`@typescript-eslint/no-unnecessary-type-arguments`](./docs/rules/no-unnecessary-type-arguments.md) | Enforces that type arguments will not be used if not required | | :wrench: | :thought_balloon: | | [`@typescript-eslint/no-unnecessary-type-assertion`](./docs/rules/no-unnecessary-type-assertion.md) | Warns if a type assertion does not change the type of an expression | :heavy_check_mark: | :wrench: | :thought_balloon: | -| [`@typescript-eslint/no-untyped-public-signature`](./docs/rules/no-untyped-public-signature.md) | Disallow untyped public methods | | | | | [`@typescript-eslint/no-unused-expressions`](./docs/rules/no-unused-expressions.md) | Disallow unused expressions | | | | | [`@typescript-eslint/no-unused-vars`](./docs/rules/no-unused-vars.md) | Disallow unused variables | :heavy_check_mark: | | | | [`@typescript-eslint/no-unused-vars-experimental`](./docs/rules/no-unused-vars-experimental.md) | Disallow unused variables and arguments | | | :thought_balloon: | diff --git a/packages/eslint-plugin/docs/rules/explicit-module-boundary-types.md b/packages/eslint-plugin/docs/rules/explicit-module-boundary-types.md new file mode 100644 index 00000000000..cc7d5f1cc03 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/explicit-module-boundary-types.md @@ -0,0 +1,208 @@ +# Require explicit return and argument types on exported functions' and classes' public class methods (`explicit-module-boundary-types`) + +Explicit types for function return values and arguments makes it clear to any calling code what is the module boundary's input and output. + +Consider using this rule in place of [`no-untyped-public-signature`](./no-untyped-public-signature.md) which has been deprecated. + +## Rule Details + +This rule aims to ensure that the values returned from a module are of the expected type. + +The following patterns are considered warnings: + +```ts +// Should indicate that no value is returned (void) +export function test() { + return; +} + +// Should indicate that a number is returned +export default function() { + return 1; +} + +// Should indicate that a string is returned +export var arrowFn = () => 'test'; + +// All arguments should be typed +export var arrowFn = (arg): string => `test ${arg}`; + +export class Test { + // Should indicate that no value is returned (void) + method() { + return; + } +} +``` + +The following patterns are not warnings: + +```ts +// Function is not exported +function test() { + return; +} + +// A return value of type number +export var fn = function(): number { + return 1; +}; + +// A return value of type string +export var arrowFn = (arg: string): string => `test ${arg}`; + +// Class is not exported +class Test { + method() { + return; + } +} +``` + +## Options + +The rule accepts an options object with the following properties: + +```ts +type Options = { + // if true, type annotations are also allowed on the variable of a function expression rather than on the function directly + allowTypedFunctionExpressions?: boolean; + // if true, functions immediately returning another function expression will not be checked + allowHigherOrderFunctions?: boolean; + // if true, body-less arrow functions are allowed to return an object as const + allowDirectConstAssertionInArrowFunctions?: boolean; + // an array of function/method names that will not be checked + allowedNames?: string[]; +}; + +const defaults = { + allowTypedFunctionExpressions: true, + allowHigherOrderFunctions: true, + allowedNames: [], +}; +``` + +### Configuring in a mixed JS/TS codebase + +If you are working on a codebase within which you lint non-TypeScript code (i.e. `.js`/`.jsx`), you should ensure that you should use [ESLint `overrides`](https://eslint.org/docs/user-guide/configuring#disabling-rules-only-for-a-group-of-files) to only enable the rule on `.ts`/`.tsx` files. If you don't, then you will get unfixable lint errors reported within `.js`/`.jsx` files. + +```jsonc +{ + "rules": { + // disable the rule for all files + "@typescript-eslint/explicit-module-boundary-types": "off" + }, + "overrides": [ + { + // enable the rule specifically for TypeScript files + "files": ["*.ts", "*.tsx"], + "rules": { + "@typescript-eslint/explicit-module-boundary-types": ["error"] + } + } + ] +} +``` + +### `allowTypedFunctionExpressions` + +Examples of **incorrect** code for this rule with `{ allowTypedFunctionExpressions: true }`: + +```ts +export let arrowFn = () => 'test'; + +export let funcExpr = function() { + return 'test'; +}; + +export let objectProp = { + foo: () => 1, +}; +``` + +Examples of additional **correct** code for this rule with `{ allowTypedFunctionExpressions: true }`: + +```ts +type FuncType = () => string; + +export let arrowFn: FuncType = () => 'test'; + +export let funcExpr: FuncType = function() { + return 'test'; +}; + +export let asTyped = (() => '') as () => string; +export let castTyped = <() => string>(() => ''); + +interface ObjectType { + foo(): number; +} +export let objectProp: ObjectType = { + foo: () => 1, +}; +export let objectPropAs = { + foo: () => 1, +} as ObjectType; +export let objectPropCast = { + foo: () => 1, +}; +``` + +### `allowHigherOrderFunctions` + +Examples of **incorrect** code for this rule with `{ allowHigherOrderFunctions: true }`: + +```ts +export var arrowFn = () => () => {}; + +export function fn() { + return function() {}; +} +``` + +Examples of **correct** code for this rule with `{ allowHigherOrderFunctions: true }`: + +```ts +export var arrowFn = () => (): void => {}; + +export function fn() { + return function(): void {}; +} +``` + +### `allowDirectConstAssertionInArrowFunctions` + +Examples of additional **correct** code for this rule with `{ allowDirectConstAssertionInArrowFunctions: true }`: + +```ts +export const func = (value: number) => ({ type: 'X', value } as const); +``` + +Examples of additional **incorrect** code for this rule with `{ allowDirectConstAssertionInArrowFunctions: true }`: + +```ts +export const func = (value: number) => ({ type: 'X', value }); +export const foo = () => { + return { + bar: true, + } as const; +}; +``` + +### `allowedNames` + +You may pass function/method names you would like this rule to ignore, like so: + +```cjson +{ + "@typescript-eslint/explicit-module-boundary-types": ["error", { "allowedName": ["ignoredFunctionName", "ignoredMethodName"] }] +} +``` + +## 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) + +## Further Reading + +- TypeScript [Functions](https://www.typescriptlang.org/docs/handbook/functions.html#function-types) diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index 3ae4cb1d962..43dbe291f87 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -14,6 +14,7 @@ "@typescript-eslint/default-param-last": "error", "@typescript-eslint/explicit-function-return-type": "error", "@typescript-eslint/explicit-member-accessibility": "error", + "@typescript-eslint/explicit-module-boundary-types": "error", "func-call-spacing": "off", "@typescript-eslint/func-call-spacing": "error", "indent": "off", @@ -53,7 +54,6 @@ "@typescript-eslint/no-unnecessary-qualifier": "error", "@typescript-eslint/no-unnecessary-type-arguments": "error", "@typescript-eslint/no-unnecessary-type-assertion": "error", - "@typescript-eslint/no-untyped-public-signature": "error", "no-unused-expressions": "off", "@typescript-eslint/no-unused-expressions": "error", "no-unused-vars": "off", diff --git a/packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts b/packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts new file mode 100644 index 00000000000..eeb9d6e39ae --- /dev/null +++ b/packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts @@ -0,0 +1,464 @@ +import { + TSESTree, + AST_NODE_TYPES, + AST_TOKEN_TYPES, +} from '@typescript-eslint/experimental-utils'; +import * as util from '../util'; + +type Options = [ + { + allowTypedFunctionExpressions?: boolean; + allowHigherOrderFunctions?: boolean; + allowDirectConstAssertionInArrowFunctions?: boolean; + allowedNames?: string[]; + }, +]; +type MessageIds = 'missingReturnType' | 'missingArgType'; + +export default util.createRule({ + name: 'explicit-module-boundary-types', + meta: { + type: 'problem', + docs: { + description: + "Require explicit return and argument types on exported functions' and classes' public class methods", + category: 'Stylistic Issues', + recommended: false, + }, + messages: { + missingReturnType: 'Missing return type on function.', + missingArgType: "Argument '{{name}}' should be typed.", + }, + schema: [ + { + type: 'object', + properties: { + allowTypedFunctionExpressions: { + type: 'boolean', + }, + allowHigherOrderFunctions: { + type: 'boolean', + }, + allowDirectConstAssertionInArrowFunctions: { + type: 'boolean', + }, + allowedNames: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + additionalProperties: false, + }, + ], + }, + defaultOptions: [ + { + allowTypedFunctionExpressions: true, + allowHigherOrderFunctions: true, + allowDirectConstAssertionInArrowFunctions: true, + allowedNames: [], + }, + ], + create(context, [options]) { + const sourceCode = context.getSourceCode(); + + /** + * Returns start column position + * @param node + */ + function getLocStart( + node: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression, + ): TSESTree.LineAndColumnData { + /* highlight method name */ + const parent = node.parent; + if ( + parent && + (parent.type === AST_NODE_TYPES.MethodDefinition || + (parent.type === AST_NODE_TYPES.Property && parent.method)) + ) { + return parent.loc.start; + } + + return node.loc.start; + } + + /** + * Returns end column position + * @param node + */ + function getLocEnd( + node: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression, + ): TSESTree.LineAndColumnData { + /* highlight `=>` */ + if (node.type === AST_NODE_TYPES.ArrowFunctionExpression) { + return sourceCode.getTokenBefore( + node.body, + token => + token.type === AST_TOKEN_TYPES.Punctuator && token.value === '=>', + )!.loc.end; + } + + return sourceCode.getTokenBefore(node.body!)!.loc.end; + } + + /** + * Checks if a node is a constructor. + * @param node The node to check + */ + function isConstructor(node: TSESTree.Node | undefined): boolean { + return ( + !!node && + node.type === AST_NODE_TYPES.MethodDefinition && + node.kind === 'constructor' + ); + } + + /** + * Checks if a node is a setter. + */ + function isSetter(node: TSESTree.Node | undefined): boolean { + return ( + !!node && + (node.type === AST_NODE_TYPES.MethodDefinition || + node.type === AST_NODE_TYPES.Property) && + node.kind === 'set' + ); + } + + /** + * Checks if a node is a variable declarator with a type annotation. + * `const x: Foo = ...` + */ + function isVariableDeclaratorWithTypeAnnotation( + node: TSESTree.Node, + ): boolean { + return ( + node.type === AST_NODE_TYPES.VariableDeclarator && + !!node.id.typeAnnotation + ); + } + + /** + * Checks if a node is a class property with a type annotation. + * `public x: Foo = ...` + */ + function isClassPropertyWithTypeAnnotation(node: TSESTree.Node): boolean { + return ( + node.type === AST_NODE_TYPES.ClassProperty && !!node.typeAnnotation + ); + } + + /** + * Checks if a node belongs to: + * new Foo(() => {}) + * ^^^^^^^^ + */ + function isConstructorArgument(parent: TSESTree.Node): boolean { + return parent.type === AST_NODE_TYPES.NewExpression; + } + + /** + * Checks if a node is a type cast + * `(() => {}) as Foo` + * `(() => {})` + */ + function isTypeCast(node: TSESTree.Node): boolean { + return ( + node.type === AST_NODE_TYPES.TSAsExpression || + node.type === AST_NODE_TYPES.TSTypeAssertion + ); + } + + /** + * Checks if a node belongs to: + * `const x: Foo = { prop: () => {} }` + * `const x = { prop: () => {} } as Foo` + * `const x = { prop: () => {} }` + */ + function isPropertyOfObjectWithType( + property: TSESTree.Node | undefined, + ): boolean { + if (!property || property.type !== AST_NODE_TYPES.Property) { + return false; + } + const objectExpr = property.parent; // this shouldn't happen, checking just in case + /* istanbul ignore if */ if ( + !objectExpr || + objectExpr.type !== AST_NODE_TYPES.ObjectExpression + ) { + return false; + } + + const parent = objectExpr.parent; // this shouldn't happen, checking just in case + /* istanbul ignore if */ if (!parent) { + return false; + } + + return ( + isTypeCast(parent) || + isClassPropertyWithTypeAnnotation(parent) || + isVariableDeclaratorWithTypeAnnotation(parent) || + isFunctionArgument(parent) + ); + } + + function isUnexported(node: TSESTree.Node | undefined): boolean { + while (node) { + if ( + node.type === AST_NODE_TYPES.ExportDefaultDeclaration || + node.type === AST_NODE_TYPES.ExportNamedDeclaration || + node.type === AST_NODE_TYPES.ExportSpecifier + ) { + return false; + } + + node = node.parent; + } + + return true; + } + + function isPrivateMethod( + node: TSESTree.MethodDefinition | TSESTree.TSAbstractMethodDefinition, + ): boolean { + return node.accessibility === 'private'; + } + + /** + * Checks if a function belongs to: + * `() => () => ...` + * `() => function () { ... }` + * `() => { return () => ... }` + * `() => { return function () { ... } }` + * `function fn() { return () => ... }` + * `function fn() { return function() { ... } }` + */ + function doesImmediatelyReturnFunctionExpression({ + body, + }: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression): boolean { + // Should always have a body; really checking just in case + /* istanbul ignore if */ if (!body) { + return false; + } + + // Check if body is a block with a single statement + if ( + body.type === AST_NODE_TYPES.BlockStatement && + body.body.length === 1 + ) { + const [statement] = body.body; + + // Check if that statement is a return statement with an argument + if ( + statement.type === AST_NODE_TYPES.ReturnStatement && + !!statement.argument + ) { + // If so, check that returned argument as body + body = statement.argument; + } + } + + // Check if the body being returned is a function expression + return ( + body.type === AST_NODE_TYPES.ArrowFunctionExpression || + body.type === AST_NODE_TYPES.FunctionExpression + ); + } + + /** + * Checks if a node belongs to: + * `foo(() => 1)` + */ + function isFunctionArgument( + parent: TSESTree.Node, + callee?: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression, + ): boolean { + return ( + (parent.type === AST_NODE_TYPES.CallExpression || + parent.type === AST_NODE_TYPES.OptionalCallExpression) && + // make sure this isn't an IIFE + parent.callee !== callee + ); + } + + /** + * Checks if a function belongs to: + * `() => ({ action: 'xxx' }) as const` + */ + function returnsConstAssertionDirectly( + node: TSESTree.ArrowFunctionExpression, + ): boolean { + const { body } = node; + if (body.type === AST_NODE_TYPES.TSAsExpression) { + const { typeAnnotation } = body; + if (typeAnnotation.type === AST_NODE_TYPES.TSTypeReference) { + const { typeName } = typeAnnotation; + if ( + typeName.type === AST_NODE_TYPES.Identifier && + typeName.name === 'const' + ) { + return true; + } + } + } + + return false; + } + + /** + * Checks if a function declaration/expression has a return type. + */ + function checkFunctionReturnType( + node: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression, + ): void { + const paramIdentifiers = node.params.filter( + param => param.type === AST_NODE_TYPES.Identifier, + ) as TSESTree.Identifier[]; + const untypedArgs = paramIdentifiers.filter(isArgumentUntyped); + if (untypedArgs.length) { + untypedArgs.forEach(untypedArg => + context.report({ + node, + messageId: 'missingArgType', + data: { + name: untypedArg.name, + }, + }), + ); + } + + if (isAllowedName(node.parent)) { + return; + } + + if (isUnexported(node.parent)) { + return; + } + + if ( + options.allowHigherOrderFunctions && + doesImmediatelyReturnFunctionExpression(node) + ) { + return; + } + + if ( + node.returnType || + isConstructor(node.parent) || + isSetter(node.parent) + ) { + return; + } + + context.report({ + node, + loc: { start: getLocStart(node), end: getLocEnd(node) }, + messageId: 'missingReturnType', + }); + } + + /** + * Checks if a function declaration/expression has a return type. + */ + function checkFunctionExpressionReturnType( + node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression, + ): void { + // Should always have a parent; checking just in case + /* istanbul ignore else */ if (node.parent) { + if ( + node.parent.type === AST_NODE_TYPES.MethodDefinition && + isPrivateMethod(node.parent) + ) { + return; + } + + if (options.allowTypedFunctionExpressions) { + if ( + isTypeCast(node.parent) || + isVariableDeclaratorWithTypeAnnotation(node.parent) || + isClassPropertyWithTypeAnnotation(node.parent) || + isPropertyOfObjectWithType(node.parent) || + isFunctionArgument(node.parent, node) || + isConstructorArgument(node.parent) + ) { + return; + } + } + } + + if ( + node.type === AST_NODE_TYPES.ArrowFunctionExpression && + options.allowDirectConstAssertionInArrowFunctions && + returnsConstAssertionDirectly(node) + ) { + return; + } + + checkFunctionReturnType(node); + } + + /** + * Checks if a function name is allowed and should not be checked. + */ + function isAllowedName(node: TSESTree.Node | undefined): boolean { + if (!node || !options.allowedNames || !options.allowedNames.length) { + return false; + } + + if (node.type === AST_NODE_TYPES.VariableDeclarator) { + return ( + node.id.type === AST_NODE_TYPES.Identifier && + options.allowedNames.includes(node.id.name) + ); + } else if ( + node.type === AST_NODE_TYPES.MethodDefinition || + node.type === AST_NODE_TYPES.TSAbstractMethodDefinition + ) { + if ( + node.key.type === AST_NODE_TYPES.Literal && + typeof node.key.value === 'string' + ) { + return options.allowedNames.includes(node.key.value); + } + if ( + node.key.type === AST_NODE_TYPES.TemplateLiteral && + node.key.expressions.length === 0 + ) { + return options.allowedNames.includes(node.key.quasis[0].value.raw); + } + if (!node.computed && node.key.type === AST_NODE_TYPES.Identifier) { + return options.allowedNames.includes(node.key.name); + } + } + + return false; + } + + function isArgumentUntyped(node: TSESTree.Identifier): boolean { + return ( + !node.typeAnnotation || + node.typeAnnotation.typeAnnotation.type === AST_NODE_TYPES.TSAnyKeyword + ); + } + + return { + ArrowFunctionExpression: checkFunctionExpressionReturnType, + FunctionDeclaration: checkFunctionReturnType, + FunctionExpression: checkFunctionExpressionReturnType, + }; + }, +}); diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index de4aa662ea3..1caa90bb46e 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -11,6 +11,7 @@ import consistentTypeDefinitions from './consistent-type-definitions'; import defaultParamLast from './default-param-last'; import explicitFunctionReturnType from './explicit-function-return-type'; import explicitMemberAccessibility from './explicit-member-accessibility'; +import explicitModuleBoundaryTypes from './explicit-module-boundary-types'; import funcCallSpacing from './func-call-spacing'; import genericTypeNaming from './generic-type-naming'; import indent from './indent'; @@ -92,6 +93,7 @@ export default { 'default-param-last': defaultParamLast, 'explicit-function-return-type': explicitFunctionReturnType, 'explicit-member-accessibility': explicitMemberAccessibility, + 'explicit-module-boundary-types': explicitModuleBoundaryTypes, 'func-call-spacing': funcCallSpacing, 'generic-type-naming': genericTypeNaming, indent: indent, diff --git a/packages/eslint-plugin/src/rules/no-untyped-public-signature.ts b/packages/eslint-plugin/src/rules/no-untyped-public-signature.ts index 830a245760a..303c1336b63 100644 --- a/packages/eslint-plugin/src/rules/no-untyped-public-signature.ts +++ b/packages/eslint-plugin/src/rules/no-untyped-public-signature.ts @@ -11,6 +11,8 @@ type Options = [{ ignoredMethods: string[] }]; export default util.createRule({ name: 'no-untyped-public-signature', meta: { + deprecated: true, + replacedBy: ['explicit-module-boundary-types'], docs: { description: 'Disallow untyped public methods', category: 'Best Practices', 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 new file mode 100644 index 00000000000..f67218588d7 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/explicit-module-boundary-types.test.ts @@ -0,0 +1,802 @@ +import rule from '../../src/rules/explicit-module-boundary-types'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('explicit-module-boundary-types', rule, { + valid: [ + { + filename: 'test.ts', + code: ` +function test(): void { + return; +} + `, + }, + { + filename: 'test.ts', + code: ` +export function test(): void { + return; +} + `, + }, + { + filename: 'test.ts', + code: ` +export var fn = function(): number { + return 1; +}; + `, + }, + { + filename: 'test.ts', + code: ` +export var arrowFn = (): string => 'test'; + `, + }, + { + filename: 'test.ts', + code: ` +class Test { + constructor() {} + get prop() { + return 1; + } + set prop() {} + method() { + return; + } + arrow = (): string => 'arrow'; +} + `, + }, + { + filename: 'test.ts', + code: ` +export class Test { + constructor() {} + get prop(): number { + return 1; + } + set prop() {} + private method(one) { + return; + } + arrow = (): string => 'arrow'; +} + `, + }, + { + filename: 'test.ts', + code: ` +export var arrowFn: Foo = () => 'test'; + `, + options: [ + { + allowTypedFunctionExpressions: true, + }, + ], + }, + { + filename: 'test.ts', + code: ` +export var funcExpr: Foo = function() { return 'test'; }; + `, + options: [ + { + allowTypedFunctionExpressions: true, + }, + ], + }, + { + filename: 'test.ts', + code: `const x = (() => {}) as Foo`, + options: [{ allowTypedFunctionExpressions: true }], + }, + { + filename: 'test.ts', + code: `const x = (() => {})`, + options: [{ allowTypedFunctionExpressions: true }], + }, + { + filename: 'test.ts', + code: ` +export const x = { + foo: () => {}, +} as Foo + `, + options: [{ allowTypedFunctionExpressions: true }], + }, + { + filename: 'test.ts', + code: ` +export const x = { + foo: () => {}, +} + `, + options: [{ allowTypedFunctionExpressions: true }], + }, + { + filename: 'test.ts', + code: ` +export const x: Foo = { + foo: () => {}, +} + `, + options: [{ allowTypedFunctionExpressions: true }], + }, + // https://github.com/typescript-eslint/typescript-eslint/issues/484 + { + filename: 'test.ts', + code: ` +type MethodType = () => void; + +export class App { + public method: MethodType = () => {} +} + `, + options: [{ allowTypedFunctionExpressions: true }], + }, + // https://github.com/typescript-eslint/typescript-eslint/issues/525 + { + filename: 'test.ts', + code: ` +export const myObj = { + set myProp(val: number) { + this.myProp = val; + }, +}; + `, + }, + { + filename: 'test.ts', + code: ` +export default () => (): void => {}; + `, + options: [{ allowHigherOrderFunctions: true }], + }, + { + filename: 'test.ts', + code: ` +export default () => function (): void {}; + `, + options: [{ allowHigherOrderFunctions: true }], + }, + { + filename: 'test.ts', + code: ` +export default () => { return (): void => {} }; + `, + options: [{ allowHigherOrderFunctions: true }], + }, + { + filename: 'test.ts', + code: ` +export default () => { return function (): void {} }; + `, + options: [{ allowHigherOrderFunctions: true }], + }, + { + filename: 'test.ts', + code: ` +export function fn() { return (): void => {} }; + `, + options: [{ allowHigherOrderFunctions: true }], + }, + { + filename: 'test.ts', + code: ` +export function fn() { return function (): void {} }; + `, + options: [{ allowHigherOrderFunctions: true }], + }, + { + filename: 'test.ts', + code: ` +export function FunctionDeclaration() { + return function FunctionExpression_Within_FunctionDeclaration() { + return function FunctionExpression_Within_FunctionExpression() { + return () => { // ArrowFunctionExpression_Within_FunctionExpression + return () => // ArrowFunctionExpression_Within_ArrowFunctionExpression + (): number => 1 // ArrowFunctionExpression_Within_ArrowFunctionExpression_WithNoBody + } + } + } +} + `, + options: [{ allowHigherOrderFunctions: true }], + }, + { + filename: 'test.ts', + code: ` +export default () => () => { return (): void => { return; } }; + `, + options: [{ allowHigherOrderFunctions: true }], + }, + { + filename: 'test.ts', + code: ` +export class Accumulator { + private count: number = 0; + + public accumulate(fn: () => number): void { + this.count += fn(); + } +} + +new Accumulator().accumulate(() => 1); + `, + options: [ + { + allowTypedFunctionExpressions: true, + }, + ], + }, + { + filename: 'test.ts', + code: ` +export const func1 = (value: number) => (({ type: "X", value }) as const); +export const func2 = (value: number) => ({ type: "X", value } as const); +export const func3 = (value: number) => (x as const); +export const func4 = (value: number) => x as const; + `, + options: [ + { + allowDirectConstAssertionInArrowFunctions: true, + }, + ], + }, + { + filename: 'test.ts', + code: ` +export const func1 = (value: string) => value; +export const func2 = (value: number) => ({ type: "X", value }); + `, + options: [ + { + allowedNames: ['func1', 'func2'], + }, + ], + }, + { + filename: 'test.ts', + code: ` +export class Test { + constructor() {} + get prop() { + return 1; + } + set prop() {} + method() { + return; + } + arrow = (): string => 'arrow'; +} + `, + options: [ + { + allowedNames: ['prop', 'method'], + }, + ], + }, + ], + invalid: [ + { + filename: 'test.ts', + code: ` +export function test( + a: number, + b: number, +) { + return; +} + `, + errors: [ + { + messageId: 'missingReturnType', + line: 2, + endLine: 5, + column: 8, + endColumn: 2, + }, + ], + }, + { + filename: 'test.ts', + code: ` +export function test() { + return; +} + `, + errors: [ + { + messageId: 'missingReturnType', + line: 2, + endLine: 2, + column: 8, + endColumn: 23, + }, + ], + }, + { + filename: 'test.ts', + code: ` +export var fn = function() { + return 1; +}; + `, + errors: [ + { + messageId: 'missingReturnType', + line: 2, + endLine: 2, + column: 17, + endColumn: 27, + }, + ], + }, + { + filename: 'test.ts', + code: ` +export var arrowFn = () => 'test'; + `, + errors: [ + { + messageId: 'missingReturnType', + line: 2, + endLine: 2, + column: 22, + endColumn: 27, + }, + ], + }, + { + filename: 'test.ts', + code: ` +export class Test { + constructor() {} + get prop() { + return 1; + } + set prop() {} + method() { + return; + } + arrow = (arg) => 'arrow'; + private method() { + return; + } +} + `, + errors: [ + { + messageId: 'missingReturnType', + line: 4, + endLine: 4, + column: 3, + endColumn: 13, + }, + { + messageId: 'missingReturnType', + line: 8, + endLine: 8, + column: 3, + endColumn: 11, + }, + { + messageId: 'missingArgType', + line: 11, + endLine: 11, + column: 11, + endColumn: 27, + }, + { + messageId: 'missingReturnType', + line: 11, + endLine: 11, + column: 11, + endColumn: 19, + }, + ], + }, + { + filename: 'test.ts', + code: ` +export class Foo { + public a = () => {}; + public b = function () {}; + public c = function test() {}; + + static d = () => {}; + static e = function () {}; +} + `, + errors: [ + { + messageId: 'missingReturnType', + line: 3, + endLine: 3, + column: 14, + endColumn: 19, + }, + { + messageId: 'missingReturnType', + line: 4, + endLine: 4, + column: 14, + endColumn: 25, + }, + { + messageId: 'missingReturnType', + line: 5, + endLine: 5, + column: 14, + endColumn: 29, + }, + { + messageId: 'missingReturnType', + line: 7, + endLine: 7, + column: 14, + endColumn: 19, + }, + { + messageId: 'missingReturnType', + line: 8, + endLine: 8, + column: 14, + endColumn: 25, + }, + ], + }, + { + filename: 'test.ts', + code: "export var arrowFn = () => 'test';", + options: [{ allowTypedFunctionExpressions: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 1, + endLine: 1, + column: 22, + endColumn: 27, + }, + ], + }, + { + filename: 'test.ts', + code: "export var funcExpr = function() { return 'test'; };", + options: [{ allowTypedFunctionExpressions: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 1, + endLine: 1, + column: 23, + endColumn: 33, + }, + ], + }, + { + filename: 'test.ts', + code: 'export const x = (() => {}) as Foo', + options: [{ allowTypedFunctionExpressions: false }], + errors: [ + { + messageId: 'missingReturnType', + line: 1, + endLine: 1, + column: 19, + endColumn: 24, + }, + ], + }, + { + filename: 'test.ts', + code: ` +interface Foo {} +export const x = { + foo: () => {}, +} as Foo + `, + options: [{ allowTypedFunctionExpressions: false }], + errors: [ + { + messageId: 'missingReturnType', + line: 4, + endLine: 4, + column: 8, + endColumn: 13, + }, + ], + }, + { + filename: 'test.ts', + code: ` +interface Foo {} +export const x: Foo = { + foo: () => {}, +} + `, + options: [{ allowTypedFunctionExpressions: false }], + errors: [ + { + messageId: 'missingReturnType', + line: 4, + endLine: 4, + column: 8, + endColumn: 13, + }, + ], + }, + { + filename: 'test.ts', + code: 'export default () => () => {};', + options: [{ allowHigherOrderFunctions: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 1, + endLine: 1, + column: 22, + endColumn: 27, + }, + ], + }, + { + filename: 'test.ts', + code: 'export default () => function () {};', + options: [{ allowHigherOrderFunctions: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 1, + endLine: 1, + column: 22, + endColumn: 33, + }, + ], + }, + { + filename: 'test.ts', + code: 'export default () => { return () => {} };', + options: [{ allowHigherOrderFunctions: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 1, + endLine: 1, + column: 31, + endColumn: 36, + }, + ], + }, + { + filename: 'test.ts', + code: 'export default () => { return function () {} };', + options: [{ allowHigherOrderFunctions: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 1, + endLine: 1, + column: 31, + endColumn: 42, + }, + ], + }, + { + filename: 'test.ts', + code: 'export function fn() { return () => {} };', + options: [{ allowHigherOrderFunctions: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 1, + endLine: 1, + column: 31, + endColumn: 36, + }, + ], + }, + { + filename: 'test.ts', + code: 'export function fn() { return function () {} };', + options: [{ allowHigherOrderFunctions: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 1, + endLine: 1, + column: 31, + endColumn: 42, + }, + ], + }, + { + filename: 'test.ts', + code: ` +export function FunctionDeclaration() { + return function FunctionExpression_Within_FunctionDeclaration() { + return function FunctionExpression_Within_FunctionExpression() { + return () => { // ArrowFunctionExpression_Within_FunctionExpression + return () => // ArrowFunctionExpression_Within_ArrowFunctionExpression + () => 1 // ArrowFunctionExpression_Within_ArrowFunctionExpression_WithNoBody + } + } + } +} + `, + options: [{ allowHigherOrderFunctions: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 7, + endLine: 7, + column: 11, + endColumn: 16, + }, + ], + }, + { + filename: 'test.ts', + code: 'export default () => () => { return () => { return; } };', + options: [{ allowHigherOrderFunctions: true }], + errors: [ + { + messageId: 'missingReturnType', + line: 1, + endLine: 1, + column: 37, + endColumn: 42, + }, + ], + }, + { + filename: 'test.ts', + code: 'export default (() => true)()', + options: [ + { + allowTypedFunctionExpressions: false, + }, + ], + errors: [ + { + messageId: 'missingReturnType', + line: 1, + endLine: 1, + column: 17, + endColumn: 22, + }, + ], + }, + { + filename: 'test.ts', + code: ` +export const func1 = (value: number) => ({ type: "X", value } as any); +export const func2 = (value: number) => ({ type: "X", value } as Action); + `, + options: [ + { + allowDirectConstAssertionInArrowFunctions: true, + }, + ], + errors: [ + { + messageId: 'missingReturnType', + line: 2, + endLine: 2, + column: 22, + endColumn: 40, + }, + { + messageId: 'missingReturnType', + line: 3, + endLine: 3, + column: 22, + endColumn: 40, + }, + ], + }, + { + filename: 'test.ts', + code: ` +export const func = (value: number) => ({ type: "X", value } as const); + `, + options: [ + { + allowDirectConstAssertionInArrowFunctions: false, + }, + ], + errors: [ + { + messageId: 'missingReturnType', + line: 2, + endLine: 2, + column: 21, + endColumn: 39, + }, + ], + }, + { + filename: 'test.ts', + code: ` +export class Test { + constructor() {} + get prop() { + return 1; + } + set prop() {} + method() { + return; + } + arrow = (): string => 'arrow'; +} + `, + options: [ + { + allowedNames: ['prop'], + }, + ], + errors: [ + { + messageId: 'missingReturnType', + line: 8, + endLine: 8, + column: 3, + endColumn: 11, + }, + ], + }, + { + filename: 'test.ts', + code: ` +export const func1 = (value: number) => value; +export const func2 = (value: number) => value; + `, + options: [ + { + allowedNames: ['func2'], + }, + ], + errors: [ + { + messageId: 'missingReturnType', + line: 2, + endLine: 2, + column: 22, + endColumn: 40, + }, + ], + }, + { + filename: 'test.ts', + code: 'export function fn(test): string { return "123" };', + errors: [ + { + messageId: 'missingArgType', + line: 1, + endLine: 1, + column: 8, + endColumn: 50, + }, + ], + }, + { + filename: 'test.ts', + code: 'export const fn = (one: number, two): string => "123";', + errors: [ + { + messageId: 'missingArgType', + line: 1, + endLine: 1, + column: 19, + endColumn: 54, + }, + ], + }, + ], +});