Repository: yannickcr/eslint-plugin-react
Pull request #1497 (2017-10-25 05:36:08)

New rule: jsx-one-expression-per-line
From issue https://github.com/yannickcr/eslint-plugin-react/issues/1491.

Tested against a small react-native app with 50-100 components and it works great.


Patch:
diff --git a/index.js b/index.js
index 13b72fdd..8a8df3b7 100644
--- a/index.js
+++ b/index.js
@@ -22,6 +22,7 @@ const allRules = {
   'jsx-indent-props': require('./lib/rules/jsx-indent-props'),
   'jsx-key': require('./lib/rules/jsx-key'),
   'jsx-max-props-per-line': require('./lib/rules/jsx-max-props-per-line'),
+  'jsx-one-expression-per-line': require('./lib/rules/jsx-one-expression-per-line'),
   'jsx-no-bind': require('./lib/rules/jsx-no-bind'),
   'jsx-no-comment-textnodes': require('./lib/rules/jsx-no-comment-textnodes'),
   'jsx-no-duplicate-props': require('./lib/rules/jsx-no-duplicate-props'),
diff --git a/lib/rules/jsx-one-expression-per-line.js b/lib/rules/jsx-one-expression-per-line.js
new file mode 100644
index 00000000..0ee17462
--- /dev/null
+++ b/lib/rules/jsx-one-expression-per-line.js
@@ -0,0 +1,181 @@
+/**
+ * @fileoverview Limit to one element tag per line in JSX
+ * @author Mark Ivan Allen <Vydia.com>
+ */
+
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'Limit to one element tag per line in JSX',
+      category: 'Stylistic Issues',
+      recommended: false
+    },
+    fixable: 'whitespace',
+    schema: []
+  },
+
+  create: function (context) {
+    const sourceCode = context.getSourceCode();
+
+    function nodeKey (node) {
+      return `${node.loc.start.line},${node.loc.start.column}`;
+    }
+
+    function nodeDescriptor (n) {
+      return n.openingElement ? n.openingElement.name.name : sourceCode.getText(n).replace(/\n/g, '');
+    }
+
+    return {
+      JSXElement: function (node) {
+        const children = node.children;
+
+        if (!children || !children.length) {
+          return;
+        }
+
+        const openingElement = node.openingElement;
+        const closingElement = node.closingElement;
+        const openingElementEndLine = openingElement.loc.end.line;
+        const closingElementStartLine = closingElement.loc.start.line;
+
+        const childrenGroupedByLine = {};
+        const fixDetailsByNode = {};
+
+        children.forEach(child => {
+          let countNewLinesBeforeContent = 0;
+          let countNewLinesAfterContent = 0;
+
+          if (child.type === 'Literal') {
+            if (/^\s*$/.test(child.raw)) {
+              return;
+            }
+
+            countNewLinesBeforeContent = (child.raw.match(/^ *\n/g) || []).length;
+            countNewLinesAfterContent = (child.raw.match(/\n *$/g) || []).length;
+          }
+
+          const startLine = child.loc.start.line + countNewLinesBeforeContent;
+          const endLine = child.loc.end.line - countNewLinesAfterContent;
+
+          if (startLine === endLine) {
+            if (!childrenGroupedByLine[startLine]) {
+              childrenGroupedByLine[startLine] = [];
+            }
+            childrenGroupedByLine[startLine].push(child);
+          } else {
+            if (!childrenGroupedByLine[startLine]) {
+              childrenGroupedByLine[startLine] = [];
+            }
+            childrenGroupedByLine[startLine].push(child);
+            if (!childrenGroupedByLine[endLine]) {
+              childrenGroupedByLine[endLine] = [];
+            }
+            childrenGroupedByLine[endLine].push(child);
+          }
+        });
+
+        Object.keys(childrenGroupedByLine).forEach(_line => {
+          const line = parseInt(_line, 10);
+          const firstIndex = 0;
+          const lastIndex = childrenGroupedByLine[line].length - 1;
+
+          childrenGroupedByLine[line].forEach((child, i) => {
+            let prevChild;
+            let nextChild;
+
+            if (i === firstIndex) {
+              if (line === openingElementEndLine) {
+                prevChild = openingElement;
+              }
+            } else {
+              prevChild = childrenGroupedByLine[line][i - 1];
+            }
+
+            if (i === lastIndex) {
+              if (line === closingElementStartLine) {
+                nextChild = closingElement;
+              }
+            } else {
+              // We don't need to append a trailing because the next child will prepend a leading.
+              // nextChild = childrenGroupedByLine[line][i + 1];
+            }
+
+            function spaceBetweenPrev () {
+              return (prevChild.type === 'Literal' && / $/.test(prevChild.raw)) ||
+                (child.type === 'Literal' && /^ /.test(child.raw)) ||
+                sourceCode.isSpaceBetweenTokens(prevChild, child);
+            }
+
+            function spaceBetweenNext () {
+              return (nextChild.type === 'Literal' && /^ /.test(nextChild.raw)) ||
+                (child.type === 'Literal' && / $/.test(child.raw)) ||
+                sourceCode.isSpaceBetweenTokens(child, nextChild);
+            }
+
+            if (!prevChild && !nextChild) {
+              return;
+            }
+
+            const source = sourceCode.getText(child);
+            const leadingSpace = !!(prevChild && spaceBetweenPrev());
+            const trailingSpace = !!(nextChild && spaceBetweenNext());
+            const leadingNewLine = !!prevChild;
+            const trailingNewLine = !!nextChild;
+
+            const key = nodeKey(child);
+
+            if (!fixDetailsByNode[key]) {
+              fixDetailsByNode[key] = {
+                node: child,
+                source: source,
+                descriptor: nodeDescriptor(child)
+              };
+            }
+
+            if (leadingSpace) {
+              fixDetailsByNode[key].leadingSpace = true;
+            }
+            if (leadingNewLine) {
+              fixDetailsByNode[key].leadingNewLine = true;
+            }
+            if (trailingNewLine) {
+              fixDetailsByNode[key].trailingNewLine = true;
+            }
+            if (trailingSpace) {
+              fixDetailsByNode[key].trailingSpace = true;
+            }
+          });
+        });
+
+        Object.keys(fixDetailsByNode).forEach(key => {
+          const details = fixDetailsByNode[key];
+
+          const nodeToReport = details.node;
+          const descriptor = details.descriptor;
+          const source = details.source.replace(/(^ +| +(?=\n)*$)/g, '');
+
+          const leadingSpaceString = details.leadingSpace ? '\n{\' \'}' : '';
+          const trailingSpaceString = details.trailingSpace ? '{\' \'}\n' : '';
+          const leadingNewLineString = details.leadingNewLine ? '\n' : '';
+          const trailingNewLineString = details.trailingNewLine ? '\n' : '';
+
+          const replaceText = `${leadingSpaceString}${leadingNewLineString}${source}${trailingNewLineString}${trailingSpaceString}`;
+
+          context.report({
+            node: nodeToReport,
+            message: `\`${descriptor}\` must be placed on a new line`,
+            fix: function (fixer) {
+              return fixer.replaceText(nodeToReport, replaceText);
+            }
+          });
+        });
+      }
+    };
+  }
+};


