diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index dd269dc2484..31a96173d90 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -187,6 +187,7 @@ In these cases, we create what we call an extension rule; a rule within our plug | [`@typescript-eslint/indent`](./docs/rules/indent.md) | Enforce consistent indentation | | :wrench: | | | [`@typescript-eslint/init-declarations`](./docs/rules/init-declarations.md) | require or disallow initialization in variable declarations | | | | | [`@typescript-eslint/keyword-spacing`](./docs/rules/keyword-spacing.md) | Enforce consistent spacing before and after keywords | | :wrench: | | +| [`@typescript-eslint/lines-between-class-members`](./docs/rules/lines-between-class-members.md) | Require or disallow an empty line between class members | | :wrench: | | | [`@typescript-eslint/no-array-constructor`](./docs/rules/no-array-constructor.md) | Disallow generic `Array` constructors | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/no-dupe-class-members`](./docs/rules/no-dupe-class-members.md) | Disallow duplicate class members | | | | | [`@typescript-eslint/no-empty-function`](./docs/rules/no-empty-function.md) | Disallow empty functions | :heavy_check_mark: | | | diff --git a/packages/eslint-plugin/docs/rules/lines-between-class-members.md b/packages/eslint-plugin/docs/rules/lines-between-class-members.md new file mode 100644 index 00000000000..06cdf55421e --- /dev/null +++ b/packages/eslint-plugin/docs/rules/lines-between-class-members.md @@ -0,0 +1,73 @@ +# Require or disallow an empty line between class members (`lines-between-class-members`) + +This rule improves readability by enforcing lines between class members. It will not check empty lines before the first member and after the last member. This rule require or disallow an empty line between class members. + +## Rule Details + +This rule extends the base [`eslint/lines-between-class-members`](https://eslint.org/docs/rules/lines-between-class-members) rule. +It adds support for ignoring overload methods in a class. + +See the [ESLint documentation](https://eslint.org/docs/rules/lines-between-class-members) for more details on the `lines-between-class-members` rule. + +## Rule Changes + +```cjson +{ + // note you must disable the base rule as it can report incorrect errors + "lines-between-class-members": "off", + "@typescript-eslint/lines-between-class-members": ["error"] +} +``` + +In addition to the options supported by the `lines-between-class-members` rule in ESLint core, the rule adds the following options: + +## Options + +This rule has a string option and an object option. + +- Object option: + + - `"exceptAfterOverload": true` (default) - Skip checking empty lines after overload class members + - `"exceptAfterOverload": false` - **do not** skip checking empty lines after overload class members + +- [See the other options allowed](https://github.com/eslint/eslint/blob/master/docs/rules/lines-between-class-members.md#options) + +### `exceptAfterOverload: true` + +Examples of **correct** code for the `{ "exceptAfterOverload": true }` option: + +```ts +/*eslint @typescript-eslint/lines-between-class-members: ["error", "always", { "exceptAfterOverload": true }]*/ + +class foo { + bar(a: string): void; + bar(a: string, b: string): void; + bar(a: string, b: string) {} + + baz() {} + + qux() {} +} +``` + +### `exceptAfterOverload: false` + +Examples of **correct** code for the `{ "exceptAfterOverload": false }` option: + +```ts +/*eslint @typescript-eslint/lines-between-class-members: ["error", "always", { "exceptAfterOverload": false }]*/ + +class foo { + bar(a: string): void; + + bar(a: string, b: string): void; + + bar(a: string, b: string) {} + + baz() {} + + qux() {} +} +``` + +Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/lines-between-class-members.md) diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index cf27dc57be5..3182e67de26 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -24,6 +24,8 @@ "@typescript-eslint/func-call-spacing": "error", "indent": "off", "@typescript-eslint/indent": "error", + "lines-between-class-members": "off", + "@typescript-eslint/lines-between-class-members": "error", "init-declarations": "off", "@typescript-eslint/init-declarations": "error", "keyword-spacing": "off", diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 9c1c26444d7..9a97a3c5bf0 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -100,6 +100,7 @@ import typeAnnotationSpacing from './type-annotation-spacing'; import typedef from './typedef'; import unboundMethod from './unbound-method'; import unifiedSignatures from './unified-signatures'; +import linesBetweenClassMembers from './lines-between-class-members'; export default { 'adjacent-overload-signatures': adjacentOverloadSignatures, @@ -204,4 +205,5 @@ export default { typedef: typedef, 'unbound-method': unboundMethod, 'unified-signatures': unifiedSignatures, + 'lines-between-class-members': linesBetweenClassMembers, }; diff --git a/packages/eslint-plugin/src/rules/lines-between-class-members.ts b/packages/eslint-plugin/src/rules/lines-between-class-members.ts new file mode 100644 index 00000000000..26b09d528f2 --- /dev/null +++ b/packages/eslint-plugin/src/rules/lines-between-class-members.ts @@ -0,0 +1,66 @@ +import { + AST_NODE_TYPES, + TSESTree, +} from '@typescript-eslint/experimental-utils'; +import baseRule from 'eslint/lib/rules/lines-between-class-members'; +import * as util from '../util'; + +type Options = util.InferOptionsTypeFromRule; +type MessageIds = util.InferMessageIdsTypeFromRule; + +const schema = util.deepMerge( + { ...baseRule.meta.schema }, + { + 1: { + exceptAfterOverload: { + type: 'booleean', + default: true, + }, + }, + }, +); + +export default util.createRule({ + name: 'lines-between-class-members', + meta: { + type: 'layout', + docs: { + description: 'Require or disallow an empty line between class members', + category: 'Stylistic Issues', + recommended: false, + extendsBaseRule: true, + }, + fixable: 'whitespace', + schema, + messages: baseRule.meta.messages, + }, + defaultOptions: [ + 'always', + { + exceptAfterOverload: true, + exceptAfterSingleLine: false, + }, + ], + create(context, options) { + const rules = baseRule.create(context); + const exceptAfterOverload = + options[1]?.exceptAfterOverload && options[0] === 'always'; + + function isOverload(node: TSESTree.Node): boolean { + return ( + node.type === AST_NODE_TYPES.MethodDefinition && + node.value.type === AST_NODE_TYPES.TSEmptyBodyFunctionExpression + ); + } + + return { + ClassBody(node): void { + const body = exceptAfterOverload + ? node.body.filter(node => !isOverload(node)) + : node.body; + + rules.ClassBody({ ...node, body }); + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/lines-between-class-members.test.ts b/packages/eslint-plugin/tests/rules/lines-between-class-members.test.ts new file mode 100644 index 00000000000..b25b0ca6b3c --- /dev/null +++ b/packages/eslint-plugin/tests/rules/lines-between-class-members.test.ts @@ -0,0 +1,328 @@ +/* eslint-disable eslint-comments/no-use */ +// this rule tests the new lines, which prettier will want to fix and break the tests +/* eslint "@typescript-eslint/internal/plugin-test-formatting": ["error", { formatWithPrettier: false }] */ +/* eslint-enable eslint-comments/no-use */ +import rule from '../../src/rules/lines-between-class-members'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('lines-between-class-members', rule, { + valid: [ + { + code: ` +class foo { +baz1() { } + +baz2() { } + +bar(a: string): void; +bar(a: string, b:string): void; +bar(a: string, b:string) { + +} + +qux1() { } + +qux2() { } +}; + `, + options: ['always'], + }, + { + code: ` +class foo { +bar(a: string): void; +bar(a: string, b:string): void; +bar(a: string, b:string) { + +} + +baz() { } + +qux() { } +}; + `, + options: ['always', { exceptAfterOverload: true }], + }, + { + code: ` +class foo { +bar(a: string): void; +bar(a: string, b:string): void; +bar(a: string, b:string) { + +} + +baz() { } +qux() { } +}; + `, + options: [ + 'always', + { exceptAfterOverload: true, exceptAfterSingleLine: true }, + ], + }, + { + code: ` +class foo{ +bar(a: string):void; + +bar(a: string, b:string):void; + +bar(a: string, b:string){ + +} + +baz() { } + +qux() { } +}; + `, + options: [ + 'always', + { exceptAfterOverload: false, exceptAfterSingleLine: false }, + ], + }, + { + code: ` +class foo { +bar(a: string):void +bar(a: string, b:string):void; +bar(a: string, b:string){ + +} +baz() { } +qux() { } +}; + `, + options: [ + 'never', + { exceptAfterOverload: true, exceptAfterSingleLine: true }, + ], + }, + { + code: ` +class foo{ +bar(a: string):void +bar(a: string, b:string):void; +bar(a: string, b:string){ + +} +baz() { } +qux() { } +}; + `, + options: [ + 'never', + { exceptAfterOverload: true, exceptAfterSingleLine: true }, + ], + }, + ], + invalid: [ + { + code: ` +class foo { +baz1() { } +baz2() { } + +bar(a: string): void; +bar(a: string, b:string): void; +bar(a: string, b:string) { + +} + +qux1() { } +qux2() { } +}; + `, + output: ` +class foo { +baz1() { } + +baz2() { } + +bar(a: string): void; +bar(a: string, b:string): void; +bar(a: string, b:string) { + +} + +qux1() { } + +qux2() { } +}; + `, + options: ['always'], + errors: [ + { + messageId: 'always', + }, + { + messageId: 'always', + }, + ], + }, + { + code: ` +class foo { +bar(a: string): void; +bar(a: string, b:string): void; +bar(a: string, b:string) { + +} +baz() { } +qux() { } +} + `, + output: ` +class foo { +bar(a: string): void; +bar(a: string, b:string): void; +bar(a: string, b:string) { + +} + +baz() { } + +qux() { } +} + `, + options: ['always', { exceptAfterOverload: true }], + errors: [ + { + messageId: 'always', + }, + { + messageId: 'always', + }, + ], + }, + { + code: ` +class foo { +bar(a: string): void; +bar(a: string, b:string): void; +bar(a: string, b:string) { + +} +baz() { } +qux() { } +} + `, + output: ` +class foo { +bar(a: string): void; +bar(a: string, b:string): void; +bar(a: string, b:string) { + +} + +baz() { } +qux() { } +} + `, + options: [ + 'always', + { exceptAfterOverload: true, exceptAfterSingleLine: true }, + ], + errors: [ + { + messageId: 'always', + }, + ], + }, + { + code: ` +class foo { +bar(a: string): void; +bar(a: string, b:string): void; +bar(a: string, b:string) { + +} + +baz() { } +qux() { } +} + `, + output: ` +class foo { +bar(a: string): void; + +bar(a: string, b:string): void; + +bar(a: string, b:string) { + +} + +baz() { } + +qux() { } +} + `, + options: [ + 'always', + { exceptAfterOverload: false, exceptAfterSingleLine: false }, + ], + errors: [ + { + messageId: 'always', + }, + { + messageId: 'always', + }, + { + messageId: 'always', + }, + ], + }, + { + code: ` +class foo{ +bar(a: string):void; + +bar(a: string, b:string):void; + +bar(a: string, b:string){ + +} + +baz() { } + +qux() { } +}; + `, + output: ` +class foo{ +bar(a: string):void; +bar(a: string, b:string):void; +bar(a: string, b:string){ + +} +baz() { } +qux() { } +}; + `, + options: [ + 'never', + { exceptAfterOverload: true, exceptAfterSingleLine: true }, + ], + errors: [ + { + messageId: 'never', + }, + { + messageId: 'never', + }, + { + messageId: 'never', + }, + { + messageId: 'never', + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index b4ec293e4f5..b32a895404d 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -623,6 +623,25 @@ declare module 'eslint/lib/rules/no-extra-semi' { export = rule; } +declare module 'eslint/lib/rules/lines-between-class-members' { + import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; + + const rule: TSESLint.RuleModule< + 'always' | 'never', + [ + 'always' | 'never', + { + exceptAfterSingleLine?: boolean; + exceptAfterOverload?: boolean; + }?, + ], + { + ClassBody(node: TSESTree.ClassBody): void; + } + >; + export = rule; +} + declare module 'eslint/lib/rules/init-declarations' { import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';