Repository: vuejs/eslint-plugin-vue
Pull request #1141 (2020-05-19 04:44:39)

Add `vue/prefer-template` rule
This PR adds the wrapper rule of the [prefer-template](https://eslint.org/docs/rules/prefer-template) core rule to apply to the expressions in `<template>`.


Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 83eda569..d68509de 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -271,6 +271,7 @@ For example:
 | [vue/brace-style](./brace-style.md) | enforce consistent brace style for blocks | :wrench: |
 | [vue/camelcase](./camelcase.md) | enforce camelcase naming convention |  |
 | [vue/comma-dangle](./comma-dangle.md) | require or disallow trailing commas | :wrench: |
+| [vue/comma-spacing](./comma-spacing.md) | enforce consistent spacing before and after commas | :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/dot-location](./dot-location.md) | enforce consistent newlines before and after dots | :wrench: |
 | [vue/eqeqeq](./eqeqeq.md) | require the use of `===` and `!==` | :wrench: |
@@ -282,6 +283,7 @@ For example:
 | [vue/match-component-file-name](./match-component-file-name.md) | require component name property to match its file name |  |
 | [vue/max-len](./max-len.md) | enforce a maximum line length |  |
 | [vue/no-boolean-default](./no-boolean-default.md) | disallow boolean defaults | :wrench: |
