diff --git a/docs/rules/lines-between-class-members.md b/docs/rules/lines-between-class-members.md index f950a6b0105..27084c28346 100644 --- a/docs/rules/lines-between-class-members.md +++ b/docs/rules/lines-between-class-members.md @@ -9,6 +9,7 @@ Examples of **incorrect** code for this rule: ```js /* eslint lines-between-class-members: ["error", "always"]*/ class MyClass { + x; foo() { //... } @@ -23,6 +24,8 @@ Examples of **correct** code for this rule: ```js /* eslint lines-between-class-members: ["error", "always"]*/ class MyClass { + x; + foo() { //... } @@ -33,6 +36,17 @@ class MyClass { } ``` +Examples of additional **correct** code for this rule: + +```js +/* eslint lines-between-class-members: ["error", "always"]*/ +class MyClass { + x = 1 + + ;in = 2 +} +``` + ### Options This rule has a string option and an object option. @@ -52,12 +66,15 @@ Examples of **incorrect** code for this rule with the string option: ```js /* eslint lines-between-class-members: ["error", "always"]*/ class Foo{ + x; bar(){} baz(){} } /* eslint lines-between-class-members: ["error", "never"]*/ class Foo{ + x; + bar(){} baz(){} @@ -69,6 +86,8 @@ Examples of **correct** code for this rule with the string option: ```js /* eslint lines-between-class-members: ["error", "always"]*/ class Foo{ + x; + bar(){} baz(){} @@ -76,6 +95,7 @@ class Foo{ /* eslint lines-between-class-members: ["error", "never"]*/ class Foo{ + x; bar(){} baz(){} } @@ -86,6 +106,7 @@ Examples of **correct** code for this rule with the object option: ```js /* eslint lines-between-class-members: ["error", "always", { "exceptAfterSingleLine": true }]*/ class Foo{ + x; // single line class member bar(){} // single line class member baz(){ // multi line class member diff --git a/lib/rules/lines-between-class-members.js b/lib/rules/lines-between-class-members.js index cb5c02df9cf..e4c05f3050c 100644 --- a/lib/rules/lines-between-class-members.js +++ b/lib/rules/lines-between-class-members.js @@ -4,6 +4,10 @@ */ "use strict"; +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + const astUtils = require("./utils/ast-utils"); //------------------------------------------------------------------------------ @@ -52,6 +56,51 @@ module.exports = { const sourceCode = context.getSourceCode(); + /** + * Gets a pair of tokens that should be used to check lines between two class member nodes. + * + * In most cases, this returns the very last token of the current node and + * the very first token of the next node. + * For example: + * + * class C { + * x = 1; // curLast: `;` nextFirst: `in` + * in = 2 + * } + * + * There is only one exception. If the given node ends with a semicolon, and it looks like + * a semicolon-less style's semicolon - one that is not on the same line as the preceding + * token, but is on the line where the next class member starts - this returns the preceding + * token and the semicolon as boundary tokens. + * For example: + * + * class C { + * x = 1 // curLast: `1` nextFirst: `;` + * ;in = 2 + * } + * When determining the desired layout of the code, we should treat this semicolon as + * a part of the next class member node instead of the one it technically belongs to. + * @param {ASTNode} curNode Current class member node. + * @param {ASTNode} nextNode Next class member node. + * @returns {Token} The actual last token of `node`. + * @private + */ + function getBoundaryTokens(curNode, nextNode) { + const lastToken = sourceCode.getLastToken(curNode); + const prevToken = sourceCode.getTokenBefore(lastToken); + const nextToken = sourceCode.getFirstToken(nextNode); // skip possible lone `;` between nodes + + const isSemicolonLessStyle = ( + astUtils.isSemicolonToken(lastToken) && + !astUtils.isTokenOnSameLine(prevToken, lastToken) && + astUtils.isTokenOnSameLine(lastToken, nextToken) + ); + + return isSemicolonLessStyle + ? { curLast: prevToken, nextFirst: lastToken } + : { curLast: lastToken, nextFirst: nextToken }; + } + /** * Return the last token among the consecutive tokens that have no exceed max line difference in between, before the first token in the next member. * @param {Token} prevLastToken The last token in the previous member node. @@ -100,8 +149,7 @@ module.exports = { for (let i = 0; i < body.length - 1; i++) { const curFirst = sourceCode.getFirstToken(body[i]); - const curLast = sourceCode.getLastToken(body[i]); - const nextFirst = sourceCode.getFirstToken(body[i + 1]); + const { curLast, nextFirst } = getBoundaryTokens(body[i], body[i + 1]); const isMulti = !astUtils.isTokenOnSameLine(curFirst, curLast); const skip = !isMulti && options[1].exceptAfterSingleLine; const beforePadding = findLastConsecutiveTokenAfter(curLast, nextFirst, 1); diff --git a/tests/lib/rules/lines-between-class-members.js b/tests/lib/rules/lines-between-class-members.js index 20f82d4945e..feb9c085e37 100644 --- a/tests/lib/rules/lines-between-class-members.js +++ b/tests/lib/rules/lines-between-class-members.js @@ -62,7 +62,12 @@ ruleTester.run("lines-between-class-members", rule, { { code: "class foo{ bar(){}\nbaz(){}}", options: ["always", { exceptAfterSingleLine: true }] }, { code: "class foo{ bar(){\n}\n\nbaz(){}}", options: ["always", { exceptAfterSingleLine: true }] }, - { code: "class foo{\naaa;\n#bbb;\nccc(){\n}\n\n#ddd(){\n}\n}", options: ["always", { exceptAfterSingleLine: true }] } + { code: "class foo{\naaa;\n#bbb;\nccc(){\n}\n\n#ddd(){\n}\n}", options: ["always", { exceptAfterSingleLine: true }] }, + + // semicolon-less style (semicolons are at the beginning of lines) + { code: "class C { foo\n\n;bar }", options: ["always"] }, + { code: "class C { foo\n;bar }", options: ["always", { exceptAfterSingleLine: true }] }, + { code: "class C { foo\n;bar }", options: ["never"] } ], invalid: [ { @@ -165,6 +170,44 @@ ruleTester.run("lines-between-class-members", rule, { output: "class C {\nfield1 = () => {\n}\n\nfield2\nfield3\n}", options: ["always", { exceptAfterSingleLine: true }], errors: [alwaysError] + }, + { + code: "class C { foo;bar }", + output: "class C { foo;\nbar }", + options: ["always"], + errors: [alwaysError] + }, + { + code: "class C { foo;\nbar; }", + output: "class C { foo;\n\nbar; }", + options: ["always"], + errors: [alwaysError] + }, + { + code: "class C { foo;\n;bar }", + output: "class C { foo;\n\n;bar }", + options: ["always"], + errors: [alwaysError] + }, + + // semicolon-less style (semicolons are at the beginning of lines) + { + code: "class C { foo\n;bar }", + output: "class C { foo\n\n;bar }", + options: ["always"], + errors: [alwaysError] + }, + { + code: "class C { foo\n\n;bar }", + output: "class C { foo\n;bar }", + options: ["never"], + errors: [neverError] + }, + { + code: "class C { foo\n;;bar }", + output: "class C { foo\n\n;;bar }", + options: ["always"], + errors: [alwaysError] } ] });