Repository: vuejs/eslint-plugin-vue
Pull request #1209 (2020-06-12 11:33:22)

Supports Optional Chaining
And change the supported version of ESLint from 6.0.0 to 6.2.0.

### Core

- Change presets configs `parserOptions.ecmaVersion` from 2018 to 2020.
- Change supported version of ESLint from 6.0.0 to 6.2.0.
- Upgrade `eslint-utils`

### Rules

- [x] custom-event-name-casing
- [x] no-async-in-computed-properties
- [x] no-deprecated-dollar-listeners-api
- [x] no-deprecated-dollar-scopedslots-api
- [x] no-deprecated-events-api
- [x] no-deprecated-vue-config-keycodes
- [x] no-lifecycle-after-await
- [x] no-multiple-slot-args
- [x] no-mutating-props
- [x] no-ref-as-operand
- [x] no-setup-props-destructure
- [x] no-side-effects-in-computed-properties
- [x] no-unused-properties
- [x] no-watch-after-await
- [x] order-in-components
- [x] require-default-prop
- [x] require-explicit-emits
- [x] require-slots-as-functions
- [x] require-valid-default-prop
- [x] this-in-template
- [x] v-on-function-call
- [x] valid-v-bind-sync
- [x] valid-v-model


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index d20690e4..d593e239 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -43,7 +43,7 @@ jobs:
       - run:
           name: Install eslint@6
           command: |
-            npm install -D eslint@6.0.0
+            npm install -D eslint@6.2.0
       - run:
           name: Install dependencies
           command: npm install
diff --git a/.eslintrc.js b/.eslintrc.js
index f5eb3a23..704f0973 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -135,7 +135,9 @@ module.exports = {
           {
             pattern: `https://eslint.vuejs.org/rules/{{name}}.html`
           }
-        ]
+        ],
+
+        'eslint-plugin/fixer-return': 'off'
       }
     }
   ]
