Repository: vuejs/eslint-plugin-vue
Pull request #1326 (2020-10-10 02:15:38)

Add `vue/array-bracket-newline` rule
`vue/array-bracket-newline` rule that applies `array-bracket-newline` rule to expressions in <template>.


Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 1ffb102f..f2d9b847 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -281,13 +281,14 @@ For example:
 ```json
 {
   "rules": {
-    "vue/component-name-in-template-casing": "error"
+    "vue/block-tag-newline": "error"
   }
 }
 ```
 
 | Rule ID | Description |    |
 |:--------|:------------|:---|
+| [vue/block-tag-newline](./block-tag-newline.md) | enforce line breaks after opening and before closing block-level tags | :wrench: |
 | [vue/component-name-in-template-casing](./component-name-in-template-casing.md) | enforce specific casing for the component naming style in template | :wrench: |
 | [vue/html-comment-content-newline](./html-comment-content-newline.md) | enforce unified line brake in HTML comments | :wrench: |
 | [vue/html-comment-content-spacing](./html-comment-content-spacing.md) | enforce unified spacing in HTML comments | :wrench: |
@@ -325,6 +326,7 @@ The following rules extend the rules provided by ESLint itself and apply them to
 
 | Rule ID | Description |    |
 |:--------|:------------|:---|
+| [vue/array-bracket-newline](./array-bracket-newline.md) | enforce linebreaks after opening and before closing array brackets | :wrench: |
 | [vue/array-bracket-spacing](./array-bracket-spacing.md) | enforce consistent spacing inside array brackets | :wrench: |
 | [vue/arrow-spacing](./arrow-spacing.md) | enforce consistent spacing before and after the arrow in arrow functions | :wrench: |
 | [vue/block-spacing](./block-spacing.md) | disallow or enforce spaces inside of blocks after opening block and before closing block | :wrench: |