+| [vue/no-duplicate-attr-inheritance](./no-duplicate-attr-inheritance.md) | enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"` |  |
 | [vue/no-empty-pattern](./no-empty-pattern.md) | disallow empty destructuring patterns |  |
 | [vue/no-irregular-whitespace](./no-irregular-whitespace.md) | disallow irregular whitespace |  |
 | [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions |  |
@@ -292,6 +294,7 @@ For example:
 | [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: |
 | [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: |
 | [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: |
+| [vue/prefer-template](./prefer-template.md) | require template literals instead of string concatenation | :wrench: |
 | [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported |  |
 | [vue/require-explicit-emits](./require-explicit-emits.md) | require `emits` option with name triggered by `$emit()` |  |
 | [vue/require-name-property](./require-name-property.md) | require a name property in Vue components |  |
diff --git a/docs/rules/comma-spacing.md b/docs/rules/comma-spacing.md
new file mode 100644
index 00000000..d596ee4e
--- /dev/null
+++ b/docs/rules/comma-spacing.md
@@ -0,0 +1,23 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/comma-spacing
+description: enforce consistent spacing before and after commas
+---
+# vue/comma-spacing
+> enforce consistent spacing before and after commas
+
+- :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 [comma-spacing] rule but it applies to the expressions in `<template>`.
+
+## :books: Further reading
+
+- [comma-spacing]
+
+[comma-spacing]: https://eslint.org/docs/rules/comma-spacing
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/comma-spacing.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/comma-spacing.js)
diff --git a/docs/rules/no-duplicate-attr-inheritance.md b/docs/rules/no-duplicate-attr-inheritance.md
new file mode 100644
index 00000000..959124e4
--- /dev/null
+++ b/docs/rules/no-duplicate-attr-inheritance.md
@@ -0,0 +1,56 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-duplicate-attr-inheritance
+description: enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"`
+---
+# vue/no-duplicate-attr-inheritance
+> enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"`
+
+## :book: Rule Details
+
+This rule aims to prevent duplicated attribute inheritance.  
+This rule to warn to apply `inheritAttrs: false` when it detects `v-bind="$attrs"` being used.
+
+<eslint-code-block :rules="{'vue/no-duplicate-attr-inheritance': ['error']}">
+
+```vue
+<template>
+  <MyInput v-bind="$attrs" />
+</template>
+<script>
+export default {
+  /* ✓ GOOD */
+  inheritAttrs: false
+}
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/no-duplicate-attr-inheritance': ['error']}">
+
+```vue
+<template>
+  <MyInput v-bind="$attrs" />
+</template>
+<script>
+export default {
+  /* ✗ BAD */
+  // inheritAttrs: true (default)
+}
+```
+
+</eslint-code-block>
+
+### Options
+
+Nothing.
+
+## Further Reading
+
+- [API - inheritAttrs](https://vuejs.org/v2/api/index.html#inheritAttrs)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-duplicate-attr-inheritance.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-duplicate-attr-inheritance.js)
diff --git a/docs/rules/no-multiple-template-root.md b/docs/rules/no-multiple-template-root.md
index e22cb0f4..3151ffe7 100644
--- a/docs/rules/no-multiple-template-root.md
+++ b/docs/rules/no-multiple-template-root.md
@@ -9,8 +9,9 @@ description: disallow adding multiple root nodes to the template
 
 - :gear: This rule is included in all of `"plugin:vue/essential"`, `"plugin:vue/strongly-recommended"` and `"plugin:vue/recommended"`.
 
-This rule checks whether template contains single root element valid for Vue 2.
+## :book: Rule Details
 
+This rule checks whether template contains single root element valid for Vue 2.
 
 <eslint-code-block :rules="{'vue/no-multiple-template-root': ['error']}">
 
diff --git a/docs/rules/prefer-template.md b/docs/rules/prefer-template.md
new file mode 100644
index 00000000..cbc6d812
--- /dev/null
+++ b/docs/rules/prefer-template.md
@@ -0,0 +1,23 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/prefer-template
+description: require template literals instead of string concatenation
+---
+# vue/prefer-template
+> require template literals instead of string concatenation
+
+- :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 [prefer-template] rule but it applies to the expressions in `<template>`.
+
+## :books: Further reading
+
+- [prefer-template]
+
+[prefer-template]: https://eslint.org/docs/rules/prefer-template
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/prefer-template.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/prefer-template.js)
diff --git a/lib/configs/no-layout-rules.js b/lib/configs/no-layout-rules.js
index 24e7d0f8..4a3fe836 100644
--- a/lib/configs/no-layout-rules.js
+++ b/lib/configs/no-layout-rules.js
@@ -10,6 +10,7 @@ module.exports = {
     'vue/block-spacing': 'off',
     'vue/brace-style': 'off',
     'vue/comma-dangle': 'off',
+    'vue/comma-spacing': 'off',
     'vue/dot-location': 'off',
     'vue/html-closing-bracket-newline': 'off',
     'vue/html-closing-bracket-spacing': 'off',
diff --git a/lib/index.js b/lib/index.js
index 6adbbecd..66db5a3e 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -15,6 +15,7 @@ module.exports = {
     'brace-style': require('./rules/brace-style'),
     'camelcase': require('./rules/camelcase'),
     'comma-dangle': require('./rules/comma-dangle'),
+    'comma-spacing': require('./rules/comma-spacing'),
     'comment-directive': require('./rules/comment-directive'),
     'component-definition-name-casing': require('./rules/component-definition-name-casing'),
     'component-name-in-template-casing': require('./rules/component-name-in-template-casing'),
@@ -58,6 +59,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-duplicate-attr-inheritance': require('./rules/no-duplicate-attr-inheritance'),
     'no-duplicate-attributes': require('./rules/no-duplicate-attributes'),
     'no-empty-pattern': require('./rules/no-empty-pattern'),
     'no-irregular-whitespace': require('./rules/no-irregular-whitespace'),
@@ -89,6 +91,7 @@ module.exports = {
     'object-curly-spacing': require('./rules/object-curly-spacing'),
     'order-in-components': require('./rules/order-in-components'),
     'padding-line-between-blocks': require('./rules/padding-line-between-blocks'),
+    'prefer-template': require('./rules/prefer-template'),
     'prop-name-casing': require('./rules/prop-name-casing'),
     'require-component-is': require('./rules/require-component-is'),
     'require-default-prop': require('./rules/require-default-prop'),
diff --git a/lib/rules/comma-spacing.js b/lib/rules/comma-spacing.js
new file mode 100644
index 00000000..f51bb7c6
--- /dev/null
+++ b/lib/rules/comma-spacing.js
@@ -0,0 +1,12 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const { wrapCoreRule } = require('../utils')
+
+// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
+module.exports = wrapCoreRule(
+  require('eslint/lib/rules/comma-spacing'),
+  { skipDynamicArguments: true, skipDynamicArgumentsReport: true }
+)
diff --git a/lib/rules/no-deprecated-dollar-listeners-api.js b/lib/rules/no-deprecated-dollar-listeners-api.js
index 29c0655d..b833b462 100644
--- a/lib/rules/no-deprecated-dollar-listeners-api.js
+++ b/lib/rules/no-deprecated-dollar-listeners-api.js
@@ -50,11 +50,19 @@ module.exports = {
       },
       utils.defineVueVisitor(context,
         {
-          'MemberExpression > ThisExpression' (node) {
-            if (node.parent.property.name !== '$listeners') return
+          'MemberExpression' (node) {
+            if (
+              node.property.type !== 'Identifier' ||
+              node.property.name !== '$listeners'
+            ) {
+              return
+            }
+            if (!utils.isThis(node.object, context)) {
+              return
+            }
 
             context.report({
-              node: node.parent.property,
+              node: node.property,
               messageId: 'deprecated'
             })
           }
diff --git a/lib/rules/no-deprecated-events-api.js b/lib/rules/no-deprecated-events-api.js
index 8537469e..7989ffb5 100644
--- a/lib/rules/no-deprecated-events-api.js
+++ b/lib/rules/no-deprecated-events-api.js
@@ -32,11 +32,21 @@ module.exports = {
   create (context) {
     return utils.defineVueVisitor(context,
       {
-        'CallExpression > MemberExpression > ThisExpression' (node) {
-          if (!['$on', '$off', '$once'].includes(node.parent.property.name)) return
+        'CallExpression > MemberExpression' (node) {
+          const call = node.parent
+          if (
+            call.callee !== node ||
+            node.property.type !== 'Identifier' ||
+            !['$on', '$off', '$once'].includes(node.property.name)
+          ) {
+            return
+          }
+          if (!utils.isThis(node.object, context)) {
+            return
+          }
 
           context.report({
-            node: node.parent.parent,
+            node: node.property,
             messageId: 'noDeprecatedEventsApi'
           })
         }
diff --git a/lib/rules/no-duplicate-attr-inheritance.js b/lib/rules/no-duplicate-attr-inheritance.js
new file mode 100644
index 00000000..6ed2da07
--- /dev/null
+++ b/lib/rules/no-duplicate-attr-inheritance.js
@@ -0,0 +1,62 @@
+/**
+ * @fileoverview Disable inheritAttrs when using v-bind="$attrs"
+ * @author Hiroki Osame
+ */
+'use strict'
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"`',
+      categories: undefined,
+      recommended: false,
+      url: 'https://eslint.vuejs.org/rules/no-duplicate-attr-inheritance.html'
+    },
+    fixable: null,
+    schema: [
+      // fill in your schema
+    ]
+  },
+
+  create (context) {
+    let inheritsAttrs = true
+
+    return Object.assign(
+      utils.executeOnVue(context, (node) => {
+        const inheritAttrsProp = node.properties.find(prop => (prop.type === 'Property' && utils.getStaticPropertyName(prop) === 'inheritAttrs'))
+
+        if (inheritAttrsProp && inheritAttrsProp.value.type === 'Literal') {
+          inheritsAttrs = inheritAttrsProp.value.value
+        }
+      }),
+      utils.defineTemplateBodyVisitor(context, {
+        "VAttribute[directive=true][key.name.name='bind'][key.argument=null] > VExpressionContainer" (node) {
+          if (!inheritsAttrs) {
+            return
+          }
+          const attrsRef = node.references.find(reference => {
+            if (reference.variable != null) {
+              // Not vm reference
+              return false
+            }
+            return reference.id.name === '$attrs'
+          })
+
+          if (attrsRef) {
+            context.report({
+              node: attrsRef.id,
+              message: 'Set "inheritAttrs" to false.'
+            })
+          }
+        }
+      })
+    )
+  }
+}
diff --git a/lib/rules/no-lifecycle-after-await.js b/lib/rules/no-lifecycle-after-await.js
index b1c3c243..a009f0e4 100644
--- a/lib/rules/no-lifecycle-after-await.js
+++ b/lib/rules/no-lifecycle-after-await.js
@@ -80,6 +80,10 @@ module.exports = {
             }
 
             if (lifecycleHookCallNodes.has(node)) {
+              if (node.arguments.length >= 2) {
+                // Has target instance. e.g. `onMounted(() => {}, instance)`
+                return
+              }
               context.report({
                 node,
                 messageId: 'forbidden'
diff --git a/lib/rules/prefer-template.js b/lib/rules/prefer-template.js
new file mode 100644
index 00000000..70e041e5
--- /dev/null
+++ b/lib/rules/prefer-template.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(
+  require('eslint/lib/rules/prefer-template')
+)
diff --git a/lib/rules/require-explicit-emits.js b/lib/rules/require-explicit-emits.js
index 2addf847..8bfe481b 100644
--- a/lib/rules/require-explicit-emits.js
+++ b/lib/rules/require-explicit-emits.js
@@ -3,7 +3,6 @@
  * See LICENSE file in root directory for full license.
  */
 'use strict'
-// @ts-check
 
 /**
  * @typedef {import('vue-eslint-parser').AST.ESLintLiteral} Literal
@@ -262,8 +261,7 @@ module.exports = {
 
           // verify $emit
           if (emit && emit.name === '$emit') {
-            const objectType = emit.member.object.type
-            if (objectType === 'Identifier' || objectType === 'ThisExpression') {
+            if (utils.isThis(emit.member.object, context)) {
               // verify this.$emit()
               verify(emitsDeclarations, nameLiteralNode, vueNode)
             }
diff --git a/lib/rules/return-in-emits-validator.js b/lib/rules/return-in-emits-validator.js
index dd5e551f..64e62b8c 100644
--- a/lib/rules/return-in-emits-validator.js
+++ b/lib/rules/return-in-emits-validator.js
@@ -74,6 +74,14 @@ module.exports = {
           },
           ':function' (node) {
             scopeStack = { upper: scopeStack, functionNode: node, hasReturnValue: false, possibleOfReturnTrue: false }
+
+            if (node.type === 'ArrowFunctionExpression' && node.expression) {
+              scopeStack.hasReturnValue = true
+
+              if (!isFalsy(node.body)) {
+                scopeStack.possibleOfReturnTrue = true
+              }
+            }
           },
           ReturnStatement (node) {
             if (node.argument) {
diff --git a/lib/utils/index.js b/lib/utils/index.js
index 3e1ce4c5..7787c138 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -41,6 +41,7 @@ const VOID_ELEMENT_NAMES = new Set(require('./void-elements.json'))
 const assert = require('assert')
 const path = require('path')
 const vueEslintParser = require('vue-eslint-parser')
+const { findVariable } = require('eslint-utils')
 
 /**
  * @type { WeakMap<RuleContext, Token[]> }
@@ -53,9 +54,24 @@ const componentComments = new WeakMap()
  * @param {TokenStore} tokenStore The token store object for template.
  */
 function wrapContextToOverrideTokenMethods (context, tokenStore) {
-  const sourceCode = new Proxy(context.getSourceCode(), {
+  const eslintSourceCode = context.getSourceCode()
+  let tokensAndComments
+  function getTokensAndComments () {
+    if (tokensAndComments) {
+      return tokensAndComments
+    }
+    const templateBody = eslintSourceCode.ast.templateBody
+    tokensAndComments = templateBody ? tokenStore.getTokens(templateBody, {
+      includeComments: true
+    }) : []
+    return tokensAndComments
+  }
+  const sourceCode = new Proxy(Object.assign({}, eslintSourceCode), {
     get (object, key) {
-      return key in tokenStore ? tokenStore[key] : object[key]
+      if (key === 'tokensAndComments') {
+        return getTokensAndComments()
+      }
+      return key in tokenStore ? tokenStore[key] : eslintSourceCode[key]
     }
   })
 
@@ -67,6 +83,50 @@ function wrapContextToOverrideTokenMethods (context, tokenStore) {
   }
 }
 
+/**
+ * Wrap the rule context object to override report method to skip the dynamic argument.
+ * @param {RuleContext} context The rule context object.
+ */
+function wrapContextToOverrideReportMethodToSkipDynamicArgument (context) {
+  const sourceCode = context.getSourceCode()
+  const templateBody = sourceCode.ast.templateBody
+  if (!templateBody) {
+    return context
+  }
+  const directiveKeyRanges = []
+  const traverseNodes = vueEslintParser.AST.traverseNodes
+  traverseNodes(templateBody, {
+    enterNode (node, parent) {
+      if (
+        parent && parent.type === 'VDirectiveKey' && node.type === 'VExpressionContainer'
+      ) {
+        directiveKeyRanges.push(node.range)
+      }
+    },
+    leaveNode () {}
+  })
+
+  return {
+    __proto__: context,
+    report (descriptor, ...args) {
+      let range = null
+      if (descriptor.loc) {
+        range = [sourceCode.getIndexFromLoc(descriptor.loc.start), sourceCode.getIndexFromLoc(descriptor.loc.end)]
+      } else if (descriptor.node) {
+        range = descriptor.node.range
+      }
+      if (range) {
+        for (const directiveKeyRange of directiveKeyRanges) {
+          if (range[0] < directiveKeyRange[1] && directiveKeyRange[0] < range[1]) {
+            return
+          }
+        }
+      }
+      context.report(descriptor, ...args)
+    }
+  }
+}
+
 // ------------------------------------------------------------------------------
 // Exports
 // ------------------------------------------------------------------------------
@@ -96,13 +156,14 @@ module.exports = {
   /**
    * Wrap a given core rule to apply it to Vue.js template.
    * @param {Rule} coreRule The core rule implementation to wrap.
-   * @param {Object|undefined} options The option of this rule.
+   * @param {Object} [options] The option of this rule.
    * @param {string|undefined} options.category The category of this rule.
    * @param {boolean|undefined} options.skipDynamicArguments If `true`, skip validation within dynamic arguments.
+   * @param {boolean|undefined} options.skipDynamicArgumentsReport If `true`, skip report within dynamic arguments.
    * @returns {Rule} The wrapped rule implementation.
    */
   wrapCoreRule (coreRule, options) {
-    const { category, skipDynamicArguments } = options || {}
+    const { category, skipDynamicArguments, skipDynamicArgumentsReport } = options || {}
     return {
       create (context) {
         const tokenStore =
@@ -115,6 +176,10 @@ module.exports = {
           context = wrapContextToOverrideTokenMethods(context, tokenStore)
         }
 
+        if (skipDynamicArgumentsReport) {
+          context = wrapContextToOverrideReportMethodToSkipDynamicArgument(context)
+        }
+
         // Move `Program` handlers to `VElement[parent.type!='VElement']`
         const handlers = coreRule.create(context)
         if (handlers.Program) {
@@ -855,7 +920,34 @@ module.exports = {
    * @param {T} node
    * @return {T}
    */
-  unwrapTypes
+  unwrapTypes,
+
+  /**
+   * Check whether the given node is `this` or variable that stores `this`.
+   * @param  {ASTNode} node The node to check
+   * @returns {boolean} `true` if the given node is `this`.
+   */
+  isThis (node, context) {
+    if (node.type === 'ThisExpression') {
+      return true
+    }
+    if (node.type !== 'Identifier') {
+      return false
+    }
+    const variable = findVariable(context.getScope(), node)
+
+    if (variable != null && variable.defs.length === 1) {
+      const def = variable.defs[0]
+      if (
+        def.parent &&
+        def.parent.kind === 'const' &&
+        def.node.id.type === 'Identifier'
+      ) {
+        return def.node && def.node.init && def.node.init.type === 'ThisExpression'
+      }
+    }
+    return false
+  }
 }
 
 /**


Test patch:
diff --git a/tests/lib/rules/comma-spacing.js b/tests/lib/rules/comma-spacing.js
new file mode 100644
index 00000000..26b0ba1a
--- /dev/null
+++ b/tests/lib/rules/comma-spacing.js
@@ -0,0 +1,283 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/comma-spacing')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2020 }
+})
+
+tester.run('comma-spacing', rule, {
+  valid: [
+    `<template>
+      <button @click="
+        var foo = 1, bar = 2;
+      ">
+        OK
+      </button>
+    </template>`,
+    `<template>
+      <DateInput :value="[2000, 12, 31]" />
+    </template>`,
+    `<template>
+      <DateInput :value="{y: 2000, m: 12, d: 31}" />
+    </template>`,
+    `<template>
+      <CustomDlg @ok="foo(a, b)" />
+    </template>`,
+    `<template>
+      <CustomDlg @ok="(a, b) => {}" />
+    </template>`,
+    `<template>
+      <CustomDlg @ok="function (a, b) {}" />
+    </template>`,
+    `<template>
+      <CustomList>
+        <li slot-scope="a, b">{{ a }}</li>
+      </CustomList>
+    </template>`,
+    {
+      code: `
+        <template>
+          <button @click="
+            fn(a ,b)
+          "/>
+        </template>`,
+      options: [{ before: true, after: false }]
+    },
+    `<template>
+      <div :[fn(a,b)]="val" />
+    </template>`,
+    `<template>
+      <div :[[,]]="val" />
+    </template>`,
+    `<template>
+      <div :[a,]="val" />
+    </template>`,
+    `<script>
+    fn = (a,b) => {}
+    </script>`,
+    `fn = (a,b) => {}`
+  ],
+  invalid: [
+    {
+      code: `
+        <template>
+          <button @click="
+            var foo = 1 ,bar = 2;
+          ">
+            NG
+          </button>
+        </template>`,
+      output: `
+        <template>
+          <button @click="
+            var foo = 1, bar = 2;
+          ">
+            NG
+          </button>
+        </template>`,
+      errors: [
+        {
+          message: 'There should be no space before \',\'.',
+          line: 4,
+          column: 25
+        },
+        {
+          message: 'A space is required after \',\'.',
+          line: 4,
+          column: 25
+        }
+      ]
+    },
+    {
+      code: `
+        <template>
+          <DateInput :value="[2000 ,12 ,31]" />
+        </template>`,
+      output: `
+        <template>
+          <DateInput :value="[2000, 12, 31]" />
+        </template>`,
+      errors: [
+        {
+          message: 'There should be no space before \',\'.',
+          line: 3
+        },
+        {
+          message: 'A space is required after \',\'.',
+          line: 3
+        },
+        {
+          message: 'There should be no space before \',\'.',
+          line: 3
+        },
+        {
+          message: 'A space is required after \',\'.',
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `
+        <template>
+          <DateInput :value="{y: 2000 ,m: 12 ,d: 31}" />
+        </template>`,
+      output: `
+        <template>
+          <DateInput :value="{y: 2000, m: 12, d: 31}" />
+        </template>`,
+      errors: [
+        {
+          message: 'There should be no space before \',\'.',
+          line: 3
+        },
+        {
+          message: 'A space is required after \',\'.',
+          line: 3
+        },
+        {
+          message: 'There should be no space before \',\'.',
+          line: 3
+        },
+        {
+          message: 'A space is required after \',\'.',
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `
+        <template>
+          <CustomDlg @ok="foo(a ,b)" />
+        </template>`,
+      output: `
+        <template>
+          <CustomDlg @ok="foo(a, b)" />
+        </template>`,
+      errors: [
+        {
+          message: 'There should be no space before \',\'.',
+          line: 3
+        },
+        {
+          message: 'A space is required after \',\'.',
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `
+        <template>
+          <CustomDlg @ok="(a ,b) => {}" />
+        </template>`,
+      output: `
+        <template>
+          <CustomDlg @ok="(a, b) => {}" />
+        </template>`,
+      errors: [
+        {
+          message: 'There should be no space before \',\'.',
+          line: 3
+        },
+        {
+          message: 'A space is required after \',\'.',
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `
+        <template>
+          <CustomDlg @ok="function (a ,b) {}" />
+        </template>`,
+      output: `
+        <template>
+          <CustomDlg @ok="function (a, b) {}" />
+        </template>`,
+      errors: [
+        {
+          message: 'There should be no space before \',\'.',
+          line: 3
+        },
+        {
+          message: 'A space is required after \',\'.',
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `
+        <template>
+          <CustomList>
+            <li slot-scope="a ,b">{{ a }}</li>
+          </CustomList>
+        </template>`,
+      output: `
+        <template>
+          <CustomList>
+            <li slot-scope="a, b">{{ a }}</li>
+          </CustomList>
+        </template>`,
+      errors: [
+        {
+          message: 'There should be no space before \',\'.',
+          line: 4
+        },
+        {
+          message: 'A space is required after \',\'.',
+          line: 4
+        }
+      ]
+    },
+    {
+      code: `
+        <template>
+          {{ [a /*comment*/ ,/*comment*/b] }}
+        </template>`,
+      output: `
+        <template>
+          {{ [a /*comment*/, /*comment*/b] }}
+        </template>`,
+      errors: [
+        {
+          message: 'There should be no space before \',\'.',
+          line: 3
+        },
+        {
+          message: 'A space is required after \',\'.',
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `
+        <template>
+          <button @click="
+            fn(a, b)
+          "/>
+        </template>`,
+      options: [{ before: true, after: false }],
+      output: `
+        <template>
+          <button @click="
+            fn(a ,b)
+          "/>
+        </template>`,
+      errors: [
+        {
+          message: 'A space is required before \',\'.',
+          line: 4
+        },
+        {
+          message: 'There should be no space after \',\'.',
+          line: 4
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-deprecated-dollar-listeners-api.js b/tests/lib/rules/no-deprecated-dollar-listeners-api.js
index 76f7c73c..98a0d627 100644
--- a/tests/lib/rules/no-deprecated-dollar-listeners-api.js
+++ b/tests/lib/rules/no-deprecated-dollar-listeners-api.js
@@ -101,6 +101,21 @@ ruleTester.run('no-deprecated-dollar-listeners-api', rule, {
         }
         </script>
       `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+          computed: {
+            foo () {
+              const {vm} = this
+              return vm.$listeners
+            }
+          }
+        }
+        </script>
+      `
     }
   ],
 
@@ -179,6 +194,53 @@ ruleTester.run('no-deprecated-dollar-listeners-api', rule, {
           endColumn: 33
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+          computed: {
+            foo () {
+              const vm = this
+              return vm.$listeners
+            }
+          }
+        }
+        </script>
+      `,
+      errors: [
+        {
+          line: 7,
+          column: 25,
+          messageId: 'deprecated'
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+          computed: {
+            foo () {
+              const vm = this
+              function fn() {
+                return vm.$listeners
+              }
+              return fn()
+            }
+          }
+        }
+        </script>
+      `,
+      errors: [
+        {
+          line: 8,
+          column: 27,
+          messageId: 'deprecated'
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/no-deprecated-events-api.js b/tests/lib/rules/no-deprecated-events-api.js
index 215a47d8..268c0a71 100644
--- a/tests/lib/rules/no-deprecated-events-api.js
+++ b/tests/lib/rules/no-deprecated-events-api.js
@@ -103,6 +103,17 @@ ruleTester.run('no-deprecated-events-api', rule, {
         }
       `,
       parserOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          mounted () {
+            a(this.$on)
+          }
+        }
+      `,
+      parserOptions
     }
   ],
 
@@ -155,6 +166,24 @@ ruleTester.run('no-deprecated-events-api', rule, {
         message: 'The Events api `$on`, `$off` `$once` is deprecated. Using external library instead, for example mitt.',
         line: 4
       }]
+    },
+    {
+      filename: 'test.js',
+      code: `
+        app.component('some-comp', {
+          mounted () {
+            const vm = this
+            vm.$on('start', function (args) {
+              console.log('start', args)
+            })
+          }
+        })
+      `,
+      parserOptions,
+      errors: [{
+        message: 'The Events api `$on`, `$off` `$once` is deprecated. Using external library instead, for example mitt.',
+        line: 5
+      }]
     }
   ]
 })
diff --git a/tests/lib/rules/no-duplicate-attr-inheritance.js b/tests/lib/rules/no-duplicate-attr-inheritance.js
new file mode 100644
index 00000000..1c862ef8
--- /dev/null
+++ b/tests/lib/rules/no-duplicate-attr-inheritance.js
@@ -0,0 +1,113 @@
+/**
+ * @fileoverview Disable inheritAttrs when using v-bind=&#34;$attrs&#34;
+ * @author Hiroki Osame
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+var rule = require('../../../lib/rules/no-duplicate-attr-inheritance')
+
+var RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+var ruleTester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2018,
+    sourceType: 'module'
+  }
+})
+ruleTester.run('no-duplicate-attr-inheritance', rule, {
+
+  valid: [
+    {
+      filename: 'test.vue',
+      code: '<template><div><div></div></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template><div><div></div></div></template>
+        <script>
+        export default { inheritAttrs: true }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template><div><div></div></div></template>
+        <script>
+        const data = {};
+        export default {
+          ...data,
+          inheritAttrs: true
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template><div><div></div></div></template>
+        <script>
+        const inheritAttrs = false;
+        export default { inheritAttrs }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template><div><div v-bind="$attrs"></div></div></template>
+        <script>
+        export default { inheritAttrs: false }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template><div><div v-bind="$attrs"></div></div></template>
+        <script>
+        export default { inheritAttrs: 0 }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template><div><div v-bind:foo="$attrs"></div></div></template>
+        <script>
+        export default {  }
+        </script>
+      `
+    }
+  ],
+
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: '<template><div><div v-bind="$attrs"></div></div></template>',
+      errors: ['Set "inheritAttrs" to false.']
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template><div><div v-bind="$attrs"></div></div></template>
+        <script>
+        export default {
+          inheritAttrs: true
+        }
+        </script>
+      `,
+      errors: ['Set "inheritAttrs" to false.']
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-lifecycle-after-await.js b/tests/lib/rules/no-lifecycle-after-await.js
index eaa99f84..404ad18b 100644
--- a/tests/lib/rules/no-lifecycle-after-await.js
+++ b/tests/lib/rules/no-lifecycle-after-await.js
@@ -27,7 +27,8 @@ tester.run('no-lifecycle-after-await', rule, {
       }
       </script>
       `
-    }, {
+    },
+    {
       filename: 'test.vue',
       code: `
       <script>
@@ -39,7 +40,8 @@ tester.run('no-lifecycle-after-await', rule, {
       }
       </script>
       `
-    }, {
+    },
+    {
       filename: 'test.vue',
       code: `
       <script>
@@ -78,6 +80,32 @@ tester.run('no-lifecycle-after-await', rule, {
       }
       </script>
       `
+    },
+    // has target
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import {onBeforeMount, onBeforeUnmount, onBeforeUpdate, onErrorCaptured, onMounted, onRenderTracked, onRenderTriggered, onUnmounted, onUpdated, onActivated, onDeactivated} from 'vue'
+      export default {
+        async setup() {
+          await doSomething()
+
+          onBeforeMount(() => { /* ... */ }, instance)
+          onBeforeUnmount(() => { /* ... */ }, instance)
+          onBeforeUpdate(() => { /* ... */ }, instance)
+          onErrorCaptured(() => { /* ... */ }, instance)
+          onMounted(() => { /* ... */ }, instance)
+          onRenderTracked(() => { /* ... */ }, instance)
+          onRenderTriggered(() => { /* ... */ }, instance)
+          onUnmounted(() => { /* ... */ }, instance)
+          onUpdated(() => { /* ... */ }, instance)
+          onActivated(() => { /* ... */ }, instance)
+          onDeactivated(() => { /* ... */ }, instance)
+        }
+      }
+      </script>
+      `
     }
   ],
   invalid: [
diff --git a/tests/lib/rules/prefer-template.js b/tests/lib/rules/prefer-template.js
new file mode 100644
index 00000000..73ad5e5b
--- /dev/null
+++ b/tests/lib/rules/prefer-template.js
@@ -0,0 +1,63 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/prefer-template')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2020 }
+})
+
+tester.run('prefer-template', rule, {
+  valid: [
+    `
+    <template>
+      <div :class="[\`foo-\${bar}\`]" />
+    </template>
+    `,
+    `
+    <template>
+      <div :[\`foo\${bar}\`]="value" />
+    </template>
+    `
+  ],
+  invalid: [
+    {
+      code: `
+      <template>
+        <div :class="['foo-' + bar]" />
+      </template>
+      `,
+      output: `
+      <template>
+        <div :class="[\`foo-\${  bar}\`]" />
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Unexpected string concatenation.',
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+        <div :['foo'+bar]="value" />
+      </template>`,
+      output: `
+      <template>
+        <div :[\`foo\${bar}\`]="value" />
+      </template>`,
+      errors: [
+        {
+          message: 'Unexpected string concatenation.',
+          line: 3
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/return-in-emits-validator.js b/tests/lib/rules/return-in-emits-validator.js
index 8ee6b75e..f988e6d9 100644
--- a/tests/lib/rules/return-in-emits-validator.js
+++ b/tests/lib/rules/return-in-emits-validator.js
@@ -38,6 +38,7 @@ ruleTester.run('return-in-emits-validator', rule, {
             baz: (e) => {
               return e
             },
+            baz2: (e) => e,
             qux () {
               if (foo) {
                 return true
@@ -174,6 +175,22 @@ ruleTester.run('return-in-emits-validator', rule, {
         line: 5
       }]
     },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+          emits: {
+            foo: () => false
+          }
+        }
+        </script>
+      `,
+      errors: [{
+        message: 'Expected to return a true value in "foo" emits validator.',
+        line: 5
+      }]
+    },
     {
       filename: 'test.vue',
       code: `