Repository: vuejs/eslint-plugin-vue (js)

Issue:
[FR] `inheritAttrs: false` when using `v-bind="$attrs"`
Components that inherit `$attrs` into a nested element in the component typically wouldn't want to get duplicated attributes applied to the top-level element. Might be a good linting rule to warn to apply `inheritAttrs: false` when it detects `v-bind="$attrs"` being used.

Pull request:
Add `vue/no-duplicate-attr-inheritance` rule (from #627)
This PR is a modification of PR #627 to be accepted by the current structure.

close #627 
close #626

Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 83eda569..b89debd6 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -282,6 +282,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 |  |
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/lib/index.js b/lib/index.js
index 6adbbecd..131c834d 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -58,6 +58,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'),
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/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..c31ed0e6 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[]> }
@@ -855,7 +856,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/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/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: `