diff --git a/docs/src/rules/semi.md b/docs/src/rules/semi.md index 63f2070d218..6fd05501cd1 100644 --- a/docs/src/rules/semi.md +++ b/docs/src/rules/semi.md @@ -80,7 +80,8 @@ String option: Object option (when `"always"`): -* `"omitLastInOneLineBlock": true` ignores the last semicolon in a block in which its braces (and therefore the content of the block) are in the same line +* `"omitLastInOneLineBlock": true` disallows the last semicolon in a block in which its braces (and therefore the content of the block) are in the same line +* `"omitLastInOneLineClassBody": true` disallows the last semicolon in a class body in which its braces (and therefore the content of the class body) are in the same line Object option (when `"never"`): @@ -132,6 +133,52 @@ class Foo { ::: +#### omitLastInOneLineBlock + +Examples of additional **correct** code for this rule with the `"always", { "omitLastInOneLineBlock": true }` options: + +::: correct + +```js +/*eslint semi: ["error", "always", { "omitLastInOneLineBlock": true}] */ + +if (foo) { bar() } + +if (foo) { bar(); baz() } + +function f() { bar(); baz() } + +class C { + foo() { bar(); baz() } + + static { bar(); baz() } +} +``` + +::: + +#### omitLastInOneLineClassBody + +Examples of additional **correct** code for this rule with the `"always", { "omitLastInOneLineClassBody": true }` options: + +::: correct + +```js +/*eslint semi: ["error", "always", { "omitLastInOneLineClassBody": true}] */ + +export class SomeClass{ + logType(){ + console.log(this.type); + console.log(this.anotherType); + } +} + +export class Variant1 extends SomeClass{type=1} +export class Variant2 extends SomeClass{type=2; anotherType=3} +``` + +::: + ### never Examples of **incorrect** code for this rule with the `"never"` option: @@ -190,30 +237,6 @@ class Foo { ::: -#### omitLastInOneLineBlock - -Examples of additional **correct** code for this rule with the `"always", { "omitLastInOneLineBlock": true }` options: - -::: correct - -```js -/*eslint semi: ["error", "always", { "omitLastInOneLineBlock": true}] */ - -if (foo) { bar() } - -if (foo) { bar(); baz() } - -function f() { bar(); baz() } - -class C { - foo() { bar(); baz() } - - static { bar(); baz() } -} -``` - -::: - #### beforeStatementContinuationChars Examples of additional **incorrect** code for this rule with the `"never", { "beforeStatementContinuationChars": "always" }` options: diff --git a/lib/rules/semi.js b/lib/rules/semi.js index 1e49273c2e9..c312e0bc40a 100644 --- a/lib/rules/semi.js +++ b/lib/rules/semi.js @@ -58,7 +58,8 @@ module.exports = { { type: "object", properties: { - omitLastInOneLineBlock: { type: "boolean" } + omitLastInOneLineBlock: { type: "boolean" }, + omitLastInOneLineClassBody: { type: "boolean" } }, additionalProperties: false } @@ -83,6 +84,7 @@ module.exports = { const options = context.options[1]; const never = context.options[0] === "never"; const exceptOneLine = Boolean(options && options.omitLastInOneLineBlock); + const exceptOneLineClassBody = Boolean(options && options.omitLastInOneLineClassBody); const beforeStatementContinuationChars = options && options.beforeStatementContinuationChars || "any"; const sourceCode = context.getSourceCode(); @@ -334,6 +336,27 @@ module.exports = { return false; } + /** + * Checks a node to see if it's the last item in a one-liner `ClassBody` node. + * ClassBody is a one-liner if its braces (and consequently everything between them) are on the same line. + * @param {ASTNode} node The node to check. + * @returns {boolean} whether the node is the last item in a one-liner ClassBody. + */ + function isLastInOneLinerClassBody(node) { + const parent = node.parent; + const nextToken = sourceCode.getTokenAfter(node); + + if (!nextToken || nextToken.value !== "}") { + return false; + } + + if (parent.type === "ClassBody") { + return parent.loc.start.line === parent.loc.end.line; + } + + return false; + } + /** * Checks a node to see if it's followed by a semicolon. * @param {ASTNode} node The node to check. @@ -354,10 +377,12 @@ module.exports = { } } else { const oneLinerBlock = (exceptOneLine && isLastInOneLinerBlock(node)); + const oneLinerClassBody = (exceptOneLineClassBody && isLastInOneLinerClassBody(node)); + const oneLinerBlockOrClassBody = oneLinerBlock || oneLinerClassBody; - if (isSemi && oneLinerBlock) { + if (isSemi && oneLinerBlockOrClassBody) { report(node, true); - } else if (!isSemi && !oneLinerBlock) { + } else if (!isSemi && !oneLinerBlockOrClassBody) { report(node); } } diff --git a/tests/lib/rules/semi.js b/tests/lib/rules/semi.js index d87382182ea..1811e14ee7b 100644 --- a/tests/lib/rules/semi.js +++ b/tests/lib/rules/semi.js @@ -111,6 +111,81 @@ ruleTester.run("semi", rule, { { code: "class C {\n static {\n bar(); baz(); } \n}", options: ["always", { omitLastInOneLineBlock: true }], parserOptions: { ecmaVersion: 2022 } }, { code: "class C {\n static { bar(); baz(); \n} \n}", options: ["always", { omitLastInOneLineBlock: true }], parserOptions: { ecmaVersion: 2022 } }, + // omitLastInOneLineClassBody: true + { + code: ` + export class SomeClass{ + logType(){ + console.log(this.type); + } + } + + export class Variant1 extends SomeClass{type=1} + export class Variant2 extends SomeClass{type=2} + export class Variant3 extends SomeClass{type=3} + export class Variant4 extends SomeClass{type=4} + export class Variant5 extends SomeClass{type=5} + `, + options: ["always", { omitLastInOneLineClassBody: true }], + parserOptions: { ecmaVersion: 2022, sourceType: "module" } + }, + { + code: ` + export class SomeClass{ + logType(){ + console.log(this.type); + console.log(this.anotherType); + } + } + + export class Variant1 extends SomeClass{type=1; anotherType=2} + `, + options: ["always", { omitLastInOneLineClassBody: true }], + parserOptions: { ecmaVersion: 2022, sourceType: "module" } + }, + { + code: ` + export class SomeClass{ + logType(){ + console.log(this.type); + } + } + + export class Variant1 extends SomeClass{type=1;} + export class Variant2 extends SomeClass{type=2;} + export class Variant3 extends SomeClass{type=3;} + export class Variant4 extends SomeClass{type=4;} + export class Variant5 extends SomeClass{type=5;} + `, + options: ["always", { omitLastInOneLineClassBody: false }], + parserOptions: { ecmaVersion: 2022, sourceType: "module" } + }, + { + code: "class C {\nfoo;}", + options: ["always", { omitLastInOneLineClassBody: true }], + parserOptions: { ecmaVersion: 2022 } + }, + { + code: "class C {foo;\n}", + options: ["always", { omitLastInOneLineClassBody: true }], + parserOptions: { ecmaVersion: 2022 } + }, + { + code: "class C {foo;\nbar;}", + options: ["always", { omitLastInOneLineClassBody: true }], + parserOptions: { ecmaVersion: 2022 } + }, + { + code: "{ foo; }", + options: ["always", { omitLastInOneLineClassBody: true }], + parserOptions: { ecmaVersion: 2022 } + }, + { + code: "class C\n{ foo }", + options: ["always", { omitLastInOneLineClassBody: true }], + parserOptions: { ecmaVersion: 2022 } + }, + // method definitions and static blocks don't have a semicolon. { code: "class A { a() {} b() {} }", parserOptions: { ecmaVersion: 6 } }, { code: "var A = class { a() {} b() {} };", parserOptions: { ecmaVersion: 6 } }, @@ -2309,6 +2384,134 @@ ruleTester.run("semi", rule, { endLine: 1, endColumn: 18 }] + }, + + // omitLastInOneLineClassBody + { + code: ` + export class SomeClass{ + logType(){ + console.log(this.type); + } + } + + export class Variant1 extends SomeClass{type=1} + `, + output: ` + export class SomeClass{ + logType(){ + console.log(this.type); + } + } + + export class Variant1 extends SomeClass{type=1;} + `, + options: ["always", { omitLastInOneLineClassBody: false }], + parserOptions: { ecmaVersion: 2022, sourceType: "module" }, + errors: [ + { + messageId: "missingSemi", + line: 8, + column: 63, + endLine: 8, + endColumn: 64 + } + ] + }, + { + code: ` + export class SomeClass{ + logType(){ + console.log(this.type); + } + } + + export class Variant1 extends SomeClass{type=1} + `, + output: ` + export class SomeClass{ + logType(){ + console.log(this.type); + } + } + + export class Variant1 extends SomeClass{type=1;} + `, + options: ["always", { omitLastInOneLineClassBody: false, omitLastInOneLineBlock: true }], + parserOptions: { ecmaVersion: 2022, sourceType: "module" }, + errors: [ + { + messageId: "missingSemi", + line: 8, + column: 63, + endLine: 8, + endColumn: 64 + } + ] + }, + { + code: ` + export class SomeClass{ + logType(){ + console.log(this.type); + } + } + + export class Variant1 extends SomeClass{type=1;} + `, + output: ` + export class SomeClass{ + logType(){ + console.log(this.type); + } + } + + export class Variant1 extends SomeClass{type=1} + `, + options: ["always", { omitLastInOneLineClassBody: true, omitLastInOneLineBlock: false }], + parserOptions: { ecmaVersion: 2022, sourceType: "module" }, + errors: [ + { + messageId: "extraSemi", + line: 8, + column: 63, + endLine: 8, + endColumn: 64 + } + ] + }, + { + code: ` + export class SomeClass{ + logType(){ + console.log(this.type); + console.log(this.anotherType); + } + } + + export class Variant1 extends SomeClass{type=1; anotherType=2} + `, + output: ` + export class SomeClass{ + logType(){ + console.log(this.type); + console.log(this.anotherType); + } + } + + export class Variant1 extends SomeClass{type=1; anotherType=2;} + `, + options: ["always", { omitLastInOneLineClassBody: false, omitLastInOneLineBlock: true }], + parserOptions: { ecmaVersion: 2022, sourceType: "module" }, + errors: [ + { + messageId: "missingSemi", + line: 9, + column: 78, + endLine: 9, + endColumn: 79 + } + ] } ] });