Repository: ember-template-lint/ember-template-lint
Pull request #951 (2019-11-23 21:11:44)

Implement `no-multiple-empty-lines` rule
Resolves https://github.com/ember-template-lint/ember-template-lint/issues/385

The implementation of this rules is mostly adapted from https://github.com/eslint/eslint/blob/master/lib/rules/no-multiple-empty-lines.js


Patch:
diff --git a/docs/rule/no-multiple-empty-lines.md b/docs/rule/no-multiple-empty-lines.md
new file mode 100644
index 00000000..6bc5caad
--- /dev/null
+++ b/docs/rule/no-multiple-empty-lines.md
@@ -0,0 +1,41 @@
+## no-multiple-empty-lines
+
+Some developers prefer to have multiple blank lines removed, while others feel
+that it helps improve readability. Whitespace is useful for separating logical
+sections of code, but excess whitespace takes up more of the screen.
+
+This rule aims to reduce the scrolling required when reading through your code.
+It will warn when the maximum amount of empty lines has been exceeded.
+
+### Examples
+
+This rule **forbids** the following:
+
+```hbs
+<div>foo</div>
+
+
+<div>bar</div>
+```
+
+This rule **allows** the following:
+
+```hbs
+<div>foo</div>
+
+<div>bar</div>
+```
+
+```hbs
+<div>foo</div>
+<div>bar</div>
+```
+
+### Configuration
+
+* object -- containing the following properties:
+  * number -- `max` --  (default: `1`) enforces a maximum number of consecutive empty lines.
+
+### References
+
+* https://eslint.org/docs/rules/no-multiple-empty-lines
diff --git a/docs/rules.md b/docs/rules.md
index 480c55a7..40481647 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -28,6 +28,7 @@
 * [no-invalid-interactive](rule/no-invalid-interactive.md)
 * [no-log](rule/no-log.md)
 * [no-meta-redirect-with-time-limit](rule/no-meta-redirect-with-time-limit.md)
+* [no-multiple-empty-lines](rule/no-multiple-empty-lines.md)
 * [no-negated-condition](rule/no-negated-condition.md)
 * [no-nested-interactive](rule/no-nested-interactive.md)
 * [no-obsolete-elements](rule/no-obsolete-elements.md)
