Repository: rwjblue/ember-template-lint
Pull request #234 (2017-06-28 19:37:40)

Linebreak style rule added
Added new rule to ensure consistent line breaks as discussed in issue #219.

initram: Let me know if I should try to squash some of those commits.
initram: I have rebased my changes to the current head of master and have added the spaces you asked for. I am guessing that you can squash the commits when you merge this in. (if you think it is a problem) Otherwise let me know and I will do it.

Patch:
diff --git a/docs/rule/linebreak-style.md b/docs/rule/linebreak-style.md
new file mode 100644
index 0000000..dc7cc84
--- /dev/null
+++ b/docs/rule/linebreak-style.md
@@ -0,0 +1,8 @@
+#### linebreak-style
+
+Having consistent linebreaks is important to make sure that the source code is rendered correctly in editors.
+
+This rule is configured with a boolean, or a string value:
+
+* boolean -- `true` for enabled in `unix` mode / `false` for disabling this rule
+* string -- `unix` for LF linebreaks / `windows` for CRLF linebreaks
\ No newline at end of file
diff --git a/lib/rules/base.js b/lib/rules/base.js
index 646b527..b93c8a1 100644
--- a/lib/rules/base.js
+++ b/lib/rules/base.js
@@ -15,7 +15,7 @@ const REMOVE_HANDLER = {};
 // see it already in the array and not log it.
 const loggedNodes = [];
 
-const reLineEnds = /(?:\r\n?|\n)/;
+const reLines = /(.*?(?:\r\n?|\n|$))/gm;
 const reWhitespace = /\s+/;
 const reTree = /^(.*)-tree$/;
 const reToggleRule = /^template-lint-(enable|disable)$/;
@@ -61,7 +61,7 @@ module.exports = class BaseRule {
     this._templateEnvironmentData = env;
 
     // split into a source array (allow windows and posix line endings)
-    this.source = env.rawSource.split(reLineEnds);
+    this.source = env.rawSource.match(reLines);
 
   }
 
@@ -562,6 +562,6 @@ module.exports = class BaseRule {
       }
     }
 
-    return string.join('\n');
+    return string.join('');
   }
 };
diff --git a/lib/rules/index.js b/lib/rules/index.js
index d4abf40..0c90e3e 100644
--- a/lib/rules/index.js
+++ b/lib/rules/index.js
@@ -19,5 +19,6 @@ module.exports = {
   'style-concatenation': require('./lint-style-concatenation'),
   'simple-unless': require('./lint-simple-unless'),
   'unused-block-params': require('./lint-unused-block-params'),
-  'no-duplicate-attributes': require('./lint-duplicate-attributes')
+  'no-duplicate-attributes': require('./lint-duplicate-attributes'),
+  'linebreak-style': require('./lint-linebreak-style')
 };
