diff --git a/docs/src/rules/lines-between-class-members.md b/docs/src/rules/lines-between-class-members.md index 8daf2a2f78d..55627501096 100644 --- a/docs/src/rules/lines-between-class-members.md +++ b/docs/src/rules/lines-between-class-members.md @@ -69,14 +69,19 @@ class MyClass { ### Options -This rule has a string option and an object option. +This rule has two options, first option can be string or object, second option is object. -String option: +First option can be string `"always"` or `"never"` or an object with a property named `enforce`: * `"always"`(default) require an empty line after class members * `"never"` disallows an empty line after class members +* `Object`: An object with a property named `enforce`. The enforce property should be an array of objects, each specifying the configuration for enforcing empty lines between specific pairs of class members. + * **enforce**: You can supply any number of configurations. If a member pair matches multiple configurations, the last matched configuration will be used. If a member pair does not match any configurations, it will be ignored. Each object should have the following properties: + * **blankLine**: Can be set to either `"always"` or `"never"`, indicating whether a blank line should be required or disallowed between the specified members. + * **prev**: Specifies the type of the preceding class member. It can be `"method"` for class methods, `"field"` for class fields, or `"*"` for any class member. + * **next**: Specifies the type of the following class member. It follows the same options as `prev`. -Object option: +Second option is an object with a property named `exceptAfterSingleLine`: * `"exceptAfterSingleLine": false`(default) **do not** skip checking empty lines after single-line class members * `"exceptAfterSingleLine": true` skip checking empty lines after single-line class members @@ -129,6 +134,146 @@ class Foo{ ::: +Examples of **incorrect** code for this rule with the array of configurations option: + +::: incorrect + +```js +// disallows blank lines between methods +/*eslint lines-between-class-members: [ + "error", + { + enforce: [ + { blankLine: "never", prev: "method", next: "method" } + ] + }, +]*/ + +class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} +} +``` + +::: + +::: incorrect + +```js +// requires blank lines around fields, disallows blank lines between methods +/*eslint lines-between-class-members: [ + "error", + { + enforce: [ + { blankLine: "always", prev: "*", next: "field" }, + { blankLine: "always", prev: "field", next: "*" }, + { blankLine: "never", prev: "method", next: "method" } + ] + }, +]*/ + +class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + + get area() { + return this.method1(); + } + + method2() {} +} +``` + +::: + +Examples of **correct** code for this rule with the array of configurations option: + +::: correct + +```js +// disallows blank lines between methods +/*eslint lines-between-class-members: [ + "error", + { + enforce: [ + { blankLine: "never", prev: "method", next: "method" } + ] + }, +]*/ + +class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} +} +``` + +::: + +::: correct + +```js +// requires blank lines around fields, disallows blank lines between methods +/*eslint lines-between-class-members: [ + "error", + { + enforce: [ + { blankLine: "always", prev: "*", next: "field" }, + { blankLine: "always", prev: "field", next: "*" }, + { blankLine: "never", prev: "method", next: "method" } + ] + }, +]*/ + +class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} +} +``` + +::: + Examples of **correct** code for this rule with the object option: ::: correct @@ -148,6 +293,40 @@ class Foo{ ::: +::: correct + +```js +/*eslint lines-between-class-members: [ + "error", + { + enforce: [ + { blankLine: "always", prev: "*", next: "method" }, + { blankLine: "always", prev: "method", next: "*" }, + { blankLine: "always", prev: "field", next: "field" } + ] + }, + { exceptAfterSingleLine: true } +]*/ + +class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + + method2() {} +} +``` + +::: + ## When Not To Use It If you don't want to enforce empty lines between class members, you can disable this rule. diff --git a/lib/rules/lines-between-class-members.js b/lib/rules/lines-between-class-members.js index dee4bab5f54..3d0a5e6738e 100644 --- a/lib/rules/lines-between-class-members.js +++ b/lib/rules/lines-between-class-members.js @@ -10,6 +10,21 @@ const astUtils = require("./utils/ast-utils"); +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Types of class members. + * Those have `test` method to check it matches to the given class member. + * @private + */ +const ClassMemberTypes = { + "*": { test: () => true }, + field: { test: node => node.type === "PropertyDefinition" }, + method: { test: node => node.type === "MethodDefinition" } +}; + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -29,7 +44,32 @@ module.exports = { schema: [ { - enum: ["always", "never"] + anyOf: [ + { + type: "object", + properties: { + enforce: { + type: "array", + items: { + type: "object", + properties: { + blankLine: { enum: ["always", "never"] }, + prev: { enum: ["method", "field", "*"] }, + next: { enum: ["method", "field", "*"] } + }, + additionalProperties: false, + required: ["blankLine", "prev", "next"] + }, + minItems: 1 + } + }, + additionalProperties: false, + required: ["enforce"] + }, + { + enum: ["always", "never"] + } + ] }, { type: "object", @@ -55,6 +95,7 @@ module.exports = { options[0] = context.options[0] || "always"; options[1] = context.options[1] || { exceptAfterSingleLine: false }; + const configureList = typeof options[0] === "object" ? options[0].enforce : [{ blankLine: options[0], prev: "*", next: "*" }]; const sourceCode = context.sourceCode; /** @@ -144,6 +185,38 @@ module.exports = { return sourceCode.getTokensBetween(before, after, { includeComments: true }).length !== 0; } + /** + * Checks whether the given node matches the given type. + * @param {ASTNode} node The class member node to check. + * @param {string} type The class member type to check. + * @returns {boolean} `true` if the class member node matched the type. + * @private + */ + function match(node, type) { + return ClassMemberTypes[type].test(node); + } + + /** + * Finds the last matched configuration from the configureList. + * @param {ASTNode} prevNode The previous node to match. + * @param {ASTNode} nextNode The current node to match. + * @returns {string|null} Padding type or `null` if no matches were found. + * @private + */ + function getPaddingType(prevNode, nextNode) { + for (let i = configureList.length - 1; i >= 0; --i) { + const configure = configureList[i]; + const matched = + match(prevNode, configure.prev) && + match(nextNode, configure.next); + + if (matched) { + return configure.blankLine; + } + } + return null; + } + return { ClassBody(node) { const body = node.body; @@ -158,22 +231,34 @@ module.exports = { const isPadded = afterPadding.loc.start.line - beforePadding.loc.end.line > 1; const hasTokenInPadding = hasTokenOrCommentBetween(beforePadding, afterPadding); const curLineLastToken = findLastConsecutiveTokenAfter(curLast, nextFirst, 0); + const paddingType = getPaddingType(body[i], body[i + 1]); + + if (paddingType === "never" && isPadded) { + context.report({ + node: body[i + 1], + messageId: "never", - if ((options[0] === "always" && !skip && !isPadded) || - (options[0] === "never" && isPadded)) { + fix(fixer) { + if (hasTokenInPadding) { + return null; + } + return fixer.replaceTextRange([beforePadding.range[1], afterPadding.range[0]], "\n"); + } + }); + } else if (paddingType === "always" && !skip && !isPadded) { context.report({ node: body[i + 1], - messageId: isPadded ? "never" : "always", + messageId: "always", + fix(fixer) { if (hasTokenInPadding) { return null; } - return isPadded - ? fixer.replaceTextRange([beforePadding.range[1], afterPadding.range[0]], "\n") - : fixer.insertTextAfter(curLineLastToken, "\n"); + return fixer.insertTextAfter(curLineLastToken, "\n"); } }); } + } } }; diff --git a/tests/lib/rules/lines-between-class-members.js b/tests/lib/rules/lines-between-class-members.js index feb9c085e37..2ee17f713c3 100644 --- a/tests/lib/rules/lines-between-class-members.js +++ b/tests/lib/rules/lines-between-class-members.js @@ -50,24 +50,810 @@ ruleTester.run("lines-between-class-members", rule, { "class C {\naaa;\n\n#bbb;\n\nccc(){}\n\n#ddd(){}\n}", { code: "class foo{ bar(){}\nbaz(){}}", options: ["never"] }, - { code: "class foo{ bar(){}\n/*comments*/baz(){}}", options: ["never"] }, - { code: "class foo{ bar(){}\n//comments\nbaz(){}}", options: ["never"] }, - { code: "class foo{ bar(){}/* comments\n\n*/baz(){}}", options: ["never"] }, - { code: "class foo{ bar(){}/* \ncomments\n*/baz(){}}", options: ["never"] }, - { code: "class foo{ bar(){}\n/* \ncomments\n*/\nbaz(){}}", options: ["never"] }, + { + code: "class foo{ bar(){}\n/*comments*/baz(){}}", + options: ["never"] + }, + { + code: "class foo{ bar(){}\n//comments\nbaz(){}}", + options: ["never"] + }, + { + code: "class foo{ bar(){}/* comments\n\n*/baz(){}}", + options: ["never"] + }, + { + code: "class foo{ bar(){}/* \ncomments\n*/baz(){}}", + options: ["never"] + }, + { + code: "class foo{ bar(){}\n/* \ncomments\n*/\nbaz(){}}", + options: ["never"] + }, { code: "class foo{ bar(){}\n\nbaz(){}}", options: ["always"] }, - { code: "class foo{ bar(){}\n\n/*comments*/baz(){}}", options: ["always"] }, - { code: "class foo{ bar(){}\n\n//comments\nbaz(){}}", options: ["always"] }, + { + code: "class foo{ bar(){}\n\n/*comments*/baz(){}}", + options: ["always"] + }, + { + code: "class foo{ bar(){}\n\n//comments\nbaz(){}}", + options: ["always"] + }, - { 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{ 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 }] + }, // 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"] } + { + code: "class C { foo\n;bar }", + options: ["always", { exceptAfterSingleLine: true }] + }, + { code: "class C { foo\n;bar }", options: ["never"] }, + + // enforce option with blankLine: "always" + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "method" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "method" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "field" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "field" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "*" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "field", next: "method" } + ] + } + ] + }, + + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "field", next: "field" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "field", next: "*" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "*", next: "method" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "*", next: "field" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { enforce: [{ blankLine: "always", prev: "*", next: "*" }] } + ] + }, + + // enforce option - blankLine: "never" + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "method" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "method" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "field" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "field" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "*" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "field", next: "method" } + ] + } + ] + }, + + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "field", next: "field" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [{ blankLine: "never", prev: "field", next: "*" }] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "*", next: "method" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [{ blankLine: "never", prev: "*", next: "field" }] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { enforce: [{ blankLine: "never", prev: "*", next: "*" }] } + ] + }, + + // enforce option - multiple configurations + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + + // requires blank lines around methods, disallows blank lines between fields + enforce: [ + { blankLine: "always", prev: "*", next: "method" }, + { blankLine: "always", prev: "method", next: "*" }, + { blankLine: "never", prev: "field", next: "field" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + + // requires blank lines around fields, disallows blank lines between methods + enforce: [ + { blankLine: "always", prev: "*", next: "field" }, + { blankLine: "always", prev: "field", next: "*" }, + { blankLine: "never", prev: "method", next: "method" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + + // requires blank lines around methods and fields + enforce: [ + { blankLine: "always", prev: "*", next: "method" }, + { blankLine: "always", prev: "method", next: "*" }, + { blankLine: "always", prev: "field", next: "field" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + + // requires blank lines around methods and fields + enforce: [ + { blankLine: "never", prev: "*", next: "method" }, + { blankLine: "never", prev: "method", next: "*" }, + { blankLine: "never", prev: "field", next: "field" }, + + // This should take precedence over the above + { blankLine: "always", prev: "*", next: "method" }, + { blankLine: "always", prev: "method", next: "*" }, + { blankLine: "always", prev: "field", next: "field" } + ] + } + ] + }, + + // enforce with exceptAfterSingleLine option + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + + // requires blank lines around methods and fields + enforce: [ + { blankLine: "always", prev: "*", next: "method" }, + { blankLine: "always", prev: "method", next: "*" }, + { blankLine: "always", prev: "field", next: "field" } + ] + }, + { + exceptAfterSingleLine: true + } + ] + } ], invalid: [ { @@ -75,97 +861,116 @@ ruleTester.run("lines-between-class-members", rule, { output: "class foo{ bar(){}\n\nbaz(){}}", options: ["always"], errors: [alwaysError] - }, { + }, + { code: "class foo{ bar(){}\n\nbaz(){}}", output: "class foo{ bar(){}\nbaz(){}}", options: ["never"], errors: [neverError] - }, { + }, + { code: "class foo{ bar(){\n}\nbaz(){}}", output: "class foo{ bar(){\n}\n\nbaz(){}}", options: ["always", { exceptAfterSingleLine: true }], errors: [alwaysError] - }, { + }, + { code: "class foo{ bar(){\n}\n/* comment */\nbaz(){}}", output: "class foo{ bar(){\n}\n\n/* comment */\nbaz(){}}", options: ["always", { exceptAfterSingleLine: true }], errors: [alwaysError] - }, { + }, + { code: "class foo{ bar(){}\n\n// comment\nbaz(){}}", output: "class foo{ bar(){}\n// comment\nbaz(){}}", options: ["never"], errors: [neverError] - }, { + }, + { code: "class foo{ bar(){}\n\n/* comment */\nbaz(){}}", output: "class foo{ bar(){}\n/* comment */\nbaz(){}}", options: ["never"], errors: [neverError] - }, { + }, + { code: "class foo{ bar(){}\n/* comment-1 */\n\n/* comment-2 */\nbaz(){}}", output: "class foo{ bar(){}\n/* comment-1 */\n/* comment-2 */\nbaz(){}}", options: ["never"], errors: [neverError] - }, { + }, + { code: "class foo{ bar(){}\n\n/* comment */\n\nbaz(){}}", output: null, options: ["never"], errors: [neverError] - }, { + }, + { code: "class foo{ bar(){}\n\n// comment\n\nbaz(){}}", output: null, options: ["never"], errors: [neverError] - }, { + }, + { code: "class foo{ bar(){}\n/* comment-1 */\n\n/* comment-2 */\n\n/* comment-3 */\nbaz(){}}", output: null, options: ["never"], errors: [neverError] - }, { + }, + { code: "class foo{ bar(){}\n/* comment-1 */\n\n;\n\n/* comment-3 */\nbaz(){}}", output: null, options: ["never"], errors: [neverError] - }, { + }, + { code: "class A {\nfoo() {}// comment\n;\n/* comment */\nbar() {}\n}", output: "class A {\nfoo() {}// comment\n\n;\n/* comment */\nbar() {}\n}", options: ["always"], errors: [alwaysError] - }, { + }, + { code: "class A {\nfoo() {}\n/* comment */;\n;\n/* comment */\nbar() {}\n}", output: "class A {\nfoo() {}\n\n/* comment */;\n;\n/* comment */\nbar() {}\n}", options: ["always"], errors: [alwaysError] - }, { + }, + { code: "class foo{ bar(){};\nbaz(){}}", output: "class foo{ bar(){};\n\nbaz(){}}", options: ["always"], errors: [alwaysError] - }, { + }, + { code: "class foo{ bar(){} // comment \nbaz(){}}", output: "class foo{ bar(){} // comment \n\nbaz(){}}", options: ["always"], errors: [alwaysError] - }, { + }, + { code: "class A {\nfoo() {}\n/* comment */;\n;\nbar() {}\n}", output: "class A {\nfoo() {}\n\n/* comment */;\n;\nbar() {}\n}", options: ["always"], errors: [alwaysError] - }, { + }, + { code: "class C {\nfield1\nfield2\n}", output: "class C {\nfield1\n\nfield2\n}", options: ["always"], errors: [alwaysError] - }, { + }, + { code: "class C {\n#field1\n#field2\n}", output: "class C {\n#field1\n\n#field2\n}", options: ["always"], errors: [alwaysError] - }, { + }, + { code: "class C {\nfield1\n\nfield2\n}", output: "class C {\nfield1\nfield2\n}", options: ["never"], errors: [neverError] - }, { + }, + { code: "class C {\nfield1 = () => {\n}\nfield2\nfield3\n}", output: "class C {\nfield1 = () => {\n}\n\nfield2\nfield3\n}", options: ["always", { exceptAfterSingleLine: true }], @@ -208,6 +1013,1610 @@ ruleTester.run("lines-between-class-members", rule, { output: "class C { foo\n\n;;bar }", options: ["always"], errors: [alwaysError] + }, + + // enforce option with blankLine: "always" + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "method" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 11, + column: 17 + }, + { + messageId: "always", + line: 14, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "method" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 13, + column: 17 + }, + { + messageId: "always", + line: 16, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "field" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "field" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "*" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + }, + { + messageId: "always", + line: 11, + column: 17 + }, + { + messageId: "always", + line: 14, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "field", next: "method" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 9, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "field", next: "field" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 8, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "field", next: "*" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 8, + column: 17 + }, + { + messageId: "always", + line: 9, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "*", next: "method" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 9, + column: 17 + }, + { + messageId: "always", + line: 10, + column: 17 + }, + { + messageId: "always", + line: 13, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "*", next: "field" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + }, + { + messageId: "always", + line: 8, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { enforce: [{ blankLine: "always", prev: "*", next: "*" }] } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + }, + { + messageId: "always", + line: 8, + column: 17 + }, + { + messageId: "always", + line: 9, + column: 17 + }, + { + messageId: "always", + line: 10, + column: 17 + }, + { + messageId: "always", + line: 13, + column: 17 + } + ] + }, + + // enforce option - blankLine: "never" + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} +get area() { + return this.method1(); + } +method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "method" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 11, + column: 17 + }, + { + messageId: "never", + line: 15, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} +get area() { + return this.method1(); + } +method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "method" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 14, + column: 17 + }, + { + messageId: "never", + line: 18, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } +fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "field" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 8, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } +fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "field" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 8, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } +fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} +get area() { + return this.method1(); + } +method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "*" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 8, + column: 17 + }, + { + messageId: "never", + line: 14, + column: 17 + }, + { + messageId: "never", + line: 18, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; +method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "field", next: "method" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 12, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; +#fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "field", next: "field" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 10, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; +#fieldB = 'Field B'; +method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "field", next: "*" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 10, + column: 17 + }, + { + messageId: "never", + line: 12, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; +method1() {} +get area() { + return this.method1(); + } +method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "*", next: "method" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 12, + column: 17 + }, + { + messageId: "never", + line: 14, + column: 17 + }, + { + messageId: "never", + line: 18, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } +fieldA = 'Field A'; +#fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "*", next: "field" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 8, + column: 17 + }, + { + messageId: "never", + line: 10, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } +fieldA = 'Field A'; +#fieldB = 'Field B'; +method1() {} +get area() { + return this.method1(); + } +method2() {} + } + `, + options: [ + { enforce: [{ blankLine: "never", prev: "*", next: "*" }] } + ], + errors: [ + { + messageId: "never", + line: 8, + column: 17 + }, + { + messageId: "never", + line: 10, + column: 17 + }, + { + messageId: "never", + line: 12, + column: 17 + }, + { + messageId: "never", + line: 14, + column: 17 + }, { + messageId: "never", + line: 18, + column: 17 + } + ] + }, + + // enforce option - multiple configurations + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; +#fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + + // requires blank lines around methods, disallows blank lines between fields + enforce: [ + { blankLine: "always", prev: "*", next: "method" }, + { blankLine: "always", prev: "method", next: "*" }, + { blankLine: "never", prev: "field", next: "field" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + }, + { + messageId: "never", + line: 9, + column: 17 + }, + { + messageId: "always", + line: 10, + column: 17 + }, + { + messageId: "always", + line: 11, + column: 17 + }, { + messageId: "always", + line: 14, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} +get area() { + return this.method1(); + } +method2() {} + } + `, + options: [ + { + + // requires blank lines around fields, disallows blank lines between methods + enforce: [ + { blankLine: "always", prev: "*", next: "field" }, + { blankLine: "always", prev: "field", next: "*" }, + { blankLine: "never", prev: "method", next: "method" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + }, + { + messageId: "always", + line: 8, + column: 17 + }, + { + messageId: "always", + line: 9, + column: 17 + }, + { + messageId: "never", + line: 11, + column: 17 + }, { + messageId: "never", + line: 15, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + + // requires blank lines around methods and fields + enforce: [ + { blankLine: "always", prev: "*", next: "method" }, + { blankLine: "always", prev: "method", next: "*" }, + { blankLine: "always", prev: "field", next: "field" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + }, + { + messageId: "always", + line: 8, + column: 17 + }, + { + messageId: "always", + line: 9, + column: 17 + }, + { + messageId: "always", + line: 10, + column: 17 + }, { + messageId: "always", + line: 13, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + + // requires blank lines around methods and fields + enforce: [ + { blankLine: "never", prev: "*", next: "method" }, + { blankLine: "never", prev: "method", next: "*" }, + { blankLine: "never", prev: "field", next: "field" }, + + // This should take precedence over the above + { blankLine: "always", prev: "*", next: "method" }, + { blankLine: "always", prev: "method", next: "*" }, + { blankLine: "always", prev: "field", next: "field" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + }, + { + messageId: "always", + line: 8, + column: 17 + }, + { + messageId: "always", + line: 9, + column: 17 + }, + { + messageId: "always", + line: 10, + column: 17 + }, { + messageId: "always", + line: 13, + column: 17 + } + ] + }, + + // enforce with exceptAfterSingleLine option + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + + // requires blank lines around methods and fields + enforce: [ + { blankLine: "always", prev: "*", next: "method" }, + { blankLine: "always", prev: "method", next: "*" }, + { blankLine: "always", prev: "field", next: "field" } + ] + }, + { + exceptAfterSingleLine: true + } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + }, + { + messageId: "always", + line: 13, + column: 17 + } + ] } ] });