diff --git a/docs/.vuepress/components/eslint-code-block.vue b/docs/.vuepress/components/eslint-code-block.vue
index a4d6232a..391ae549 100644
--- a/docs/.vuepress/components/eslint-code-block.vue
+++ b/docs/.vuepress/components/eslint-code-block.vue
@@ -89,7 +89,7 @@ export default {
         rules: this.rules,
         parser: 'vue-eslint-parser',
         parserOptions: {
-          ecmaVersion: 2019,
+          ecmaVersion: 2020,
           sourceType: 'module',
           ecmaFeatures: {
             jsx: true
diff --git a/docs/rules/valid-v-bind-sync.md b/docs/rules/valid-v-bind-sync.md
index 9ebd51fd..163d8b21 100644
--- a/docs/rules/valid-v-bind-sync.md
+++ b/docs/rules/valid-v-bind-sync.md
@@ -15,7 +15,8 @@ This rule checks whether every `.sync` modifier on `v-bind` directives is valid.
 
 This rule reports `.sync` modifier on `v-bind` directives in the following cases:
 
-- The `.sync` modifier does not have the attribute value which is valid as LHS. E.g. `<MyComponent v-bind:aaa.sync="foo() + bar()" />`
+- The `.sync` modifier does not have the attribute value which is valid as LHS. E.g. `<MyComponent v-bind:aaa.sync="foo() + bar()" />`, `<MyComponent v-bind:aaa.sync="a?.b" />`
+- The `.sync` modifier has potential null object property access. E.g. `<MyComponent v-bind:aaa.sync="(a?.b).c" />`
 - The `.sync` modifier is on non Vue-components. E.g. `<input v-bind:aaa.sync="foo"></div>`
 - The `.sync` modifier's reference is iteration variables. E.g. `<div v-for="x in list"><MyComponent v-bind:aaa.sync="x" /></div>`
 
@@ -36,6 +37,9 @@ This rule reports `.sync` modifier on `v-bind` directives in the following cases
   <MyComponent v-bind:aaa.sync="foo + bar" />
   <MyComponent :aaa.sync="foo + bar" />
 
+  <MyComponent :aaa.sync="a?.b.c" />
+  <MyComponent :aaa.sync="(a?.b).c" />
+
   <input v-bind:aaa.sync="foo">
   <input :aaa.sync="foo">
 
diff --git a/docs/rules/valid-v-model.md b/docs/rules/valid-v-model.md
index f3a0afca..345e3bf9 100644
--- a/docs/rules/valid-v-model.md
+++ b/docs/rules/valid-v-model.md
@@ -18,7 +18,8 @@ This rule reports `v-model` directives in the following cases:
 - The directive used on HTMLElement has an argument. E.g. `<input v-model:aaa="foo">`
 - The directive used on HTMLElement has modifiers which are not supported. E.g. `<input v-model.bbb="foo">`
 - The directive does not have that attribute value. E.g. `<input v-model>`
-- The directive does not have the attribute value which is valid as LHS. E.g. `<input v-model="foo() + bar()">`
+- The directive does not have the attribute value which is valid as LHS. E.g. `<input v-model="foo() + bar()">`, `<input v-model="a?.b">`
+- The directive has potential null object property access. E.g. `<input v-model="(a?.b).c">`
 - The directive is on unsupported elements. E.g. `<div v-model="foo"></div>`
 - The directive is on `<input>` elements which their types are `file`. E.g. `<input type="file" v-model="foo">`
 - The directive's reference is iteration variables. E.g. `<div v-for="x in list"><input type="file" v-model="x"></div>`
@@ -44,6 +45,8 @@ This rule reports `v-model` directives in the following cases:
   <input v-model:aaa="foo">
   <input v-model.bbb="foo">
   <input v-model="foo + bar">
+  <input v-model="a?.b.c">
+  <input v-model="(a?.b).c">
   <div v-model="foo"/>
   <div v-for="todo in todos">
     <input v-model="todo">
diff --git a/docs/user-guide/README.md b/docs/user-guide/README.md
index 4579d6c7..8de4d3e9 100644
--- a/docs/user-guide/README.md
+++ b/docs/user-guide/README.md
@@ -18,7 +18,7 @@ yarn add -D eslint eslint-plugin-vue@next
 ```
 
 ::: tip Requirements
-- ESLint v6.0.0 and above
+- ESLint v6.2.0 and above
 - Node.js v8.10.0 and above
 :::
 
diff --git a/lib/configs/base.js b/lib/configs/base.js
index 71279b9a..2521a623 100644
--- a/lib/configs/base.js
+++ b/lib/configs/base.js
@@ -6,7 +6,7 @@
 module.exports = {
   parser: require.resolve('vue-eslint-parser'),
   parserOptions: {
-    ecmaVersion: 2018,
+    ecmaVersion: 2020,
     sourceType: 'module'
   },
   env: {
diff --git a/lib/rules/component-name-in-template-casing.js b/lib/rules/component-name-in-template-casing.js
index a4771c0b..dffca0c8 100644
--- a/lib/rules/component-name-in-template-casing.js
+++ b/lib/rules/component-name-in-template-casing.js
@@ -135,16 +135,13 @@ module.exports = {
                 name,
                 caseType
               },
-              fix: (fixer) => {
+              *fix(fixer) {
+                yield fixer.replaceText(open, `<${casingName}`)
                 const endTag = node.endTag
-                if (!endTag) {
-                  return fixer.replaceText(open, `<${casingName}`)
+                if (endTag) {
+                  const endTagOpen = tokens.getFirstToken(endTag)
+                  yield fixer.replaceText(endTagOpen, `</${casingName}`)
                 }
-                const endTagOpen = tokens.getFirstToken(endTag)
-                return [
-                  fixer.replaceText(open, `<${casingName}`),
-                  fixer.replaceText(endTagOpen, `</${casingName}`)
-                ]
               }
             })
           }
diff --git a/lib/rules/custom-event-name-casing.js b/lib/rules/custom-event-name-casing.js
index cd7f7344..6c5f072c 100644
--- a/lib/rules/custom-event-name-casing.js
+++ b/lib/rules/custom-event-name-casing.js
@@ -48,7 +48,7 @@ function getNameParamNode(node) {
  * @param {CallExpression} node CallExpression
  */
 function getCalleeMemberNode(node) {
-  const callee = node.callee
+  const callee = utils.skipChainExpression(node.callee)
 
   if (callee.type === 'MemberExpression') {
     const name = utils.getStaticPropertyName(callee)
@@ -116,7 +116,7 @@ module.exports = {
       utils.compositingVisitors(
         utils.defineVueVisitor(context, {
           onSetupFunctionEnter(node, { node: vueNode }) {
-            const contextParam = utils.unwrapAssignmentPattern(node.params[1])
+            const contextParam = utils.skipDefaultParamValue(node.params[1])
             if (!contextParam) {
               // no arguments
               return
diff --git a/lib/rules/html-self-closing.js b/lib/rules/html-self-closing.js
index f8841f78..b54c2067 100644
--- a/lib/rules/html-self-closing.js
+++ b/lib/rules/html-self-closing.js
@@ -163,7 +163,7 @@ module.exports = {
                 elementType: ELEMENT_TYPE_MESSAGES[elementType],
                 name: node.rawName
               },
-              fix: (fixer) => {
+              fix(fixer) {
                 const tokens = context.parserServices.getTemplateBodyTokenStore()
                 const close = tokens.getLastToken(node.startTag)
                 if (close.type !== 'HTMLTagClose') {
@@ -187,7 +187,7 @@ module.exports = {
                 elementType: ELEMENT_TYPE_MESSAGES[elementType],
                 name: node.rawName
               },
-              fix: (fixer) => {
+              fix(fixer) {
                 const tokens = context.parserServices.getTemplateBodyTokenStore()
                 const close = tokens.getLastToken(node.startTag)
                 if (close.type !== 'HTMLSelfClosingTagClose') {
diff --git a/lib/rules/no-async-in-computed-properties.js b/lib/rules/no-async-in-computed-properties.js
index da11e042..9812d61c 100644
--- a/lib/rules/no-async-in-computed-properties.js
+++ b/lib/rules/no-async-in-computed-properties.js
@@ -26,16 +26,17 @@ const TIMED_FUNCTIONS = [
  * @param {CallExpression} node
  */
 function isTimedFunction(node) {
+  const callee = utils.skipChainExpression(node.callee)
   return (
     ((node.type === 'CallExpression' &&
-      node.callee.type === 'Identifier' &&
-      TIMED_FUNCTIONS.indexOf(node.callee.name) !== -1) ||
+      callee.type === 'Identifier' &&
+      TIMED_FUNCTIONS.indexOf(callee.name) !== -1) ||
       (node.type === 'CallExpression' &&
-        node.callee.type === 'MemberExpression' &&
-        node.callee.object.type === 'Identifier' &&
-        node.callee.object.name === 'window' &&
-        node.callee.property.type === 'Identifier' &&
-        TIMED_FUNCTIONS.indexOf(node.callee.property.name) !== -1)) &&
+        callee.type === 'MemberExpression' &&
+        callee.object.type === 'Identifier' &&
+        callee.object.name === 'window' &&
+        callee.property.type === 'Identifier' &&
+        TIMED_FUNCTIONS.indexOf(callee.property.name) !== -1)) &&
     node.arguments.length
   )
 }
@@ -44,18 +45,16 @@ function isTimedFunction(node) {
  * @param {CallExpression} node
  */
 function isPromise(node) {
-  if (
-    node.type === 'CallExpression' &&
-    node.callee.type === 'MemberExpression'
-  ) {
+  const callee = utils.skipChainExpression(node.callee)
+  if (node.type === 'CallExpression' && callee.type === 'MemberExpression') {
     return (
       // hello.PROMISE_FUNCTION()
-      (node.callee.property.type === 'Identifier' &&
-        PROMISE_FUNCTIONS.indexOf(node.callee.property.name) !== -1) || // Promise.PROMISE_METHOD()
-      (node.callee.object.type === 'Identifier' &&
-        node.callee.object.name === 'Promise' &&
-        node.callee.property.type === 'Identifier' &&
-        PROMISE_METHODS.indexOf(node.callee.property.name) !== -1)
+      (callee.property.type === 'Identifier' &&
+        PROMISE_FUNCTIONS.indexOf(callee.property.name) !== -1) || // Promise.PROMISE_METHOD()
+      (callee.object.type === 'Identifier' &&
+        callee.object.name === 'Promise' &&
+        callee.property.type === 'Identifier' &&
+        PROMISE_METHODS.indexOf(callee.property.name) !== -1)
     )
   }
   return false
diff --git a/lib/rules/no-deprecated-events-api.js b/lib/rules/no-deprecated-events-api.js
index a09f36bf..f393f59e 100644
--- a/lib/rules/no-deprecated-events-api.js
+++ b/lib/rules/no-deprecated-events-api.js
@@ -32,13 +32,26 @@ module.exports = {
   /** @param {RuleContext} context */
   create(context) {
     return utils.defineVueVisitor(context, {
-      /** @param {MemberExpression & {parent: CallExpression}} node */
-      'CallExpression > MemberExpression'(node) {
-        const call = node.parent
+      /** @param {MemberExpression & ({parent: CallExpression} | {parent: ChainExpression & {parent: CallExpression}})} node */
+      'CallExpression > MemberExpression, CallExpression > ChainExpression > MemberExpression'(
+        node
+      ) {
+        const call =
+          node.parent.type === 'ChainExpression'
+            ? node.parent.parent
+            : node.parent
+
+        if (call.optional) {
+          // It is OK because checking whether it is deprecated.
+          // e.g. `this.$on?.()`
+          return
+        }
+
         if (
-          call.callee !== node ||
-          node.property.type !== 'Identifier' ||
-          !['$on', '$off', '$once'].includes(node.property.name)
+          utils.skipChainExpression(call.callee) !== node ||
+          !['$on', '$off', '$once'].includes(
+            utils.getStaticPropertyName(node) || ''
+          )
         ) {
           return
         }
diff --git a/lib/rules/no-deprecated-v-bind-sync.js b/lib/rules/no-deprecated-v-bind-sync.js
index 97dfe51b..786ecb92 100644
--- a/lib/rules/no-deprecated-v-bind-sync.js
+++ b/lib/rules/no-deprecated-v-bind-sync.js
@@ -39,7 +39,7 @@ module.exports = {
             node,
             loc: node.loc,
             messageId: 'syncModifierIsDeprecated',
-            fix: (fixer) => {
+            fix(fixer) {
               if (node.key.argument == null) {
                 // is using spread syntax
                 return null
diff --git a/lib/rules/no-deprecated-v-on-number-modifiers.js b/lib/rules/no-deprecated-v-on-number-modifiers.js
index 544c1ee9..24341580 100644
--- a/lib/rules/no-deprecated-v-on-number-modifiers.js
+++ b/lib/rules/no-deprecated-v-on-number-modifiers.js
@@ -47,7 +47,7 @@ module.exports = {
           context.report({
             node: modifier,
             messageId: 'numberModifierIsDeprecated',
-            fix: (fixer) => {
+            fix(fixer) {
               const key = keyCodeToKey[keyCodes]
               if (!key) return null
 
diff --git a/lib/rules/no-deprecated-vue-config-keycodes.js b/lib/rules/no-deprecated-vue-config-keycodes.js
index b30db3fe..4db268e4 100644
--- a/lib/rules/no-deprecated-vue-config-keycodes.js
+++ b/lib/rules/no-deprecated-vue-config-keycodes.js
@@ -4,6 +4,8 @@
  */
 'use strict'
 
+const utils = require('../utils')
+
 // ------------------------------------------------------------------------------
 // Rule Definition
 // ------------------------------------------------------------------------------
@@ -31,7 +33,7 @@ module.exports = {
       "MemberExpression[property.type='Identifier'][property.name='keyCodes']"(
         node
       ) {
-        const config = node.object
+        const config = utils.skipChainExpression(node.object)
         if (
           config.type !== 'MemberExpression' ||
           config.property.type !== 'Identifier' ||
diff --git a/lib/rules/no-multiple-slot-args.js b/lib/rules/no-multiple-slot-args.js
index f98e058e..474a56ba 100644
--- a/lib/rules/no-multiple-slot-args.js
+++ b/lib/rules/no-multiple-slot-args.js
@@ -100,15 +100,12 @@ module.exports = {
     return utils.defineVueVisitor(context, {
       /** @param {MemberExpression} node */
       MemberExpression(node) {
-        const object = node.object
+        const object = utils.skipChainExpression(node.object)
         if (object.type !== 'MemberExpression') {
           return
         }
-        if (
-          object.property.type !== 'Identifier' ||
-          (object.property.name !== '$slots' &&
-            object.property.name !== '$scopedSlots')
-        ) {
+        const name = utils.getStaticPropertyName(object)
+        if (!name || (name !== '$slots' && name !== '$scopedSlots')) {
           return
         }
         if (!utils.isThis(object.object, context)) {
diff --git a/lib/rules/no-setup-props-destructure.js b/lib/rules/no-setup-props-destructure.js
index 3c6ce444..e77f63c4 100644
--- a/lib/rules/no-setup-props-destructure.js
+++ b/lib/rules/no-setup-props-destructure.js
@@ -49,18 +49,18 @@ module.exports = {
         return
       }
 
+      const rightNode = utils.skipChainExpression(right)
       if (
         left.type !== 'ArrayPattern' &&
         left.type !== 'ObjectPattern' &&
-        right.type !== 'MemberExpression'
+        rightNode.type !== 'MemberExpression'
       ) {
         return
       }
-
       /** @type {Expression | Super} */
-      let rightId = right
+      let rightId = rightNode
       while (rightId.type === 'MemberExpression') {
-        rightId = rightId.object
+        rightId = utils.skipChainExpression(rightId.object)
       }
       if (rightId.type === 'Identifier' && propsReferenceIds.has(rightId)) {
         report(left, 'getProperty')
@@ -84,7 +84,7 @@ module.exports = {
         }
       },
       onSetupFunctionEnter(node) {
-        const propsParam = utils.unwrapAssignmentPattern(node.params[0])
+        const propsParam = utils.skipDefaultParamValue(node.params[0])
         if (!propsParam) {
           // no arguments
           return
diff --git a/lib/rules/no-unused-properties.js b/lib/rules/no-unused-properties.js
index 951d6a2d..f7f30dc8 100644
--- a/lib/rules/no-unused-properties.js
+++ b/lib/rules/no-unused-properties.js
@@ -221,7 +221,7 @@ function getObjectPatternPropertyPatternTracker(pattern) {
 }
 
 /**
- * @param {Identifier | MemberExpression | ThisExpression} node
+ * @param {Identifier | MemberExpression | ChainExpression | ThisExpression} node
  * @param {RuleContext} context
  * @returns {UsedProps}
  */
@@ -304,6 +304,14 @@ function extractPatternOrThisProperties(node, context) {
         }
       }
     }
+  } else if (parent.type === 'ChainExpression') {
+    const { usedNames, unknown, calls } = extractPatternOrThisProperties(
+      parent,
+      context
+    )
+    result.usedNames.addAll(usedNames)
+    result.unknown = result.unknown || unknown
+    result.calls.push(...calls)
   }
   return result
 }
diff --git a/lib/rules/no-useless-mustaches.js b/lib/rules/no-useless-mustaches.js
index 89d8d542..b8a2057d 100644
--- a/lib/rules/no-useless-mustaches.js
+++ b/lib/rules/no-useless-mustaches.js
@@ -142,7 +142,7 @@ module.exports = {
             return null
           }
 
-          return [fixer.replaceText(node, text.replace(/\\([\s\S])/g, '$1'))]
+          return fixer.replaceText(node, text.replace(/\\([\s\S])/g, '$1'))
         }
       })
     }
diff --git a/lib/rules/no-useless-v-bind.js b/lib/rules/no-useless-v-bind.js
index d64e9553..01549ca6 100644
--- a/lib/rules/no-useless-v-bind.js
+++ b/lib/rules/no-useless-v-bind.js
@@ -111,10 +111,10 @@ module.exports = {
       context.report({
         node,
         messageId: 'unexpected',
-        fix(fixer) {
+        *fix(fixer) {
           if (hasComment || hasEscape) {
             // cannot fix
-            return null
+            return
           }
           const text = sourceCode.getText(value)
           const quoteChar = text[0]
@@ -126,6 +126,8 @@ module.exports = {
             node.key.name.range[1] + (shorthand ? 0 : 1)
           ]
 
+          yield fixer.removeRange(keyDirectiveRange)
+
           let attrValue
           if (quoteChar === '"') {
             attrValue = strValue.replace(DOUBLE_QUOTES_RE, '&quot;')
@@ -136,10 +138,7 @@ module.exports = {
               .replace(DOUBLE_QUOTES_RE, '&quot;')
               .replace(SINGLE_QUOTES_RE, '&apos;')
           }
-          return [
-            fixer.removeRange(keyDirectiveRange),
-            fixer.replaceText(expression, attrValue)
-          ]
+          yield fixer.replaceText(expression, attrValue)
         }
       })
     }
diff --git a/lib/rules/no-watch-after-await.js b/lib/rules/no-watch-after-await.js
index fc46dcd6..5f037fb3 100644
--- a/lib/rules/no-watch-after-await.js
+++ b/lib/rules/no-watch-after-await.js
@@ -7,7 +7,8 @@ const { ReferenceTracker } = require('eslint-utils')
 const utils = require('../utils')
 
 /**
- * @param {CallExpression} node
+ * @param {CallExpression | ChainExpression} node
+ * @returns {boolean}
  */
 function isMaybeUsedStopHandle(node) {
   const parent = node.parent
@@ -32,6 +33,9 @@ function isMaybeUsedStopHandle(node) {
       // [watch()]
       return true
     }
+    if (parent.type === 'ChainExpression') {
+      return isMaybeUsedStopHandle(parent)
+    }
   }
   return false
 }
diff --git a/lib/rules/order-in-components.js b/lib/rules/order-in-components.js
index fcff89ab..92b9b4c1 100644
--- a/lib/rules/order-in-components.js
+++ b/lib/rules/order-in-components.js
@@ -185,7 +185,9 @@ function isNotSideEffectsNode(node, visitorKeys) {
         node.type !== 'ConditionalExpression' &&
         // es2015
         node.type !== 'SpreadElement' &&
-        node.type !== 'TemplateLiteral'
+        node.type !== 'TemplateLiteral' &&
+        // es2020
+        node.type !== 'ChainExpression'
       ) {
         // Can not be sure that a node has no side effects
         result = false
@@ -289,7 +291,7 @@ module.exports = {
               firstUnorderedPropertyName: firstUnorderedProperty.name,
               line
             },
-            fix(fixer) {
+            *fix(fixer) {
               const propertyNode = property.node
               const firstUnorderedPropertyNode = firstUnorderedProperty.node
               const hasSideEffectsPossibility = propertiesNodes
@@ -302,7 +304,7 @@ module.exports = {
                     !isNotSideEffectsNode(property, sourceCode.visitorKeys)
                 )
               if (hasSideEffectsPossibility) {
-                return null
+                return
               }
               const afterComma = sourceCode.getTokenAfter(propertyNode)
               const hasAfterComma = isComma(afterComma)
@@ -313,6 +315,11 @@ module.exports = {
                 ? afterComma.range[1]
                 : propertyNode.range[1]
 
+              const removeStart = hasAfterComma
+                ? codeStart
+                : beforeComma.range[0]
+              yield fixer.removeRange([removeStart, codeEnd])
+
               const propertyCode =
                 sourceCode.text.slice(codeStart, codeEnd) +
                 (hasAfterComma ? '' : ',')
@@ -320,14 +327,7 @@ module.exports = {
                 firstUnorderedPropertyNode
               )
 
-              const removeStart = hasAfterComma
-                ? codeStart
-                : beforeComma.range[0]
-
-              return [
-                fixer.removeRange([removeStart, codeEnd]),
-                fixer.insertTextAfter(insertTarget, propertyCode)
-              ]
+              yield fixer.insertTextAfter(insertTarget, propertyCode)
             }
           })
         }
diff --git a/lib/rules/padding-line-between-blocks.js b/lib/rules/padding-line-between-blocks.js
index cc477370..c1b65245 100644
--- a/lib/rules/padding-line-between-blocks.js
+++ b/lib/rules/padding-line-between-blocks.js
@@ -48,14 +48,14 @@ function verifyForNever(context, prevBlock, nextBlock, betweenTokens) {
   context.report({
     node: nextBlock,
     messageId: 'never',
-    fix(fixer) {
-      return paddingLines.map(([prevToken, nextToken]) => {
+    *fix(fixer) {
+      for (const [prevToken, nextToken] of paddingLines) {
         const start = prevToken.range[1]
         const end = nextToken.range[0]
         const paddingText = context.getSourceCode().text.slice(start, end)
         const lastSpaces = splitLines(paddingText).pop()
-        return fixer.replaceTextRange([start, end], `\n${lastSpaces}`)
-      })
+        yield fixer.replaceTextRange([start, end], `\n${lastSpaces}`)
+      }
     }
   })
 }
diff --git a/lib/rules/require-default-prop.js b/lib/rules/require-default-prop.js
index ec73e369..e0e29b69 100644
--- a/lib/rules/require-default-prop.js
+++ b/lib/rules/require-default-prop.js
@@ -82,12 +82,18 @@ module.exports = {
     function findPropsWithoutDefaultValue(props) {
       return props.filter((prop) => {
         if (prop.value.type !== 'ObjectExpression') {
-          return (
-            (prop.value.type !== 'CallExpression' &&
-              prop.value.type !== 'Identifier') ||
-            (prop.value.type === 'Identifier' &&
-              NATIVE_TYPES.has(prop.value.name))
-          )
+          if (prop.value.type === 'Identifier') {
+            return NATIVE_TYPES.has(prop.value.name)
+          }
+          if (
+            prop.value.type === 'CallExpression' ||
+            prop.value.type === 'MemberExpression'
+          ) {
+            // OK
+            return false
+          }
+          // NG
+          return true
         }
 
         return (
@@ -99,7 +105,7 @@ module.exports = {
 
     /**
      * Detects whether given value node is a Boolean type
-     * @param {Expression | Pattern} value
+     * @param {Expression} value
      * @return {boolean}
      */
     function isValueNodeOfBooleanType(value) {
@@ -123,7 +129,7 @@ module.exports = {
      * @return {Boolean}
      */
     function isBooleanProp(prop) {
-      const value = utils.unwrapTypes(prop.value)
+      const value = utils.skipTSAsExpression(prop.value)
 
       return (
         isValueNodeOfBooleanType(value) ||
diff --git a/lib/rules/require-explicit-emits.js b/lib/rules/require-explicit-emits.js
index 7dc1706b..27ae600f 100644
--- a/lib/rules/require-explicit-emits.js
+++ b/lib/rules/require-explicit-emits.js
@@ -261,7 +261,7 @@ module.exports = {
          * @param {VueObjectData} data
          */
         'CallExpression[arguments.0.type=Literal]'(node, { node: vueNode }) {
-          const callee = node.callee
+          const callee = utils.skipChainExpression(node.callee)
           const nameLiteralNode = node.arguments[0]
           if (!nameLiteralNode || typeof nameLiteralNode.value !== 'string') {
             // cannot check
@@ -287,20 +287,22 @@ module.exports = {
             if (callee.type === 'Identifier' && emitReferenceIds.has(callee)) {
               // verify setup(props,{emit}) {emit()}
               verify(emitsDeclarations, nameLiteralNode, vueNode)
-            } else if (
-              emit &&
-              emit.name === 'emit' &&
-              emit.member.object.type === 'Identifier' &&
-              contextReferenceIds.has(emit.member.object)
-            ) {
-              // verify setup(props,context) {context.emit()}
-              verify(emitsDeclarations, nameLiteralNode, vueNode)
+            } else if (emit && emit.name === 'emit') {
+              const memObject = utils.skipChainExpression(emit.member.object)
+              if (
+                memObject.type === 'Identifier' &&
+                contextReferenceIds.has(memObject)
+              ) {
+                // verify setup(props,context) {context.emit()}
+                verify(emitsDeclarations, nameLiteralNode, vueNode)
+              }
             }
           }
 
           // verify $emit
           if (emit && emit.name === '$emit') {
-            if (utils.isThis(emit.member.object, context)) {
+            const memObject = utils.skipChainExpression(emit.member.object)
+            if (utils.isThis(memObject, context)) {
               // verify this.$emit()
               verify(emitsDeclarations, nameLiteralNode, vueNode)
             }
diff --git a/lib/rules/require-slots-as-functions.js b/lib/rules/require-slots-as-functions.js
index 36a3f933..658071d2 100644
--- a/lib/rules/require-slots-as-functions.js
+++ b/lib/rules/require-slots-as-functions.js
@@ -33,7 +33,7 @@ module.exports = {
   create(context) {
     /**
      * Verify the given node
-     * @param {MemberExpression | Identifier} node The node to verify
+     * @param {MemberExpression | Identifier | ChainExpression} node The node to verify
      * @param {Expression} reportNode The node to report
      */
     function verify(node, reportNode) {
@@ -58,6 +58,12 @@ module.exports = {
         return
       }
 
+      if (parent.type === 'ChainExpression') {
+        // (this.$slots?.foo).x
+        verify(parent, reportNode)
+        return
+      }
+
       if (
         // this.$slots.foo.xxx
         parent.type === 'MemberExpression' ||
@@ -97,14 +103,11 @@ module.exports = {
     return utils.defineVueVisitor(context, {
       /** @param {MemberExpression} node */
       MemberExpression(node) {
-        const object = node.object
+        const object = utils.skipChainExpression(node.object)
         if (object.type !== 'MemberExpression') {
           return
         }
-        if (
-          object.property.type !== 'Identifier' ||
-          object.property.name !== '$slots'
-        ) {
+        if (utils.getStaticPropertyName(object) !== '$slots') {
           return
         }
         if (!utils.isThis(object.object, context)) {
diff --git a/lib/rules/require-valid-default-prop.js b/lib/rules/require-valid-default-prop.js
index a0444db0..ffebef07 100644
--- a/lib/rules/require-valid-default-prop.js
+++ b/lib/rules/require-valid-default-prop.js
@@ -48,7 +48,7 @@ function getPropertyNode(obj, name) {
 }
 
 /**
- * @param {Expression | Pattern} node
+ * @param {Expression} node
  * @returns {string[]}
  */
 function getTypes(node) {
@@ -122,10 +122,11 @@ module.exports = {
     }
 
     /**
-     * @param {Expression | Pattern} node
+     * @param {Expression} targetNode
      * @returns { StandardValueType | FunctionExprValueType | FunctionValueType | null }
      */
-    function getValueType(node) {
+    function getValueType(targetNode) {
+      const node = utils.skipChainExpression(targetNode)
       if (node.type === 'CallExpression') {
         // Symbol(), Number() ...
         if (
diff --git a/lib/rules/syntaxes/slot-attribute.js b/lib/rules/syntaxes/slot-attribute.js
index bf84361f..6f38166e 100644
--- a/lib/rules/syntaxes/slot-attribute.js
+++ b/lib/rules/syntaxes/slot-attribute.js
@@ -56,9 +56,9 @@ module.exports = {
      * @param {VAttribute|VDirective} slotAttr node of `slot`
      * @param {string | null} slotName name of `slot`
      * @param {boolean} vBind `true` if `slotAttr` is `v-bind:slot`
-     * @returns {Fix[]} fix data
+     * @returns {IterableIterator<Fix>} fix data
      */
-    function fixSlotToVSlot(fixer, slotAttr, slotName, vBind) {
+    function* fixSlotToVSlot(fixer, slotAttr, slotName, vBind) {
       const element = slotAttr.parent
       const scopeAttr = element.attributes.find(
         (attr) =>
@@ -78,11 +78,10 @@ module.exports = {
           : ''
 
       const replaceText = `v-slot${nameArgument}${scopeValue}`
-      const fixers = [fixer.replaceText(slotAttr || scopeAttr, replaceText)]
+      yield fixer.replaceText(slotAttr || scopeAttr, replaceText)
       if (slotAttr && scopeAttr) {
-        fixers.push(fixer.remove(scopeAttr))
+        yield fixer.remove(scopeAttr)
       }
-      return fixers
     }
     /**
      * Reports `slot` node
@@ -94,12 +93,12 @@ module.exports = {
         node: slotAttr.key,
         messageId: 'forbiddenSlotAttribute',
         // fix to use `v-slot`
-        fix(fixer) {
+        *fix(fixer) {
           if (!canConvertFromSlotToVSlot(slotAttr)) {
-            return null
+            return
           }
           const slotName = slotAttr.value && slotAttr.value.value
-          return fixSlotToVSlot(fixer, slotAttr, slotName, false)
+          yield* fixSlotToVSlot(fixer, slotAttr, slotName, false)
         }
       })
     }
@@ -113,15 +112,15 @@ module.exports = {
         node: slotAttr.key,
         messageId: 'forbiddenSlotAttribute',
         // fix to use `v-slot`
-        fix(fixer) {
+        *fix(fixer) {
           if (!canConvertFromVBindSlotToVSlot(slotAttr)) {
-            return null
+            return
           }
           const slotName =
             slotAttr.value &&
             slotAttr.value.expression &&
             sourceCode.getText(slotAttr.value.expression).trim()
-          return fixSlotToVSlot(fixer, slotAttr, slotName, true)
+          yield* fixSlotToVSlot(fixer, slotAttr, slotName, true)
         }
       })
     }
diff --git a/lib/rules/syntaxes/slot-scope-attribute.js b/lib/rules/syntaxes/slot-scope-attribute.js
index e05be5c3..2afd790b 100644
--- a/lib/rules/syntaxes/slot-scope-attribute.js
+++ b/lib/rules/syntaxes/slot-scope-attribute.js
@@ -74,16 +74,17 @@ module.exports = {
       context.report({
         node: scopeAttr.key,
         messageId: 'forbiddenSlotScopeAttribute',
-        fix: fixToUpgrade
-          ? // fix to use `v-slot`
-            (fixer) => {
-              const startTag = scopeAttr.parent
-              if (!canConvertToVSlot(startTag)) {
-                return null
-              }
-              return fixSlotScopeToVSlot(fixer, scopeAttr)
-            }
-          : null
+        fix(fixer) {
+          if (!fixToUpgrade) {
+            return null
+          }
+          // fix to use `v-slot`
+          const startTag = scopeAttr.parent
+          if (!canConvertToVSlot(startTag)) {
+            return null
+          }
+          return fixSlotScopeToVSlot(fixer, scopeAttr)
+        }
       })
     }
 
diff --git a/lib/rules/syntaxes/v-slot.js b/lib/rules/syntaxes/v-slot.js
index 9d563a2d..ae71dc9d 100644
--- a/lib/rules/syntaxes/v-slot.js
+++ b/lib/rules/syntaxes/v-slot.js
@@ -69,7 +69,7 @@ module.exports = {
         node: vSlotAttr.key,
         messageId: 'forbiddenVSlot',
         // fix to use `slot` (downgrade)
-        fix: (fixer) => {
+        fix(fixer) {
           if (!canConvertToSlot(vSlotAttr)) {
             return null
           }
diff --git a/lib/rules/v-on-function-call.js b/lib/rules/v-on-function-call.js
index 04eb7979..4befe9f8 100644
--- a/lib/rules/v-on-function-call.js
+++ b/lib/rules/v-on-function-call.js
@@ -85,6 +85,10 @@ module.exports = {
       if (expression.type !== 'CallExpression' || expression.arguments.length) {
         return null
       }
+      if (expression.optional) {
+        // Allow optional chaining
+        return null
+      }
       const callee = expression.callee
       if (callee.type !== 'Identifier') {
         return null
diff --git a/lib/rules/valid-v-bind-sync.js b/lib/rules/valid-v-bind-sync.js
index 3799d089..48452f82 100644
--- a/lib/rules/valid-v-bind-sync.js
+++ b/lib/rules/valid-v-bind-sync.js
@@ -32,7 +32,22 @@ function isValidElement(node) {
 }
 
 /**
- * Check whether the given node can be LHS.
+ * Check whether the given node is a MemberExpression containing an optional chaining.
+ * e.g.
+ * - `a?.b`
+ * - `a?.b.c`
+ * @param {ASTNode} node The node to check.
+ * @returns {boolean} `true` if the node is a MemberExpression containing an optional chaining.
+ */
+function isOptionalChainingMemberExpression(node) {
+  return (
+    node.type === 'ChainExpression' &&
+    node.expression.type === 'MemberExpression'
+  )
+}
+
+/**
+ * Check whether the given node can be LHS (left-hand side).
  * @param {ASTNode} node The node to check.
  * @returns {boolean} `true` if the node can be LHS.
  */
@@ -40,6 +55,33 @@ function isLhs(node) {
   return node.type === 'Identifier' || node.type === 'MemberExpression'
 }
 
+/**
+ * Check whether the given node is a MemberExpression of a possibly null object.
+ * e.g.
+ * - `(a?.b).c`
+ * - `(null).foo`
+ * @param {ASTNode} node The node to check.
+ * @returns {boolean} `true` if the node is a MemberExpression of a possibly null object.
+ */
+function maybeNullObjectMemberExpression(node) {
+  if (node.type !== 'MemberExpression') {
+    return false
+  }
+  const { object } = node
+  if (object.type === 'ChainExpression') {
+    // `(a?.b).c`
+    return true
+  }
+  if (object.type === 'Literal' && object.value === null && !object.bigint) {
+    // `(null).foo`
+    return true
+  }
+  if (object.type === 'MemberExpression') {
+    return maybeNullObjectMemberExpression(object)
+  }
+  return false
+}
+
 // ------------------------------------------------------------------------------
 // Rule Definition
 // ------------------------------------------------------------------------------
@@ -57,8 +99,12 @@ module.exports = {
     messages: {
       unexpectedInvalidElement:
         "'.sync' modifiers aren't supported on <{{name}}> non Vue-components.",
+      unexpectedOptionalChaining:
+        "Optional chaining cannot appear in 'v-bind' with '.sync' modifiers.",
       unexpectedNonLhsExpression:
         "'.sync' modifiers require the attribute value which is valid as LHS.",
+      unexpectedNullObject:
+        "'.sync' modifier has potential null object property access.",
       unexpectedUpdateIterationVariable:
         "'.sync' modifiers cannot update the iteration variable '{{varName}}' itself."
     }
@@ -83,16 +129,33 @@ module.exports = {
           })
         }
 
-        if (!node.value || !node.value.expression) {
+        if (!node.value) {
           return
         }
 
-        if (!isLhs(node.value.expression)) {
+        const expression = node.value.expression
+        if (!expression) {
+          // Parsing error
+          return
+        }
+        if (isOptionalChainingMemberExpression(expression)) {
+          context.report({
+            node,
+            loc: node.loc,
+            messageId: 'unexpectedOptionalChaining'
+          })
+        } else if (!isLhs(expression)) {
           context.report({
             node,
             loc: node.loc,
             messageId: 'unexpectedNonLhsExpression'
           })
+        } else if (maybeNullObjectMemberExpression(expression)) {
+          context.report({
+            node,
+            loc: node.loc,
+            messageId: 'unexpectedNullObject'
+          })
         }
 
         for (const reference of node.value.references) {
diff --git a/lib/rules/valid-v-model.js b/lib/rules/valid-v-model.js
index 45178736..adfe9c2d 100644
--- a/lib/rules/valid-v-model.js
+++ b/lib/rules/valid-v-model.js
@@ -37,7 +37,22 @@ function isValidElement(node) {
 }
 
 /**
- * Check whether the given node can be LHS.
+ * Check whether the given node is a MemberExpression containing an optional chaining.
+ * e.g.
+ * - `a?.b`
+ * - `a?.b.c`
+ * @param {ASTNode} node The node to check.
+ * @returns {boolean} `true` if the node is a MemberExpression containing an optional chaining.
+ */
+function isOptionalChainingMemberExpression(node) {
+  return (
+    node.type === 'ChainExpression' &&
+    node.expression.type === 'MemberExpression'
+  )
+}
+
+/**
+ * Check whether the given node can be LHS (left-hand side).
  * @param {ASTNode} node The node to check.
  * @returns {boolean} `true` if the node can be LHS.
  */
@@ -45,6 +60,33 @@ function isLhs(node) {
   return node.type === 'Identifier' || node.type === 'MemberExpression'
 }
 
+/**
+ * Check whether the given node is a MemberExpression of a possibly null object.
+ * e.g.
+ * - `(a?.b).c`
+ * - `(null).foo`
+ * @param {ASTNode} node The node to check.
+ * @returns {boolean} `true` if the node is a MemberExpression of a possibly null object.
+ */
+function maybeNullObjectMemberExpression(node) {
+  if (node.type !== 'MemberExpression') {
+    return false
+  }
+  const { object } = node
+  if (object.type === 'ChainExpression') {
+    // `(a?.b).c`
+    return true
+  }
+  if (object.type === 'Literal' && object.value === null && !object.bigint) {
+    // `(null).foo`
+    return true
+  }
+  if (object.type === 'MemberExpression') {
+    return maybeNullObjectMemberExpression(object)
+  }
+  return false
+}
+
 /**
  * Get the variable by names.
  * @param {string} name The variable name to find.
@@ -76,6 +118,7 @@ function getVariable(name, leafNode) {
 // Rule Definition
 // ------------------------------------------------------------------------------
 
+/** @type {RuleModule}  */
 module.exports = {
   meta: {
     type: 'problem',
@@ -85,7 +128,25 @@ module.exports = {
       url: 'https://eslint.vuejs.org/rules/valid-v-model.html'
     },
     fixable: null,
-    schema: []
+    schema: [],
+    messages: {
+      unexpectedInvalidElement:
+        "'v-model' directives aren't supported on <{{name}}> elements.",
+      unexpectedInputFile:
+        "'v-model' directives don't support 'file' input type.",
+      unexpectedArgument: "'v-model' directives require no argument.",
+      unexpectedModifier:
+        "'v-model' directives don't support the modifier '{{name}}'.",
+      missingValue: "'v-model' directives require that attribute value.",
+      unexpectedOptionalChaining:
+        "Optional chaining cannot appear in 'v-model' directives.",
+      unexpectedNonLhsExpression:
+        "'v-model' directives require the attribute value which is valid as LHS.",
+      unexpectedNullObject:
+        "'v-model' directive has potential null object property access.",
+      unexpectedUpdateIterationVariable:
+        "'v-model' directives cannot update the iteration variable '{{varName}}' itself."
+    }
   },
   /** @param {RuleContext} context */
   create(context) {
@@ -99,8 +160,7 @@ module.exports = {
           context.report({
             node,
             loc: node.loc,
-            message:
-              "'v-model' directives aren't supported on <{{name}}> elements.",
+            messageId: 'unexpectedInvalidElement',
             data: { name }
           })
         }
@@ -109,7 +169,7 @@ module.exports = {
           context.report({
             node,
             loc: node.loc,
-            message: "'v-model' directives don't support 'file' input type."
+            messageId: 'unexpectedInputFile'
           })
         }
 
@@ -118,7 +178,7 @@ module.exports = {
             context.report({
               node,
               loc: node.loc,
-              message: "'v-model' directives require no argument."
+              messageId: 'unexpectedArgument'
             })
           }
 
@@ -127,8 +187,7 @@ module.exports = {
               context.report({
                 node,
                 loc: node.loc,
-                message:
-                  "'v-model' directives don't support the modifier '{{name}}'.",
+                messageId: 'unexpectedModifier',
                 data: { name: modifier.name }
               })
             }
@@ -139,20 +198,32 @@ module.exports = {
           context.report({
             node,
             loc: node.loc,
-            message: "'v-model' directives require that attribute value."
+            messageId: 'missingValue'
           })
           return
         }
-        if (!node.value.expression) {
+        const expression = node.value.expression
+        if (!expression) {
           // Parsing error
           return
         }
-        if (!isLhs(node.value.expression)) {
+        if (isOptionalChainingMemberExpression(expression)) {
           context.report({
             node,
             loc: node.loc,
-            message:
-              "'v-model' directives require the attribute value which is valid as LHS."
+            messageId: 'unexpectedOptionalChaining'
+          })
+        } else if (!isLhs(expression)) {
+          context.report({
+            node,
+            loc: node.loc,
+            messageId: 'unexpectedNonLhsExpression'
+          })
+        } else if (maybeNullObjectMemberExpression(expression)) {
+          context.report({
+            node,
+            loc: node.loc,
+            messageId: 'unexpectedNullObject'
           })
         }
 
@@ -167,8 +238,8 @@ module.exports = {
             context.report({
               node,
               loc: node.loc,
-              message:
-                "'v-model' directives cannot update the iteration variable '{{varName}}' itself.",
+              messageId: 'unexpectedUpdateIterationVariable',
+
               data: { varName: id.name }
             })
           }
diff --git a/lib/utils/index.js b/lib/utils/index.js
index 0a6f7bf3..13053052 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -315,7 +315,6 @@ module.exports = {
       })
     }
   },
-
   /**
    * Checks whether the given value is defined.
    * @template T
@@ -323,18 +322,6 @@ module.exports = {
    * @returns {v is T}
    */
   isDef,
-  /**
-   * Check whether the given node is the root element or not.
-   * @param {VElement} node The element node to check.
-   * @returns {boolean} `true` if the node is the root element.
-   */
-  isRootElement(node) {
-    return (
-      node.parent.type === 'VDocumentFragment' ||
-      node.parent.parent.type === 'VDocumentFragment'
-    )
-  },
-
   /**
    * Get the previous sibling element of the given element.
    * @param {VElement} node The element node to get the previous sibling element.
@@ -648,36 +635,6 @@ module.exports = {
   isHtmlVoidElementName(name) {
     return VOID_ELEMENT_NAMES.has(name)
   },
-
-  /**
-   * Parse member expression node to get array with all of its parts
-   * @param {ESNode} node MemberExpression
-   * @returns {string[]}
-   */
-  parseMemberExpression(node) {
-    const members = []
-
-    if (node.type === 'MemberExpression') {
-      /** @type {Expression | Super} */
-      let memberExpression = node
-
-      while (memberExpression.type === 'MemberExpression') {
-        if (memberExpression.property.type === 'Identifier') {
-          members.push(memberExpression.property.name)
-        }
-        memberExpression = memberExpression.object
-      }
-
-      if (memberExpression.type === 'ThisExpression') {
-        members.push('this')
-      } else if (memberExpression.type === 'Identifier') {
-        members.push(memberExpression.name)
-      }
-    }
-
-    return members.reverse()
-  },
-
   /**
    * Gets the property name of a given node.
    * @param {Property|AssignmentProperty|MethodDefinition|MemberExpression} node - The node to get.
@@ -724,7 +681,7 @@ module.exports = {
             type: 'object',
             key: prop.key,
             propName,
-            value: unwrapTypes(prop.value),
+            value: skipTSAsExpression(prop.value),
             node: prop
           }
         }
@@ -732,7 +689,7 @@ module.exports = {
           type: 'object',
           key: null,
           propName: null,
-          value: unwrapTypes(prop.value),
+          value: skipTSAsExpression(prop.value),
           node: prop
         }
       })
@@ -795,7 +752,7 @@ module.exports = {
             type: 'object',
             key: prop.key,
             emitName,
-            value: unwrapTypes(prop.value),
+            value: skipTSAsExpression(prop.value),
             node: prop
           }
         }
@@ -803,7 +760,7 @@ module.exports = {
           type: 'object',
           key: null,
           emitName: null,
-          value: unwrapTypes(prop.value),
+          value: skipTSAsExpression(prop.value),
           node: prop
         }
       })
@@ -862,7 +819,7 @@ module.exports = {
       .map((cp) => {
         const key = getStaticPropertyName(cp)
         /** @type {Expression} */
-        const propValue = unwrapTypes(cp.value)
+        const propValue = skipTSAsExpression(cp.value)
         /** @type {BlockStatement | null} */
         let value = null
 
@@ -1056,7 +1013,7 @@ module.exports = {
         const callee = callExpr.callee
 
         if (callee.type === 'MemberExpression') {
-          const calleeObject = unwrapTypes(callee.object)
+          const calleeObject = skipTSAsExpression(callee.object)
 
           if (
             calleeObject.type === 'Identifier' &&
@@ -1125,7 +1082,7 @@ module.exports = {
    */
   *iterateObjectExpression(node, groupName) {
     /** @type {Set<Property> | undefined} */
-    let usedGetter = new Set()
+    let usedGetter
     for (const item of node.properties) {
       if (item.type === 'Property') {
         const key = item.key
@@ -1313,52 +1270,15 @@ module.exports = {
   getMemberChaining(node) {
     /** @type {MemberExpression[]} */
     const nodes = []
-    let n = node
+    let n = skipChainExpression(node)
 
     while (n.type === 'MemberExpression') {
       nodes.push(n)
-      n = n.object
+      n = skipChainExpression(n.object)
     }
 
     return [n, ...nodes.reverse()]
   },
-  /**
-   * Parse CallExpression or MemberExpression to get simplified version without arguments
-   *
-   * @param  {ESNode} node The node to parse (MemberExpression | CallExpression)
-   * @return {String} eg. 'this.asd.qwe().map().filter().test.reduce()'
-   */
-  parseMemberOrCallExpression(node) {
-    const parsedCallee = []
-    let n = node
-    let isFunc
-
-    while (n.type === 'MemberExpression' || n.type === 'CallExpression') {
-      if (n.type === 'CallExpression') {
-        n = n.callee
-        isFunc = true
-      } else {
-        if (n.computed) {
-          parsedCallee.push(`[]${isFunc ? '()' : ''}`)
-        } else if (n.property.type === 'Identifier') {
-          parsedCallee.push(n.property.name + (isFunc ? '()' : ''))
-        }
-        isFunc = false
-        n = n.object
-      }
-    }
-
-    if (n.type === 'Identifier') {
-      parsedCallee.push(n.name)
-    }
-
-    if (n.type === 'ThisExpression') {
-      parsedCallee.push('this')
-    }
-
-    return parsedCallee.reverse().join('.').replace(/\.\[/g, '[')
-  },
-
   /**
    * return two string editdistance
    * @param {string} a string a to compare
@@ -1418,18 +1338,17 @@ module.exports = {
    */
   isPropertyChain,
   /**
-   * Unwrap typescript types like "X as F"
-   * @template T
-   * @param {T} node
-   * @return {T}
+   * Retrieve `TSAsExpression#expression` value if the given node a `TSAsExpression` node. Otherwise, pass through it.
+   */
+  skipTSAsExpression,
+  /**
+   * Retrieve `AssignmentPattern#left` value if the given node a `AssignmentPattern` node. Otherwise, pass through it.
    */
-  unwrapTypes,
+  skipDefaultParamValue,
   /**
-   * Unwrap AssignmentPattern like "(a = 1) => ret"
-   * @param { AssignmentPattern | RestElement | ArrayPattern | ObjectPattern | Identifier } node
-   * @return { RestElement | ArrayPattern | ObjectPattern | Identifier}
+   * Retrieve `ChainExpression#expression` value if the given node a `ChainExpression` node. Otherwise, pass through it.
    */
-  unwrapAssignmentPattern,
+  skipChainExpression,
 
   /**
    * Check whether the given node is `this` or variable that stores `this`.
@@ -1479,6 +1398,7 @@ module.exports = {
   findMutating(props) {
     /** @type {MemberExpression[]} */
     const pathNodes = []
+    /** @type {MemberExpression | Identifier | ChainExpression} */
     let node = props
     let target = node.parent
     while (true) {
@@ -1499,10 +1419,9 @@ module.exports = {
           pathNodes
         }
       } else if (target.type === 'CallExpression') {
-        if (node !== props && target.callee === node) {
-          const callName = getStaticPropertyName(
-            /** @type {MemberExpression} */ (node)
-          )
+        if (pathNodes.length > 0 && target.callee === node) {
+          const mem = pathNodes[pathNodes.length - 1]
+          const callName = getStaticPropertyName(mem)
           if (
             callName &&
             /^push|pop|shift|unshift|reverse|splice|sort|copyWithin|fill$/u.exec(
@@ -1525,6 +1444,10 @@ module.exports = {
           target = target.parent
           continue // loop
         }
+      } else if (target.type === 'ChainExpression') {
+        node = target
+        target = target.parent
+        continue // loop
       }
 
       return null
@@ -1660,14 +1583,12 @@ function findProperty(node, name, filter) {
        * @returns {prop is Property}
        */
       (prop) =>
-        prop.type === 'Property' &&
-        getStaticPropertyName(prop) === name &&
-        filter(prop)
+        isProperty(prop) && getStaticPropertyName(prop) === name && filter(prop)
     : /**
        * @param {Property | SpreadElement} prop
        * @returns {prop is Property}
        */
-      (prop) => prop.type === 'Property' && getStaticPropertyName(prop) === name
+      (prop) => isProperty(prop) && getStaticPropertyName(prop) === name
   return node.properties.find(predicate) || null
 }
 
@@ -1685,14 +1606,15 @@ function findAssignmentProperty(node, name, filter) {
        * @returns {prop is AssignmentProperty}
        */
       (prop) =>
-        prop.type === 'Property' &&
+        isAssignmentProperty(prop) &&
         getStaticPropertyName(prop) === name &&
         filter(prop)
     : /**
        * @param {AssignmentProperty | RestElement} prop
        * @returns {prop is AssignmentProperty}
        */
-      (prop) => prop.type === 'Property' && getStaticPropertyName(prop) === name
+      (prop) =>
+        isAssignmentProperty(prop) && getStaticPropertyName(prop) === name
   return node.properties.find(predicate) || null
 }
 
@@ -1722,20 +1644,21 @@ function isVElement(node) {
 }
 
 /**
- * Unwrap typescript types like "X as F"
- * @template T
- * @param {T} node
- * @return {T}
+ * Retrieve `TSAsExpression#expression` value if the given node a `TSAsExpression` node. Otherwise, pass through it.
+ * @template T Node type
+ * @param {T | TSAsExpression} node The node to address.
+ * @returns {T} The `TSAsExpression#expression` value if the node is a `TSAsExpression` node. Otherwise, the node.
  */
-function unwrapTypes(node) {
+function skipTSAsExpression(node) {
   if (!node) {
     return node
   }
   // @ts-expect-error
   if (node.type === 'TSAsExpression') {
     // @ts-expect-error
-    return unwrapTypes(node.expression)
+    return skipTSAsExpression(node.expression)
   }
+  // @ts-expect-error
   return node
 }
 
@@ -1766,18 +1689,40 @@ function isPropertyChain(prop, node) {
 }
 
 /**
- * Unwrap AssignmentPattern like "(a = 1) => ret"
- * @param { AssignmentPattern | RestElement | ArrayPattern | ObjectPattern | Identifier } node
- * @return { RestElement | ArrayPattern | ObjectPattern | Identifier}
+ * Retrieve `AssignmentPattern#left` value if the given node a `AssignmentPattern` node. Otherwise, pass through it.
+ * @template T Node type
+ * @param {T | AssignmentPattern} node The node to address.
+ * @return {T} The `AssignmentPattern#left` value if the node is a `AssignmentPattern` node. Otherwise, the node.
  */
-function unwrapAssignmentPattern(node) {
+function skipDefaultParamValue(node) {
   if (!node) {
     return node
   }
+  // @ts-expect-error
   if (node.type === 'AssignmentPattern') {
     // @ts-expect-error
-    return unwrapAssignmentPattern(node.left)
+    return skipDefaultParamValue(node.left)
   }
+  // @ts-expect-error
+  return node
+}
+
+/**
+ * Retrieve `ChainExpression#expression` value if the given node a `ChainExpression` node. Otherwise, pass through it.
+ * @template T Node type
+ * @param {T | ChainExpression} node The node to address.
+ * @returns {T} The `ChainExpression#expression` value if the node is a `ChainExpression` node. Otherwise, the node.
+ */
+function skipChainExpression(node) {
+  if (!node) {
+    return node
+  }
+  // @ts-expect-error
+  if (node.type === 'ChainExpression') {
+    // @ts-expect-error
+    return skipChainExpression(node.expression)
+  }
+  // @ts-expect-error
   return node
 }
 
@@ -1879,7 +1824,7 @@ function isVueComponent(node) {
     const callee = node.callee
 
     if (callee.type === 'MemberExpression') {
-      const calleeObject = unwrapTypes(callee.object)
+      const calleeObject = skipTSAsExpression(callee.object)
 
       if (calleeObject.type === 'Identifier') {
         const propName = getStaticPropertyName(callee)
@@ -1933,7 +1878,8 @@ function isVueComponent(node) {
   function isObjectArgument(node) {
     return (
       node.arguments.length > 0 &&
-      unwrapTypes(node.arguments.slice(-1)[0]).type === 'ObjectExpression'
+      skipTSAsExpression(node.arguments.slice(-1)[0]).type ===
+        'ObjectExpression'
     )
   }
 }
@@ -1951,7 +1897,7 @@ function isVueInstance(node) {
       callee.type === 'Identifier' &&
       callee.name === 'Vue' &&
       node.arguments.length &&
-      unwrapTypes(node.arguments[0]).type === 'ObjectExpression'
+      skipTSAsExpression(node.arguments[0]).type === 'ObjectExpression'
   )
 }
 
@@ -1971,7 +1917,7 @@ function getVueObjectType(context, node) {
     const filePath = context.getFilename()
     if (
       isVueComponentFile(parent, filePath) &&
-      unwrapTypes(parent.declaration) === node
+      skipTSAsExpression(parent.declaration) === node
     ) {
       return 'export'
     }
@@ -1979,13 +1925,16 @@ function getVueObjectType(context, node) {
     // Vue.component('xxx', {}) || component('xxx', {})
     if (
       isVueComponent(parent) &&
-      unwrapTypes(parent.arguments.slice(-1)[0]) === node
+      skipTSAsExpression(parent.arguments.slice(-1)[0]) === node
     ) {
       return 'definition'
     }
   } else if (parent.type === 'NewExpression') {
     // new Vue({})
-    if (isVueInstance(parent) && unwrapTypes(parent.arguments[0]) === node) {
+    if (
+      isVueInstance(parent) &&
+      skipTSAsExpression(parent.arguments[0]) === node
+    ) {
       return 'instance'
     }
   }
diff --git a/package.json b/package.json
index f1a5d94d..2dacb75b 100644
--- a/package.json
+++ b/package.json
@@ -50,10 +50,10 @@
     "node": ">=8.10"
   },
   "peerDependencies": {
-    "eslint": "^6.0.0 || ^7.0.0"
+    "eslint": "^6.2.0 || ^7.0.0"
   },
   "dependencies": {
-    "eslint-utils": "^2.0.0",
+    "eslint-utils": "^2.1.0",
     "natural-compare": "^1.4.0",
     "semver": "^7.3.2",
     "vue-eslint-parser": "^7.1.0"
diff --git a/tools/update-lib-configs.js b/tools/update-lib-configs.js
index 5379decb..b5b2488c 100644
--- a/tools/update-lib-configs.js
+++ b/tools/update-lib-configs.js
@@ -49,7 +49,7 @@ function formatCategory(category) {
 module.exports = {
   parser: require.resolve('vue-eslint-parser'),
   parserOptions: {
-    ecmaVersion: 2018,
+    ecmaVersion: 2020,
     sourceType: 'module'
   },
   env: {


Test patch:
diff --git a/tests/lib/rules/custom-event-name-casing.js b/tests/lib/rules/custom-event-name-casing.js
index b767223e..934bb079 100644
--- a/tests/lib/rules/custom-event-name-casing.js
+++ b/tests/lib/rules/custom-event-name-casing.js
@@ -10,7 +10,7 @@ const rule = require('../../../lib/rules/custom-event-name-casing')
 const tester = new RuleTester({
   parser: require.resolve('vue-eslint-parser'),
   parserOptions: {
-    ecmaVersion: 2019,
+    ecmaVersion: 2020,
     sourceType: 'module'
   }
 })
@@ -216,6 +216,66 @@ tester.run('custom-event-name-casing', rule, {
           endColumn: 32
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <input
+          @click="$emit?.('fooBar')">
+      </template>
+      <script>
+      export default {
+        setup(props, context) {
+          return {
+            onInput(value) {
+              context?.emit?.('barBaz')
+            }
+          }
+        },
+        methods: {
+          onClick() {
+            this?.$emit?.('bazQux')
+          }
+        }
+      }
+      </script>
+      `,
+      errors: [
+        "Custom event name 'fooBar' must be kebab-case.",
+        "Custom event name 'barBaz' must be kebab-case.",
+        "Custom event name 'bazQux' must be kebab-case."
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <input
+          @click="$emit?.('fooBar')">
+      </template>
+      <script>
+      export default {
+        setup(props, context) {
+          return {
+            onInput(value) {
+              (context?.emit)?.('barBaz')
+            }
+          }
+        },
+        methods: {
+          onClick() {
+            (this?.$emit)?.('bazQux')
+          }
+        }
+      }
+      </script>
+      `,
+      errors: [
+        "Custom event name 'fooBar' must be kebab-case.",
+        "Custom event name 'barBaz' must be kebab-case.",
+        "Custom event name 'bazQux' must be kebab-case."
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/no-async-in-computed-properties.js b/tests/lib/rules/no-async-in-computed-properties.js
index de676909..8ea81b5c 100644
--- a/tests/lib/rules/no-async-in-computed-properties.js
+++ b/tests/lib/rules/no-async-in-computed-properties.js
@@ -12,7 +12,7 @@ const rule = require('../../../lib/rules/no-async-in-computed-properties')
 const RuleTester = require('eslint').RuleTester
 
 const parserOptions = {
-  ecmaVersion: 2018,
+  ecmaVersion: 2020,
   sourceType: 'module'
 }
 
@@ -302,6 +302,34 @@ ruleTester.run('no-async-in-computed-properties', rule, {
         }
       ]
     },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            foo: function () {
+              return bar?.then?.(response => {})
+            }
+          }
+        }
+      `,
+      parserOptions,
+      errors: ['Unexpected asynchronous action in "foo" computed property.']
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            foo: function () {
+              return (bar?.then)?.(response => {})
+            }
+          }
+        }
+      `,
+      parserOptions,
+      errors: ['Unexpected asynchronous action in "foo" computed property.']
+    },
     {
       filename: 'test.vue',
       code: `
@@ -551,6 +579,66 @@ ruleTester.run('no-async-in-computed-properties', rule, {
           line: 12
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      export default {
+        computed: {
+          foo: function () {
+            setTimeout?.(() => { }, 0)
+            window?.setTimeout?.(() => { }, 0)
+            setInterval(() => { }, 0)
+            window?.setInterval?.(() => { }, 0)
+            setImmediate?.(() => { })
+            window?.setImmediate?.(() => { })
+            requestAnimationFrame?.(() => {})
+            window?.requestAnimationFrame?.(() => {})
+          }
+        }
+      }
+      `,
+      parserOptions,
+      errors: [
+        'Unexpected timed function in "foo" computed property.',
+        'Unexpected timed function in "foo" computed property.',
+        'Unexpected timed function in "foo" computed property.',
+        'Unexpected timed function in "foo" computed property.',
+        'Unexpected timed function in "foo" computed property.',
+        'Unexpected timed function in "foo" computed property.',
+        'Unexpected timed function in "foo" computed property.',
+        'Unexpected timed function in "foo" computed property.'
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      export default {
+        computed: {
+          foo: function () {
+            setTimeout?.(() => { }, 0)
+            ;(window?.setTimeout)?.(() => { }, 0)
+            setInterval(() => { }, 0)
+            ;(window?.setInterval)?.(() => { }, 0)
+            setImmediate?.(() => { })
+            ;(window?.setImmediate)?.(() => { })
+            requestAnimationFrame?.(() => {})
+            ;(window?.requestAnimationFrame)?.(() => {})
+          }
+        }
+      }
+      `,
+      parserOptions,
+      errors: [
+        'Unexpected timed function in "foo" computed property.',
+        'Unexpected timed function in "foo" computed property.',
+        'Unexpected timed function in "foo" computed property.',
+        'Unexpected timed function in "foo" computed property.',
+        'Unexpected timed function in "foo" computed property.',
+        'Unexpected timed function in "foo" computed property.',
+        'Unexpected timed function in "foo" computed property.',
+        'Unexpected timed function in "foo" computed property.'
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/no-deprecated-dollar-listeners-api.js b/tests/lib/rules/no-deprecated-dollar-listeners-api.js
index deaff075..8644d13a 100644
--- a/tests/lib/rules/no-deprecated-dollar-listeners-api.js
+++ b/tests/lib/rules/no-deprecated-dollar-listeners-api.js
@@ -18,7 +18,7 @@ const RuleTester = require('eslint').RuleTester
 
 const ruleTester = new RuleTester({
   parser: require.resolve('vue-eslint-parser'),
-  parserOptions: { ecmaVersion: 2018, sourceType: 'module' }
+  parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
 })
 ruleTester.run('no-deprecated-dollar-listeners-api', rule, {
   valid: [
@@ -240,6 +240,30 @@ ruleTester.run('no-deprecated-dollar-listeners-api', rule, {
           messageId: 'deprecated'
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+          computed: {
+            foo () {
+              const vm = this
+              const a = vm?.$listeners
+              const b = this?.$listeners
+            }
+          }
+        }
+        </script>
+      `,
+      errors: [
+        {
+          messageId: 'deprecated'
+        },
+        {
+          messageId: 'deprecated'
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/no-deprecated-dollar-scopedslots-api.js b/tests/lib/rules/no-deprecated-dollar-scopedslots-api.js
index 303e5de9..a29211a2 100644
--- a/tests/lib/rules/no-deprecated-dollar-scopedslots-api.js
+++ b/tests/lib/rules/no-deprecated-dollar-scopedslots-api.js
@@ -18,7 +18,7 @@ const RuleTester = require('eslint').RuleTester
 
 const ruleTester = new RuleTester({
   parser: require.resolve('vue-eslint-parser'),
-  parserOptions: { ecmaVersion: 2018, sourceType: 'module' }
+  parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
 })
 ruleTester.run('no-deprecated-dollar-scopedslots-api', rule, {
   valid: [
@@ -283,6 +283,41 @@ ruleTester.run('no-deprecated-dollar-scopedslots-api', rule, {
           messageId: 'deprecated'
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+          render () {
+            const vm = this
+            const a = vm?.$scopedSlots
+            const b = this?.$scopedSlots
+            return a.foo('bar')
+          }
+        }
+        </script>
+      `,
+      output: `
+        <script>
+        export default {
+          render () {
+            const vm = this
+            const a = vm?.$slots
+            const b = this?.$slots
+            return a.foo('bar')
+          }
+        }
+        </script>
+      `,
+      errors: [
+        {
+          messageId: 'deprecated'
+        },
+        {
+          messageId: 'deprecated'
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/no-deprecated-events-api.js b/tests/lib/rules/no-deprecated-events-api.js
index b52651af..3022690b 100644
--- a/tests/lib/rules/no-deprecated-events-api.js
+++ b/tests/lib/rules/no-deprecated-events-api.js
@@ -13,7 +13,7 @@ const rule = require('../../../lib/rules/no-deprecated-events-api')
 const RuleTester = require('eslint').RuleTester
 
 const parserOptions = {
-  ecmaVersion: 2018,
+  ecmaVersion: 2020,
   sourceType: 'module'
 }
 
@@ -113,6 +113,20 @@ ruleTester.run('no-deprecated-events-api', rule, {
         }
       `,
       parserOptions
+    },
+    {
+      filename: 'test.js',
+      code: `
+        app.component('some-comp', {
+          mounted () {
+            // It is OK because checking whether it is deprecated.
+            this.$on?.('start', foo)
+            this.$off?.('start', foo)
+            this.$once?.('start', foo)
+          }
+        })
+      `,
+      parserOptions
     }
   ],
 
@@ -195,6 +209,42 @@ ruleTester.run('no-deprecated-events-api', rule, {
           line: 5
         }
       ]
+    },
+    {
+      filename: 'test.js',
+      code: `
+        app.component('some-comp', {
+          mounted () {
+            this?.$on('start')
+            this?.$off('start')
+            this?.$once('start')
+          }
+        })
+      `,
+      parserOptions,
+      errors: [
+        'The Events api `$on`, `$off` `$once` is deprecated. Using external library instead, for example mitt.',
+        'The Events api `$on`, `$off` `$once` is deprecated. Using external library instead, for example mitt.',
+        'The Events api `$on`, `$off` `$once` is deprecated. Using external library instead, for example mitt.'
+      ]
+    },
+    {
+      filename: 'test.js',
+      code: `
+        app.component('some-comp', {
+          mounted () {
+            ;(this?.$on)('start')
+            ;(this?.$off)('start')
+            ;(this?.$once)('start')
+          }
+        })
+      `,
+      parserOptions,
+      errors: [
+        'The Events api `$on`, `$off` `$once` is deprecated. Using external library instead, for example mitt.',
+        'The Events api `$on`, `$off` `$once` is deprecated. Using external library instead, for example mitt.',
+        'The Events api `$on`, `$off` `$once` is deprecated. Using external library instead, for example mitt.'
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/no-deprecated-vue-config-keycodes.js b/tests/lib/rules/no-deprecated-vue-config-keycodes.js
index eebd7943..dae0ec80 100644
--- a/tests/lib/rules/no-deprecated-vue-config-keycodes.js
+++ b/tests/lib/rules/no-deprecated-vue-config-keycodes.js
@@ -17,7 +17,7 @@ const RuleTester = require('eslint').RuleTester
 
 const ruleTester = new RuleTester({
   parser: require.resolve('vue-eslint-parser'),
-  parserOptions: { ecmaVersion: 2015 }
+  parserOptions: { ecmaVersion: 2020 }
 })
 
 ruleTester.run('no-deprecated-vue-config-keycodes', rule, {
@@ -51,6 +51,16 @@ ruleTester.run('no-deprecated-vue-config-keycodes', rule, {
           endColumn: 20
         }
       ]
+    },
+    {
+      filename: 'test.js',
+      code: 'Vue?.config?.keyCodes',
+      errors: ['`Vue.config.keyCodes` are deprecated.']
+    },
+    {
+      filename: 'test.js',
+      code: '(Vue?.config)?.keyCodes',
+      errors: ['`Vue.config.keyCodes` are deprecated.']
     }
   ]
 })
diff --git a/tests/lib/rules/no-lifecycle-after-await.js b/tests/lib/rules/no-lifecycle-after-await.js
index dd166079..17dfada4 100644
--- a/tests/lib/rules/no-lifecycle-after-await.js
+++ b/tests/lib/rules/no-lifecycle-after-await.js
@@ -8,7 +8,7 @@ const rule = require('../../../lib/rules/no-lifecycle-after-await')
 
 const tester = new RuleTester({
   parser: require.resolve('vue-eslint-parser'),
-  parserOptions: { ecmaVersion: 2019, sourceType: 'module' }
+  parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
 })
 
 tester.run('no-lifecycle-after-await', rule, {
@@ -204,6 +204,26 @@ tester.run('no-lifecycle-after-await', rule, {
           line: 18
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import {onMounted} from 'vue'
+      export default {
+        async setup() {
+          await doSomething()
+
+          onMounted?.(() => { /* ... */ }) // error
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'forbidden'
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/no-multiple-slot-args.js b/tests/lib/rules/no-multiple-slot-args.js
index fb42767b..0720a951 100644
--- a/tests/lib/rules/no-multiple-slot-args.js
+++ b/tests/lib/rules/no-multiple-slot-args.js
@@ -18,7 +18,7 @@ const RuleTester = require('eslint').RuleTester
 
 const ruleTester = new RuleTester({
   parser: require.resolve('vue-eslint-parser'),
-  parserOptions: { ecmaVersion: 2018, sourceType: 'module' }
+  parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
 })
 ruleTester.run('no-multiple-slot-args', rule, {
   valid: [
@@ -109,6 +109,90 @@ ruleTester.run('no-multiple-slot-args', rule, {
         }
       ]
     },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        render (h) {
+          this?.$scopedSlots?.default?.(foo, bar)
+          this?.$scopedSlots?.foo?.(foo, bar)
+          const vm = this
+          vm?.$scopedSlots?.default?.(foo, bar)
+          vm?.$scopedSlots?.foo?.(foo, bar)
+        }
+      }
+      </script>
+      `,
+      errors: [
+        'Unexpected multiple arguments.',
+        'Unexpected multiple arguments.',
+        'Unexpected multiple arguments.',
+        'Unexpected multiple arguments.'
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        render (h) {
+          this.$scopedSlots.default?.(foo, bar)
+          this.$scopedSlots.foo?.(foo, bar)
+          const vm = this
+          vm.$scopedSlots.default?.(foo, bar)
+          vm.$scopedSlots.foo?.(foo, bar)
+        }
+      }
+      </script>
+      `,
+      errors: [
+        'Unexpected multiple arguments.',
+        'Unexpected multiple arguments.',
+        'Unexpected multiple arguments.',
+        'Unexpected multiple arguments.'
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        render (h) {
+          ;(this?.$scopedSlots)?.default?.(foo, bar)
+          ;(this?.$scopedSlots?.foo)?.(foo, bar)
+          const vm = this
+          ;(vm?.$scopedSlots)?.default?.(foo, bar)
+          ;(vm?.$scopedSlots?.foo)?.(foo, bar)
+        }
+      }
+      </script>
+      `,
+      errors: [
+        'Unexpected multiple arguments.',
+        'Unexpected multiple arguments.'
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        render (h) {
+          ;(this?.$scopedSlots).default(foo, bar)
+          ;(this?.$scopedSlots?.foo)(foo, bar)
+          const vm = this
+          ;(vm?.$scopedSlots).default(foo, bar)
+          ;(vm?.$scopedSlots?.foo)(foo, bar)
+        }
+      }
+      </script>
+      `,
+      errors: [
+        'Unexpected multiple arguments.',
+        'Unexpected multiple arguments.'
+      ]
+    },
     {
       filename: 'test.vue',
       code: `
diff --git a/tests/lib/rules/no-mutating-props.js b/tests/lib/rules/no-mutating-props.js
index 1e9e0973..e46ae0fd 100644
--- a/tests/lib/rules/no-mutating-props.js
+++ b/tests/lib/rules/no-mutating-props.js
@@ -343,6 +343,52 @@ ruleTester.run('no-mutating-props', rule, {
         }
       ]
     },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div>
+            <div v-text="prop1?.shift?.()"></div>
+            <div v-text="prop2?.slice?.(0)?.shift?.()"></div>
+            <div v-if="this?.prop3"></div>
+            <div v-if="this?.prop4 < 10"></div>
+            <div v-text="this?.prop5?.shift?.()"></div>
+            <div v-text="this?.prop6?.slice?.(0)?.shift?.()"></div>
+          </div>
+        </template>
+        <script>
+          export default {
+            props: ['prop1', 'prop2', 'prop3', 'prop4', 'prop5', 'prop6']
+          }
+        </script>
+      `,
+      errors: [
+        'Unexpected mutation of "prop1" prop.',
+        'Unexpected mutation of "prop5" prop.'
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div>
+            <div v-text="(prop1?.shift)?.()"></div>
+            <div v-text="(this?.prop2)?.shift?.()"></div>
+            <div v-text="(this?.prop3?.shift)?.()"></div>
+          </div>
+        </template>
+        <script>
+          export default {
+            props: ['prop1', 'prop2', 'prop3']
+          }
+        </script>
+      `,
+      errors: [
+        'Unexpected mutation of "prop1" prop.',
+        'Unexpected mutation of "prop2" prop.',
+        'Unexpected mutation of "prop3" prop.'
+      ]
+    },
     {
       filename: 'test.vue',
       code: `
@@ -419,6 +465,28 @@ ruleTester.run('no-mutating-props', rule, {
         }
       ]
     },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+          export default {
+            props: ['foo', 'bar', 'baz'],
+            methods: {
+              openModal() {
+                this?.foo?.push?.('something')
+                ;(this?.bar)?.push?.('something')
+                ;(this?.baz?.push)?.('something')
+              }
+            }
+          }
+        </script>
+      `,
+      errors: [
+        'Unexpected mutation of "foo" prop.',
+        'Unexpected mutation of "bar" prop.',
+        'Unexpected mutation of "baz" prop.'
+      ]
+    },
     {
       filename: 'test.vue',
       code: `
diff --git a/tests/lib/rules/no-ref-as-operand.js b/tests/lib/rules/no-ref-as-operand.js
index ca553e35..50cfd324 100644
--- a/tests/lib/rules/no-ref-as-operand.js
+++ b/tests/lib/rules/no-ref-as-operand.js
@@ -8,7 +8,7 @@ const rule = require('../../../lib/rules/no-ref-as-operand')
 
 const tester = new RuleTester({
   parser: require.resolve('vue-eslint-parser'),
-  parserOptions: { ecmaVersion: 2019, sourceType: 'module' }
+  parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
 })
 
 tester.run('no-ref-as-operand', rule, {
@@ -492,6 +492,20 @@ tester.run('no-ref-as-operand', rule, {
           messageId: 'requireDotValue'
         }
       ]
+    },
+    {
+      code: `
+      <script>
+        import { ref } from 'vue'
+        const foo = ref(123)
+        const bar = foo?.bar
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'requireDotValue'
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/no-setup-props-destructure.js b/tests/lib/rules/no-setup-props-destructure.js
index c0e35df0..781a3ce0 100644
--- a/tests/lib/rules/no-setup-props-destructure.js
+++ b/tests/lib/rules/no-setup-props-destructure.js
@@ -8,7 +8,7 @@ const rule = require('../../../lib/rules/no-setup-props-destructure')
 
 const tester = new RuleTester({
   parser: require.resolve('vue-eslint-parser'),
-  parserOptions: { ecmaVersion: 2019, sourceType: 'module' }
+  parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
 })
 
 tester.run('no-setup-props-destructure', rule, {
@@ -342,6 +342,38 @@ tester.run('no-setup-props-destructure', rule, {
         }
       ]
     },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup(p) {
+          const x = p.foo
+          const y = p?.bar
+          const z = (p?.baz).qux
+
+          const xc = p?.foo?.()
+          const yc = (p?.bar)?.()
+          const zc = (p?.baz.qux)?.()
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'getProperty',
+          line: 5
+        },
+        {
+          messageId: 'getProperty',
+          line: 6
+        },
+        {
+          messageId: 'getProperty',
+          line: 7
+        }
+      ]
+    },
     {
       filename: 'test.vue',
       code: `
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 40caaf0c..5aefc95c 100644
--- a/tests/lib/rules/no-side-effects-in-computed-properties.js
+++ b/tests/lib/rules/no-side-effects-in-computed-properties.js
@@ -12,7 +12,7 @@ const rule = require('../../../lib/rules/no-side-effects-in-computed-properties'
 const RuleTester = require('eslint').RuleTester
 
 const parserOptions = {
-  ecmaVersion: 2018,
+  ecmaVersion: 2020,
   sourceType: 'module'
 }
 
@@ -338,6 +338,27 @@ ruleTester.run('no-side-effects-in-computed-properties', rule, {
           message: 'Unexpected side effect in "test1" computed property.'
         }
       ]
+    },
+    {
+      code: `Vue.component('test', {
+        computed: {
+          test1() {
+            return this?.something?.reverse?.()
+          },
+          test2() {
+            return (this?.something)?.reverse?.()
+          },
+          test3() {
+            return (this?.something?.reverse)?.()
+          },
+        }
+      })`,
+      parserOptions,
+      errors: [
+        'Unexpected side effect in "test1" computed property.',
+        'Unexpected side effect in "test2" computed property.',
+        'Unexpected side effect in "test3" computed property.'
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/no-unused-properties.js b/tests/lib/rules/no-unused-properties.js
index 355fb8df..4319d14f 100644
--- a/tests/lib/rules/no-unused-properties.js
+++ b/tests/lib/rules/no-unused-properties.js
@@ -995,6 +995,41 @@ tester.run('no-unused-properties', rule, {
             props: [, 'count']
           }
         </script>
+        `
+    },
+    // optional chaining
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+        import { ref, onMounted } from 'vue'
+
+        export default {
+          props: ['foo', 'bar'],
+          methods: {
+            fn () {
+              fn(this)
+            }
+          }
+        }
+
+        function fn(a) {
+          return a?.foo + a?.bar
+        }
+      </script>`
+    },
+    {
+      filename: 'test.js',
+      code: `
+      Vue.component('MyButton', {
+        functional: true,
+        props: ['foo', 'bar'],
+        render: function (createElement, ctx) {
+          const a = ctx
+          const b = a?.props?.foo
+          const c = (a?.props)?.bar
+        }
+      })
       `
     }
   ],
diff --git a/tests/lib/rules/no-watch-after-await.js b/tests/lib/rules/no-watch-after-await.js
index 9bc7c04e..c7cf7315 100644
--- a/tests/lib/rules/no-watch-after-await.js
+++ b/tests/lib/rules/no-watch-after-await.js
@@ -8,7 +8,7 @@ const rule = require('../../../lib/rules/no-watch-after-await')
 
 const tester = new RuleTester({
   parser: require.resolve('vue-eslint-parser'),
-  parserOptions: { ecmaVersion: 2019, sourceType: 'module' }
+  parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
 })
 
 tester.run('no-watch-after-await', rule, {
@@ -97,6 +97,27 @@ tester.run('no-watch-after-await', rule, {
       Vue.component('test', {
         el: foo()
       })`
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      import {watch, watchEffect} from 'vue'
+      export default {
+        async setup() {
+          await doSomething()
+          const a = watchEffect?.(() => { /* ... */ })
+          const b = watch?.(foo, () => { /* ... */ })
+          c = watch?.()
+          d(watch?.())
+          e = {
+            foo: watch?.()
+          }
+          f = [watch?.()]
+        }
+      }
+      </script>
+      `
     }
   ],
   invalid: [
diff --git a/tests/lib/rules/order-in-components.js b/tests/lib/rules/order-in-components.js
index 5c99608f..76d51096 100644
--- a/tests/lib/rules/order-in-components.js
+++ b/tests/lib/rules/order-in-components.js
@@ -879,6 +879,7 @@ ruleTester.run('order-in-components', rule, {
           testYield: function* () {},
           testTemplate: \`a:\${a},b:\${b},c:\${c}.\`,
           testNullish: a ?? b,
+          testOptionalChaining: a?.b?.c,
           name: 'burger',
         };
       `,
@@ -897,13 +898,14 @@ ruleTester.run('order-in-components', rule, {
           testYield: function* () {},
           testTemplate: \`a:\${a},b:\${b},c:\${c}.\`,
           testNullish: a ?? b,
+          testOptionalChaining: a?.b?.c,
         };
       `,
       errors: [
         {
           message:
             'The "name" property should be above the "data" property on line 3.',
-          line: 14
+          line: 15
         }
       ]
     }
diff --git a/tests/lib/rules/require-default-prop.js b/tests/lib/rules/require-default-prop.js
index cdc0f417..dcd55156 100644
--- a/tests/lib/rules/require-default-prop.js
+++ b/tests/lib/rules/require-default-prop.js
@@ -11,7 +11,7 @@
 const rule = require('../../../lib/rules/require-default-prop')
 const RuleTester = require('eslint').RuleTester
 const parserOptions = {
-  ecmaVersion: 2018,
+  ecmaVersion: 2020,
   sourceType: 'module'
 }
 
@@ -148,7 +148,8 @@ ruleTester.run('require-default-prop', rule, {
           props: {
             bar,
             baz: prop,
-            bar1: foo()
+            baz1: prop.foo,
+            bar2: foo()
           }
         }
       `
@@ -285,7 +286,7 @@ ruleTester.run('require-default-prop', rule, {
       ]
     },
 
-    // computed propertys
+    // computed properties
     {
       filename: 'test.vue',
       code: `
@@ -370,6 +371,22 @@ ruleTester.run('require-default-prop', rule, {
         }
       `,
       errors: ["Prop 'foo' requires default value to be set."]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: {
+            bar,
+            baz: prop?.foo,
+            bar1: foo?.(),
+          }
+        }
+      `,
+      errors: [
+        "Prop 'baz' requires default value to be set.",
+        "Prop 'bar1' requires default value to be set."
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/require-explicit-emits.js b/tests/lib/rules/require-explicit-emits.js
index cef95dd7..d59d7860 100644
--- a/tests/lib/rules/require-explicit-emits.js
+++ b/tests/lib/rules/require-explicit-emits.js
@@ -10,7 +10,7 @@ const rule = require('../../../lib/rules/require-explicit-emits')
 const tester = new RuleTester({
   parser: require.resolve('vue-eslint-parser'),
   parserOptions: {
-    ecmaVersion: 2019,
+    ecmaVersion: 2020,
     sourceType: 'module'
   }
 })
@@ -1514,6 +1514,43 @@ emits: {'foo': null}
           ]
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        methods: {
+          click () {
+            const vm = this
+            vm?.$emit?.('foo')
+            ;(vm?.$emit)?.('bar')
+          }
+        }
+      }
+      </script>
+      `,
+      errors: [
+        'The "foo" event has been triggered but not declared on `emits` option.',
+        'The "bar" event has been triggered but not declared on `emits` option.'
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup(p, c) {
+          c?.emit?.('foo')
+          ;(c?.emit)?.('bar')
+        }
+      }
+      </script>
+      `,
+      errors: [
+        'The "foo" event has been triggered but not declared on `emits` option.',
+        'The "bar" event has been triggered but not declared on `emits` option.'
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/require-slots-as-functions.js b/tests/lib/rules/require-slots-as-functions.js
index 9d79a1ec..ea83b569 100644
--- a/tests/lib/rules/require-slots-as-functions.js
+++ b/tests/lib/rules/require-slots-as-functions.js
@@ -18,7 +18,7 @@ const RuleTester = require('eslint').RuleTester
 
 const ruleTester = new RuleTester({
   parser: require.resolve('vue-eslint-parser'),
-  parserOptions: { ecmaVersion: 2018, sourceType: 'module' }
+  parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
 })
 ruleTester.run('require-slots-as-functions', rule, {
   valid: [
@@ -86,29 +86,27 @@ ruleTester.run('require-slots-as-functions', rule, {
         }
       ]
     },
-
     {
       filename: 'test.vue',
       code: `
       <script>
       export default {
         render (h) {
-          let children
-
-          const [node] = this.$slots.foo
-          const bar = [this.$slots[foo]]
-
-          children = this.$slots.foo
-
-          return h('div', children.filter(test))
+          var bar = this.$slots.foo?.bar // NG
+          var bar = this.$slots.foo?.() // OK
+          var bar = (this.$slots?.foo)?.bar // NG
+          var bar = (this.$slots?.foo)?.() // OK
+          var bar = (this?.$slots)?.foo?.bar // NG
+          var bar = (this?.$slots)?.foo?.() // OK
+          return h('div', bar)
         }
       }
       </script>
       `,
       errors: [
-        'Property in `$slots` should be used as function.',
-        'Property in `$slots` should be used as function.',
-        'Property in `$slots` should be used as function.'
+        { messageId: 'unexpected', line: 5 },
+        { messageId: 'unexpected', line: 7 },
+        { messageId: 'unexpected', line: 9 }
       ]
     }
   ]
diff --git a/tests/lib/rules/require-valid-default-prop.js b/tests/lib/rules/require-valid-default-prop.js
index 2617e146..58adccad 100644
--- a/tests/lib/rules/require-valid-default-prop.js
+++ b/tests/lib/rules/require-valid-default-prop.js
@@ -195,6 +195,18 @@ ruleTester.run('require-valid-default-prop', rule, {
         }
       }`,
       parserOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `export default {
+        props: {
+          foo: {
+            type: Number,
+            default: Number?.()
+          }
+        }
+      }`,
+      parserOptions
     }
   ],
 
@@ -756,6 +768,19 @@ ruleTester.run('require-valid-default-prop', rule, {
           line: 11
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `export default {
+        props: {
+          foo: {
+            type: String,
+            default: Number?.()
+          }
+        }
+      }`,
+      parserOptions,
+      errors: errorMessage('string')
     }
   ]
 })
diff --git a/tests/lib/rules/this-in-template.js b/tests/lib/rules/this-in-template.js
index 2d7bd46c..5f36b570 100644
--- a/tests/lib/rules/this-in-template.js
+++ b/tests/lib/rules/this-in-template.js
@@ -18,7 +18,7 @@ const RuleTester = require('eslint').RuleTester
 
 const ruleTester = new RuleTester({
   parser: require.resolve('vue-eslint-parser'),
-  parserOptions: { ecmaVersion: 2015 }
+  parserOptions: { ecmaVersion: 2020 }
 })
 
 function createValidTests(prefix, options) {
@@ -188,7 +188,8 @@ ruleTester.run('this-in-template', rule, {
   valid: ['', '<template></template>', '<template><div></div></template>']
     .concat(createValidTests('', []))
     .concat(createValidTests('', ['never']))
-    .concat(createValidTests('this.', ['always'])),
+    .concat(createValidTests('this.', ['always']))
+    .concat(createValidTests('this?.', ['always'])),
   invalid: []
     .concat(
       createInvalidTests(
@@ -196,6 +197,12 @@ ruleTester.run('this-in-template', rule, {
         [],
         "Unexpected usage of 'this'.",
         'ThisExpression'
+      ),
+      createInvalidTests(
+        'this?.',
+        [],
+        "Unexpected usage of 'this'.",
+        'ThisExpression'
       )
     )
     .concat(
@@ -204,6 +211,12 @@ ruleTester.run('this-in-template', rule, {
         ['never'],
         "Unexpected usage of 'this'.",
         'ThisExpression'
+      ),
+      createInvalidTests(
+        'this?.',
+        ['never'],
+        "Unexpected usage of 'this'.",
+        'ThisExpression'
       )
     )
     .concat(
diff --git a/tests/lib/rules/v-on-function-call.js b/tests/lib/rules/v-on-function-call.js
index b78a9237..4ba6f5b0 100644
--- a/tests/lib/rules/v-on-function-call.js
+++ b/tests/lib/rules/v-on-function-call.js
@@ -16,7 +16,7 @@ const rule = require('../../../lib/rules/v-on-function-call')
 
 const tester = new RuleTester({
   parser: require.resolve('vue-eslint-parser'),
-  parserOptions: { ecmaVersion: 2015 }
+  parserOptions: { ecmaVersion: 2020 }
 })
 
 tester.run('v-on-function-call', rule, {
@@ -106,6 +106,11 @@ tester.run('v-on-function-call', rule, {
         <div @click="fn() /* comment */"></div>
       </template>`,
       options: ['never', { ignoreIncludesComment: true }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div @click="foo?.()"></div></template>',
+      options: ['never']
     }
   ],
   invalid: [
diff --git a/tests/lib/rules/valid-v-bind-sync.js b/tests/lib/rules/valid-v-bind-sync.js
index ea8bdf33..dfaf161e 100644
--- a/tests/lib/rules/valid-v-bind-sync.js
+++ b/tests/lib/rules/valid-v-bind-sync.js
@@ -16,7 +16,7 @@ const rule = require('../../../lib/rules/valid-v-bind-sync')
 
 const tester = new RuleTester({
   parser: require.resolve('vue-eslint-parser'),
-  parserOptions: { ecmaVersion: 2015 }
+  parserOptions: { ecmaVersion: 2020 }
 })
 
 tester.run('valid-v-bind-sync', rule, {
@@ -350,6 +350,42 @@ tester.run('valid-v-bind-sync', rule, {
       errors: [
         "'.sync' modifiers require the attribute value which is valid as LHS."
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><MyComponent :foo.sync="foo?.bar" /></template>',
+      errors: [
+        "Optional chaining cannot appear in 'v-bind' with '.sync' modifiers."
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><MyComponent :foo.sync="foo?.bar.baz" /></template>',
+      errors: [
+        "Optional chaining cannot appear in 'v-bind' with '.sync' modifiers."
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><MyComponent :foo.sync="(a?.b)?.c" /></template>',
+      errors: [
+        "Optional chaining cannot appear in 'v-bind' with '.sync' modifiers."
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><MyComponent :foo.sync="(a?.b).c" /></template>',
+      errors: ["'.sync' modifier has potential null object property access."]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><MyComponent :foo.sync="(null).foo" /></template>',
+      errors: ["'.sync' modifier has potential null object property access."]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><MyComponent :foo.sync="(a?.b).c.d" /></template>',
+      errors: ["'.sync' modifier has potential null object property access."]
     }
   ]
 })
diff --git a/tests/lib/rules/valid-v-model.js b/tests/lib/rules/valid-v-model.js
index 9e78ef47..2369fe4d 100644
--- a/tests/lib/rules/valid-v-model.js
+++ b/tests/lib/rules/valid-v-model.js
@@ -18,7 +18,7 @@ const rule = require('../../../lib/rules/valid-v-model')
 
 const tester = new RuleTester({
   parser: require.resolve('vue-eslint-parser'),
-  parserOptions: { ecmaVersion: 2015 }
+  parserOptions: { ecmaVersion: 2020 }
 })
 
 tester.run('valid-v-model', rule, {
@@ -232,6 +232,36 @@ tester.run('valid-v-model', rule, {
       filename: 'empty-value.vue',
       code: '<template><MyComponent v-model="" /></template>',
       errors: ["'v-model' directives require that attribute value."]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><input v-model="foo?.bar"></template>',
+      errors: ["Optional chaining cannot appear in 'v-model' directives."]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><input v-model="foo?.bar.baz"></template>',
+      errors: ["Optional chaining cannot appear in 'v-model' directives."]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><input v-model="(a?.b)?.c"></template>',
+      errors: ["Optional chaining cannot appear in 'v-model' directives."]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><input v-model="(a?.b).c"></template>',
+      errors: ["'v-model' directive has potential null object property access."]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><input v-model="(null).foo"></template>',
+      errors: ["'v-model' directive has potential null object property access."]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><input v-model="(a?.b).c.d"></template>',
+      errors: ["'v-model' directive has potential null object property access."]
     }
   ]
 })
diff --git a/tests/lib/utils/index.js b/tests/lib/utils/index.js
index d1b8c076..fe5bf77f 100644
--- a/tests/lib/utils/index.js
+++ b/tests/lib/utils/index.js
@@ -1,44 +1,19 @@
 'use strict'
 
 const babelEslint = require('babel-eslint')
+const espree = require('espree')
 const utils = require('../../../lib/utils/index')
 const chai = require('chai')
 
 const assert = chai.assert
 
-describe('parseMemberExpression', () => {
-  let node
-
-  const parse = function (code) {
-    return babelEslint.parse(code).body[0].expression
-  }
-
-  it('should parse member expression', () => {
-    node = parse('this.some.nested.property')
-    assert.deepEqual(utils.parseMemberExpression(node), [
-      'this',
-      'some',
-      'nested',
-      'property'
-    ])
-
-    node = parse('another.property')
-    assert.deepEqual(utils.parseMemberExpression(node), ['another', 'property'])
-
-    node = parse('this.something')
-    assert.deepEqual(utils.parseMemberExpression(node), ['this', 'something'])
-  })
-})
-
 describe('getComputedProperties', () => {
-  let node
-
   const parse = function (code) {
     return babelEslint.parse(code).body[0].declarations[0].init
   }
 
   it('should return empty array when there is no computed property', () => {
-    node = parse(`const test = {
+    const node = parse(`const test = {
       name: 'test',
       data() {
         return {}
@@ -49,7 +24,7 @@ describe('getComputedProperties', () => {
   })
 
   it('should return computed properties', () => {
-    node = parse(`const test = {
+    const node = parse(`const test = {
       name: 'test',
       data() {
         return {}
@@ -93,7 +68,7 @@ describe('getComputedProperties', () => {
   })
 
   it('should not collide with object spread operator', () => {
-    node = parse(`const test = {
+    const node = parse(`const test = {
       name: 'test',
       computed: {
         ...mapGetters(['test']),
@@ -115,7 +90,7 @@ describe('getComputedProperties', () => {
   })
 
   it('should not collide with object spread operator inside CP', () => {
-    node = parse(`const test = {
+    const node = parse(`const test = {
       name: 'test',
       computed: {
         foo: {
@@ -138,83 +113,175 @@ describe('getComputedProperties', () => {
 })
 
 describe('getStaticPropertyName', () => {
-  let node
-
   const parse = function (code) {
     return babelEslint.parse(code).body[0].declarations[0].init
   }
 
   it('should parse property expression with identifier', () => {
-    node = parse(`const test = { computed: { } }`)
+    const node = parse(`const test = { computed: { } }`)
 
     const parsed = utils.getStaticPropertyName(node.properties[0])
     assert.ok(parsed === 'computed')
   })
   it('should parse property expression with literal', () => {
-    node = parse(`const test = { ['computed'] () {} }`)
+    const node = parse(`const test = { ['computed'] () {} }`)
 
     const parsed = utils.getStaticPropertyName(node.properties[0])
     assert.ok(parsed === 'computed')
   })
   it('should parse property expression with template literal', () => {
-    node = parse(`const test = { [\`computed\`] () {} }`)
+    const node = parse(`const test = { [\`computed\`] () {} }`)
 
     const parsed = utils.getStaticPropertyName(node.properties[0])
     assert.ok(parsed === 'computed')
   })
-  // it('should parse identifier', () => {
-  //   node = parse(`const test = { computed: { } }`)
+})
+
+describe('getStringLiteralValue', () => {
+  const parse = function (code) {
+    return babelEslint.parse(code).body[0].declarations[0].init
+  }
 
-  //   const parsed = utils.getStaticPropertyName(node.properties[0].key)
-  //   assert.ok(parsed === 'computed')
-  // })
   it('should parse literal', () => {
-    node = parse(`const test = { ['computed'] () {} }`)
+    const node = parse(`const test = { ['computed'] () {} }`)
 
     const parsed = utils.getStringLiteralValue(node.properties[0].key)
     assert.ok(parsed === 'computed')
   })
   it('should parse template literal', () => {
-    node = parse(`const test = { [\`computed\`] () {} }`)
+    const node = parse(`const test = { [\`computed\`] () {} }`)
 
     const parsed = utils.getStringLiteralValue(node.properties[0].key)
     assert.ok(parsed === 'computed')
   })
 })
 
-describe('parseMemberOrCallExpression', () => {
-  let node
-
+describe('getMemberChaining', () => {
   const parse = function (code) {
-    return babelEslint.parse(code).body[0].declarations[0].init
+    return espree.parse(code, { ecmaVersion: 2020 }).body[0].declarations[0]
+      .init
   }
 
-  it('should parse CallExpression', () => {
-    node = parse(
-      `const test = this.lorem['ipsum'].map(d => d.id).filter((a, b) => a > b).reduce((acc, d) => acc + d, 0)`
+  const jsonIgnoreKeys = ['expression', 'object']
+
+  it('should parse MemberExpression', () => {
+    const node = parse(`const test = this.lorem['ipsum'].foo.bar`)
+    const parsed = utils.getMemberChaining(node)
+    assert.equal(
+      nodeToJson(parsed, jsonIgnoreKeys),
+      nodeToJson([
+        {
+          type: 'ThisExpression'
+        },
+        {
+          type: 'MemberExpression',
+          property: {
+            type: 'Identifier',
+            name: 'lorem'
+          },
+          computed: false,
+          optional: false
+        },
+        {
+          type: 'MemberExpression',
+          property: {
+            type: 'Literal',
+            value: 'ipsum',
+            raw: "'ipsum'"
+          },
+          computed: true,
+          optional: false
+        },
+        {
+          type: 'MemberExpression',
+          property: {
+            type: 'Identifier',
+            name: 'foo'
+          },
+          computed: false,
+          optional: false
+        },
+        {
+          type: 'MemberExpression',
+          property: {
+            type: 'Identifier',
+            name: 'bar'
+          },
+          computed: false,
+          optional: false
+        }
+      ])
     )
-    const parsed = utils.parseMemberOrCallExpression(node)
-    assert.equal(parsed, 'this.lorem[].map().filter().reduce()')
   })
 
-  it('should parse MemberExpression', () => {
-    node = parse(
-      `const test = this.lorem['ipsum'][0].map(d => d.id).dolor.reduce((acc, d) => acc + d, 0).sit`
+  it('should parse optional Chaining ', () => {
+    const node = parse(`const test = (this?.lorem)['ipsum']?.[0]?.foo?.bar`)
+    const parsed = utils.getMemberChaining(node)
+    assert.equal(
+      nodeToJson(parsed, jsonIgnoreKeys),
+      nodeToJson([
+        {
+          type: 'ThisExpression'
+        },
+        {
+          type: 'MemberExpression',
+          property: {
+            type: 'Identifier',
+            name: 'lorem'
+          },
+          computed: false,
+          optional: true
+        },
+        {
+          type: 'MemberExpression',
+          property: {
+            type: 'Literal',
+            value: 'ipsum',
+            raw: "'ipsum'"
+          },
+          computed: true,
+          optional: false
+        },
+        {
+          type: 'MemberExpression',
+          property: {
+            type: 'Literal',
+            value: 0,
+            raw: '0'
+          },
+          computed: true,
+          optional: true
+        },
+        {
+          type: 'MemberExpression',
+          property: {
+            type: 'Identifier',
+            name: 'foo'
+          },
+          computed: false,
+          optional: true
+        },
+        {
+          type: 'MemberExpression',
+          property: {
+            type: 'Identifier',
+            name: 'bar'
+          },
+          computed: false,
+          optional: true
+        }
+      ])
     )
-    const parsed = utils.parseMemberOrCallExpression(node)
-    assert.equal(parsed, 'this.lorem[][].map().dolor.reduce().sit')
   })
 })
 
 describe('getRegisteredComponents', () => {
-  let node
-
   const parse = function (code) {
     return babelEslint.parse(code).body[0].declarations[0].init
   }
 
   it('should return empty array when there are no components registered', () => {
-    node = parse(`const test = {
+    const node = parse(`const test = {
       name: 'test',
     }`)
 
@@ -222,7 +289,7 @@ describe('getRegisteredComponents', () => {
   })
 
   it('should return an array with all registered components', () => {
-    node = parse(`const test = {
+    const node = parse(`const test = {
       name: 'test',
       components: {
         ...test,
@@ -249,7 +316,7 @@ describe('getRegisteredComponents', () => {
   })
 
   it('should return an array of only components whose names can be identified', () => {
-    node = parse(`const test = {
+    const node = parse(`const test = {
       name: 'test',
       components: {
         ...test,
@@ -269,15 +336,13 @@ describe('getRegisteredComponents', () => {
 })
 
 describe('getComponentProps', () => {
-  let props
-
   const parse = function (code) {
     const data = babelEslint.parse(code).body[0].declarations[0].init
     return utils.getComponentProps(data)
   }
 
   it('should return empty array when there is no component props', () => {
-    props = parse(`const test = {
+    const props = parse(`const test = {
       name: 'test',
       data() {
         return {}
@@ -288,7 +353,7 @@ describe('getComponentProps', () => {
   })
 
   it('should return empty array when component props is empty array', () => {
-    props = parse(`const test = {
+    const props = parse(`const test = {
       name: 'test',
       props: []
     }`)
@@ -297,7 +362,7 @@ describe('getComponentProps', () => {
   })
 
   it('should return empty array when component props is empty object', () => {
-    props = parse(`const test = {
+    const props = parse(`const test = {
       name: 'test',
       props: {}
     }`)
@@ -306,7 +371,7 @@ describe('getComponentProps', () => {
   })
 
   it('should return computed props', () => {
-    props = parse(`const test = {
+    const props = parse(`const test = {
       name: 'test',
       ...test,
       data() {
@@ -341,7 +406,7 @@ describe('getComponentProps', () => {
   })
 
   it('should return computed from array props', () => {
-    props = parse(`const test = {
+    const props = parse(`const test = {
       name: 'test',
       data() {
         return {}
@@ -382,3 +447,15 @@ describe('editdistance', () => {
     assert.equal(editDistance('computed', 'computd'), 1)
   })
 })
+function nodeToJson(nodes, ignores = []) {
+  return JSON.stringify(nodes, replacer, 2)
+  function replacer(key, value) {
+    if (key === 'parent' || key === 'start' || key === 'end') {
+      return undefined
+    }
+    if (ignores.includes(key)) {
+      return undefined
+    }
+    return value
+  }
+}