diff --git a/lib/rules/lint-linebreak-style.js b/lib/rules/lint-linebreak-style.js
new file mode 100644
index 0000000..96f2fa9
--- /dev/null
+++ b/lib/rules/lint-linebreak-style.js
@@ -0,0 +1,115 @@
+'use strict';
+
+/*
+ Enforce consistent linebreaks
+
+ The following values are valid configuration:
+
+   * boolean -- `true` for enabled (same as `unix`) / `false` for disabled
+   * string -- `unix` for LF linebreaks / `windows` for CRLF linebreaks
+ */
+
+const Rule = require('./base');
+
+const reLineEnds = /(\r\n?|\n)/g;
+const reLines = /(.*?(?:\r\n?|\n|$))/gm;
+
+const DEFAULT_CONFIG = {
+  linebreak: '\n'
+};
+
+function toUserString(value) {
+  return value.replace('\r', 'CR').replace('\n', 'LF');
+}
+
+module.exports = class LineBreakStyle extends Rule {
+  parseConfig(config) {
+    let configType = typeof config;
+
+    let errorMessage = 'The linebreak-style rule accepts one of the following values.\n ' +
+      '  * boolean - `true` to enable (unix style) / `false` to disable\n' +
+      '  * string -- `unix` for LF linebreaks / `windows` for CRLF linebreaks\n' +
+      '\nYou specified `' + JSON.stringify(config) + '`';
+
+    switch (configType) {
+    case 'boolean':
+      // if `true` use `DEFAULT_CONFIG`
+      return config ? DEFAULT_CONFIG : false;
+    case 'string':
+      switch (config) {
+      case 'unix':
+        return DEFAULT_CONFIG;
+      case 'windows':
+        return {linebreak: '\r\n'};
+      default:
+        throw new Error(errorMessage);
+      }
+    case 'undefined':
+      return false;
+    default:
+      throw new Error(errorMessage);
+    }
+  }
+
+  visitor() {
+    return {
+      TextNode(node) {
+        this._checkNodeAndLog(node);
+      },
+      MustacheStatement(node) {
+        this._checkNodeAndLog(node);
+      },
+      BlockStatement(node) {
+        this._checkNodeAndLog(node);
+      },
+      PartialStatement(node) {
+        this._checkNodeAndLog(node);
+      },
+      MustacheCommentStatement(node) {
+        this._checkNodeAndLog(node);
+      },
+      CommentStatement(node) {
+        this._checkNodeAndLog(node);
+      },
+      ElementNode(node) {
+        this._checkNodeAndLog(node);
+      }
+    };
+  }
+
+  _getWrongLinebreakFromLine(source) {
+    let goodLinebreak = this.config.linebreak;
+    let linebreaks = source.match(reLineEnds);
+
+    if(linebreaks) {
+      let linebreak = linebreaks[0];
+      if(linebreak && linebreak !== goodLinebreak) {
+        return linebreak;
+      }
+    }
+    
+    return null;
+  }
+
+  _checkNodeAndLog(node) {
+    if (!node.loc) { return; }
+    let nodeSource = this.sourceForNode(node);
+    
+    let lines = nodeSource.match(reLines);
+    for(var i = 0; i < lines.length; i++) {
+      let sourceLine = lines[i];
+      let wrongLineBreak = this._getWrongLinebreakFromLine(sourceLine);
+      if (wrongLineBreak) {
+        let wrongLineBreakForDisplay = toUserString(wrongLineBreak);
+        let goodLineBreakForDisplay = toUserString(this.config.linebreak);
+
+        this.log({
+          message: `Wrong linebreak used. Expected ${goodLineBreakForDisplay} but found ${wrongLineBreakForDisplay}`,
+          line: i + node.loc.start.line,
+          column: sourceLine.length - wrongLineBreak.length + (i === 0 ? node.loc.start.column : 0),
+          source: wrongLineBreak
+        });
+      }
+    }
+  }
+};


Test patch:
diff --git a/test/unit/rules/lint-linebreak-style-test.js b/test/unit/rules/lint-linebreak-style-test.js
new file mode 100644
index 0000000..f316a90
--- /dev/null
+++ b/test/unit/rules/lint-linebreak-style-test.js
@@ -0,0 +1,102 @@
+'use strict';
+
+const generateRuleTests = require('../../helpers/rule-test-harness');
+
+generateRuleTests({
+  name: 'linebreak-style',
+
+  config: true,
+
+  good: [
+    'testing this',
+    'testing \n this',
+    {
+      config: 'windows',
+      template: 'testing\r\nthis'
+    },
+    {
+      config: 'unix',
+      template: 'testing\nthis'
+    },
+    {
+      config: true,
+      template: 'testing\nthis'
+    },
+  ],
+
+  bad: [
+    {
+      template: '\r\n',
+
+      result: {
+        rule: 'linebreak-style',
+        moduleId: 'layout.hbs',
+        message: 'Wrong linebreak used. Expected LF but found CRLF',
+        line: 1,
+        column: 0,
+        source: '\r\n'
+      }
+    },
+    {
+      template: '{{#if test}}\r\n{{/if}}',
+
+      result: {
+        rule: 'linebreak-style',
+        moduleId: 'layout.hbs',
+        message: 'Wrong linebreak used. Expected LF but found CRLF',
+        line: 1,
+        column: 12,
+        source: '\r\n'
+      }
+    },
+    {
+      template: '{{blah}}\r\n{{blah}}',
+
+      result: {
+        rule: 'linebreak-style',
+        moduleId: 'layout.hbs',
+        message: 'Wrong linebreak used. Expected LF but found CRLF',
+        line: 1,
+        column: 8,
+        source: '\r\n'
+      }
+    },
+    {
+      template: '{{blah}}\r\n',
+
+      result: {
+        rule: 'linebreak-style',
+        moduleId: 'layout.hbs',
+        message: 'Wrong linebreak used. Expected LF but found CRLF',
+        line: 1,
+        column: 8,
+        source: '\r\n'
+      }
+    },
+    {
+      template: '{{blah arg="\r\n"}}',
+
+      result: {
+        rule: 'linebreak-style',
+        moduleId: 'layout.hbs',
+        message: 'Wrong linebreak used. Expected LF but found CRLF',
+        line: 1,
+        column: 12,
+        source: '\r\n'
+      }
+    },
+    {
+      config: 'windows',
+      template: '\n',
+
+      result: {
+        rule: 'linebreak-style',
+        moduleId: 'layout.hbs',
+        message: 'Wrong linebreak used. Expected CRLF but found LF',
+        line: 1,
+        column: 0,
+        source: '\n'
+      }
+    }
+  ]
+});