Repository: nzakas/eslint-plugin-typescript (js)

Issue:
member-delimiter-style single-line override
Today, with `member-delimiter-style: error`:

```ts
export interface Foo { [key: string]: any }
//                                    ^ Expected a semicolon.
```

Suggestion: add an override/option to disable this check for single line type signatures

Pull request description:
Update: Add ignore single line option to member-delimiter-style (fixes #58)
Adds a new option to ignore single line interface and type literal declarations

Patch:
diff --git a/docs/rules/member-delimiter-style.md b/docs/rules/member-delimiter-style.md
index 6f8160c..9cb0b87 100644
--- a/docs/rules/member-delimiter-style.md
+++ b/docs/rules/member-delimiter-style.md
@@ -40,7 +40,7 @@ type Bar = {
 }
 ```
 
-The rule also enforces the presence of the delimiter in the last member of the interface and/or type literal.
+The rule also enforces the presence of the delimiter in the last member of the interface and/or type literal, allowing single line declarations to be ignored.
 
 ## Rule Details
 
@@ -48,17 +48,23 @@ This rule aims to standardise the way interface and type literal members are del
 
 ## Options
 
-This rule, in its default state, does not require any argument, in which case a **semicolon** is used as a delimiter and **all members** require a delimiter.
+This rule, in its default state, does not require any argument, in which case a **semicolon** is used as a
+delimiter and **all members** require a delimiter, except in single line interfaces or type literals, in which case
+the delimiter of the **last member** can be omitted.
+
 The rule can also take one or more of the following options:
 - `"delimiter": "semi"`, (default) use this to require a semicolon.
 - `"delimiter": "comma"`, use this to require a comma.
 - `"delimiter": "none"`, use this to require a linebreak.
 - `"requireLast": true`, (default) use this to require a delimiter for all members of the interface and/or type literal.
 - `"requireLast": false`, use this to ignore the last member of the interface and/or type literal.
+- `"ignoreSingleLine": true`, (default) use this to override the `requireLast` in single line declarations.
+- `"ignoreSingleLine": false`, use this to enfore the `requireLast` in single line declarations.
 - `"overrides"`, overrides the default options for **interfaces** and **type literals**.
 
 ### defaults
-Examples of **incorrect** code for this rule with the defaults `{ delimiter: "semi", requireLast: true }` or no option at all:
+Examples of **incorrect** code for this rule with the defaults
+`{ delimiter: "semi", requireLast: true, ignoreSingleLine: true }` or no option at all:
 ```ts
 // missing semicolon delimiter
 interface Foo {
@@ -97,17 +103,26 @@ type Baz = {
 }
 ```
 
-Examples of **correct** code for this rule with the default `{ delimiter: "semi", requireLast: true }`:
+Examples of **correct** code for this rule with the default
+`{ delimiter: "semi", requireLast: true, ignoreSingleLine: true }`:
 ```ts
 interface Foo {
     name: string;
     greet(): string;
 }
 
+interface Foo { name: string }
+
+interface Foo { name: string; }
+
 type Bar = {
     name: string;
     greet(): string;
 }
+
+type Bar = { name: string }
+
+type Bar = { name: string; }
 ```
 
 ### delimiter - semi
@@ -231,7 +246,7 @@ interface Foo {
 }
 
 // using incorrect delimiter
-interface Bar {
+type Bar = {
     name: string,
     greet(): string
 }
@@ -283,6 +298,45 @@ type Baz = {
 }
 ```
 
+### ignoreSingleLine
+Examples of **incorrect** code for this rule with `{ ignoreSingleLine: true }`:
+```ts
+// using incorrect delimiter
+interface Foo { name: string, }
+
+// using incorrect delimiter
+type Bar = { name: string, }
+```
+
+Examples of **correct** code for this rule with `{ ignoreSingleLine: true }`:
+```ts
+// can have a delimiter or not
+interface Foo { name: string }
+
+interface Foo { name: string; }
+
+// can have a delimiter or not
+type Bar = { name: string }
+
+type Bar = { name: string; }
+```
+
+Examples of **incorrect** code for this rule with `{ ignoreSingleLine: false }`:
+```ts
+// missing delimiter
+interface Foo { name: string }
+
+// missing delimiter
+type Bar = { name: string }
+```
+
+Examples of **correct** code for this rule with `{ ignoreSingleLine: false }`:
+```ts
+interface Foo { name: string; }
+
+type Bar = { name: string; }
+```
+
 ### overrides - interface
 Examples of **incorrect** code for this rule with `{ delimiter: "comma", requireLast: true, overrides: { interface: { delimiter: "semi" } } }`:
 ```ts
diff --git a/lib/rules/member-delimiter-style.js b/lib/rules/member-delimiter-style.js
index 99c5e4c..283b69a 100644
--- a/lib/rules/member-delimiter-style.js
+++ b/lib/rules/member-delimiter-style.js
@@ -12,7 +12,8 @@ const definition = {
     type: "object",
     properties: {
         delimiter: { enum: ["none", "semi", "comma"] },
-        requireLast: { type: "boolean" }
+        requireLast: { type: "boolean" },
+        ignoreSingleLine: { type: "boolean" }
     },
     additionalProperties: false
 };
@@ -31,6 +32,7 @@ module.exports = {
                 properties: {
                     delimiter: { enum: ["none", "semi", "comma"] },
                     requireLast: { type: "boolean" },
+                    ignoreSingleLine: { type: "boolean" },
                     overrides: {
                         type: "object",
                         properties: {
@@ -50,7 +52,11 @@ module.exports = {
         const options = context.options[0] || {};
 
         const overrides = options.overrides || {};
-        const defaults = { delimiter: "semi", requireLast: true };
+        const defaults = {
+            delimiter: "semi",
+            requireLast: true,
+            ignoreSingleLine: true
+        };
 
         const interfaceOptions = Object.assign(
             {},
@@ -73,11 +79,14 @@ module.exports = {
          * Check the last token in the given member.
          * @param {ASTNode} member the member to be evaluated.
          * @param {Object} opts the options to be validated.
-         * @param {boolean} isLast a flag indicating `member` is the last in the interface or type literal.
+         * @param {boolean} isLast a flag indicating `member` is the last in the
+         *                         interface or type literal.
+         * @param {boolean} isSameLine a flag indicating the interface or type
+         *                             literal was declared in a single line.
          * @returns {void}
          * @private
          */
-        function checkLastToken(member, opts, isLast) {
+        function checkLastToken(member, opts, isLast, isSameLine) {
             let message;
             const lastToken = sourceCode.getLastToken(member, {
                 includeComments: false
@@ -98,7 +107,15 @@ module.exports = {
                 lastToken.value !== "," &&
                 opts.delimiter !== "none"
             ) {
-                if (!isLast || (isLast && opts.requireLast)) {
+                let canOmit = isLast;
+
+                if (canOmit) {
+                    canOmit =
+                        !opts.requireLast ||
+                        (isSameLine && opts.ignoreSingleLine);
+                }
+
+                if (!canOmit) {
                     message =
                         opts.delimiter === "semi"
                             ? "Expected a semicolon."
@@ -131,6 +148,7 @@ module.exports = {
          * @private
          */
         function checkMemberSeparatorStyle(node) {
+            const isSingleLine = node.loc.start.line === node.loc.end.line;
             const isInterface = node.type === "TSInterfaceBody";
             const members = isInterface ? node.body : node.members;
 
@@ -138,7 +156,8 @@ module.exports = {
                 checkLastToken(
                     member,
                     isInterface ? interfaceOptions : typeLiteralOptions,
-                    index === members.length - 1
+                    index === members.length - 1,
+                    isSingleLine
                 );
             });
         }


Test patch:
diff --git a/tests/lib/rules/member-delimiter-style.js b/tests/lib/rules/member-delimiter-style.js
index 7d6735e..de634b7 100644
--- a/tests/lib/rules/member-delimiter-style.js
+++ b/tests/lib/rules/member-delimiter-style.js
@@ -622,6 +622,168 @@ type Bar = {
                 }
             ],
             parser: "typescript-eslint-parser"
+        },
+        {
+            code: "interface Foo { [key: string]: any }",
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: "interface Foo { [key: string]: any; }",
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: "interface Foo { [key: string]: any }",
+            options: [
+                {
+                    ignoreSingleLine: true
+                }
+            ],
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: "interface Foo { [key: string]: any }",
+            options: [
+                {
+                    delimiter: "comma",
+                    ignoreSingleLine: true
+                }
+            ],
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: "interface Foo { [key: string]: any; }",
+            options: [
+                {
+                    ignoreSingleLine: false
+                }
+            ],
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: "interface Foo { [key: string]: any, }",
+            options: [
+                {
+                    delimiter: "comma",
+                    ignoreSingleLine: false
+                }
+            ],
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: "interface Foo { [key: string]: any }",
+            options: [
+                {
+                    ignoreSingleLine: false,
+                    overrides: {
+                        interface: { ignoreSingleLine: true }
+                    }
+                }
+            ],
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: "interface Foo { [key: string]: any }",
+            options: [
+                {
+                    requireLast: true,
+                    ignoreSingleLine: true
+                }
+            ],
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: "interface Foo { [key: string]: any; }",
+            options: [
+                {
+                    requireLast: true,
+                    ignoreSingleLine: false
+                }
+            ],
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: "interface Foo { [key: string]: any }",
+            options: [
+                {
+                    requireLast: true,
+                    ignoreSingleLine: false,
+                    overrides: {
+                        interface: { ignoreSingleLine: true }
+                    }
+                }
+            ],
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: "type Foo = { [key: string]: any }",
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: "type Foo = { [key: string]: any; }",
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: "type Foo = { [key: string]: any }",
+            options: [
+                {
+                    ignoreSingleLine: true
+                }
+            ],
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: "type Foo = { [key: string]: any; }",
+            options: [
+                {
+                    ignoreSingleLine: false
+                }
+            ],
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: "type Foo = { [key: string]: any }",
+            options: [
+                {
+                    ignoreSingleLine: false,
+                    overrides: {
+                        typeLiteral: { ignoreSingleLine: true }
+                    }
+                }
+            ],
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: "type Foo = { [key: string]: any }",
+            options: [
+                {
+                    requireLast: true,
+                    ignoreSingleLine: true
+                }
+            ],
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: "type Foo = { [key: string]: any; }",
+            options: [
+                {
+                    requireLast: true,
+                    ignoreSingleLine: false
+                }
+            ],
+            parser: "typescript-eslint-parser"
+        },
+        {
+            code: "type Foo = { [key: string]: any }",
+            options: [
+                {
+                    requireLast: true,
+                    ignoreSingleLine: false,
+                    overrides: {
+                        typeLiteral: { ignoreSingleLine: true }
+                    }
+                }
+            ],
+            parser: "typescript-eslint-parser"
         }
     ],
     invalid: [
@@ -2414,6 +2576,166 @@ type Bar = {
                     column: 17
                 }
             ]
+        },
+        {
+            code: `
+interface Foo {
+    [key: string]: any
+}
+            `,
+            parser: "typescript-eslint-parser",
+            errors: [
+                {
+                    message: "Expected a semicolon.",
+                    line: 3,
+                    column: 23
+                }
+            ]
+        },
+        {
+            code: `
+interface Foo {
+    [key: string]: any
+}
+            `,
+            parser: "typescript-eslint-parser",
+            options: [{ ignoreSingleLine: true }],
+            errors: [
+                {
+                    message: "Expected a semicolon.",
+                    line: 3,
+                    column: 23
+                }
+            ]
+        },
+        {
+            code: "interface Foo { [key: string]: any }",
+            options: [{ ignoreSingleLine: false }],
+            parser: "typescript-eslint-parser",
+            errors: [
+                {
+                    message: "Expected a semicolon.",
+                    line: 1,
+                    column: 35
+                }
+            ]
+        },
+        {
+            code: "interface Foo { [key: string]: any }",
+            options: [
+                {
+                    requireLast: true,
+                    ignoreSingleLine: false
+                }
+            ],
+            parser: "typescript-eslint-parser",
+            errors: [
+                {
+                    message: "Expected a semicolon.",
+                    line: 1,
+                    column: 35
+                }
+            ]
+        },
+        {
+            code: "interface Foo { [key: string]: any }",
+            options: [
+                {
+                    requireLast: true,
+                    ignoreSingleLine: true,
+                    overrides: {
+                        interface: { ignoreSingleLine: false }
+                    }
+                }
+            ],
+            parser: "typescript-eslint-parser",
+            errors: [
+                {
+                    message: "Expected a semicolon.",
+                    line: 1,
+                    column: 35
+                }
+            ]
+        },
+        {
+            code: `
+type Foo = {
+    [key: string]: any
+}
+            `,
+            parser: "typescript-eslint-parser",
+            errors: [
+                {
+                    message: "Expected a semicolon.",
+                    line: 3,
+                    column: 23
+                }
+            ]
+        },
+        {
+            code: `
+type Foo = {
+    [key: string]: any
+}
+            `,
+            parser: "typescript-eslint-parser",
+            options: [{ ignoreSingleLine: true }],
+            errors: [
+                {
+                    message: "Expected a semicolon.",
+                    line: 3,
+                    column: 23
+                }
+            ]
+        },
+        {
+            code: "type Foo = { [key: string]: any }",
+            options: [{ ignoreSingleLine: false }],
+            parser: "typescript-eslint-parser",
+            errors: [
+                {
+                    message: "Expected a semicolon.",
+                    line: 1,
+                    column: 32
+                }
+            ]
+        },
+        {
+            code: "type Foo = { [key: string]: any }",
+            options: [
+                {
+                    requireLast: true,
+                    ignoreSingleLine: false
+                }
+            ],
+            parser: "typescript-eslint-parser",
+            errors: [
+                {
+                    message: "Expected a semicolon.",
+                    line: 1,
+                    column: 32
+                }
+            ]
+        },
+        {
+            code: "type Foo = { [key: string]: any }",
+            options: [
+                {
+                    requireLast: true,
+                    ignoreSingleLine: true,
+                    overrides: {
+                        typeLiteral: { ignoreSingleLine: false }
+                    }
+                }
+            ],
+            parser: "typescript-eslint-parser",
+            errors: [
+                {
+                    message: "Expected a semicolon.",
+                    line: 1,
+                    column: 32
+                }
+            ]
         }
     ]
 });