Test patch:
diff --git a/tests/lib/rules/jsx-one-expression-per-line.js b/tests/lib/rules/jsx-one-expression-per-line.js
new file mode 100644
index 00000000..ad9b3baf
--- /dev/null
+++ b/tests/lib/rules/jsx-one-expression-per-line.js
@@ -0,0 +1,857 @@
+/**
+ * @fileoverview Limit to one element tag per line in JSX
+ * @author Mark Ivan Allen <Vydia.com>
+ */
+
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/jsx-one-expression-per-line');
+const RuleTester = require('eslint').RuleTester;
+
+const parserOptions = {
+  ecmaVersion: 8,
+  sourceType: 'module',
+  ecmaFeatures: {
+    experimentalObjectRestSpread: true,
+    jsx: true
+  }
+};
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({parserOptions});
+ruleTester.run('jsx-one-expression-per-line', rule, {
+  valid: [{
+    code: '<App />'
+  }, {
+    code: '<App></App>'
+  }, {
+    code: '<App foo="bar" />'
+  }, {
+    code: [
+      '<App>',
+      '  <Foo />',
+      '</App>'
+    ].join('\n')
+  }, {
+    code: [
+      '<App>',
+      '  <Foo />',
+      '  <Bar />',
+      '</App>'
+    ].join('\n')
+  }, {
+    code: [
+      '<App>',
+      '  <Foo></Foo>',
+      '</App>'
+    ].join('\n')
+  }, {
+    code: [
+      '<App>',
+      '  foo bar baz  whatever  ',
+      '</App>'
+    ].join('\n')
+  }, {
+    code: [
+      '<App>',
+      '  <Foo>',
+      '  </Foo>',
+      '</App>'
+    ].join('\n')
+  }, {
+    code: [
+      '<App',
+      '  foo="bar"',
+      '>',
+      '<Foo />',
+      '</App>'
+    ].join('\n')
+  }, {
+    code: [
+      '<',
+      'App',
+      '>',
+      '  <',
+      '    Foo',
+      '  />',
+      '</',
+      'App',
+      '>'
+    ].join('\n')
+  }],
+
+  invalid: [{
+    code: '<App>{"foo"}</App>',
+    output: [
+      '<App>',
+      '{"foo"}',
+      '</App>'
+    ].join('\n'),
+    errors: [{message: '`{"foo"}` must be placed on a new line'}],
+    parserOptions: parserOptions
+  }, {
+    code: '<App>foo</App>',
+    output: [
+      '<App>',
+      'foo',
+      '</App>'
+    ].join('\n'),
+    errors: [{message: '`foo` must be placed on a new line'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<div>',
+      '  foo {"bar"}',
+      '</div>'
+    ].join('\n'),
+    output: [
+      '<div>',
+      '  foo ',
+      '{\' \'}',
+      '{"bar"}',
+      '</div>'
+    ].join('\n'),
+    errors: [
+      {message: '`{"bar"}` must be placed on a new line'}
+    ],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<div>',
+      '  {"foo"} bar',
+      '</div>'
+    ].join('\n'),
+    output: [
+      '<div>',
+      '  {"foo"}',
+      '{\' \'}',
+      'bar',
+      '</div>'
+    ].join('\n'),
+    errors: [
+      {message: '` bar` must be placed on a new line'}
+    ],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App>',
+      '  <Foo /><Bar />',
+      '</App>'
+    ].join('\n'),
+    output: [
+      '<App>',
+      '  <Foo />',
+      '<Bar />',
+      '</App>'
+    ].join('\n'),
+    errors: [{message: '`Bar` must be placed on a new line'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<div>',
+      '  <span />foo',
+      '</div>'
+    ].join('\n'),
+    output: [
+      '<div>',
+      '  <span />',
+      'foo',
+      '</div>'
+    ].join('\n'),
+    errors: [{message: '`foo` must be placed on a new line'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<div>',
+      '  <span />{"foo"}',
+      '</div>'
+    ].join('\n'),
+    output: [
+      '<div>',
+      '  <span />',
+      '{"foo"}',
+      '</div>'
+    ].join('\n'),
+    errors: [{message: '`{"foo"}` must be placed on a new line'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<div>',
+      '  {"foo"} { I18n.t(\'baz\') }',
+      '</div>'
+    ].join('\n'),
+    output: [
+      '<div>',
+      '  {"foo"} ',
+      '{\' \'}',
+      '{ I18n.t(\'baz\') }',
+      '</div>'
+    ].join('\n'),
+    errors: [
+      {message: '`{ I18n.t(\'baz\') }` must be placed on a new line'}
+    ],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<Text style={styles.foo}>{ bar } <Text/> { I18n.t(\'baz\') }</Text>'
+    ].join('\n'),
+    output: [
+      '<Text style={styles.foo}>',
+      '{ bar } ',
+      '{\' \'}',
+      '<Text/> ',
+      '{\' \'}',
+      '{ I18n.t(\'baz\') }',
+      '</Text>'
+    ].join('\n'),
+    errors: [
+      {message: '`{ bar }` must be placed on a new line'},
+      {message: '`Text` must be placed on a new line'},
+      {message: '`{ I18n.t(\'baz\') }` must be placed on a new line'}
+    ],
+    parserOptions: parserOptions
+
+  }, {
+    code: [
+      '<Text style={styles.foo}> <Bar/> <Baz/></Text>'
+    ].join('\n'),
+    output: [
+      '<Text style={styles.foo}> ',
+      '{\' \'}',
+      '<Bar/> ',
+      '{\' \'}',
+      '<Baz/>',
+      '</Text>'
+    ].join('\n'),
+    errors: [
+      {message: '`Bar` must be placed on a new line'},
+      {message: '`Baz` must be placed on a new line'}
+    ],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<Text style={styles.foo}> <Bar/> <Baz/> <Bunk/> <Bruno/> </Text>'
+    ].join('\n'),
+    output: [
+      '<Text style={styles.foo}> ',
+      '{\' \'}',
+      '<Bar/> ',
+      '{\' \'}',
+      '<Baz/> ',
+      '{\' \'}',
+      '<Bunk/> ',
+      '{\' \'}',
+      '<Bruno/>',
+      '{\' \'}',
+      ' </Text>'
+    ].join('\n'),
+    errors: [
+      {message: '`Bar` must be placed on a new line'},
+      {message: '`Baz` must be placed on a new line'},
+      {message: '`Bunk` must be placed on a new line'},
+      {message: '`Bruno` must be placed on a new line'}
+    ],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<Text style={styles.foo}> <Bar /></Text>'
+    ].join('\n'),
+    output: [
+      '<Text style={styles.foo}> ',
+      '{\' \'}',
+      '<Bar />',
+      '</Text>'
+    ].join('\n'),
+    errors: [
+      {message: '`Bar` must be placed on a new line'}
+    ],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<Text style={styles.foo}> <Bar />',
+      '</Text>'
+    ].join('\n'),
+    output: [
+      '<Text style={styles.foo}> ',
+      '{\' \'}',
+      '<Bar />',
+      '</Text>'
+    ].join('\n'),
+    errors: [
+      {message: '`Bar` must be placed on a new line'}
+    ],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<Text style={styles.foo}>',
+      '  <Bar /> <Baz />',
+      '</Text>'
+    ].join('\n'),
+    output: [
+      '<Text style={styles.foo}>',
+      '  <Bar /> ',
+      '{\' \'}',
+      '<Baz />',
+      '</Text>'
+    ].join('\n'),
+    errors: [
+      {message: '`Baz` must be placed on a new line'}
+    ],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<Text style={styles.foo}>',
+      '  { bar } { I18n.t(\'baz\') }',
+      '</Text>'
+    ].join('\n'),
+    output: [
+      '<Text style={styles.foo}>',
+      '  { bar } ',
+      '{\' \'}',
+      '{ I18n.t(\'baz\') }',
+      '</Text>'
+    ].join('\n'),
+    errors: [
+      {message: '`{ I18n.t(\'baz\') }` must be placed on a new line'}
+    ],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<div>',
+      '  foo<input />',
+      '</div>'
+    ].join('\n'),
+    output: [
+      '<div>',
+      '  foo',
+      '<input />',
+      '</div>'
+    ].join('\n'),
+    errors: [
+      {message: '`input` must be placed on a new line'}
+    ],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<div>',
+      '  {"foo"}<span />',
+      '</div>'
+    ].join('\n'),
+    output: [
+      '<div>',
+      '  {"foo"}',
+      '<span />',
+      '</div>'
+    ].join('\n'),
+    errors: [{message: '`span` must be placed on a new line'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<div>',
+      '  foo <input />',
+      '</div>'
+    ].join('\n'),
+    output: [
+      '<div>',
+      '  foo ',
+      '{\' \'}',
+      '<input />',
+      '</div>'
+    ].join('\n'),
+    errors: [{message: '`input` must be placed on a new line'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<div>',
+      '  <input /> foo',
+      '</div>'
+    ].join('\n'),
+    output: [
+      '<div>',
+      '  <input />',
+      '{\' \'}',
+      'foo',
+      '</div>'
+    ].join('\n'),
+    errors: [{message: '` foo` must be placed on a new line'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<div>',
+      '  <span /> <input />',
+      '</div>'
+    ].join('\n'),
+    output: [
+      '<div>',
+      '  <span /> ',
+      '{\' \'}',
+      '<input />',
+      '</div>'
+    ].join('\n'),
+    errors: [
+      {message: '`input` must be placed on a new line'}
+    ],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<div>',
+      '  <span />',
+      '{\' \'}<input />',
+      '</div>'
+    ].join('\n'),
+    output: [
+      '<div>',
+      '  <span />',
+      '{\' \'}',
+      '<input />',
+      '</div>'
+    ].join('\n'),
+    errors: [
+      {message: '`input` must be placed on a new line'}
+    ],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<div>',
+      '  {"foo"} <input />',
+      '</div>'
+    ].join('\n'),
+    output: [
+      '<div>',
+      '  {"foo"} ',
+      '{\' \'}',
+      '<input />',
+      '</div>'
+    ].join('\n'),
+    errors: [
+      {message: '`input` must be placed on a new line'}
+    ],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<div>',
+      '  {"foo"} bar',
+      '</div>'
+    ].join('\n'),
+    output: [
+      '<div>',
+      '  {"foo"}',
+      '{\' \'}',
+      'bar',
+      '</div>'
+    ].join('\n'),
+    errors: [{message: '` bar` must be placed on a new line'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<div>',
+      '  foo {"bar"}',
+      '</div>'
+    ].join('\n'),
+    output: [
+      '<div>',
+      '  foo ',
+      '{\' \'}',
+      '{"bar"}',
+      '</div>'
+    ].join('\n'),
+    errors: [
+      {message: '`{"bar"}` must be placed on a new line'}
+    ],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<div>',
+      '  <input /> {"foo"}',
+      '</div>'
+    ].join('\n'),
+    output: [
+      '<div>',
+      '  <input /> ',
+      '{\' \'}',
+      '{"foo"}',
+      '</div>'
+    ].join('\n'),
+    errors: [
+      {message: '`{"foo"}` must be placed on a new line'}
+    ],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App>',
+      '  <Foo></Foo><Bar></Bar>',
+      '</App>'
+    ].join('\n'),
+    output: [
+      '<App>',
+      '  <Foo></Foo>',
+      '<Bar></Bar>',
+      '</App>'
+    ].join('\n'),
+    errors: [{message: '`Bar` must be placed on a new line'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App>',
+      '<Foo></Foo></App>'
+    ].join('\n'),
+    output: [
+      '<App>',
+      '<Foo></Foo>',
+      '</App>'
+    ].join('\n'),
+    errors: [{message: '`Foo` must be placed on a new line'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App><Foo />',
+      '</App>'
+    ].join('\n'),
+    output: [
+      '<App>',
+      '<Foo />',
+      '</App>'
+    ].join('\n'),
+    errors: [{message: '`Foo` must be placed on a new line'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App>',
+      '<Foo/></App>'
+    ].join('\n'),
+    output: [
+      '<App>',
+      '<Foo/>',
+      '</App>'
+    ].join('\n'),
+    errors: [{message: '`Foo` must be placed on a new line'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App><Foo',
+      '/>',
+      '</App>'
+    ].join('\n'),
+    output: [
+      '<App>',
+      '<Foo',
+      '/>',
+      '</App>'
+    ].join('\n'),
+    errors: [{message: '`Foo` must be placed on a new line'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App',
+      '>',
+      '<Foo /></App>'
+    ].join('\n'),
+    output: [
+      '<App',
+      '>',
+      '<Foo />',
+      '</App>'
+    ].join('\n'),
+    errors: [{message: '`Foo` must be placed on a new line'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App',
+      '>',
+      '<Foo',
+      '/></App>'
+    ].join('\n'),
+    output: [
+      '<App',
+      '>',
+      '<Foo',
+      '/>',
+      '</App>'
+    ].join('\n'),
+    errors: [{message: '`Foo` must be placed on a new line'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App',
+      '><Foo />',
+      '</App>'
+    ].join('\n'),
+    output: [
+      '<App',
+      '>',
+      '<Foo />',
+      '</App>'
+    ].join('\n'),
+    errors: [{message: '`Foo` must be placed on a new line'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App>',
+      '  <Foo></Foo',
+      '></App>'
+    ].join('\n'),
+    output: [
+      '<App>',
+      '  <Foo></Foo',
+      '>',
+      '</App>'
+    ].join('\n'),
+    errors: [{message: '`Foo` must be placed on a new line'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App>',
+      '  <Foo></',
+      'Foo></App>'
+    ].join('\n'),
+    output: [
+      '<App>',
+      '  <Foo></',
+      'Foo>',
+      '</App>'
+    ].join('\n'),
+    errors: [{message: '`Foo` must be placed on a new line'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App>',
+      '  <Foo></',
+      'Foo></App>'
+    ].join('\n'),
+    output: [
+      '<App>',
+      '  <Foo></',
+      'Foo>',
+      '</App>'
+    ].join('\n'),
+    errors: [{message: '`Foo` must be placed on a new line'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App>',
+      '  <Foo></',
+      'Foo><Bar />',
+      '</App>'
+    ].join('\n'),
+    output: [
+      '<App>',
+      '  <Foo></',
+      'Foo>',
+      '<Bar />',
+      '</App>'
+    ].join('\n'),
+    errors: [{message: '`Bar` must be placed on a new line'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App>',
+      '  <Foo>',
+      '    <Bar /></Foo>',
+      '</App>'
+    ].join('\n'),
+    output: [
+      '<App>',
+      '  <Foo>',
+      '    <Bar />',
+      '</Foo>',
+      '</App>'
+    ].join('\n'),
+    errors: [{message: '`Bar` must be placed on a new line'}],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App>',
+      '  <Foo>',
+      '    <Bar> baz </Bar>',
+      '  </Foo>',
+      '</App>'
+    ].join('\n'),
+    output: [
+      '<App>',
+      '  <Foo>',
+      '    <Bar>',
+      '{\' \'}',
+      'baz',
+      '{\' \'}',
+      '</Bar>',
+      '  </Foo>',
+      '</App>'
+    ].join('\n'),
+    errors: [
+      {message: '` baz ` must be placed on a new line'}
+    ],
+    parserOptions: parserOptions
+  }, {
+    // Would be nice to handle in one pass, but multipass works fine.
+    code: [
+      '<App>',
+      '  foo {"bar"} baz',
+      '</App>'
+    ].join('\n'),
+    output: [
+      '<App>',
+      '  foo ',
+      '{\' \'}',
+      '{"bar"} baz',
+      '</App>'
+    ].join('\n'),
+    errors: [
+      {message: '`{"bar"}` must be placed on a new line'},
+      {message: '` baz` must be placed on a new line'}
+    ],
+    parserOptions: parserOptions
+  }, {
+    // Would be nice to handle in one pass, but multipass works fine.
+    code: [
+      '<App>',
+      '  foo {"bar"}',
+      '</App>'
+    ].join('\n'),
+    output: [
+      '<App>',
+      '  foo ',
+      '{\' \'}',
+      '{"bar"}',
+      '</App>'
+    ].join('\n'),
+    errors: [
+      {message: '`{"bar"}` must be placed on a new line'}
+    ],
+    parserOptions: parserOptions
+  }, {
+    // Would be nice to handle in one pass, but multipass works fine.
+    code: [
+      '<App>',
+      '  foo ',
+      '{\' \'}',
+      '{"bar"} baz',
+      '</App>'
+    ].join('\n'),
+    output: [
+      '<App>',
+      '  foo ',
+      '{\' \'}',
+      '{"bar"}',
+      '{\' \'}',
+      'baz',
+      '</App>'
+    ].join('\n'),
+    errors: [
+      {message: '` baz` must be placed on a new line'}
+    ],
+    parserOptions: parserOptions
+  }, {
+    // Would be nice to handle in one pass, but multipass works fine.
+    code: [
+      '<App>',
+      '',
+      '  foo {"bar"} baz',
+      '',
+      '</App>'
+    ].join('\n'),
+    output: [
+      '<App>',
+      '',
+      '  foo ',
+      '{\' \'}',
+      '{"bar"} baz',
+      '',
+      '</App>'
+    ].join('\n'),
+    errors: [
+      {message: '`{"bar"}` must be placed on a new line'},
+      {message: '` baz` must be placed on a new line'}
+    ],
+    parserOptions: parserOptions
+  }, {
+    // Would be nice to handle in one pass, but multipass works fine.
+    code: [
+      '<App>',
+      '',
+      '  foo ',
+      '{\' \'}',
+      '{"bar"} baz',
+      '',
+      '</App>'
+    ].join('\n'),
+    output: [
+      '<App>',
+      '',
+      '  foo ',
+      '{\' \'}',
+      '{"bar"}',
+      '{\' \'}',
+      'baz',
+      '',
+      '</App>'
+    ].join('\n'),
+    errors: [
+      {message: '` baz` must be placed on a new line'}
+    ],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App>{',
+      '  foo',
+      '}</App>'
+    ].join('\n'),
+    output: [
+      '<App>',
+      '{',
+      '  foo',
+      '}',
+      '</App>'
+    ].join('\n'),
+    errors: [
+      {message: '`{  foo}` must be placed on a new line'}
+    ],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App> {',
+      '  foo',
+      '} </App>'
+    ].join('\n'),
+    output: [
+      '<App> ',
+      '{\' \'}',
+      '{',
+      '  foo',
+      '}',
+      '{\' \'}',
+      ' </App>'
+    ].join('\n'),
+    errors: [
+      {message: '`{  foo}` must be placed on a new line'}
+    ],
+    parserOptions: parserOptions
+  }, {
+    code: [
+      '<App> ',
+      '{\' \'}',
+      '{',
+      '  foo',
+      '} </App>'
+    ].join('\n'),
+    output: [
+      '<App> ',
+      '{\' \'}',
+      '{',
+      '  foo',
+      '}',
+      '{\' \'}',
+      ' </App>'
+    ].join('\n'),
+    errors: [
+      {message: '`{  foo}` must be placed on a new line'}
+    ],
+    parserOptions: parserOptions
+  }]
+});