From 425f65ceafa2e760d13bd37f9672f40da803faa5 Mon Sep 17 00:00:00 2001 From: Alexander T Date: Thu, 19 Dec 2019 10:37:07 +0200 Subject: [PATCH] feat(eslint-plugin): add no-extra-semi [extension] (#1237) --- packages/eslint-plugin/README.md | 1 + .../eslint-plugin/docs/rules/no-extra-semi.md | 17 + packages/eslint-plugin/src/configs/all.json | 2 + packages/eslint-plugin/src/rules/index.ts | 2 + .../eslint-plugin/src/rules/no-extra-semi.ts | 31 ++ .../tests/rules/no-extra-semi.test.ts | 360 ++++++++++++++++++ .../eslint-plugin/tools/generate-configs.ts | 1 + .../eslint-plugin/typings/eslint-rules.d.ts | 15 + 8 files changed, 429 insertions(+) create mode 100644 packages/eslint-plugin/docs/rules/no-extra-semi.md create mode 100644 packages/eslint-plugin/src/rules/no-extra-semi.ts create mode 100644 packages/eslint-plugin/tests/rules/no-extra-semi.test.ts diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 012a95d23f9..0891413add3 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -121,6 +121,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/no-explicit-any`](./docs/rules/no-explicit-any.md) | Disallow usage of the `any` type | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/no-extra-non-null-assertion`](./docs/rules/no-extra-non-null-assertion.md) | Disallow extra non-null assertion | | | | | [`@typescript-eslint/no-extra-parens`](./docs/rules/no-extra-parens.md) | Disallow unnecessary parentheses | | :wrench: | | +| [`@typescript-eslint/no-extra-semi`](./docs/rules/no-extra-semi.md) | Disallow unnecessary semicolons | | :wrench: | | | [`@typescript-eslint/no-extraneous-class`](./docs/rules/no-extraneous-class.md) | Forbids the use of classes as namespaces | | | | | [`@typescript-eslint/no-floating-promises`](./docs/rules/no-floating-promises.md) | Requires Promise-like values to be handled appropriately. | | | :thought_balloon: | | [`@typescript-eslint/no-for-in-array`](./docs/rules/no-for-in-array.md) | Disallow iterating over an array with a for-in loop | :heavy_check_mark: | | :thought_balloon: | diff --git a/packages/eslint-plugin/docs/rules/no-extra-semi.md b/packages/eslint-plugin/docs/rules/no-extra-semi.md new file mode 100644 index 00000000000..bb4597a9318 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-extra-semi.md @@ -0,0 +1,17 @@ +# Disallow unnecessary semicolons + +## Rule Details + +This rule extends the base [`eslint/no-extra-semi`](https://eslint.org/docs/rules/no-extra-semi) rule. + +## How to use + +```cjson +{ + // note you must disable the base rule as it can report incorrect errors + "no-extra-semi": "off", + "@typescript-eslint/no-extra-semi": ["error"] +} +``` + +Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/no-extra-semi.md) diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index c3ee46bc95a..00a164e1dfc 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -34,6 +34,8 @@ "@typescript-eslint/no-extra-non-null-assertion": "error", "no-extra-parens": "off", "@typescript-eslint/no-extra-parens": "error", + "no-extra-semi": "off", + "@typescript-eslint/no-extra-semi": "error", "@typescript-eslint/no-extraneous-class": "error", "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-for-in-array": "error", diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index a4dc193e939..ee66fbc8a35 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -24,6 +24,7 @@ import noEmptyInterface from './no-empty-interface'; import noExplicitAny from './no-explicit-any'; import noExtraNonNullAssertion from './no-extra-non-null-assertion'; import noExtraParens from './no-extra-parens'; +import noExtraSemi from './no-extra-semi'; import noExtraneousClass from './no-extraneous-class'; import noFloatingPromises from './no-floating-promises'; import noForInArray from './no-for-in-array'; @@ -100,6 +101,7 @@ export default { 'no-explicit-any': noExplicitAny, 'no-extra-non-null-assertion': noExtraNonNullAssertion, 'no-extra-parens': noExtraParens, + 'no-extra-semi': noExtraSemi, 'no-extraneous-class': noExtraneousClass, 'no-floating-promises': noFloatingPromises, 'no-for-in-array': noForInArray, diff --git a/packages/eslint-plugin/src/rules/no-extra-semi.ts b/packages/eslint-plugin/src/rules/no-extra-semi.ts new file mode 100644 index 00000000000..db18236979e --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-extra-semi.ts @@ -0,0 +1,31 @@ +import baseRule from 'eslint/lib/rules/no-extra-semi'; +import * as util from '../util'; + +type Options = util.InferOptionsTypeFromRule; +type MessageIds = util.InferMessageIdsTypeFromRule; + +export default util.createRule({ + name: 'no-extra-semi', + meta: { + type: 'suggestion', + docs: { + description: 'Disallow unnecessary semicolons', + category: 'Possible Errors', + recommended: false, + }, + fixable: 'code', + schema: baseRule.meta.schema, + messages: baseRule.meta.messages, + }, + defaultOptions: [], + create(context) { + const rules = baseRule.create(context); + + return { + ...rules, + ClassProperty(node): void { + rules.MethodDefinition(node as never); + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/no-extra-semi.test.ts b/packages/eslint-plugin/tests/rules/no-extra-semi.test.ts new file mode 100644 index 00000000000..c1e07c18681 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-extra-semi.test.ts @@ -0,0 +1,360 @@ +import rule from '../../src/rules/no-extra-semi'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('no-extra-semi', rule, { + valid: [ + { + code: 'var x = 5;', + }, + { + code: 'function foo() {}', + }, + { + code: 'for(;;);', + }, + { + code: 'while(0);', + }, + { + code: 'do;while(0);', + }, + { + code: 'for(a in b);', + }, + { + code: 'for(a of b);', + parserOptions: { ecmaVersion: 6 }, + }, + { + code: 'if(true);', + }, + { + code: 'if(true); else;', + }, + { + code: 'foo: ;', + }, + { + code: 'with(foo);', + }, + + // Class body. + { + code: 'class A { }', + parserOptions: { ecmaVersion: 6 }, + }, + { + code: 'var A = class { };', + parserOptions: { ecmaVersion: 6 }, + }, + { + code: 'class A { a() { this; } }', + parserOptions: { ecmaVersion: 6 }, + }, + { + code: 'var A = class { a() { this; } };', + parserOptions: { ecmaVersion: 6 }, + }, + { + code: 'class A { } a;', + parserOptions: { ecmaVersion: 6 }, + }, + + // modules + { + code: 'export const x = 42;', + parserOptions: { + ecmaVersion: 6, + sourceType: 'module', + }, + }, + { + code: 'export default 42;', + parserOptions: { + ecmaVersion: 6, + sourceType: 'module', + }, + }, + + // Class Property + { + code: ` +export class Foo { + public foo: number = 0; +} + `, + }, + { + code: ` +export class Foo { + public foo: number = 0; public bar: number = 1; +} + `, + }, + ], + invalid: [ + { + code: 'var x = 5;;', + output: 'var x = 5;', + errors: [ + { + messageId: 'unexpected', + }, + ], + }, + { + code: 'function foo(){};', + output: 'function foo(){}', + errors: [ + { + messageId: 'unexpected', + }, + ], + }, + { + code: 'for(;;);;', + output: 'for(;;);', + errors: [ + { + messageId: 'unexpected', + }, + ], + }, + { + code: 'while(0);;', + output: 'while(0);', + errors: [ + { + messageId: 'unexpected', + }, + ], + }, + { + code: 'do;while(0);;', + output: 'do;while(0);', + errors: [ + { + messageId: 'unexpected', + }, + ], + }, + { + code: 'for(a in b);;', + output: 'for(a in b);', + errors: [ + { + messageId: 'unexpected', + }, + ], + }, + { + code: 'for(a of b);;', + output: 'for(a of b);', + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'unexpected', + }, + ], + }, + { + code: 'if(true);;', + output: 'if(true);', + errors: [ + { + messageId: 'unexpected', + }, + ], + }, + { + code: 'if(true){} else;;', + output: 'if(true){} else;', + errors: [ + { + messageId: 'unexpected', + }, + ], + }, + { + code: 'if(true){;} else {;}', + output: 'if(true){} else {}', + errors: [ + { + messageId: 'unexpected', + }, + { + messageId: 'unexpected', + }, + ], + }, + { + code: 'foo:;;', + output: 'foo:;', + errors: [ + { + messageId: 'unexpected', + }, + ], + }, + { + code: 'with(foo);;', + output: 'with(foo);', + errors: [ + { + messageId: 'unexpected', + }, + ], + }, + { + code: 'with(foo){;}', + output: 'with(foo){}', + errors: [ + { + messageId: 'unexpected', + }, + ], + }, + + // Class body. + { + code: 'class A { ; }', + output: 'class A { }', + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'unexpected', + column: 11, + }, + ], + }, + { + code: 'class A { /*a*/; }', + output: 'class A { /*a*/ }', + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'unexpected', + column: 16, + }, + ], + }, + { + code: 'class A { ; a() {} }', + output: 'class A { a() {} }', + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'unexpected', + column: 11, + }, + ], + }, + { + code: 'class A { a() {}; }', + output: 'class A { a() {} }', + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'unexpected', + column: 17, + }, + ], + }, + { + code: 'class A { a() {}; b() {} }', + output: 'class A { a() {} b() {} }', + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'unexpected', + column: 17, + }, + ], + }, + { + code: 'class A {; a() {}; b() {}; }', + output: 'class A { a() {} b() {} }', + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'unexpected', + column: 10, + }, + { + messageId: 'unexpected', + column: 18, + }, + { + messageId: 'unexpected', + column: 26, + }, + ], + }, + { + code: 'class A { a() {}; get b() {} }', + output: 'class A { a() {} get b() {} }', + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'unexpected', + column: 17, + }, + ], + }, + { + code: ` +class Foo { + public foo: number = 0;; +} + `, + output: ` +class Foo { + public foo: number = 0; +} + `, + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'unexpected', + column: 26, + }, + ], + }, + { + code: ` +class Foo { + public foo: number = 0;; public bar: number = 1;; + public baz: number = 1;; +} + `, + output: ` +class Foo { + public foo: number = 0; public bar: number = 1; + public baz: number = 1; +} + `, + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'unexpected', + line: 3, + column: 26, + }, + { + messageId: 'unexpected', + line: 3, + column: 51, + }, + { + messageId: 'unexpected', + line: 4, + column: 26, + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/tools/generate-configs.ts b/packages/eslint-plugin/tools/generate-configs.ts index ff6c5c63a0c..d6b814135fd 100644 --- a/packages/eslint-plugin/tools/generate-configs.ts +++ b/packages/eslint-plugin/tools/generate-configs.ts @@ -29,6 +29,7 @@ const BASE_RULES_TO_BE_OVERRIDDEN = new Set([ 'no-array-constructor', 'no-empty-function', 'no-extra-parens', + 'no-extra-semi', 'no-magic-numbers', 'quotes', 'no-unused-expressions', diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index 62f45419b18..f64ebe424a9 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -535,3 +535,18 @@ declare module 'eslint/lib/rules/brace-style' { >; export = rule; } + +declare module 'eslint/lib/rules/no-extra-semi' { + import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; + + const rule: TSESLint.RuleModule< + 'unexpected', + [], + { + EmptyStatement(node: TSESTree.EmptyStatement): void; + ClassBody(node: TSESTree.ClassBody): void; + MethodDefinition(node: TSESTree.MethodDefinition): void; + } + >; + export = rule; +}