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;
+}