diff --git a/lib/rules/base.js b/lib/rules/base.js
index 878ea0be..cbc47943 100644
--- a/lib/rules/base.js
+++ b/lib/rules/base.js
@@ -569,15 +569,17 @@ module.exports = class BaseRule {
   // mostly copy/pasta from tildeio/htmlbars with a few tweaks:
   // https://github.com/tildeio/htmlbars/blob/v0.14.17/packages/htmlbars-syntax/lib/parser.js#L59-L90
   sourceForNode(node) {
-    if (!node.loc) {
-      return;
+    if (node.loc) {
+      return this.sourceForLoc(node.loc);
     }
+  }
 
-    let firstLine = node.loc.start.line - 1;
-    let lastLine = node.loc.end.line - 1;
+  sourceForLoc(loc) {
+    let firstLine = loc.start.line - 1;
+    let lastLine = loc.end.line - 1;
     let currentLine = firstLine - 1;
-    let firstColumn = node.loc.start.column;
-    let lastColumn = node.loc.end.column;
+    let firstColumn = loc.start.column;
+    let lastColumn = loc.end.column;
     let string = [];
     let line;
 
diff --git a/lib/rules/index.js b/lib/rules/index.js
index 888ab646..0ca14120 100644
--- a/lib/rules/index.js
+++ b/lib/rules/index.js
@@ -32,6 +32,7 @@ module.exports = {
   'no-invalid-interactive': require('./lint-no-invalid-interactive'),
   'no-log': require('./lint-no-log'),
   'no-meta-redirect-with-time-limit': require('./lint-no-meta-redirect-with-time-limit'),
+  'no-multiple-empty-lines': require('./lint-no-multiple-empty-lines'),
   'no-negated-condition': require('./lint-no-negated-condition'),
   'no-nested-interactive': require('./lint-no-nested-interactive'),
   'no-obsolete-elements': require('./lint-no-obsolete-elements'),
diff --git a/lib/rules/lint-no-multiple-empty-lines.js b/lib/rules/lint-no-multiple-empty-lines.js
new file mode 100644
index 00000000..878aa9b3
--- /dev/null
+++ b/lib/rules/lint-no-multiple-empty-lines.js
@@ -0,0 +1,65 @@
+'use strict';
+
+const Rule = require('./base');
+
+module.exports = class NoMultipleEmptyLines extends Rule {
+  parseConfig(config) {
+    return config || {};
+  }
+
+  visitor() {
+    // Swallow the final newline, as some editors add it automatically and we don't want it to cause an issue
+    let allLines =
+      this.source[this.source.length - 1] === '' ? this.source.slice(0, -1) : this.source;
+
+    let first = true;
+
+    return {
+      Program: {
+        exit() {
+          if (!first) {
+            return;
+          }
+
+          let max = 'max' in this.config ? this.config.max : 1;
+
+          allLines
+
+            // Given a list of lines, first get a list of line numbers that are non-empty.
+            .reduce((nonEmptyLineNumbers, line, index) => {
+              if (line.trim()) {
+                nonEmptyLineNumbers.push(index + 1);
+              }
+              return nonEmptyLineNumbers;
+            }, [])
+
+            // Add a value at the end to allow trailing empty lines to be checked.
+            .concat(allLines.length + 1)
+
+            // Given two line numbers of non-empty lines, report the lines between if the difference is too large.
+            .reduce((lastLineNumber, lineNumber) => {
+              if (lineNumber - lastLineNumber - 1 > max) {
+                let message = `More than ${max} blank ${max === 1 ? 'line' : 'lines'} not allowed.`;
+
+                let loc = {
+                  start: { line: lastLineNumber + max, column: 0 },
+                  end: { line: lineNumber, column: 0 },
+                };
+
+                this.log({
+                  message,
+                  line: loc.start.line,
+                  column: loc.start.column,
+                  source: this.sourceForLoc(loc),
+                });
+              }
+
+              return lineNumber;
+            }, 0);
+
+          first = false;
+        },
+      },
+    };
+  }
+};


Test patch:
diff --git a/test/unit/rules/lint-no-multiple-empty-lines-test.js b/test/unit/rules/lint-no-multiple-empty-lines-test.js
new file mode 100644
index 00000000..6bff3d8b
--- /dev/null
+++ b/test/unit/rules/lint-no-multiple-empty-lines-test.js
@@ -0,0 +1,63 @@
+'use strict';
+
+const generateRuleTests = require('../../helpers/rule-test-harness');
+
+generateRuleTests({
+  name: 'no-multiple-empty-lines',
+
+  good: [
+    '<div>foo</div><div>bar</div>',
+    '<div>foo</div><div>bar</div>',
+    '<div>foo</div>\n<div>bar</div>',
+    '<div>foo</div>\r\n<div>bar</div>',
+    '<div>foo</div>\n\n<div>bar</div>',
+    '<div>foo</div>\r\n\r\n<div>bar</div>',
+    '\n<div>foo</div>\n\n<div>bar</div>\n',
+    {
+      config: { max: 2 },
+      template: '<div>foo</div>\n\n\n<div>bar</div>',
+    },
+    {
+      config: { max: 2 },
+      template: '<div>foo</div>\r\n\r\n\r\n<div>bar</div>',
+    },
+  ],
+
+  bad: [
+    {
+      template: '<div>foo</div>\n\n\n<div>bar</div>',
+
+      result: {
+        moduleId: 'layout.hbs',
+        message: 'More than 1 blank line not allowed.',
+        line: 2,
+        column: 0,
+        source: '\n\n',
+      },
+    },
+    {
+      template: '<div>foo</div>\n\n\n\n\n<div>bar</div>',
+
+      result: {
+        moduleId: 'layout.hbs',
+        message: 'More than 1 blank line not allowed.',
+        line: 2,
+        column: 0,
+        source: '\n\n\n\n',
+      },
+    },
+    {
+      config: { max: 3 },
+
+      template: '<div>foo</div>\n\n\n\n\n<div>bar</div>',
+
+      result: {
+        moduleId: 'layout.hbs',
+        message: 'More than 3 blank lines not allowed.',
+        line: 4,
+        column: 0,
+        source: '\n\n',
+      },
+    },
+  ],
+});