diff --git a/packages/eslint-plugin/docs/rules/no-empty-function.md b/packages/eslint-plugin/docs/rules/no-empty-function.md index e06961d4259..2b37c4da396 100644 --- a/packages/eslint-plugin/docs/rules/no-empty-function.md +++ b/packages/eslint-plugin/docs/rules/no-empty-function.md @@ -44,4 +44,37 @@ See the [ESLint documentation](https://eslint.org/docs/rules/no-empty-function) } ``` +## Options + +This rule has an object option: + +- `allow` (`string[]`) + - `"protected-constructors"` - Protected class constructors. + - `"private-constructors"` - Private class constructors. + - [See the other options allowed](https://github.com/eslint/eslint/blob/master/docs/rules/no-empty-function.md#options) + +#### allow: protected-constructors + +Examples of **correct** code for the `{ "allow": ["protected-constructors"] }` option: + +```ts +/*eslint @typescript-eslint/no-empty-function: ["error", { "allow": ["protected-constructors"] }]*/ + +class Foo { + protected constructor() {} +} +``` + +#### allow: private-constructors + +Examples of **correct** code for the `{ "allow": ["private-constructors"] }` option: + +```ts +/*eslint @typescript-eslint/no-empty-function: ["error", { "allow": ["private-constructors"] }]*/ + +class Foo { + private constructor() {} +} +``` + Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/no-empty-function.md) diff --git a/packages/eslint-plugin/src/rules/no-empty-function.ts b/packages/eslint-plugin/src/rules/no-empty-function.ts index 31aa5d8bb63..b0c88342a49 100644 --- a/packages/eslint-plugin/src/rules/no-empty-function.ts +++ b/packages/eslint-plugin/src/rules/no-empty-function.ts @@ -8,6 +8,32 @@ import * as util from '../util'; type Options = util.InferOptionsTypeFromRule; type MessageIds = util.InferMessageIdsTypeFromRule; +const schema = util.deepMerge( + Array.isArray(baseRule.meta.schema) + ? baseRule.meta.schema[0] + : baseRule.meta.schema, + { + properties: { + allow: { + items: { + enum: [ + 'functions', + 'arrowFunctions', + 'generatorFunctions', + 'methods', + 'generatorMethods', + 'getters', + 'setters', + 'constructors', + 'private-constructors', + 'protected-constructors', + ], + }, + }, + }, + }, +); + export default util.createRule({ name: 'no-empty-function', meta: { @@ -17,7 +43,7 @@ export default util.createRule({ category: 'Best Practices', recommended: 'error', }, - schema: baseRule.meta.schema, + schema: [schema], messages: baseRule.meta.messages, }, defaultOptions: [ @@ -25,24 +51,13 @@ export default util.createRule({ allow: [], }, ], - create(context) { + create(context, [{ allow = [] }]) { const rules = baseRule.create(context); - /** - * Checks if the node is a constructor - * @param node the node to ve validated - * @returns true if the node is a constructor - * @private - */ - function isConstructor( - node: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression, - ): boolean { - return !!( - node.parent && - node.parent.type === 'MethodDefinition' && - node.parent.kind === 'constructor' - ); - } + const isAllowedProtectedConstructors = allow.includes( + 'protected-constructors', + ); + const isAllowedPrivateConstructors = allow.includes('private-constructors'); /** * Check if the method body is empty @@ -74,30 +89,44 @@ export default util.createRule({ } /** - * Checks if the method is a concise constructor (no function body, but has parameter properties) * @param node the node to be validated - * @returns true if the method is a concise constructor + * @returns true if the constructor is allowed to be empty * @private */ - function isConciseConstructor( + function isAllowedEmptyConstructor( node: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression, ): boolean { - // Check TypeScript specific nodes - return ( - isConstructor(node) && isBodyEmpty(node) && hasParameterProperties(node) - ); + const parent = node.parent; + if ( + isBodyEmpty(node) && + parent?.type === 'MethodDefinition' && + parent.kind === 'constructor' + ) { + const { accessibility } = parent; + + return ( + // allow protected constructors + (accessibility === 'protected' && isAllowedProtectedConstructors) || + // allow private constructors + (accessibility === 'private' && isAllowedPrivateConstructors) || + // allow constructors which have parameter properties + hasParameterProperties(node) + ); + } + + return false; } return { FunctionDeclaration(node): void { - if (!isConciseConstructor(node)) { - rules.FunctionDeclaration(node); - } + rules.FunctionDeclaration(node); }, FunctionExpression(node): void { - if (!isConciseConstructor(node)) { - rules.FunctionExpression(node); + if (isAllowedEmptyConstructor(node)) { + return; } + + rules.FunctionExpression(node); }, }; }, diff --git a/packages/eslint-plugin/tests/rules/no-empty-function.test.ts b/packages/eslint-plugin/tests/rules/no-empty-function.test.ts index 4b40c170867..9b5ad3507d3 100644 --- a/packages/eslint-plugin/tests/rules/no-empty-function.test.ts +++ b/packages/eslint-plugin/tests/rules/no-empty-function.test.ts @@ -32,6 +32,29 @@ ruleTester.run('no-empty-function', rule, { }`, options: [{ allow: ['methods'] }], }, + { + code: ` +class Foo { + private constructor() {} +} + `, + options: [{ allow: ['private-constructors'] }], + }, + { + code: ` +class Foo { + protected constructor() {} +} + `, + options: [{ allow: ['protected-constructors'] }], + }, + { + code: ` +function foo() { + const a = null; +} + `, + }, ], invalid: [ @@ -65,5 +88,55 @@ ruleTester.run('no-empty-function', rule, { }, ], }, + { + code: ` +class Foo { + private constructor() {} +} + `, + errors: [ + { + messageId: 'unexpected', + data: { + name: 'constructor', + }, + line: 3, + column: 25, + }, + ], + }, + { + code: ` +class Foo { + protected constructor() {} +} + `, + errors: [ + { + messageId: 'unexpected', + data: { + name: 'constructor', + }, + line: 3, + column: 27, + }, + ], + }, + { + code: ` +function foo() { +} + `, + errors: [ + { + messageId: 'unexpected', + data: { + name: "function 'foo'", + }, + line: 2, + column: 16, + }, + ], + }, ], });