Repository: vuejs/eslint-plugin-vue
Pull request #1239 (2020-07-09 09:40:09)

Add `vue/no-dupe-v-else-if` rule
This PR adds `vue/no-dupe-v-else-if` rule.

`vue/no-dupe-v-else-if` rule disallows duplicate conditions in the same `v-if` / `v-else-if` chain.

```vue
<template>
  <!-- ✗ BAD -->
  <div v-if="isSomething(x)" />
  <div v-else-if="isSomething(x)" />

  <!-- ✓ GOOD -->
  <div v-if="isSomething(x)" />
  <div v-else-if="isSomethingElse(x)" />
</template>
```

this rule was inspired by [no-dupe-else-if](https://eslint.org/docs/rules/no-dupe-else-if) rule.


Patch:
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 1dd68927..2a02e022 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -10,10 +10,10 @@ about: Create a report to help us improve
   To make sure it's not, run: yarn eslint src/your-file.vue
 -->
 
-**Tell us about your environment**	
-* **ESLint version:**	
-* **eslint-plugin-vue version:**	
-* **Node version:**	
+**Tell us about your environment**
+* **ESLint version:** 
+* **eslint-plugin-vue version:** 
+* **Node version:** 
 
 **Please show your full configuration:**
 <!-- Paste content of your .eslintrc file -->
diff --git a/docs/rules/README.md b/docs/rules/README.md
index e2353b63..3dc1c255 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -58,6 +58,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
 | [vue/no-deprecated-v-on-number-modifiers](./no-deprecated-v-on-number-modifiers.md) | disallow using deprecated number (keycode) modifiers (in Vue.js 3.0.0+) | :wrench: |
 | [vue/no-deprecated-vue-config-keycodes](./no-deprecated-vue-config-keycodes.md) | disallow using deprecated `Vue.config.keyCodes` (in Vue.js 3.0.0+) |  |
 | [vue/no-dupe-keys](./no-dupe-keys.md) | disallow duplication of field names |  |
+| [vue/no-dupe-v-else-if](./no-dupe-v-else-if.md) | disallow duplicate conditions in `v-if` / `v-else-if` chains |  |
 | [vue/no-duplicate-attributes](./no-duplicate-attributes.md) | disallow duplication of attributes |  |
 | [vue/no-lifecycle-after-await](./no-lifecycle-after-await.md) | disallow asynchronously registered lifecycle hooks |  |
 | [vue/no-mutating-props](./no-mutating-props.md) | disallow mutation of component props |  |
@@ -148,6 +149,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
 |:--------|:------------|:---|
 | [vue/attributes-order](./attributes-order.md) | enforce order of attributes | :wrench: |
 | [vue/component-tags-order](./component-tags-order.md) | enforce order of component top-level elements |  |
+| [vue/no-lone-template](./no-lone-template.md) | disallow unnecessary `<template>` |  |
 | [vue/no-multiple-slot-args](./no-multiple-slot-args.md) | disallow to pass multiple arguments to scoped slots |  |
 | [vue/no-v-html](./no-v-html.md) | disallow use of v-html to prevent XSS attack |  |
 | [vue/order-in-components](./order-in-components.md) | enforce order of properties in components | :wrench: |
@@ -170,6 +172,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
 | [vue/no-async-in-computed-properties](./no-async-in-computed-properties.md) | disallow asynchronous actions in computed properties |  |
 | [vue/no-custom-modifiers-on-v-model](./no-custom-modifiers-on-v-model.md) | disallow custom modifiers on v-model used on the component |  |
 | [vue/no-dupe-keys](./no-dupe-keys.md) | disallow duplication of field names |  |
+| [vue/no-dupe-v-else-if](./no-dupe-v-else-if.md) | disallow duplicate conditions in `v-if` / `v-else-if` chains |  |
 | [vue/no-duplicate-attributes](./no-duplicate-attributes.md) | disallow duplication of attributes |  |
 | [vue/no-multiple-template-root](./no-multiple-template-root.md) | disallow adding multiple root nodes to the template |  |
 | [vue/no-mutating-props](./no-mutating-props.md) | disallow mutation of component props |  |
@@ -256,6 +259,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
 |:--------|:------------|:---|
 | [vue/attributes-order](./attributes-order.md) | enforce order of attributes | :wrench: |
 | [vue/component-tags-order](./component-tags-order.md) | enforce order of component top-level elements |  |
+| [vue/no-lone-template](./no-lone-template.md) | disallow unnecessary `<template>` |  |
 | [vue/no-multiple-slot-args](./no-multiple-slot-args.md) | disallow to pass multiple arguments to scoped slots |  |
 | [vue/no-v-html](./no-v-html.md) | disallow use of v-html to prevent XSS attack |  |
 | [vue/order-in-components](./order-in-components.md) | enforce order of properties in components | :wrench: |
diff --git a/docs/rules/no-dupe-v-else-if.md b/docs/rules/no-dupe-v-else-if.md
new file mode 100644
index 00000000..2960bdbb
--- /dev/null
+++ b/docs/rules/no-dupe-v-else-if.md
@@ -0,0 +1,98 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-dupe-v-else-if
+description: disallow duplicate conditions in `v-if` / `v-else-if` chains
+---
+# vue/no-dupe-v-else-if
+> disallow duplicate conditions in `v-if` / `v-else-if` chains
+
+- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
+
+## :book: Rule Details
+
+This rule disallows duplicate conditions in the same `v-if` / `v-else-if` chain.
+
+<eslint-code-block :rules="{'vue/no-dupe-v-else-if': ['error']}">
+
+```vue
+<template>
+  <!-- ✗ BAD -->
+  <div v-if="isSomething(x)" />
+  <div v-else-if="isSomething(x)" />
+
+  <div v-if="a" />
+  <div v-else-if="b" />
+  <div v-else-if="c && d" />
+  <div v-else-if="c && d" />
+
+  <div v-if="n === 1" />
+  <div v-else-if="n === 2" />
+  <div v-else-if="n === 3" />
+  <div v-else-if="n === 2" />
+  <div v-else-if="n === 5" />
+
+  <!-- ✓ GOOD -->
+  <div v-if="isSomething(x)" />
+  <div v-else-if="isSomethingElse(x)" />
+
+  <div v-if="a" />
+  <div v-else-if="b" />
+  <div v-else-if="c && d" />
+  <div v-else-if="c && e" />
+
+  <div v-if="n === 1" />
+  <div v-else-if="n === 2" />
+  <div v-else-if="n === 3" />
+  <div v-else-if="n === 4" />
+  <div v-else-if="n === 5" />
+</template>
+```
+
+</eslint-code-block>
+
+This rule can also detect some cases where the conditions are not identical, but the branch can never execute due to the logic of `||` and `&&` operators.
+
+<eslint-code-block :rules="{'vue/no-dupe-v-else-if': ['error']}">
+
+```vue
+<template>
+  <!-- ✗ BAD -->
+  <div v-if="a || b" />
+  <div v-else-if="a" />
+
+  <div v-if="a" />
+  <div v-else-if="b" />
+  <div v-else-if="a || b" />
+
+  <div v-if="a" />
+  <div v-else-if="a && b" />
+
+  <div v-if="a && b" />
+  <div v-else-if="a && b && c" />
+
+  <div v-if="a || b" />
+  <div v-else-if="b && c" />
+
+  <div v-if="a" />
+  <div v-else-if="b && c" />
+  <div v-else-if="d && (c && e && b || a)" />
+</template>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :couple: Related rules
+
+- [no-dupe-else-if]
+
+[no-dupe-else-if]: https://eslint.org/docs/rules/no-dupe-else-if
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-dupe-v-else-if.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-dupe-v-else-if.js)
diff --git a/docs/rules/no-lone-template.md b/docs/rules/no-lone-template.md
new file mode 100644
index 00000000..c3ea7b71
--- /dev/null
+++ b/docs/rules/no-lone-template.md
@@ -0,0 +1,82 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-lone-template
+description: disallow unnecessary `<template>`
+---
+# vue/no-lone-template
+> disallow unnecessary `<template>`
+
+- :gear: This rule is included in `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
+
+## :book: Rule Details
+
+This rule aims to eliminate unnecessary and potentially confusing `<template>`.  
+In Vue.js 2.x, the `<template>` elements that have no specific directives have no effect.  
+In Vue.js 3.x, the `<template>` elements that have no specific directives render the `<template>` elements as is, but in most cases this may not be what you intended.
+
+<eslint-code-block :rules="{'vue/no-lone-template': ['error']}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <template v-if="foo">...</template>
+  <template v-else-if="bar">...</template>
+  <template v-else>...</template>
+  <template v-for="e in list">...</template>
+  <template v-slot>...</template>
+
+  <!-- ✗ BAD -->
+  <template>...</template>
+  <template/>
+</template>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+```json
+{
+  "vue/no-lone-template": ["error", {
+    "ignoreAccessible": false
+  }]
+}
+```
+
+- `ignoreAccessible` ... If `true`, ignore accessible `<template>` elements. default `false`.  
+  Note: this option is useless if you are using Vue.js 2.x.
+
+### `"ignoreAccessible": true`
+
+<eslint-code-block :rules="{'vue/no-lone-template': ['error', { 'ignoreAccessible': true }]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <template ref="foo">...</template>
+  <template id="bar">...</template>
+
+  <!-- ✗ BAD -->
+  <template class="baz">...</template>
+</template>
+```
+
+</eslint-code-block>
+
+## :mute: When Not To Use It
+
+If you are using Vue.js 3.x and want to define the `<template>` element intentionally, you will have to turn this rule off or use `"ignoreAccessible"` option.
+
+## :couple: Related rules
+
+- [vue/no-template-key]
+- [no-lone-blocks]
+
+[no-lone-blocks]: https://eslint.org/docs/rules/no-lone-blocks
+[vue/no-template-key]: ./no-template-key.md
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-lone-template.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-lone-template.js)
diff --git a/docs/user-guide/README.md b/docs/user-guide/README.md
index 469cdb94..4579d6c7 100644
--- a/docs/user-guide/README.md
+++ b/docs/user-guide/README.md
@@ -65,7 +65,7 @@ eslint "src/**/*.{js,vue}"
 If you installed [@vue/cli-plugin-eslint](https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint), you should have the `lint` script added to your `package.json`. That means you can just run `yarn lint` or `npm run lint`.
 :::
 
-#### How to use a custom parser?
+### How to use a custom parser?
 
 If you want to use custom parsers such as [babel-eslint](https://www.npmjs.com/package/babel-eslint) or [@typescript-eslint/parser](https://www.npmjs.com/package/@typescript-eslint/parser), you have to use the `parserOptions.parser` option instead of the `parser` option. Because this plugin requires [vue-eslint-parser](https://www.npmjs.com/package/vue-eslint-parser) to parse `.vue` files, this plugin doesn't work if you overwrite the `parser` option.
 
@@ -197,7 +197,7 @@ If you already use another parser (e.g. `"parser": "babel-eslint"`), please move
 + "parser": "vue-eslint-parser",
   "parserOptions": {
 +     "parser": "babel-eslint",
-      "ecmaVersion": 2017,
+      "ecmaVersion": 2020,
       "sourceType": "module"
   }
 ```
@@ -264,3 +264,22 @@ module.exports = {
 
 [prettier]: https://prettier.io/
 [eslint-config-prettier]: https://github.com/prettier/eslint-config-prettier
+
+### Using JSX.
+
+If you are using JSX, you need to enable JSX in your ESLint configuration.
+
+```diff
+  "parserOptions": {
+      "ecmaVersion": 2020,
+      "ecmaFeatures": {
++         "jsx": true
+      }
+  }
+```
+
+See also [ESLint - Specifying Parser Options](https://eslint.org/docs/user-guide/configuring#specifying-parser-options).
+
+The same configuration is required when using JSX with TypeScript (TSX) in the `.vue` file.  
+See also [here](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/parser/README.md#parseroptionsecmafeaturesjsx).  
+Note that you cannot use angle-bracket type assertion style (`var x = <foo>bar;`) when using `jsx: true`.
diff --git a/lib/configs/base.js b/lib/configs/base.js
index 747f2e65..71279b9a 100644
--- a/lib/configs/base.js
+++ b/lib/configs/base.js
@@ -7,10 +7,7 @@ module.exports = {
   parser: require.resolve('vue-eslint-parser'),
   parserOptions: {
     ecmaVersion: 2018,
-    sourceType: 'module',
-    ecmaFeatures: {
-      jsx: true
-    }
+    sourceType: 'module'
   },
   env: {
     browser: true,
diff --git a/lib/configs/essential.js b/lib/configs/essential.js
index 9005982b..722cf1fa 100644
--- a/lib/configs/essential.js
+++ b/lib/configs/essential.js
@@ -11,6 +11,7 @@ module.exports = {
     'vue/no-async-in-computed-properties': 'error',
     'vue/no-custom-modifiers-on-v-model': 'error',
     'vue/no-dupe-keys': 'error',
+    'vue/no-dupe-v-else-if': 'error',
     'vue/no-duplicate-attributes': 'error',
     'vue/no-multiple-template-root': 'error',
     'vue/no-mutating-props': 'error',
diff --git a/lib/configs/recommended.js b/lib/configs/recommended.js
index e6388e15..61781f75 100644
--- a/lib/configs/recommended.js
+++ b/lib/configs/recommended.js
@@ -8,6 +8,7 @@ module.exports = {
   rules: {
     'vue/attributes-order': 'warn',
     'vue/component-tags-order': 'warn',
+    'vue/no-lone-template': 'warn',
     'vue/no-multiple-slot-args': 'warn',
     'vue/no-v-html': 'warn',
     'vue/order-in-components': 'warn',
diff --git a/lib/configs/vue3-essential.js b/lib/configs/vue3-essential.js
index 432df355..5adbbcd2 100644
--- a/lib/configs/vue3-essential.js
+++ b/lib/configs/vue3-essential.js
@@ -26,6 +26,7 @@ module.exports = {
     'vue/no-deprecated-v-on-number-modifiers': 'error',
     'vue/no-deprecated-vue-config-keycodes': 'error',
     'vue/no-dupe-keys': 'error',
+    'vue/no-dupe-v-else-if': 'error',
     'vue/no-duplicate-attributes': 'error',
     'vue/no-lifecycle-after-await': 'error',
     'vue/no-mutating-props': 'error',
diff --git a/lib/configs/vue3-recommended.js b/lib/configs/vue3-recommended.js
index 318690c3..452703dc 100644
--- a/lib/configs/vue3-recommended.js
+++ b/lib/configs/vue3-recommended.js
@@ -8,6 +8,7 @@ module.exports = {
   rules: {
     'vue/attributes-order': 'warn',
     'vue/component-tags-order': 'warn',
+    'vue/no-lone-template': 'warn',
     'vue/no-multiple-slot-args': 'warn',
     'vue/no-v-html': 'warn',
     'vue/order-in-components': 'warn',
diff --git a/lib/index.js b/lib/index.js
index ae86686a..282ddd39 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -67,6 +67,7 @@ module.exports = {
     'no-deprecated-v-on-number-modifiers': require('./rules/no-deprecated-v-on-number-modifiers'),
     'no-deprecated-vue-config-keycodes': require('./rules/no-deprecated-vue-config-keycodes'),
     'no-dupe-keys': require('./rules/no-dupe-keys'),
+    'no-dupe-v-else-if': require('./rules/no-dupe-v-else-if'),
     'no-duplicate-attr-inheritance': require('./rules/no-duplicate-attr-inheritance'),
     'no-duplicate-attributes': require('./rules/no-duplicate-attributes'),
     'no-empty-component-block': require('./rules/no-empty-component-block'),
@@ -74,6 +75,7 @@ module.exports = {
     'no-extra-parens': require('./rules/no-extra-parens'),
     'no-irregular-whitespace': require('./rules/no-irregular-whitespace'),
     'no-lifecycle-after-await': require('./rules/no-lifecycle-after-await'),
+    'no-lone-template': require('./rules/no-lone-template'),
     'no-multi-spaces': require('./rules/no-multi-spaces'),
     'no-multiple-objects-in-class': require('./rules/no-multiple-objects-in-class'),
     'no-multiple-slot-args': require('./rules/no-multiple-slot-args'),
diff --git a/lib/rules/no-dupe-v-else-if.js b/lib/rules/no-dupe-v-else-if.js
new file mode 100644
index 00000000..8e3a3dec
--- /dev/null
+++ b/lib/rules/no-dupe-v-else-if.js
@@ -0,0 +1,193 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+/**
+ * @typedef {NonNullable<VExpressionContainer['expression']>} VExpression
+ */
+/**
+ * @typedef {object} OrOperands
+ * @property {VExpression} OrOperands.node
+ * @property {AndOperands[]} OrOperands.operands
+ *
+ * @typedef {object} AndOperands
+ * @property {VExpression} AndOperands.node
+ * @property {VExpression[]} AndOperands.operands
+ */
+/**
+ * Splits the given node by the given logical operator.
+ * @param {string} operator Logical operator `||` or `&&`.
+ * @param {VExpression} node The node to split.
+ * @returns {VExpression[]} Array of conditions that makes the node when joined by the operator.
+ */
+function splitByLogicalOperator(operator, node) {
+  if (node.type === 'LogicalExpression' && node.operator === operator) {
+    return [
+      ...splitByLogicalOperator(operator, node.left),
+      ...splitByLogicalOperator(operator, node.right)
+    ]
+  }
+  return [node]
+}
+
+/**
+ * @param {VExpression} node
+ */
+function splitByOr(node) {
+  return splitByLogicalOperator('||', node)
+}
+/**
+ * @param {VExpression} node
+ */
+function splitByAnd(node) {
+  return splitByLogicalOperator('&&', node)
+}
+
+/**
+ * @param {VExpression} node
+ * @returns {OrOperands}
+ */
+function buildOrOperands(node) {
+  const orOperands = splitByOr(node)
+  return {
+    node,
+    operands: orOperands.map((orOperand) => {
+      const andOperands = splitByAnd(orOperand)
+      return {
+        node: orOperand,
+        operands: andOperands
+      }
+    })
+  }
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description:
+        'disallow duplicate conditions in `v-if` / `v-else-if` chains',
+      categories: ['vue3-essential', 'essential'],
+      url: 'https://eslint.vuejs.org/rules/no-dupe-v-else-if.html'
+    },
+    fixable: null,
+    schema: [],
+    messages: {
+      unexpected:
+        'This branch can never execute. Its condition is a duplicate or covered by previous conditions in the `v-if` / `v-else-if` chain.'
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    const tokenStore =
+      context.parserServices.getTemplateBodyTokenStore &&
+      context.parserServices.getTemplateBodyTokenStore()
+    /**
+     * Determines whether the two given nodes are considered to be equal. In particular, given that the nodes
+     * represent expressions in a boolean context, `||` and `&&` can be considered as commutative operators.
+     * @param {VExpression} a First node.
+     * @param {VExpression} b Second node.
+     * @returns {boolean} `true` if the nodes are considered to be equal.
+     */
+    function equal(a, b) {
+      if (a.type !== b.type) {
+        return false
+      }
+
+      if (
+        a.type === 'LogicalExpression' &&
+        b.type === 'LogicalExpression' &&
+        (a.operator === '||' || a.operator === '&&') &&
+        a.operator === b.operator
+      ) {
+        return (
+          (equal(a.left, b.left) && equal(a.right, b.right)) ||
+          (equal(a.left, b.right) && equal(a.right, b.left))
+        )
+      }
+
+      return utils.equalTokens(a, b, tokenStore)
+    }
+
+    /**
+     * Determines whether the first given AndOperands is a subset of the second given AndOperands.
+     *
+     * e.g. A: (a && b), B: (a && b && c): B is a subset of A.
+     *
+     * @param {AndOperands} operandsA The AndOperands to compare from.
+     * @param {AndOperands} operandsB The AndOperands to compare against.
+     * @returns {boolean} `true` if the `andOperandsA` is a subset of the `andOperandsB`.
+     */
+    function isSubset(operandsA, operandsB) {
+      return operandsA.operands.every((operandA) =>
+        operandsB.operands.some((operandB) => equal(operandA, operandB))
+      )
+    }
+
+    return utils.defineTemplateBodyVisitor(context, {
+      "VAttribute[directive=true][key.name.name='else-if']"(node) {
+        if (!node.value || !node.value.expression) {
+          return
+        }
+        const test = node.value.expression
+        const conditionsToCheck =
+          test.type === 'LogicalExpression' && test.operator === '&&'
+            ? [...splitByAnd(test), test]
+            : [test]
+        const listToCheck = conditionsToCheck.map(buildOrOperands)
+
+        /** @type {VElement | null} */
+        let current = node.parent.parent
+        while (current && (current = utils.prevSibling(current))) {
+          const vIf = utils.getDirective(current, 'if')
+          const currentTestDir = vIf || utils.getDirective(current, 'else-if')
+          if (!currentTestDir) {
+            return
+          }
+          if (currentTestDir.value && currentTestDir.value.expression) {
+            const currentOrOperands = buildOrOperands(
+              currentTestDir.value.expression
+            )
+
+            for (const condition of listToCheck) {
+              const operands = (condition.operands = condition.operands.filter(
+                (orOperand) => {
+                  return !currentOrOperands.operands.some((currentOrOperand) =>
+                    isSubset(currentOrOperand, orOperand)
+                  )
+                }
+              ))
+              if (!operands.length) {
+                context.report({
+                  node: condition.node,
+                  messageId: 'unexpected'
+                })
+                return
+              }
+            }
+          }
+
+          if (vIf) {
+            return
+          }
+        }
+      }
+    })
+  }
+}
diff --git a/lib/rules/no-lone-template.js b/lib/rules/no-lone-template.js
new file mode 100644
index 00000000..b56c6081
--- /dev/null
+++ b/lib/rules/no-lone-template.js
@@ -0,0 +1,129 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+// https://github.com/vuejs/vue-next/blob/64e2f4643602c5980361e66674141e61ba60ef70/packages/compiler-core/src/parse.ts#L405
+const SPECIAL_TEMPLATE_DIRECTIVES = new Set([
+  'if',
+  'else',
+  'else-if',
+  'for',
+  'slot'
+])
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'disallow unnecessary `<template>`',
+      categories: ['vue3-recommended', 'recommended'],
+      url: 'https://eslint.vuejs.org/rules/no-lone-template.html'
+    },
+    fixable: null,
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          ignoreAccessible: {
+            type: 'boolean'
+          }
+        },
+        additionalProperties: false
+      }
+    ],
+    messages: {
+      requireDirective: '`<template>` require directive.'
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    const options = context.options[0] || {}
+    const ignoreAccessible = options.ignoreAccessible === true
+
+    /**
+     * @param {VAttribute | VDirective} attr
+     */
+    function getKeyName(attr) {
+      if (attr.directive) {
+        if (attr.key.name.name !== 'bind') {
+          // no v-bind
+          return null
+        }
+        if (
+          !attr.key.argument ||
+          attr.key.argument.type === 'VExpressionContainer'
+        ) {
+          // unknown
+          return null
+        }
+        return attr.key.argument.name
+      }
+      return attr.key.name
+    }
+
+    return utils.defineTemplateBodyVisitor(context, {
+      /** @param {VStartTag} node */
+      "VElement[name='template'][parent.type='VElement'] > VStartTag"(node) {
+        if (
+          node.attributes.some((attr) => {
+            if (attr.directive) {
+              const directiveName = attr.key.name.name
+              if (SPECIAL_TEMPLATE_DIRECTIVES.has(directiveName)) {
+                return true
+              }
+              if (directiveName === 'slot-scope') {
+                // `slot-scope` is deprecated in Vue.js 2.6
+                return true
+              }
+              if (directiveName === 'scope') {
+                // `scope` is deprecated in Vue.js 2.5
+                return true
+              }
+            }
+
+            const keyName = getKeyName(attr)
+            if (keyName === 'slot') {
+              // `slot` is deprecated in Vue.js 2.6
+              return true
+            }
+
+            return false
+          })
+        ) {
+          return
+        }
+
+        if (
+          ignoreAccessible &&
+          node.attributes.some((attr) => {
+            const keyName = getKeyName(attr)
+            return keyName === 'id' || keyName === 'ref'
+          })
+        ) {
+          return
+        }
+
+        context.report({
+          node,
+          messageId: 'requireDirective'
+        })
+      }
+    })
+  }
+}
diff --git a/lib/utils/index.js b/lib/utils/index.js
index a0710a8d..7fae6553 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -1530,6 +1530,32 @@ module.exports = {
 
       return null
     }
+  },
+
+  /**
+   * Checks whether or not the tokens of two given nodes are same.
+   * @param {ASTNode} left A node 1 to compare.
+   * @param {ASTNode} right A node 2 to compare.
+   * @param {ParserServices.TokenStore | SourceCode} sourceCode The ESLint source code object.
+   * @returns {boolean} the source code for the given node.
+   */
+  equalTokens(left, right, sourceCode) {
+    const tokensL = sourceCode.getTokens(left)
+    const tokensR = sourceCode.getTokens(right)
+
+    if (tokensL.length !== tokensR.length) {
+      return false
+    }
+    for (let i = 0; i < tokensL.length; ++i) {
+      if (
+        tokensL[i].type !== tokensR[i].type ||
+        tokensL[i].value !== tokensR[i].value
+      ) {
+        return false
+      }
+    }
+
+    return true
   }
 }
 
diff --git a/tools/update-lib-configs.js b/tools/update-lib-configs.js
index ff026eca..5379decb 100644
--- a/tools/update-lib-configs.js
+++ b/tools/update-lib-configs.js
@@ -50,10 +50,7 @@ module.exports = {
   parser: require.resolve('vue-eslint-parser'),
   parserOptions: {
     ecmaVersion: 2018,
-    sourceType: 'module',
-    ecmaFeatures: {
-      jsx: true,
-    }
+    sourceType: 'module'
   },
   env: {
     browser: true,
diff --git a/typings/eslint-plugin-vue/util-types/ast/ast.ts b/typings/eslint-plugin-vue/util-types/ast/ast.ts
index bcdeb93c..357b5640 100644
--- a/typings/eslint-plugin-vue/util-types/ast/ast.ts
+++ b/typings/eslint-plugin-vue/util-types/ast/ast.ts
@@ -12,32 +12,104 @@ export type VNodeListenerMap = {
   'VAttribute:exit': V.VAttribute | V.VDirective
   'VAttribute[directive=false]': V.VAttribute
   'VAttribute[directive=false]:exit': V.VAttribute
-  "VAttribute[directive=true][key.name.name='bind']": V.VDirective
-  "VAttribute[directive=true][key.name.name='bind']:exit": V.VDirective
+  "VAttribute[directive=true][key.name.name='bind']": V.VDirective & {
+    value:
+      | (V.VExpressionContainer & {
+          expression: ES.Expression | V.VFilterSequenceExpression | null
+        })
+      | null
+  }
+  "VAttribute[directive=true][key.name.name='bind']:exit": V.VDirective & {
+    value:
+      | (V.VExpressionContainer & {
+          expression: ES.Expression | V.VFilterSequenceExpression | null
+        })
+      | null
+  }
   "VAttribute[directive=true][key.name.name='cloak']": V.VDirective
   "VAttribute[directive=true][key.name.name='cloak']:exit": V.VDirective
-  "VAttribute[directive=true][key.name.name='else-if']": V.VDirective
-  "VAttribute[directive=true][key.name.name='else-if']:exit": V.VDirective
+  "VAttribute[directive=true][key.name.name='else-if']": V.VDirective & {
+    value:
+      | (V.VExpressionContainer & { expression: ES.Expression | null })
+      | null
+  }
+  "VAttribute[directive=true][key.name.name='else-if']:exit": V.VDirective & {
+    value:
+      | (V.VExpressionContainer & { expression: ES.Expression | null })
+      | null
+  }
   "VAttribute[directive=true][key.name.name='else']": V.VDirective
   "VAttribute[directive=true][key.name.name='else']:exit": V.VDirective
-  "VAttribute[directive=true][key.name.name='for']": V.VDirective
-  "VAttribute[directive=true][key.name.name='for']:exit": V.VDirective
+  "VAttribute[directive=true][key.name.name='for']": V.VDirective & {
+    value:
+      | (V.VExpressionContainer & { expression: V.VForExpression | null })
+      | null
+  }
+  "VAttribute[directive=true][key.name.name='for']:exit": V.VDirective & {
+    value:
+      | (V.VExpressionContainer & { expression: V.VForExpression | null })
+      | null
+  }
   "VAttribute[directive=true][key.name.name='html']": V.VDirective
   "VAttribute[directive=true][key.name.name='html']:exit": V.VDirective
-  "VAttribute[directive=true][key.name.name='if']": V.VDirective
-  "VAttribute[directive=true][key.name.name='if']:exit": V.VDirective
-  "VAttribute[directive=true][key.name.name='model']": V.VDirective
-  "VAttribute[directive=true][key.name.name='model']:exit": V.VDirective
-  "VAttribute[directive=true][key.name.name='on']": V.VDirective
-  "VAttribute[directive=true][key.name.name='on']:exit": V.VDirective
+  "VAttribute[directive=true][key.name.name='if']": V.VDirective & {
+    value:
+      | (V.VExpressionContainer & { expression: ES.Expression | null })
+      | null
+  }
+  "VAttribute[directive=true][key.name.name='if']:exit": V.VDirective & {
+    value:
+      | (V.VExpressionContainer & { expression: ES.Expression | null })
+      | null
+  }
+  "VAttribute[directive=true][key.name.name='model']": V.VDirective & {
+    value:
+      | (V.VExpressionContainer & { expression: ES.Expression | null })
+      | null
+  }
+  "VAttribute[directive=true][key.name.name='model']:exit": V.VDirective & {
+    value:
+      | (V.VExpressionContainer & { expression: ES.Expression | null })
+      | null
+  }
+  "VAttribute[directive=true][key.name.name='on']": V.VDirective & {
+    value:
+      | (V.VExpressionContainer & {
+          expression: ES.Expression | V.VOnExpression | null
+        })
+      | null
+  }
+  "VAttribute[directive=true][key.name.name='on']:exit": V.VDirective & {
+    value:
+      | (V.VExpressionContainer & {
+          expression: ES.Expression | V.VOnExpression | null
+        })
+      | null
+  }
   "VAttribute[directive=true][key.name.name='once']": V.VDirective
   "VAttribute[directive=true][key.name.name='once']:exit": V.VDirective
   "VAttribute[directive=true][key.name.name='pre']": V.VDirective
   "VAttribute[directive=true][key.name.name='pre']:exit": V.VDirective
-  "VAttribute[directive=true][key.name.name='show']": V.VDirective
-  "VAttribute[directive=true][key.name.name='show']:exit": V.VDirective
-  "VAttribute[directive=true][key.name.name='slot']": V.VDirective
-  "VAttribute[directive=true][key.name.name='slot']:exit": V.VDirective
+  "VAttribute[directive=true][key.name.name='show']": V.VDirective & {
+    value:
+      | (V.VExpressionContainer & { expression: ES.Expression | null })
+      | null
+  }
+  "VAttribute[directive=true][key.name.name='show']:exit": V.VDirective & {
+    value:
+      | (V.VExpressionContainer & { expression: ES.Expression | null })
+      | null
+  }
+  "VAttribute[directive=true][key.name.name='slot']": V.VDirective & {
+    value:
+      | (V.VExpressionContainer & { expression: V.VSlotScopeExpression | null })
+      | null
+  }
+  "VAttribute[directive=true][key.name.name='slot']:exit": V.VDirective & {
+    value:
+      | (V.VExpressionContainer & { expression: V.VSlotScopeExpression | null })
+      | null
+  }
   "VAttribute[directive=true][key.name.name='text']": V.VDirective
   "VAttribute[directive=true][key.name.name='text']:exit": V.VDirective
   'VAttribute[value!=null]':


Test patch:
diff --git a/tests/lib/rules/no-dupe-v-else-if.js b/tests/lib/rules/no-dupe-v-else-if.js
new file mode 100644
index 00000000..0b5698fe
--- /dev/null
+++ b/tests/lib/rules/no-dupe-v-else-if.js
@@ -0,0 +1,662 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-dupe-v-else-if')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2019,
+    sourceType: 'module'
+  }
+})
+
+tester.run('no-dupe-v-else-if', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="foo" />
+        <div v-else-if="bar" />
+        <div v-else-if="baz" />
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="foo" >
+          <div v-else-if="foo" />
+        </div>
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="foo" />
+        <div v-else-if="bar" />
+        <div v-if="bar" />
+        <div v-else-if="foo" />
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="isSomething(x)" />
+        <div v-else-if="isSomethingElse(x)" />
+
+        <div v-if="a" />
+        <div v-else-if="b" />
+        <div v-else-if="c && d" />
+        <div v-else-if="c && e" />
+
+        <div v-if="n === 1" />
+        <div v-else-if="n === 2" />
+        <div v-else-if="n === 3" />
+        <div v-else-if="n === 4" />
+        <div v-else-if="n === 5" />
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="foo" />
+        <div />
+        <div v-else-if="foo" />
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if />
+        <div v-else-if />
+      </template>
+      `
+    },
+    // parse error
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="foo." />
+        <div v-else-if="foo." />
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-else-if="foo." />
+        <div v-else-if="foo" />
+      </template>
+      `
+    },
+
+    // Referred to the ESLint core rule.
+    '<template><div v-if="a" /><div v-else-if="b" /></template>',
+    '<template><div v-if="a" /><div v-else-if="b" /><div v-else-if="c" /></template>',
+    '<template><div v-if="true" /><div v-else-if="false" /></template>',
+    '<template><div v-if="1" /><div v-else-if="2" /></template>',
+    '<template><div v-if="f" /><div v-else-if="f()" /></template>',
+    '<template><div v-if="f(a)" /><div v-else-if="g(a)" /></template>',
+    '<template><div v-if="f(a)" /><div v-else-if="f(b)" /></template>',
+    '<template><div v-if="a === 1" /><div v-else-if="a === 2" /></template>',
+    '<template><div v-if="a === 1" /><div v-else-if="b === 1" /></template>',
+    '<template><div v-if="a" /></template>',
+    '<template><div v-if="a"><div v-if="a" /></div></template>',
+    '<template><div v-if="a"><div v-if="b" /></div><div v-else-if="b" /></template>',
+    '<template><div v-if="a"><div v-if="b" /><div v-else-if="a" /></div></template>',
+    '<template><div v-if="a" /><div v-else-if="!!a" /></template>',
+    '<template><div v-if="a === 1" /><div v-else-if="a === (1)" /></template>',
+    '<template><div v-if="a || b" /><div v-else-if="c || d" /></template>',
+    '<template><div v-if="a || b" /><div v-else-if="a || c" /></template>',
+    '<template><div v-if="a" /><div v-else-if="a || b" /></template>',
+    '<template><div v-if="a" /><div v-else-if="b" /><div v-else-if="a || b || c" /></template>',
+    '<template><div v-if="a && b" /><div v-else-if="a" /><div v-else-if="b" /></template>',
+    '<template><div v-if="a && b" /><div v-else-if="b && c" /><div v-else-if="a && c" /></template>',
+    '<template><div v-if="a && b" /><div v-else-if="b || c" /></template>',
+    '<template><div v-if="a" /><div v-else-if="b && (a || c)" /></template>',
+    '<template><div v-if="a" /><div v-else-if="b && (c || d && a)" /></template>',
+    '<template><div v-if="a && b && c" /><div v-else-if="a && b && (c || d)" /></template>'
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="foo" />
+        <div v-else-if="foo" />
+      </template>
+      `,
+      errors: [
+        {
+          message:
+            'This branch can never execute. Its condition is a duplicate or covered by previous conditions in the `v-if` / `v-else-if` chain.',
+          line: 4,
+          column: 25,
+          endLine: 4,
+          endColumn: 28
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="isSomething(x)" />
+        <div v-else-if="isSomething(x)" />
+      </template>
+      `,
+      errors: [
+        {
+          messageId: 'unexpected',
+          line: 4
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="a" />
+        <div v-else-if="b" />
+        <div v-else-if="c && d" />
+        <div v-else-if="c && d" />
+      </template>
+      `,
+      errors: [
+        {
+          messageId: 'unexpected',
+          line: 6
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="n === 1" />
+        <div v-else-if="n === 2" />
+        <div v-else-if="n === 3" />
+        <div v-else-if="n === 2" />
+        <div v-else-if="n === 5" />
+      </template>
+      `,
+      errors: [
+        {
+          messageId: 'unexpected',
+          line: 6
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="a || b" />
+        <div v-else-if="a" />
+      </template>
+      `,
+      errors: [
+        {
+          messageId: 'unexpected',
+          line: 4
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="a" />
+        <div v-else-if="b" />
+        <div v-else-if="a || b" />
+      </template>
+      `,
+      errors: [
+        {
+          messageId: 'unexpected',
+          line: 5
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="a" />
+        <div v-else-if="a && b" />
+      </template>
+      `,
+      errors: [
+        {
+          messageId: 'unexpected',
+          line: 4
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="a && b" />
+        <div v-else-if="a && b && c" />
+      </template>
+      `,
+      errors: [
+        {
+          messageId: 'unexpected',
+          line: 4
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="a || b" />
+        <div v-else-if="b && c" />
+      </template>
+      `,
+      errors: [
+        {
+          messageId: 'unexpected',
+          line: 4
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="a" />
+        <div v-else-if="b && c" />
+        <div v-else-if="d && (c && e && b || a)" />
+      </template>
+      `,
+      errors: [
+        {
+          messageId: 'unexpected',
+          line: 5
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="foo" />
+        <div v-else-if="foo && bar" />
+        <div v-else-if="baz && foo" />
+      </template>
+      `,
+      errors: [
+        {
+          messageId: 'unexpected',
+          line: 4,
+          column: 25,
+          endLine: 4,
+          endColumn: 28
+        },
+        {
+          messageId: 'unexpected',
+          line: 5,
+          column: 32,
+          endLine: 5,
+          endColumn: 35
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="a && b" />
+        <div v-else-if="a && b && c" />
+        <div v-else-if="a && c && b" />
+      </template>
+      `,
+      errors: [
+        { messageId: 'unexpected', line: 4 },
+        { messageId: 'unexpected', line: 5 }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div v-if="a || b" />
+        <div v-else-if="a" />
+        <div v-else-if="b" />
+      </template>
+      `,
+      errors: [
+        { messageId: 'unexpected', line: 4 },
+        { messageId: 'unexpected', line: 5 }
+      ]
+    },
+    {
+      filename: 'foo.vue',
+      code: `
+      <template>
+        <div v-if      ="((f && e) || d) && c || (b && a)" />
+        <div v-else-if ="(a && b) || (c && (d || (e && f)))" />
+        <div v-else-if ="(a && b) || (c && (d || (e && f)))" />
+      </template>
+      `,
+      errors: [{ messageId: 'unexpected' }, { messageId: 'unexpected' }]
+    },
+
+    // Referred to the ESLint core rule.
+    {
+      filename: 'test.vue',
+      code: '<template><div v-if="a" /><div v-else-if="a" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a" /><div v-else-if="b" /><div v-else-if="a" /><div v-else-if="c" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a" /><div v-else-if="b" /><div v-else-if="a" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a" /><div v-else-if="b" /><div v-else-if="c" /><div v-else-if="a" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a" /><div v-else-if="b" /><div v-else-if="b" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a" /><div v-else-if="b" /><div v-else-if="c" /><div v-else-if="b" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a" /><div v-else-if="b" /><div v-else-if="c" /><div v-else-if="b" /><div v-else-if="d" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a" /><div v-else-if="b" /><div v-else-if="c" /><div v-else-if="d" /><div v-else-if="b" /><div v-else-if="e" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a" /><div v-else-if="a" /><div v-else-if="a" /></template>',
+      errors: [{ messageId: 'unexpected' }, { messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a" /><div v-else-if="b" /><div v-else-if="a" /><div v-else-if="b" /><div v-else-if="a" /></template>',
+      errors: [
+        { messageId: 'unexpected' },
+        { messageId: 'unexpected' },
+        { messageId: 'unexpected' }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a"><div v-if="b" /></div><div v-else-if="a" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a === 1" /><div v-else-if="a === 1" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="1 < a" /><div v-else-if="1 < a" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div v-if="true" /><div v-else-if="true" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a && b" /><div v-else-if="a && b" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a && b || c" /><div v-else-if="a && b || c" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div v-if="f(a)" /><div v-else-if="f(a)" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a === 1" /><div v-else-if="a===1" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a === 1" /><div v-else-if="a === /* comment */ 1" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div v-if="a || b" /><div v-else-if="a" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a || b" /><div v-else-if="a" /><div v-else-if="b" /></template>',
+      errors: [{ messageId: 'unexpected' }, { messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a || b" /><div v-else-if="b || a" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a" /><div v-else-if="b" /><div v-else-if="a || b" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a || b" /><div v-else-if="c || d" /><div v-else-if="a || d" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="(a === b && fn(c)) || d" /><div v-else-if="fn(c) && a === b" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div v-if="a" /><div v-else-if="a && b" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a && b" /><div v-else-if="a && b && c" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a || c" /><div v-else-if="a && b || c" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a" /><div v-else-if="b" /><div v-else-if="c && a || b" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a" /><div v-else-if="b" /><div v-else-if="c && (a || b)" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a" /><div v-else-if="b && c" /><div v-else-if="d && (a || e && c && b)" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a || b && c" /><div v-else-if="b && c && d" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a || b" /><div v-else-if="b && c" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a" /><div v-else-if="b" /><div v-else-if="(a || b) && c" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="(a && (b || c)) || d" /><div v-else-if="(c || b) && e && a" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a && b || b && c" /><div v-else-if="a && b && c" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a" /><div v-else-if="b && c" /><div v-else-if="d && (c && e && b || a)" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a || (b && (c || d))" /><div v-else-if="(d || c) && b" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a || b" /><div v-else-if="(b || a) && c" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a || b" /><div v-else-if="c" /><div v-else-if="d" /><div v-else-if="b && (a || c)" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a || b || c" /><div v-else-if="a || (b && d) || (c && e)" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a || (b || c)" /><div v-else-if="a || (b && c)" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a || b" /><div v-else-if="c" /><div v-else-if="d" /><div v-else-if="(a || c) && (b || d)" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a" /><div v-else-if="b" /><div v-else-if="c && (a || d && b)" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div v-if="a" /><div v-else-if="a || a" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a || a" /><div v-else-if="a || a" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div v-if="a || a" /><div v-else-if="a" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div v-if="a" /><div v-else-if="a && a" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div v-if="a && a" /><div v-else-if="a && a" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div v-if="a && a" /><div v-else-if="a" /></template>',
+      errors: [{ messageId: 'unexpected' }]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-lone-template.js b/tests/lib/rules/no-lone-template.js
new file mode 100644
index 00000000..947934fc
--- /dev/null
+++ b/tests/lib/rules/no-lone-template.js
@@ -0,0 +1,181 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-lone-template')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2019,
+    sourceType: 'module'
+  }
+})
+
+tester.run('no-lone-template', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <template v-if="foo">...</template>
+        <template v-else-if="bar">...</template>
+        <template v-else>...</template>
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <template v-for="e in list">...</template>
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <template v-slot>...</template>
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <CoolButton>
+         <template slot="foo">...</template>
+        </CoolButton>
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <CoolButton>
+         <template slot-scope="foo">...</template>
+        </CoolButton>
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <CoolButton>
+         <template scope="foo">...</template>
+        </CoolButton>
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <template id="a">...</template>
+      </template>
+      `,
+      options: [{ ignoreAccessible: true }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <template :id="a">...</template>
+      </template>
+      `,
+      options: [{ ignoreAccessible: true }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <template ref="b">...</template>
+      </template>
+      `,
+      options: [{ ignoreAccessible: true }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <template :ref="b">...</template>
+      </template>
+      `,
+      options: [{ ignoreAccessible: true }]
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <template>...</template>
+      </template>
+      `,
+      errors: [
+        {
+          message: '`<template>` require directive.',
+          line: 3,
+          column: 9,
+          endLine: 3,
+          endColumn: 19
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <template/>
+      </template>
+      `,
+      errors: ['`<template>` require directive.']
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <template v-on:id="a">...</template>
+      </template>
+      `,
+      options: [{ ignoreAccessible: true }],
+      errors: ['`<template>` require directive.']
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <template v-bind="id">...</template>
+      </template>
+      `,
+      options: [{ ignoreAccessible: true }],
+      errors: ['`<template>` require directive.']
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <template v-bind:[foo]="id">...</template>
+      </template>
+      `,
+      options: [{ ignoreAccessible: true }],
+      errors: ['`<template>` require directive.']
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <template class="b">...</template>
+      </template>
+      `,
+      options: [{ ignoreAccessible: true }],
+      errors: ['`<template>` require directive.']
+    }
+  ]
+})