Repository: vuejs/eslint-plugin-vue
Pull request #1067 (2020-03-07 08:42:03)

New: Add `vue/no-lifecycle-after-await` rule
This PR adds the `vue/no-lifecycle-after-await` rule.

The `vue/no-lifecycle-after-await` rule reports the lifecycle hooks after `await` expression. 

---

ref #1035

kazupon: LGTM!

@michalsnik 
If you can review, I hope you would review this PR, please.
FYI: related
https://github.com/vuejs/eslint-plugin-vue/labels/Vue.js%203.x

Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 86b51d48..5b21a084 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -167,6 +167,7 @@ For example:
 | [vue/no-deprecated-v-bind-sync](./no-deprecated-v-bind-sync.md) | disallow use of deprecated `.sync` modifier on `v-bind` directive (in Vue.js 3.0.0+) | :wrench: |
 | [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-lifecycle-after-await](./no-lifecycle-after-await.md) | disallow asynchronously registered lifecycle hooks |  |
 | [vue/no-ref-as-operand](./no-ref-as-operand.md) | disallow use of value wrapped by `ref()` (Composition API) as an operand |  |
 | [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions |  |
 | [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax |  |
diff --git a/docs/rules/no-lifecycle-after-await.md b/docs/rules/no-lifecycle-after-await.md
new file mode 100644
index 00000000..aaa521f7
--- /dev/null
+++ b/docs/rules/no-lifecycle-after-await.md
@@ -0,0 +1,47 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-lifecycle-after-await
+description: disallow asynchronously registered lifecycle hooks
+---
+# vue/no-lifecycle-after-await
+> disallow asynchronously registered lifecycle hooks
+
+## :book: Rule Details
+
+This rule reports the lifecycle hooks after `await` expression.  
+In `setup()` function, `onXXX` lifecycle hooks should be registered synchronously.
+
+<eslint-code-block :rules="{'vue/no-lifecycle-after-await': ['error']}">
+
+```vue
+<script>
+import { onMounted } from 'vue'
+export default {
+  async setup() {
+    /* ✓ GOOD */
+    onMounted(() => { /* ... */ })
+
+    await doSomething()
+
+    /* ✗ BAD */
+    onMounted(() => { /* ... */ })
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further reading
+
+- [Vue RFCs - 0013-composition-api](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0013-composition-api.md)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-lifecycle-after-await.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-lifecycle-after-await.js)
diff --git a/docs/user-guide/README.md b/docs/user-guide/README.md
index 01710c07..1c4d0e38 100644
--- a/docs/user-guide/README.md
+++ b/docs/user-guide/README.md
@@ -85,6 +85,9 @@ All component-related rules are applied to code that passes any of the following
 * `Vue.component()` expression
 * `Vue.extend()` expression
 * `Vue.mixin()` expression
+* `app.component()` expression
+* `app.mixin()` expression
+* `createApp()` expression
 * `export default {}` in `.vue` or `.jsx` file
 
 However, if you want to take advantage of the rules in any of your custom objects that are Vue components, you might need to use the special comment `// @vue/component` that marks an object in the next line as a Vue component in any file, e.g.:
diff --git a/lib/index.js b/lib/index.js
index e772dc59..55d75305 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -49,6 +49,7 @@ module.exports = {
     'no-duplicate-attributes': require('./rules/no-duplicate-attributes'),
     'no-empty-pattern': require('./rules/no-empty-pattern'),
     'no-irregular-whitespace': require('./rules/no-irregular-whitespace'),
+    'no-lifecycle-after-await': require('./rules/no-lifecycle-after-await'),
     'no-multi-spaces': require('./rules/no-multi-spaces'),
     'no-multiple-template-root': require('./rules/no-multiple-template-root'),
     'no-parsing-error': require('./rules/no-parsing-error'),
diff --git a/lib/rules/no-lifecycle-after-await.js b/lib/rules/no-lifecycle-after-await.js
new file mode 100644
index 00000000..1f78165e
--- /dev/null
+++ b/lib/rules/no-lifecycle-after-await.js
@@ -0,0 +1,111 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+const { ReferenceTracker } = require('eslint-utils')
+const utils = require('../utils')
+
+const LIFECYCLE_HOOKS = ['onBeforeMount', 'onBeforeUnmount', 'onBeforeUpdate', 'onErrorCaptured', 'onMounted', 'onRenderTracked', 'onRenderTriggered', 'onUnmounted', 'onUpdated', 'onActivated', 'onDeactivated']
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'disallow asynchronously registered lifecycle hooks',
+      category: undefined,
+      url: 'https://eslint.vuejs.org/rules/no-lifecycle-after-await.html'
+    },
+    fixable: null,
+    schema: [],
+    messages: {
+      forbidden: 'The lifecycle hooks after `await` expression are forbidden.'
+    }
+  },
+  create (context) {
+    const lifecycleHookCallNodes = new Set()
+    const setupFunctions = new Map()
+    const forbiddenNodes = new Map()
+
+    function addForbiddenNode (property, node) {
+      let list = forbiddenNodes.get(property)
+      if (!list) {
+        list = []
+        forbiddenNodes.set(property, list)
+      }
+      list.push(node)
+    }
+
+    let scopeStack = { upper: null, functionNode: null }
+
+    return Object.assign(
+      {
+        'Program' () {
+          const tracker = new ReferenceTracker(context.getScope())
+          const traceMap = {
+            vue: {
+              [ReferenceTracker.ESM]: true
+            }
+          }
+          for (const lifecycleHook of LIFECYCLE_HOOKS) {
+            traceMap.vue[lifecycleHook] = {
+              [ReferenceTracker.CALL]: true
+            }
+          }
+
+          for (const { node } of tracker.iterateEsmReferences(traceMap)) {
+            lifecycleHookCallNodes.add(node)
+          }
+        },
+        'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node) {
+          if (utils.getStaticPropertyName(node) !== 'setup') {
+            return
+          }
+
+          setupFunctions.set(node.value, {
+            setupProperty: node,
+            afterAwait: false
+          })
+        },
+        ':function' (node) {
+          scopeStack = { upper: scopeStack, functionNode: node }
+        },
+        'AwaitExpression' () {
+          const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
+          if (!setupFunctionData) {
+            return
+          }
+          setupFunctionData.afterAwait = true
+        },
+        'CallExpression' (node) {
+          const setupFunctionData = setupFunctions.get(scopeStack.functionNode)
+          if (!setupFunctionData || !setupFunctionData.afterAwait) {
+            return
+          }
+
+          if (lifecycleHookCallNodes.has(node)) {
+            addForbiddenNode(setupFunctionData.setupProperty, node)
+          }
+        },
+        ':function:exit' (node) {
+          scopeStack = scopeStack.upper
+
+          setupFunctions.delete(node)
+        }
+      },
+      utils.executeOnVue(context, obj => {
+        const reportsList = obj.properties
+          .map(item => forbiddenNodes.get(item))
+          .filter(reports => !!reports)
+        for (const reports of reportsList) {
+          for (const node of reports) {
+            context.report({
+              node,
+              messageId: 'forbidden'
+            })
+          }
+        }
+      })
+    )
+  }
+}
diff --git a/lib/utils/index.js b/lib/utils/index.js
index 011ec657..d0c91829 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -264,7 +264,7 @@ module.exports = {
     return componentsNode.value.properties
       .filter(p => p.type === 'Property')
       .map(node => {
-        const name = this.getStaticPropertyName(node)
+        const name = getStaticPropertyName(node)
         return name ? { node, name } : null
       })
       .filter(comp => comp != null)
@@ -402,42 +402,7 @@ module.exports = {
    * @param {Property|MethodDefinition|MemberExpression|Literal|TemplateLiteral|Identifier} node - The node to get.
    * @return {string|null} The property name if static. Otherwise, null.
    */
-  getStaticPropertyName (node) {
-    let prop
-    switch (node && node.type) {
-      case 'Property':
-      case 'MethodDefinition':
-        prop = node.key
-        break
-      case 'MemberExpression':
-        prop = node.property
-        break
-      case 'Literal':
-      case 'TemplateLiteral':
-      case 'Identifier':
-        prop = node
-        break
-      // no default
-    }
-
-    switch (prop && prop.type) {
-      case 'Literal':
-        return String(prop.value)
-      case 'TemplateLiteral':
-        if (prop.expressions.length === 0 && prop.quasis.length === 1) {
-          return prop.quasis[0].value.cooked
-        }
-        break
-      case 'Identifier':
-        if (!node.computed) {
-          return prop.name
-        }
-        break
-      // no default
-    }
-
-    return null
-  },
+  getStaticPropertyName,
 
   /**
    * Get all props by looking at all component's properties
@@ -464,8 +429,8 @@ module.exports = {
         .filter(prop => prop.type === 'Property')
         .map(prop => {
           return {
-            key: prop.key, value: this.unwrapTypes(prop.value), node: prop,
-            propName: this.getStaticPropertyName(prop)
+            key: prop.key, value: unwrapTypes(prop.value), node: prop,
+            propName: getStaticPropertyName(prop)
           }
         })
     } else {
@@ -548,28 +513,52 @@ module.exports = {
       const callee = node.callee
 
       if (callee.type === 'MemberExpression') {
-        const calleeObject = this.unwrapTypes(callee.object)
+        const calleeObject = unwrapTypes(callee.object)
+
+        if (calleeObject.type === 'Identifier') {
+          const propName = getStaticPropertyName(callee.property)
+          if (calleeObject.name === 'Vue') {
+            // for Vue.js 2.x
+            // Vue.component('xxx', {}) || Vue.mixin({}) || Vue.extend('xxx', {})
+            const isFullVueComponentForVue2 =
+              ['component', 'mixin', 'extend'].includes(propName) &&
+              isObjectArgument(node)
+
+            return isFullVueComponentForVue2
+          }
 
-        const isFullVueComponent = calleeObject.type === 'Identifier' &&
-          calleeObject.name === 'Vue' &&
-          callee.property.type === 'Identifier' &&
-          ['component', 'mixin', 'extend'].indexOf(callee.property.name) > -1 &&
-          node.arguments.length >= 1 &&
-          node.arguments.slice(-1)[0].type === 'ObjectExpression'
+          // for Vue.js 3.x
+          // app.component('xxx', {}) || app.mixin({})
+          const isFullVueComponent =
+            ['component', 'mixin'].includes(propName) &&
+            isObjectArgument(node)
 
-        return isFullVueComponent
+          return isFullVueComponent
+        }
       }
 
       if (callee.type === 'Identifier') {
-        const isDestructedVueComponent = callee.name === 'component' &&
-          node.arguments.length >= 1 &&
-          node.arguments.slice(-1)[0].type === 'ObjectExpression'
-
-        return isDestructedVueComponent
+        if (callee.name === 'component') {
+          // for Vue.js 2.x
+          // component('xxx', {})
+          const isDestructedVueComponent = isObjectArgument(node)
+          return isDestructedVueComponent
+        }
+        if (callee.name === 'createApp') {
+          // for Vue.js 3.x
+          // createApp({})
+          const isAppVueComponent = isObjectArgument(node)
+          return isAppVueComponent
+        }
       }
     }
 
     return false
+
+    function isObjectArgument (node) {
+      return node.arguments.length > 0 &&
+        unwrapTypes(node.arguments.slice(-1)[0]).type === 'ObjectExpression'
+    }
   },
 
   /**
@@ -584,7 +573,7 @@ module.exports = {
       callee.type === 'Identifier' &&
       callee.name === 'Vue' &&
       node.arguments.length &&
-      node.arguments[0].type === 'ObjectExpression'
+      unwrapTypes(node.arguments[0]).type === 'ObjectExpression'
   },
 
   /**
@@ -647,7 +636,7 @@ module.exports = {
       'CallExpression:exit' (node) {
         // Vue.component('xxx', {}) || component('xxx', {})
         if (!_this.isVueComponent(node) || isDuplicateNode(node.arguments.slice(-1)[0])) return
-        cb(node.arguments.slice(-1)[0])
+        cb(unwrapTypes(node.arguments.slice(-1)[0]))
       }
     }
   },
@@ -664,10 +653,10 @@ module.exports = {
         const callee = callExpr.callee
 
         if (callee.type === 'MemberExpression') {
-          const calleeObject = this.unwrapTypes(callee.object)
+          const calleeObject = unwrapTypes(callee.object)
 
           if (calleeObject.type === 'Identifier' &&
-            calleeObject.name === 'Vue' &&
+            // calleeObject.name === 'Vue' && // Any names can be used in Vue.js 3.x. e.g. app.component()
             callee.property === node &&
             callExpr.arguments.length >= 1) {
             cb(callExpr)
@@ -682,9 +671,9 @@ module.exports = {
    * @param {Set} groups Name of parent group
    */
   * iterateProperties (node, groups) {
-    const nodes = node.properties.filter(p => p.type === 'Property' && groups.has(this.getStaticPropertyName(p.key)))
+    const nodes = node.properties.filter(p => p.type === 'Property' && groups.has(getStaticPropertyName(p.key)))
     for (const item of nodes) {
-      const name = this.getStaticPropertyName(item.key)
+      const name = getStaticPropertyName(item.key)
       if (!name) continue
 
       if (item.value.type === 'ArrayExpression') {
@@ -705,7 +694,7 @@ module.exports = {
   * iterateArrayExpression (node, groupName) {
     assert(node.type === 'ArrayExpression')
     for (const item of node.elements) {
-      const name = this.getStaticPropertyName(item)
+      const name = getStaticPropertyName(item)
       if (name) {
         const obj = { name, groupName, node: item }
         yield obj
@@ -721,7 +710,7 @@ module.exports = {
   * iterateObjectExpression (node, groupName) {
     assert(node.type === 'ObjectExpression')
     for (const item of node.properties) {
-      const name = this.getStaticPropertyName(item)
+      const name = getStaticPropertyName(item)
       if (name) {
         const obj = { name, groupName, node: item.key }
         yield obj
@@ -865,7 +854,56 @@ module.exports = {
    * @param {T} node
    * @return {T}
    */
-  unwrapTypes (node) {
-    return node.type === 'TSAsExpression' ? node.expression : node
+  unwrapTypes
+}
+/**
+* Unwrap typescript types like "X as F"
+* @template T
+* @param {T} node
+* @return {T}
+*/
+function unwrapTypes (node) {
+  return node.type === 'TSAsExpression' ? node.expression : node
+}
+
+/**
+ * Gets the property name of a given node.
+ * @param {Property|MethodDefinition|MemberExpression|Literal|TemplateLiteral|Identifier} node - The node to get.
+ * @return {string|null} The property name if static. Otherwise, null.
+ */
+function getStaticPropertyName (node) {
+  let prop
+  switch (node && node.type) {
+    case 'Property':
+    case 'MethodDefinition':
+      prop = node.key
+      break
+    case 'MemberExpression':
+      prop = node.property
+      break
+    case 'Literal':
+    case 'TemplateLiteral':
+    case 'Identifier':
+      prop = node
+      break
+      // no default
   }
+
+  switch (prop && prop.type) {
+    case 'Literal':
+      return String(prop.value)
+    case 'TemplateLiteral':
+      if (prop.expressions.length === 0 && prop.quasis.length === 1) {
+        return prop.quasis[0].value.cooked
+      }
+      break
+    case 'Identifier':
+      if (!node.computed) {
+        return prop.name
+      }
+      break
+      // no default
+  }
+
+  return null
 }


Test patch:
diff --git a/tests/lib/rules/component-definition-name-casing.js b/tests/lib/rules/component-definition-name-casing.js
index 98cb4367..d14eedb7 100644
--- a/tests/lib/rules/component-definition-name-casing.js
+++ b/tests/lib/rules/component-definition-name-casing.js
@@ -116,6 +116,12 @@ ruleTester.run('component-definition-name-casing', rule, {
       options: ['kebab-case'],
       parserOptions
     },
+    {
+      filename: 'test.vue',
+      code: `app.component('FooBar', component)`,
+      options: ['PascalCase'],
+      parserOptions
+    },
     {
       filename: 'test.vue',
       code: `Vue.mixin({})`,
@@ -137,6 +143,12 @@ ruleTester.run('component-definition-name-casing', rule, {
       options: ['kebab-case'],
       parserOptions
     },
+    {
+      filename: 'test.vue',
+      code: `app.component(\`fooBar\${foo}\`, component)`,
+      options: ['kebab-case'],
+      parserOptions
+    },
     // https://github.com/vuejs/eslint-plugin-vue/issues/1018
     {
       filename: 'test.js',
@@ -292,6 +304,17 @@ ruleTester.run('component-definition-name-casing', rule, {
         line: 1
       }]
     },
+    {
+      filename: 'test.vue',
+      code: `app.component('foo-bar', component)`,
+      output: `app.component('FooBar', component)`,
+      parserOptions,
+      errors: [{
+        message: 'Property name "foo-bar" is not PascalCase.',
+        type: 'Literal',
+        line: 1
+      }]
+    },
     {
       filename: 'test.vue',
       code: `(Vue as VueConstructor<Vue>).component('foo-bar', component)`,
@@ -315,6 +338,17 @@ ruleTester.run('component-definition-name-casing', rule, {
         line: 1
       }]
     },
+    {
+      filename: 'test.vue',
+      code: `app.component('foo-bar', {})`,
+      output: `app.component('FooBar', {})`,
+      parserOptions,
+      errors: [{
+        message: 'Property name "foo-bar" is not PascalCase.',
+        type: 'Literal',
+        line: 1
+      }]
+    },
     {
       filename: 'test.js',
       code: `Vue.component('foo_bar', {})`,
diff --git a/tests/lib/rules/match-component-file-name.js b/tests/lib/rules/match-component-file-name.js
index d8a1b141..a97c5941 100644
--- a/tests/lib/rules/match-component-file-name.js
+++ b/tests/lib/rules/match-component-file-name.js
@@ -429,6 +429,16 @@ ruleTester.run('match-component-file-name', rule, {
       options: [{ extensions: ['js'] }],
       parserOptions
     },
+    {
+      filename: 'MyComponent.js',
+      code: `
+        app.component('MyComponent', {
+          template: '<div />'
+        })
+      `,
+      options: [{ extensions: ['js'] }],
+      parserOptions
+    },
     {
       filename: 'MyComponent.js',
       code: `
@@ -701,6 +711,19 @@ ruleTester.run('match-component-file-name', rule, {
         message: 'Component name `MComponent` should match file name `MyComponent`.'
       }]
     },
+    {
+      filename: 'MyComponent.js',
+      code: `
+        app.component(\`MComponent\`, {
+          template: '<div />'
+        })
+      `,
+      options: [{ extensions: ['js'] }],
+      parserOptions,
+      errors: [{
+        message: 'Component name `MComponent` should match file name `MyComponent`.'
+      }]
+    },
 
     // casing
     {
diff --git a/tests/lib/rules/no-lifecycle-after-await.js b/tests/lib/rules/no-lifecycle-after-await.js
new file mode 100644
index 00000000..eaa99f84
--- /dev/null
+++ b/tests/lib/rules/no-lifecycle-after-await.js
@@ -0,0 +1,180 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-lifecycle-after-await')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2019, sourceType: 'module' }
+})
+
+tester.run('no-lifecycle-after-await', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import {onMounted} from 'vue'
+      export default {
+        async setup() {
+          onMounted(() => { /* ... */ }) // ok
+
+          await doSomething()
+        }
+      }
+      </script>
+      `
+    }, {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import {onMounted} from 'vue'
+      export default {
+        async setup() {
+          onMounted(() => { /* ... */ })
+        }
+      }
+      </script>
+      `
+    }, {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import {onBeforeMount, onBeforeUnmount, onBeforeUpdate, onErrorCaptured, onMounted, onRenderTracked, onRenderTriggered, onUnmounted, onUpdated, onActivated, onDeactivated} from 'vue'
+      export default {
+        async setup() {
+          onBeforeMount(() => { /* ... */ })
+          onBeforeUnmount(() => { /* ... */ })
+          onBeforeUpdate(() => { /* ... */ })
+          onErrorCaptured(() => { /* ... */ })
+          onMounted(() => { /* ... */ })
+          onRenderTracked(() => { /* ... */ })
+          onRenderTriggered(() => { /* ... */ })
+          onUnmounted(() => { /* ... */ })
+          onUpdated(() => { /* ... */ })
+          onActivated(() => { /* ... */ })
+          onDeactivated(() => { /* ... */ })
+
+          await doSomething()
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import {onMounted} from 'vue'
+      export default {
+        async _setup() {
+          await doSomething()
+
+          onMounted(() => { /* ... */ }) // error
+        }
+      }
+      </script>
+      `
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import {onMounted} from 'vue'
+      export default {
+        async setup() {
+          await doSomething()
+
+          onMounted(() => { /* ... */ }) // error
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message: 'The lifecycle hooks after `await` expression are forbidden.',
+          line: 8,
+          column: 11,
+          endLine: 8,
+          endColumn: 41
+        }
+      ]
+    },
+    {
+      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(() => { /* ... */ })
+          onBeforeUnmount(() => { /* ... */ })
+          onBeforeUpdate(() => { /* ... */ })
+          onErrorCaptured(() => { /* ... */ })
+          onMounted(() => { /* ... */ })
+          onRenderTracked(() => { /* ... */ })
+          onRenderTriggered(() => { /* ... */ })
+          onUnmounted(() => { /* ... */ })
+          onUpdated(() => { /* ... */ })
+          onActivated(() => { /* ... */ })
+          onDeactivated(() => { /* ... */ })
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'forbidden',
+          line: 8
+        },
+        {
+          messageId: 'forbidden',
+          line: 9
+        },
+        {
+          messageId: 'forbidden',
+          line: 10
+        },
+        {
+          messageId: 'forbidden',
+          line: 11
+        },
+        {
+          messageId: 'forbidden',
+          line: 12
+        },
+        {
+          messageId: 'forbidden',
+          line: 13
+        },
+        {
+          messageId: 'forbidden',
+          line: 14
+        },
+        {
+          messageId: 'forbidden',
+          line: 15
+        },
+        {
+          messageId: 'forbidden',
+          line: 16
+        },
+        {
+          messageId: 'forbidden',
+          line: 17
+        },
+        {
+          messageId: 'forbidden',
+          line: 18
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-reserved-component-names.js b/tests/lib/rules/no-reserved-component-names.js
index 43a34f15..0585b5c9 100644
--- a/tests/lib/rules/no-reserved-component-names.js
+++ b/tests/lib/rules/no-reserved-component-names.js
@@ -274,6 +274,11 @@ ruleTester.run('no-reserved-component-names', rule, {
       code: `Vue.component('FooBar', {})`,
       parserOptions
     },
+    {
+      filename: 'test.vue',
+      code: `app.component('FooBar', {})`,
+      parserOptions
+    },
     {
       filename: 'test.js',
       code: `
@@ -349,6 +354,18 @@ ruleTester.run('no-reserved-component-names', rule, {
         }]
       }
     }),
+    ...invalidElements.map(name => {
+      return {
+        filename: 'test.vue',
+        code: `app.component('${name}', component)`,
+        parserOptions,
+        errors: [{
+          message: `Name "${name}" is reserved.`,
+          type: 'Literal',
+          line: 1
+        }]
+      }
+    }),
     ...invalidElements.map(name => {
       return {
         filename: 'test.vue',
@@ -361,6 +378,18 @@ ruleTester.run('no-reserved-component-names', rule, {
         }]
       }
     }),
+    ...invalidElements.map(name => {
+      return {
+        filename: 'test.vue',
+        code: `app.component(\`${name}\`, {})`,
+        parserOptions,
+        errors: [{
+          message: `Name "${name}" is reserved.`,
+          type: 'TemplateLiteral',
+          line: 1
+        }]
+      }
+    }),
     ...invalidElements.map(name => {
       return {
         filename: 'test.vue',
diff --git a/tests/lib/rules/no-shared-component-data.js b/tests/lib/rules/no-shared-component-data.js
index ce0e0062..ade9e9ef 100644
--- a/tests/lib/rules/no-shared-component-data.js
+++ b/tests/lib/rules/no-shared-component-data.js
@@ -135,6 +135,30 @@ ruleTester.run('no-shared-component-data', rule, {
 return {
             foo: 'bar'
           };
+}
+        })
+      `,
+      parserOptions,
+      errors: [{
+        message: '`data` property in component must be a function.',
+        line: 3
+      }]
+    },
+    {
+      filename: 'test.js',
+      code: `
+        app.component('some-comp', {
+          data: {
+            foo: 'bar'
+          }
+        })
+      `,
+      output: `
+        app.component('some-comp', {
+          data: function() {
+return {
+            foo: 'bar'
+          };
 }
         })
       `,
diff --git a/tests/lib/rules/no-side-effects-in-computed-properties.js b/tests/lib/rules/no-side-effects-in-computed-properties.js
index df186b3a..fa14ef24 100644
--- a/tests/lib/rules/no-side-effects-in-computed-properties.js
+++ b/tests/lib/rules/no-side-effects-in-computed-properties.js
@@ -296,6 +296,25 @@ ruleTester.run('no-side-effects-in-computed-properties', rule, {
         message: 'Unexpected side effect in "test1" computed property.'
       }],
       parser: require.resolve('@typescript-eslint/parser')
+    },
+
+    {
+      code: `app.component('test', {
+        computed: {
+          test1() {
+            this.firstName = 'lorem'
+            asd.qwe.zxc = 'lorem'
+            return this.firstName + ' ' + this.lastName
+          },
+        }
+      })`,
+      parserOptions,
+      errors: [
+        {
+          line: 4,
+          message: 'Unexpected side effect in "test1" computed property.'
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/order-in-components.js b/tests/lib/rules/order-in-components.js
index b022508f..f94b4ec6 100644
--- a/tests/lib/rules/order-in-components.js
+++ b/tests/lib/rules/order-in-components.js
@@ -243,6 +243,38 @@ ruleTester.run('order-in-components', rule, {
         line: 9
       }]
     },
+    {
+      filename: 'test.js',
+      code: `
+        app.component('smart-list', {
+          name: 'app',
+          data () {
+            return {
+              msg: 'Welcome to Your Vue.js App'
+            }
+          },
+          components: {},
+          template: '<div></div>'
+        })
+      `,
+      parserOptions: { ecmaVersion: 6 },
+      output: `
+        app.component('smart-list', {
+          name: 'app',
+          components: {},
+          data () {
+            return {
+              msg: 'Welcome to Your Vue.js App'
+            }
+          },
+          template: '<div></div>'
+        })
+      `,
+      errors: [{
+        message: 'The "components" property should be above the "data" property on line 4.',
+        line: 9
+      }]
+    },
     {
       filename: 'test.js',
       code: `
diff --git a/tests/lib/rules/require-render-return.js b/tests/lib/rules/require-render-return.js
index e7fde4e6..580a6b89 100644
--- a/tests/lib/rules/require-render-return.js
+++ b/tests/lib/rules/require-render-return.js
@@ -86,7 +86,7 @@ ruleTester.run('require-render-return', rule, {
         render() {
           if (a) {
             if (b) {
-              
+
             }
             if (c) {
               return true
@@ -180,6 +180,21 @@ ruleTester.run('require-render-return', rule, {
         line: 2
       }]
     },
+    {
+      code: `app.component('test', {
+        render: function () {
+          if (a) {
+            return
+          }
+        }
+      })`,
+      parserOptions,
+      errors: [{
+        message: 'Expected to return a value in render function.',
+        type: 'Identifier',
+        line: 2
+      }]
+    },
     {
       code: `Vue.component('test2', {
         render: function () {
@@ -199,7 +214,7 @@ ruleTester.run('require-render-return', rule, {
       code: `Vue.component('test2', {
         render: function () {
           if (a) {
-            
+
           } else {
             return h('div', 'hello')
           }
diff --git a/tests/lib/utils/vue-component.js b/tests/lib/utils/vue-component.js
index db5d9733..69d663e9 100644
--- a/tests/lib/utils/vue-component.js
+++ b/tests/lib/utils/vue-component.js
@@ -108,6 +108,12 @@ function validTests (ext) {
       code: `export default Foo.extend({})`,
       parser: require.resolve('@typescript-eslint/parser'),
       parserOptions
+    },
+    {
+      filename: `test.${ext}`,
+      code: `export default Foo.extend({} as ComponentOptions)`,
+      parser: require.resolve('@typescript-eslint/parser'),
+      parserOptions
     }
   ]
 }
@@ -144,6 +150,18 @@ function invalidTests (ext) {
       parserOptions,
       errors: [makeError(1)]
     },
+    {
+      filename: `test.${ext}`,
+      code: `app.component('name', {})`,
+      parserOptions,
+      errors: [makeError(1)]
+    },
+    {
+      filename: `test.${ext}`,
+      code: `app.mixin({})`,
+      parserOptions,
+      errors: [makeError(1)]
+    },
     {
       filename: `test.${ext}`,
       code: `export default (Vue as VueConstructor<Vue>).extend({})`,
@@ -158,6 +176,19 @@ function invalidTests (ext) {
       parserOptions,
       errors: [makeError(1)]
     },
+    {
+      filename: `test.${ext}`,
+      code: `export default Vue.extend({} as ComponentOptions)`,
+      parser: require.resolve('@typescript-eslint/parser'),
+      parserOptions,
+      errors: [makeError(1)]
+    },
+    {
+      filename: `test.${ext}`,
+      code: `createApp({})`,
+      parserOptions,
+      errors: [makeError(1)]
+    },
     {
       filename: `test.${ext}`,
       code: `