diff --git a/docs/rules/array-bracket-newline.md b/docs/rules/array-bracket-newline.md
new file mode 100644
index 00000000..2bc2a088
--- /dev/null
+++ b/docs/rules/array-bracket-newline.md
@@ -0,0 +1,25 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/array-bracket-newline
+description: enforce linebreaks after opening and before closing array brackets
+---
+# vue/array-bracket-newline
+> enforce linebreaks after opening and before closing array brackets
+
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
+This rule is the same rule as core [array-bracket-newline] rule but it applies to the expressions in `<template>`.
+
+## :books: Further Reading
+
+- [array-bracket-newline]
+
+[array-bracket-newline]: https://eslint.org/docs/rules/array-bracket-newline
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/array-bracket-newline.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/array-bracket-newline.js)
+
+<sup>Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/array-bracket-newline)</sup>
diff --git a/docs/rules/block-tag-newline.md b/docs/rules/block-tag-newline.md
new file mode 100644
index 00000000..a3ab5f96
--- /dev/null
+++ b/docs/rules/block-tag-newline.md
@@ -0,0 +1,161 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/block-tag-newline
+description: enforce line breaks after opening and before closing block-level tags
+---
+# vue/block-tag-newline
+> enforce line breaks after opening and before closing block-level tags
+
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
+## :book: Rule Details
+
+This rule enforces a line break (or no line break) after opening and before closing block tags.
+
+<eslint-code-block fix :rules="{'vue/block-tag-newline': ['error']}">
+
+```vue
+<!-- ✓ GOOD -->
+<template><input></template>
+
+<script>
+export default {}
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block fix :rules="{'vue/block-tag-newline': ['error']}">
+
+```vue
+<!-- ✗ BAD -->
+<template>
+<input></template>
+
+<script>
+export default {}</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+```json
+{
+  "vue/block-tag-newline": ["error", {
+    "singleline": "always" | "never" | "consistent" | "ignore",
+    "multiline": "always" | "never" | "consistent" | "ignore",
+    "maxEmptyLines": 0,
+    "blocks": {
+      "template": {
+        "singleline": "always" | "never" | "consistent" | "ignore",
+        "multiline": "always" | "never" | "consistent" | "ignore",
+        "maxEmptyLines": 0,
+      },
+      "script": {
+        "singleline": "always" | "never" | "consistent" | "ignore",
+        "multiline": "always" | "never" | "consistent" | "ignore",
+        "maxEmptyLines": 0,
+      },
+      "my-block": {
+        "singleline": "always" | "never" | "consistent" | "ignore",
+        "multiline": "always" | "never" | "consistent" | "ignore",
+        "maxEmptyLines": 0,
+      }
+    }
+  }]
+}
+```
+
+- `singleline` ... the configuration for single-line blocks.
+  - `"consistent"` ... (default) requires consistent usage of line breaks for each pair of tags. It reports an error if one tag in the pair has a linebreak inside it and the other tag does not.
+  - `"always"` ... require one line break after opening and before closing block tags.
+  - `"never"` ... disallow line breaks after opening and before closing block tags.
+- `multiline` ... the configuration for multi-line blocks.
+  - `"consistent"` ... requires consistent usage of line breaks for each pair of tags. It reports an error if one tag in the pair has a linebreak inside it and the other tag does not.
+  - `"always"` ... (default) require one line break after opening and before closing block tags.
+  - `"never"` ... disallow line breaks after opening and before closing block tags.
+- `maxEmptyLines` ... specifies the maximum number of empty lines allowed. default 0.
+- `blocks` ... specifies for each block name.
+
+### `{ "singleline": "never", "multiline": "always" }`
+
+<eslint-code-block fix :rules="{'vue/block-tag-newline': ['error', { 'singleline': 'never', 'multiline': 'always' }]}">
+
+```vue
+<!-- ✓ GOOD -->
+<template><input></template>
+
+<script>
+export default {
+}
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block fix :rules="{'vue/block-tag-newline': ['error', { 'singleline': 'never', 'multiline': 'always' }]}">
+
+```vue
+<!-- ✗ BAD -->
+<template>
+<input>
+</template>
+
+<script>export default {
+}</script>
+```
+
+</eslint-code-block>
+
+### `{ "singleline": "always", "multiline": "always", "maxEmptyLines": 1 }`
+
+<eslint-code-block fix :rules="{'vue/block-tag-newline': ['error', { 'singleline': 'always', 'multiline': 'always', 'maxEmptyLines': 1 }]}">
+
+```vue
+<!-- ✓ GOOD -->
+<template>
+
+<input>
+
+</template>
+
+<script>
+
+export default {
+}
+
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block fix :rules="{'vue/block-tag-newline': ['error', { 'singleline': 'always', 'multiline': 'always', 'maxEmptyLines': 1 }]}">
+
+```vue
+<!-- ✗ BAD -->
+<template>
+
+
+<input>
+
+
+</template>
+
+<script>
+
+
+export default {
+}
+
+
+</script>
+```
+
+</eslint-code-block>
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/block-tag-newline.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/block-tag-newline.js)
diff --git a/lib/configs/no-layout-rules.js b/lib/configs/no-layout-rules.js
index be534fd3..59c56af8 100644
--- a/lib/configs/no-layout-rules.js
+++ b/lib/configs/no-layout-rules.js
@@ -5,9 +5,11 @@
  */
 module.exports = {
   rules: {
+    'vue/array-bracket-newline': 'off',
     'vue/array-bracket-spacing': 'off',
     'vue/arrow-spacing': 'off',
     'vue/block-spacing': 'off',
+    'vue/block-tag-newline': 'off',
     'vue/brace-style': 'off',
     'vue/comma-dangle': 'off',
     'vue/comma-spacing': 'off',
diff --git a/lib/index.js b/lib/index.js
index eb6a4291..7755e9f1 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -7,11 +7,13 @@
 
 module.exports = {
   rules: {
+    'array-bracket-newline': require('./rules/array-bracket-newline'),
     'array-bracket-spacing': require('./rules/array-bracket-spacing'),
     'arrow-spacing': require('./rules/arrow-spacing'),
     'attribute-hyphenation': require('./rules/attribute-hyphenation'),
     'attributes-order': require('./rules/attributes-order'),
     'block-spacing': require('./rules/block-spacing'),
+    'block-tag-newline': require('./rules/block-tag-newline'),
     'brace-style': require('./rules/brace-style'),
     camelcase: require('./rules/camelcase'),
     'comma-dangle': require('./rules/comma-dangle'),
diff --git a/lib/rules/array-bracket-newline.js b/lib/rules/array-bracket-newline.js
new file mode 100644
index 00000000..c667fc7e
--- /dev/null
+++ b/lib/rules/array-bracket-newline.js
@@ -0,0 +1,11 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const { wrapCoreRule } = require('../utils')
+
+// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
+module.exports = wrapCoreRule('array-bracket-newline', {
+  skipDynamicArguments: true
+})
diff --git a/lib/rules/block-tag-newline.js b/lib/rules/block-tag-newline.js
new file mode 100644
index 00000000..c88a36c2
--- /dev/null
+++ b/lib/rules/block-tag-newline.js
@@ -0,0 +1,375 @@
+/**
+ * @fileoverview Enforce line breaks style after opening and before closing block-level tags.
+ * @author Yosuke Ota
+ */
+'use strict'
+const utils = require('../utils')
+
+/**
+ * @typedef { 'always' | 'never' | 'consistent' | 'ignore' } OptionType
+ * @typedef { { singleline?: OptionType, multiline?: OptionType, maxEmptyLines?: number } } ContentsOptions
+ * @typedef { ContentsOptions & { blocks?: { [element: string]: ContentsOptions } } } Options
+ * @typedef { Required<ContentsOptions> } ArgsOptions
+ */
+
+/**
+ * @param {string} text Source code as a string.
+ * @returns {number}
+ */
+function getLinebreakCount(text) {
+  return text.split(/\r\n|[\r\n\u2028\u2029]/gu).length - 1
+}
+
+/**
+ * @param {number} lineBreaks
+ */
+function getPhrase(lineBreaks) {
+  switch (lineBreaks) {
+    case 1:
+      return '1 line break'
+    default:
+      return `${lineBreaks} line breaks`
+  }
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+const ENUM_OPTIONS = { enum: ['always', 'never', 'consistent', 'ignore'] }
+module.exports = {
+  meta: {
+    type: 'layout',
+    docs: {
+      description:
+        'enforce line breaks after opening and before closing block-level tags',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/block-tag-newline.html'
+    },
+    fixable: 'whitespace',
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          singleline: ENUM_OPTIONS,
+          multiline: ENUM_OPTIONS,
+          maxEmptyLines: { type: 'number', minimum: 0 },
+          blocks: {
+            type: 'object',
+            patternProperties: {
+              '^(?:\\S+)$': {
+                type: 'object',
+                properties: {
+                  singleline: ENUM_OPTIONS,
+                  multiline: ENUM_OPTIONS,
+                  maxEmptyLines: { type: 'number', minimum: 0 }
+                },
+                additionalProperties: false
+              }
+            },
+            additionalProperties: false
+          }
+        },
+        additionalProperties: false
+      }
+    ],
+    messages: {
+      unexpectedOpeningLinebreak:
+        "There should be no line break after '<{{tag}}>'.",
+      unexpectedClosingLinebreak:
+        "There should be no line break before '</{{tag}}>'.",
+      expectedOpeningLinebreak:
+        "Expected {{expected}} after '<{{tag}}>', but {{actual}} found.",
+      expectedClosingLinebreak:
+        "Expected {{expected}}  before '</{{tag}}>', but {{actual}} found.",
+      missingOpeningLinebreak: "A line break is required after '<{{tag}}>'.",
+      missingClosingLinebreak: "A line break is required before '</{{tag}}>'."
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    const df =
+      context.parserServices.getDocumentFragment &&
+      context.parserServices.getDocumentFragment()
+    if (!df) {
+      return {}
+    }
+
+    const sourceCode = context.getSourceCode()
+
+    /**
+     * @param {VStartTag} startTag
+     * @param {string} beforeText
+     * @param {number} beforeLinebreakCount
+     * @param {'always' | 'never'} beforeOption
+     * @param {number} maxEmptyLines
+     * @returns {void}
+     */
+    function verifyBeforeSpaces(
+      startTag,
+      beforeText,
+      beforeLinebreakCount,
+      beforeOption,
+      maxEmptyLines
+    ) {
+      if (beforeOption === 'always') {
+        if (beforeLinebreakCount === 0) {
+          context.report({
+            loc: {
+              start: startTag.loc.end,
+              end: startTag.loc.end
+            },
+            messageId: 'missingOpeningLinebreak',
+            data: { tag: startTag.parent.name },
+            fix(fixer) {
+              return fixer.insertTextAfter(startTag, '\n')
+            }
+          })
+        } else if (maxEmptyLines < beforeLinebreakCount - 1) {
+          context.report({
+            loc: {
+              start: startTag.loc.end,
+              end: sourceCode.getLocFromIndex(
+                startTag.range[1] + beforeText.length
+              )
+            },
+            messageId: 'expectedOpeningLinebreak',
+            data: {
+              tag: startTag.parent.name,
+              expected: getPhrase(maxEmptyLines + 1),
+              actual: getPhrase(beforeLinebreakCount)
+            },
+            fix(fixer) {
+              return fixer.replaceTextRange(
+                [startTag.range[1], startTag.range[1] + beforeText.length],
+                '\n'.repeat(maxEmptyLines + 1)
+              )
+            }
+          })
+        }
+      } else {
+        if (beforeLinebreakCount > 0) {
+          context.report({
+            loc: {
+              start: startTag.loc.end,
+              end: sourceCode.getLocFromIndex(
+                startTag.range[1] + beforeText.length
+              )
+            },
+            messageId: 'unexpectedOpeningLinebreak',
+            data: { tag: startTag.parent.name },
+            fix(fixer) {
+              return fixer.removeRange([
+                startTag.range[1],
+                startTag.range[1] + beforeText.length
+              ])
+            }
+          })
+        }
+      }
+    }
+    /**
+     * @param {VEndTag} endTag
+     * @param {string} afterText
+     * @param {number} afterLinebreakCount
+     * @param {'always' | 'never'} afterOption
+     * @param {number} maxEmptyLines
+     * @returns {void}
+     */
+    function verifyAfterSpaces(
+      endTag,
+      afterText,
+      afterLinebreakCount,
+      afterOption,
+      maxEmptyLines
+    ) {
+      if (afterOption === 'always') {
+        if (afterLinebreakCount === 0) {
+          context.report({
+            loc: {
+              start: endTag.loc.start,
+              end: endTag.loc.start
+            },
+            messageId: 'missingClosingLinebreak',
+            data: { tag: endTag.parent.name },
+            fix(fixer) {
+              return fixer.insertTextBefore(endTag, '\n')
+            }
+          })
+        } else if (maxEmptyLines < afterLinebreakCount - 1) {
+          context.report({
+            loc: {
+              start: sourceCode.getLocFromIndex(
+                endTag.range[0] - afterText.length
+              ),
+              end: endTag.loc.start
+            },
+            messageId: 'expectedClosingLinebreak',
+            data: {
+              tag: endTag.parent.name,
+              expected: getPhrase(maxEmptyLines + 1),
+              actual: getPhrase(afterLinebreakCount)
+            },
+            fix(fixer) {
+              return fixer.replaceTextRange(
+                [endTag.range[0] - afterText.length, endTag.range[0]],
+                '\n'.repeat(maxEmptyLines + 1)
+              )
+            }
+          })
+        }
+      } else {
+        if (afterLinebreakCount > 0) {
+          context.report({
+            loc: {
+              start: sourceCode.getLocFromIndex(
+                endTag.range[0] - afterText.length
+              ),
+              end: endTag.loc.start
+            },
+            messageId: 'unexpectedOpeningLinebreak',
+            data: { tag: endTag.parent.name },
+            fix(fixer) {
+              return fixer.removeRange([
+                endTag.range[0] - afterText.length,
+                endTag.range[0]
+              ])
+            }
+          })
+        }
+      }
+    }
+    /**
+     * @param {VElement} element
+     * @param {ArgsOptions} options
+     * @returns {void}
+     */
+    function verifyElement(element, options) {
+      const { startTag, endTag } = element
+      if (startTag.selfClosing || endTag == null) {
+        return
+      }
+      const text = sourceCode.text.slice(startTag.range[1], endTag.range[0])
+
+      const trimText = text.trim()
+      if (!trimText) {
+        return
+      }
+
+      const option =
+        options.multiline === options.singleline
+          ? options.singleline
+          : /[\n\r\u2028\u2029]/u.test(text.trim())
+          ? options.multiline
+          : options.singleline
+      if (option === 'ignore') {
+        return
+      }
+      const beforeText = /** @type {RegExpExecArray} */ (/^\s*/u.exec(text))[0]
+      const afterText = /** @type {RegExpExecArray} */ (/\s*$/u.exec(text))[0]
+      const beforeLinebreakCount = getLinebreakCount(beforeText)
+      const afterLinebreakCount = getLinebreakCount(afterText)
+
+      /** @type {'always' | 'never'} */
+      let beforeOption
+      /** @type {'always' | 'never'} */
+      let afterOption
+      if (option === 'always' || option === 'never') {
+        beforeOption = option
+        afterOption = option
+      } else {
+        // consistent
+        if (beforeLinebreakCount > 0 === afterLinebreakCount > 0) {
+          return
+        }
+        beforeOption = 'always'
+        afterOption = 'always'
+      }
+
+      verifyBeforeSpaces(
+        startTag,
+        beforeText,
+        beforeLinebreakCount,
+        beforeOption,
+        options.maxEmptyLines
+      )
+
+      verifyAfterSpaces(
+        endTag,
+        afterText,
+        afterLinebreakCount,
+        afterOption,
+        options.maxEmptyLines
+      )
+    }
+
+    /**
+     * Normalizes a given option value.
+     * @param { Options | undefined } option An option value to parse.
+     * @returns { (element: VElement) => void } Verify function.
+     */
+    function normalizeOptionValue(option) {
+      if (!option) {
+        return normalizeOptionValue({})
+      }
+
+      /** @type {ContentsOptions} */
+      const contentsOptions = option
+      /** @type {ArgsOptions} */
+      const options = {
+        singleline: contentsOptions.singleline || 'consistent',
+        multiline: contentsOptions.multiline || 'always',
+        maxEmptyLines: contentsOptions.maxEmptyLines || 0
+      }
+      const { blocks } = option
+      if (!blocks) {
+        return (element) => verifyElement(element, options)
+      }
+
+      return (element) => {
+        const { name } = element
+        const elementsOptions = blocks[name]
+        if (!elementsOptions) {
+          verifyElement(element, options)
+        } else {
+          normalizeOptionValue({
+            singleline: elementsOptions.singleline || options.singleline,
+            multiline: elementsOptions.multiline || options.multiline,
+            maxEmptyLines:
+              elementsOptions.maxEmptyLines != null
+                ? elementsOptions.maxEmptyLines
+                : options.maxEmptyLines
+          })(element)
+        }
+      }
+    }
+
+    const documentFragment = df
+
+    const verify = normalizeOptionValue(context.options[0])
+
+    /**
+     * @returns {VElement[]}
+     */
+    function getTopLevelHTMLElements() {
+      return documentFragment.children.filter(utils.isVElement)
+    }
+
+    return utils.defineTemplateBodyVisitor(
+      context,
+      {},
+      {
+        /** @param {Program} node */
+        Program(node) {
+          if (utils.hasInvalidEOF(node)) {
+            return
+          }
+
+          for (const element of getTopLevelHTMLElements()) {
+            verify(element)
+          }
+        }
+      }
+    )
+  }
+}
diff --git a/lib/rules/no-unregistered-components.js b/lib/rules/no-unregistered-components.js
index 7ec74e50..3ae28328 100644
--- a/lib/rules/no-unregistered-components.js
+++ b/lib/rules/no-unregistered-components.js
@@ -185,10 +185,12 @@ module.exports = {
         const nameProperty = utils.findProperty(obj, 'name')
 
         if (nameProperty) {
-          registeredComponents.push({
-            node: nameProperty,
-            name: nameProperty.value.value
-          })
+          if (nameProperty.value.type === 'Literal') {
+            registeredComponents.push({
+              node: nameProperty,
+              name: `${nameProperty.value.value}`
+            })
+          }
         }
       })
     )


Test patch:
diff --git a/tests/lib/rules/array-bracket-newline.js b/tests/lib/rules/array-bracket-newline.js
new file mode 100644
index 00000000..06e1890c
--- /dev/null
+++ b/tests/lib/rules/array-bracket-newline.js
@@ -0,0 +1,108 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/array-bracket-newline')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2015 }
+})
+
+tester.run('array-bracket-newline', rule, {
+  valid: [
+    '<template><div :attr="[a]" /></template>',
+    '<template><div :attr="[\na,\nb,\nc\n]" /></template>',
+    {
+      code: '<template><div :attr="[a]" /></template>',
+      options: ['never']
+    },
+    {
+      code: '<template><div :attr="[\na\n]" /></template>',
+      options: ['always']
+    },
+    '<template><div :[attr]="a" /></template>',
+    {
+      code: '<template><div :[attr]="a" /></template>',
+      options: ['always']
+    },
+    '<template><div :[[attr]]="a" /></template>',
+    {
+      code: '<template><div :[[attr]]="a" /></template>',
+      options: ['always']
+    }
+  ],
+  invalid: [
+    {
+      code: '<template><div :attr="[\na]" /></template>',
+      output: '<template><div :attr="[a]" /></template>',
+      errors: ["There should be no linebreak after '['."]
+    },
+    {
+      code: '<template><div :attr="[a\n]" /></template>',
+      output: '<template><div :attr="[a]" /></template>',
+      errors: ["There should be no linebreak before ']'."]
+    },
+    {
+      code: '<template><div :attr="[\na\n]" /></template>',
+      output: '<template><div :attr="[a]" /></template>',
+      errors: [
+        "There should be no linebreak after '['.",
+        "There should be no linebreak before ']'."
+      ]
+    },
+    {
+      code: '<template><div :attr="[\na]" /></template>',
+      options: ['never'],
+      output: '<template><div :attr="[a]" /></template>',
+      errors: ["There should be no linebreak after '['."]
+    },
+    {
+      code: '<template><div :attr="[a\n]" /></template>',
+      options: ['never'],
+      output: '<template><div :attr="[a]" /></template>',
+      errors: ["There should be no linebreak before ']'."]
+    },
+    {
+      code: '<template><div :attr="[\na\n]" /></template>',
+      options: ['never'],
+      output: '<template><div :attr="[a]" /></template>',
+      errors: [
+        "There should be no linebreak after '['.",
+        "There should be no linebreak before ']'."
+      ]
+    },
+    {
+      code: '<template><div :attr="[\na]" /></template>',
+      options: ['always'],
+      output: '<template><div :attr="[\na\n]" /></template>',
+      errors: ["A linebreak is required before ']'."]
+    },
+    {
+      code: '<template><div :attr="[a\n]" /></template>',
+      options: ['always'],
+      output: '<template><div :attr="[\na\n]" /></template>',
+      errors: ["A linebreak is required after '['."]
+    },
+    {
+      code: '<template><div :attr="[a]" /></template>',
+      options: ['always'],
+      output: '<template><div :attr="[\na\n]" /></template>',
+      errors: [
+        "A linebreak is required after '['.",
+        "A linebreak is required before ']'."
+      ]
+    },
+    {
+      code: '<template><div :[[attr]]="[a]" /></template>',
+      options: ['always'],
+      output: '<template><div :[[attr]]="[\na\n]" /></template>',
+      errors: [
+        "A linebreak is required after '['.",
+        "A linebreak is required before ']'."
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/block-tag-newline.js b/tests/lib/rules/block-tag-newline.js
new file mode 100644
index 00000000..92427879
--- /dev/null
+++ b/tests/lib/rules/block-tag-newline.js
@@ -0,0 +1,247 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/block-tag-newline')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2015 }
+})
+
+tester.run('block-tag-newline', rule, {
+  valid: [
+    '<template><input></template>\n<script>let a</script>',
+    '<template>\n<input>\n</template>\n<script>\nlet a\n</script>',
+    '<template>\n<div>\n</div>\n</template>\n<script>\nlet a\nlet b\n</script>',
+    '<template></template>\n<script></script>',
+    '<template>\n\n</template>\n<script>\n\n</script>',
+    '<template />\n<script />',
+    {
+      code: '<template><div>\n</div></template>\n<script>let a</script>',
+      options: [{ singleline: 'never', multiline: 'never' }]
+    },
+    {
+      code:
+        '<template>\n<input>\n</template>\n<script>\nlet a\nlet b\n</script>',
+      options: [{ singleline: 'always', multiline: 'always' }]
+    },
+    {
+      code:
+        '<template>\n\n<input>\n\n</template>\n<script>\n\nlet a\nlet b\n\n</script>',
+      options: [{ singleline: 'always', multiline: 'always', maxEmptyLines: 1 }]
+    },
+    {
+      code: '<template>\n<input></template>\n<script>let a\nlet b\n</script>',
+      options: [{ singleline: 'ignore', multiline: 'ignore' }]
+    },
+    {
+      code: '<template><input></template>\n<script>\nlet a\n</script>',
+      options: [{ multiline: 'never' }]
+    },
+    {
+      code:
+        '<template>\n<div>\n</div>\n</template>\n<script>\nlet a\nlet b\n</script>',
+      options: [{ singleline: 'never' }]
+    },
+    // invalid
+    '<template><div a="></div>\n</template>\n<script>\nlet a</script>'
+  ],
+  invalid: [
+    {
+      code: '<template><input>\n</template>\n<script>let a\n</script>',
+      output: '<template>\n<input>\n</template>\n<script>\nlet a\n</script>',
+      errors: [
+        {
+          message: "A line break is required after '<template>'.",
+          line: 1,
+          column: 11
+        },
+        {
+          message: "A line break is required after '<script>'.",
+          line: 3,
+          column: 9
+        }
+      ]
+    },
+    {
+      code: '<template>\n<input></template>\n<script>\nlet a</script>',
+      output: '<template>\n<input>\n</template>\n<script>\nlet a\n</script>',
+      errors: [
+        {
+          message: "A line break is required before '</template>'.",
+          line: 2,
+          column: 8
+        },
+        {
+          message: "A line break is required before '</script>'.",
+          line: 4,
+          column: 6
+        }
+      ]
+    },
+    {
+      code: '<template><div>\n</div></template>\n<script>let a\nlet b</script>',
+      output:
+        '<template>\n<div>\n</div>\n</template>\n<script>\nlet a\nlet b\n</script>',
+      errors: [
+        {
+          message: "A line break is required after '<template>'.",
+          line: 1,
+          column: 11
+        },
+        {
+          message: "A line break is required before '</template>'.",
+          line: 2,
+          column: 7
+        },
+        {
+          message: "A line break is required after '<script>'.",
+          line: 3,
+          column: 9
+        },
+        {
+          message: "A line break is required before '</script>'.",
+          line: 4,
+          column: 6
+        }
+      ]
+    },
+    {
+      code:
+        '<template>\n<div>\n</div>\n</template>\n<script>\nlet a\n</script>',
+      output: '<template><div>\n</div></template>\n<script>let a</script>',
+      options: [{ singleline: 'never', multiline: 'never' }],
+      errors: [
+        {
+          message: "There should be no line break after '<template>'.",
+          line: 1,
+          column: 11
+        },
+        {
+          message: "There should be no line break after '<template>'.",
+          line: 3,
+          column: 7
+        },
+        {
+          message: "There should be no line break after '<script>'.",
+          line: 5,
+          column: 9
+        },
+        {
+          message: "There should be no line break after '<script>'.",
+          line: 6,
+          column: 6
+        }
+      ]
+    },
+    {
+      code: '<template><div>\n</div></template>\n<script>let a</script>',
+      output:
+        '<template>\n<div>\n</div>\n</template>\n<script>\nlet a\n</script>',
+      options: [{ singleline: 'always', multiline: 'always' }],
+      errors: [
+        {
+          message: "A line break is required after '<template>'.",
+          line: 1,
+          column: 11
+        },
+        {
+          message: "A line break is required before '</template>'.",
+          line: 2,
+          column: 7
+        },
+        {
+          message: "A line break is required after '<script>'.",
+          line: 3,
+          column: 9
+        },
+        {
+          message: "A line break is required before '</script>'.",
+          line: 3,
+          column: 14
+        }
+      ]
+    },
+    {
+      code:
+        '<template>\n\n<input>\n\n</template>\n<script>\n\nlet a\nlet b\n\n</script>',
+      output:
+        '<template>\n<input>\n</template>\n<script>\nlet a\nlet b\n</script>',
+      options: [{ singleline: 'always', multiline: 'always' }],
+      errors: [
+        {
+          message:
+            "Expected 1 line break after '<template>', but 2 line breaks found.",
+          line: 1,
+          column: 11
+        },
+        {
+          message:
+            "Expected 1 line break  before '</template>', but 2 line breaks found.",
+          line: 3,
+          column: 8
+        },
+        {
+          message:
+            "Expected 1 line break after '<script>', but 2 line breaks found.",
+          line: 6,
+          column: 9
+        },
+        {
+          message:
+            "Expected 1 line break  before '</script>', but 2 line breaks found.",
+          line: 9,
+          column: 6
+        }
+      ]
+    },
+    {
+      code:
+        '<template>\n\n\n<input>\n\n</template>\n<script>\n\nlet a\nlet b\n\n\n</script>',
+      output:
+        '<template>\n\n<input>\n\n</template>\n<script>\n\nlet a\nlet b\n\n</script>',
+      options: [
+        { singleline: 'always', multiline: 'always', maxEmptyLines: 1 }
+      ],
+      errors: [
+        {
+          message:
+            "Expected 2 line breaks after '<template>', but 3 line breaks found.",
+          line: 1,
+          column: 11
+        },
+        {
+          message:
+            "Expected 2 line breaks  before '</script>', but 3 line breaks found.",
+          line: 10,
+          column: 6
+        }
+      ]
+    },
+    {
+      code:
+        '<template><input>\n\n</template>\n<script>let a\nlet b\n\n\n</script><docs>\n#</docs>',
+      output:
+        '<template><input>\n\n</template>\n<script>let a\nlet b</script><docs>\n#\n</docs>',
+      options: [
+        {
+          blocks: {
+            template: {
+              singleline: 'ignore'
+            },
+            script: {
+              multiline: 'never'
+            }
+          }
+        }
+      ],
+      errors: [
+        "There should be no line break after '<script>'.",
+        "A line break is required before '</docs>'."
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-unregistered-components.js b/tests/lib/rules/no-unregistered-components.js
index 36702690..a50e3c0f 100644
--- a/tests/lib/rules/no-unregistered-components.js
+++ b/tests/lib/rules/no-unregistered-components.js
@@ -648,6 +648,26 @@ tester.run('no-unregistered-components', rule, {
           line: 3
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <CustomComponent />
+        </template>
+        <script>
+        export default {
+          name: CustomComponent
+        }
+        </script>
+      `,
+      errors: [
+        {
+          message:
+            'The "CustomComponent" component has been used but not registered.',
+          line: 3
+        }
+      ]
     }
   ]
 })