Repository: vuejs/eslint-plugin-vue
Pull request #1017 (2019-12-27 06:15:21)

Make the indent rules supports ECMAScript 2020


Patch:
diff --git a/docs/rules/html-indent.md b/docs/rules/html-indent.md
index d82ff86e..c3a45f27 100644
--- a/docs/rules/html-indent.md
+++ b/docs/rules/html-indent.md
@@ -15,7 +15,7 @@ description: enforce consistent indentation in `<template>`
 This rule enforces a consistent indentation style in `<template>`. The default style is 2 spaces.
 
 - This rule checks all tags, also all expressions in directives and mustaches.
-- In the expressions, this rule supports ECMAScript 2017 syntaxes. It ignores unknown AST nodes, but it might be confused by non-standard syntaxes.
+- In the expressions, this rule supports ECMAScript 2020 syntaxes. It ignores unknown AST nodes, but it might be confused by non-standard syntaxes.
 
 <eslint-code-block fix :rules="{'vue/html-indent': ['error']}">
 
diff --git a/lib/rules/prop-name-casing.js b/lib/rules/prop-name-casing.js
index 82619731..5ee9b78f 100644
--- a/lib/rules/prop-name-casing.js
+++ b/lib/rules/prop-name-casing.js
@@ -24,20 +24,8 @@ function create(context) {
 
   return utils.executeOnVue(context, (obj) => {
     for (const item of utils.getComponentProps(obj)) {
-      if (item.propName == null) {
-        continue
-      }
-      const propName =
-        item.key.type === 'Literal'
-          ? item.key.value
-          : item.key.type === 'TemplateLiteral'
-          ? null
-          : item.propName
-      // TODO We should use propName.
-      // const propName = item.propName
-
-      if (typeof propName !== 'string') {
-        // (boolean | null | number | RegExp) Literal
+      const propName = item.propName
+      if (propName == null) {
         continue
       }
       if (!checker(propName)) {
diff --git a/lib/utils/indent-common.js b/lib/utils/indent-common.js
index 718f0427..9e6bfdef 100644
--- a/lib/utils/indent-common.js
+++ b/lib/utils/indent-common.js
@@ -12,6 +12,7 @@
 // Helpers
 // ------------------------------------------------------------------------------
 
+/** @type {Set<ASTNode['type']>} */
 const KNOWN_NODES = new Set([
   'ArrayExpression',
   'ArrayPattern',
@@ -24,6 +25,7 @@ const KNOWN_NODES = new Set([
   'BreakStatement',
   'CallExpression',
   'CatchClause',
+  'ChainExpression',
   'ClassBody',
   'ClassDeclaration',
   'ClassExpression',
@@ -32,8 +34,6 @@ const KNOWN_NODES = new Set([
   'DebuggerStatement',
   'DoWhileStatement',
   'EmptyStatement',
-  'ExperimentalRestProperty',
-  'ExperimentalSpreadProperty',
   'ExportAllDeclaration',
   'ExportDefaultDeclaration',
   'ExportNamedDeclaration',
@@ -48,6 +48,7 @@ const KNOWN_NODES = new Set([
   'IfStatement',
   'ImportDeclaration',
   'ImportDefaultSpecifier',
+  'ImportExpression',
   'ImportNamespaceSpecifier',
   'ImportSpecifier',
   'LabeledStatement',
@@ -97,6 +98,10 @@ const KNOWN_NODES = new Set([
   'VStartTag',
   'VText'
 ])
+const NON_STANDARD_KNOWN_NODES = new Set([
+  'ExperimentalRestProperty',
+  'ExperimentalSpreadProperty'
+])
 const LT_CHAR = /[\r\n\u2028\u2029]/
 const LINES = /[^\r\n\u2028\u2029]+(?:$|\r\n|[\r\n\u2028\u2029])/g
 const BLOCK_COMMENT_PREFIX = /^\s*\*/
@@ -300,6 +305,14 @@ function isSemicolon(token) {
 function isComma(token) {
   return token != null && token.type === 'Punctuator' && token.value === ','
 }
+/**
+ * Check whether the given token is a wildcard.
+ * @param {Token} token The token to check.
+ * @returns {boolean} `true` if the token is a wildcard.
+ */
+function isWildcard(token) {
+  return token != null && token.type === 'Punctuator' && token.value === '*'
+}
 
 /**
  * Check whether the given token is a whitespace.
@@ -321,7 +334,9 @@ function isComment(token) {
     (token.type === 'Block' ||
       token.type === 'Line' ||
       token.type === 'Shebang' ||
-      token.type.endsWith('Comment'))
+      (typeof token.type ===
+        'string' /* Although acorn supports new tokens, espree may not yet support new tokens.*/ &&
+        token.type.endsWith('Comment')))
   )
 }
 
@@ -336,7 +351,11 @@ function isNotComment(token) {
     token.type !== 'Block' &&
     token.type !== 'Line' &&
     token.type !== 'Shebang' &&
-    !token.type.endsWith('Comment')
+    !(
+      typeof token.type ===
+        'string' /* Although acorn supports new tokens, espree may not yet support new tokens.*/ &&
+      token.type.endsWith('Comment')
+    )
   )
 }
 
@@ -1330,6 +1349,15 @@ module.exports.defineVisitor = function create(
       setOffset(leftToken, 1, firstToken)
       processNodeList(node.arguments, leftToken, rightToken, 1)
     },
+    /** @param {ImportExpression} node */
+    ImportExpression(node) {
+      const firstToken = tokenStore.getFirstToken(node)
+      const rightToken = tokenStore.getLastToken(node)
+      const leftToken = tokenStore.getTokenAfter(firstToken, isLeftParen)
+
+      setOffset(leftToken, 1, firstToken)
+      processNodeList([node.source], leftToken, rightToken, 1)
+    },
     /** @param {CatchClause} node */
     CatchClause(node) {
       const firstToken = tokenStore.getFirstToken(node)
@@ -1417,7 +1445,20 @@ module.exports.defineVisitor = function create(
       if (isSemicolon(last(tokens))) {
         tokens.pop()
       }
-      setOffset(tokens, 1, firstToken)
+      if (!node.exported) {
+        setOffset(tokens, 1, firstToken)
+      } else {
+        // export * as foo from "mod"
+        const starToken = /** @type {Token} */ (tokens.find(isWildcard))
+        const asToken = tokenStore.getTokenAfter(starToken)
+        const exportedToken = tokenStore.getTokenAfter(asToken)
+        const afterTokens = tokens.slice(tokens.indexOf(exportedToken) + 1)
+
+        setOffset(starToken, 1, firstToken)
+        setOffset(asToken, 1, starToken)
+        setOffset(exportedToken, 1, starToken)
+        setOffset(afterTokens, 1, firstToken)
+      }
     },
     /** @param {ExportDefaultDeclaration} node */
     ExportDefaultDeclaration(node) {
@@ -1435,23 +1476,28 @@ module.exports.defineVisitor = function create(
         const declarationToken = tokenStore.getFirstToken(node, 1)
         setOffset(declarationToken, 1, exportToken)
       } else {
-        // export {foo, bar}; or export {foo, bar} from "mod";
-        const leftParenToken = tokenStore.getFirstToken(node, 1)
-        const rightParenToken = /** @type {Token} */ (tokenStore.getLastToken(
-          node,
-          isRightBrace
-        ))
-        setOffset(leftParenToken, 0, exportToken)
-        processNodeList(node.specifiers, leftParenToken, rightParenToken, 1)
-
-        const maybeFromToken = tokenStore.getTokenAfter(rightParenToken)
-        if (
-          maybeFromToken != null &&
-          sourceCode.getText(maybeFromToken) === 'from'
-        ) {
-          const fromToken = maybeFromToken
-          const nameToken = tokenStore.getTokenAfter(fromToken)
-          setOffset([fromToken, nameToken], 1, exportToken)
+        const firstSpecifier = node.specifiers[0]
+        if (!firstSpecifier || firstSpecifier.type === 'ExportSpecifier') {
+          // export {foo, bar}; or export {foo, bar} from "mod";
+          const leftParenToken = tokenStore.getFirstToken(node, 1)
+          const rightParenToken = /** @type {Token} */ (tokenStore.getLastToken(
+            node,
+            isRightBrace
+          ))
+          setOffset(leftParenToken, 0, exportToken)
+          processNodeList(node.specifiers, leftParenToken, rightParenToken, 1)
+
+          const maybeFromToken = tokenStore.getTokenAfter(rightParenToken)
+          if (
+            maybeFromToken != null &&
+            sourceCode.getText(maybeFromToken) === 'from'
+          ) {
+            const fromToken = maybeFromToken
+            const nameToken = tokenStore.getTokenAfter(fromToken)
+            setOffset([fromToken, nameToken], 1, exportToken)
+          }
+        } else {
+          // maybe babel-eslint
         }
       }
     },
@@ -1464,7 +1510,12 @@ module.exports.defineVisitor = function create(
     /** @param {ForInStatement | ForOfStatement} node */
     'ForInStatement, ForOfStatement'(node) {
       const forToken = tokenStore.getFirstToken(node)
-      const leftParenToken = tokenStore.getTokenAfter(forToken)
+      const awaitToken =
+        (node.type === 'ForOfStatement' &&
+          node.await &&
+          tokenStore.getTokenAfter(forToken)) ||
+        null
+      const leftParenToken = tokenStore.getTokenAfter(awaitToken || forToken)
       const leftToken = tokenStore.getTokenAfter(leftParenToken)
       const inToken = /** @type {Token} */ (tokenStore.getTokenAfter(
         leftToken,
@@ -1476,6 +1527,9 @@ module.exports.defineVisitor = function create(
         isNotLeftParen
       )
 
+      if (awaitToken != null) {
+        setOffset(awaitToken, 0, forToken)
+      }
       setOffset(leftParenToken, 1, forToken)
       setOffset(leftToken, 1, leftParenToken)
       setOffset(inToken, 1, leftToken)
@@ -1958,7 +2012,10 @@ module.exports.defineVisitor = function create(
     /** @param {ASTNode} node */
     // Ignore tokens of unknown nodes.
     '*:exit'(node) {
-      if (!KNOWN_NODES.has(node.type)) {
+      if (
+        !KNOWN_NODES.has(node.type) &&
+        !NON_STANDARD_KNOWN_NODES.has(node.type)
+      ) {
         ignore(node)
       }
     },
diff --git a/package.json b/package.json
index 4291451f..512de159 100644
--- a/package.json
+++ b/package.json
@@ -63,7 +63,7 @@
     "@types/eslint": "^6.8.1",
     "@types/natural-compare": "^1.4.0",
     "@types/node": "^13.13.5",
-    "@typescript-eslint/parser": "^2.31.0",
+    "@typescript-eslint/parser": "^3.0.2",
     "@vuepress/plugin-pwa": "^1.4.1",
     "babel-eslint": "^10.1.0",
     "chai": "^4.2.0",
diff --git a/typings/eslint-plugin-vue/global.d.ts b/typings/eslint-plugin-vue/global.d.ts
index 268cbbb1..aa23a1a2 100644
--- a/typings/eslint-plugin-vue/global.d.ts
+++ b/typings/eslint-plugin-vue/global.d.ts
@@ -121,6 +121,8 @@ declare global {
   type ClassExpression = VAST.ClassExpression
   type MetaProperty = VAST.MetaProperty
   type AwaitExpression = VAST.AwaitExpression
+  type ChainExpression = VAST.ChainExpression
+  type ChainElement = VAST.ChainElement
   type Property = VAST.Property
   type AssignmentProperty = VAST.AssignmentProperty
   type Super = VAST.Super
@@ -143,6 +145,7 @@ declare global {
   type ImportDefaultSpecifier = VAST.ImportDefaultSpecifier
   type ImportNamespaceSpecifier = VAST.ImportNamespaceSpecifier
   type ExportSpecifier = VAST.ExportSpecifier
+  type ImportExpression = VAST.ImportExpression
 
   // ---- TS Nodes ----
 
diff --git a/typings/eslint-plugin-vue/util-types/ast/ast.ts b/typings/eslint-plugin-vue/util-types/ast/ast.ts
index dc55396e..bcdeb93c 100644
--- a/typings/eslint-plugin-vue/util-types/ast/ast.ts
+++ b/typings/eslint-plugin-vue/util-types/ast/ast.ts
@@ -259,6 +259,10 @@ export type ESNodeListenerMap = {
   'ImportNamespaceSpecifier:exit': ES.ImportNamespaceSpecifier
   ExportSpecifier: ES.ExportSpecifier
   'ExportSpecifier:exit': ES.ExportSpecifier
+  ImportExpression: ES.ImportExpression
+  'ImportExpression:exit': ES.ImportExpression
+  ChainExpression: ES.ChainExpression
+  'ChainExpression:exit': ES.ChainExpression
 
   TSAsExpression: TS.TSAsExpression
   'TSAsExpression:exit': TS.TSAsExpression
diff --git a/typings/eslint-plugin-vue/util-types/ast/es-ast.ts b/typings/eslint-plugin-vue/util-types/ast/es-ast.ts
index efc81f16..701de116 100644
--- a/typings/eslint-plugin-vue/util-types/ast/es-ast.ts
+++ b/typings/eslint-plugin-vue/util-types/ast/es-ast.ts
@@ -113,6 +113,7 @@ export interface ForOfStatement extends HasParentNode {
   left: VariableDeclaration | Pattern
   right: Expression
   body: Statement
+  await: boolean
 }
 export interface LabeledStatement extends HasParentNode {
   type: 'LabeledStatement'
@@ -143,7 +144,7 @@ export interface TryStatement extends HasParentNode {
 }
 export interface CatchClause extends HasParentNode {
   type: 'CatchClause'
-  param: Pattern
+  param: Pattern | null
   body: BlockStatement
 }
 export interface WithStatement extends HasParentNode {
@@ -243,6 +244,11 @@ export interface ExportDefaultDeclaration extends HasParentNode {
 export interface ExportAllDeclaration extends HasParentNode {
   type: 'ExportAllDeclaration'
   source: Literal
+  exported: Identifier | null
+}
+export interface ImportExpression extends HasParentNode {
+  type: 'ImportExpression'
+  source: Expression
 }
 export type Expression =
   | ThisExpression
@@ -268,6 +274,8 @@ export type Expression =
   | MetaProperty
   | Identifier
   | AwaitExpression
+  | ImportExpression
+  | ChainExpression
   | JSX.JSXElement
   | JSX.JSXFragment
   | TS.TSAsExpression
@@ -404,7 +412,7 @@ export interface UpdateExpression extends HasParentNode {
 }
 export interface LogicalExpression extends HasParentNode {
   type: 'LogicalExpression'
-  operator: '||' | '&&'
+  operator: '||' | '&&' | '??'
   left: Expression
   right: Expression
 }
@@ -418,6 +426,7 @@ export interface CallExpression extends HasParentNode {
   type: 'CallExpression'
   callee: Expression | Super
   arguments: (Expression | SpreadElement)[]
+  optional: boolean
 }
 export interface Super extends HasParentNode {
   type: 'Super'
@@ -432,7 +441,13 @@ export interface MemberExpression extends HasParentNode {
   computed: boolean
   object: Expression | Super
   property: Expression
+  optional: boolean
+}
+export interface ChainExpression extends HasParentNode {
+  type: 'ChainExpression'
+  expression: ChainElement
 }
+export type ChainElement = CallExpression | MemberExpression
 export interface YieldExpression extends HasParentNode {
   type: 'YieldExpression'
   delegate: boolean
@@ -457,6 +472,7 @@ export interface TemplateElement extends HasParentNode {
   tail: boolean
   value: {
     cooked: string
+    // cooked: string | null // If the template literal is tagged and the text has an invalid escape, `cooked` will be `null`, e.g., `` tag`\unicode and \u{55}` ``
     raw: string
   }
 }


Test patch:
diff --git a/tests/fixtures/html-indent/nullish-coalescing-operator-01.vue b/tests/fixtures/html-indent/nullish-coalescing-operator-01.vue
new file mode 100644
index 00000000..927d6276
--- /dev/null
+++ b/tests/fixtures/html-indent/nullish-coalescing-operator-01.vue
@@ -0,0 +1,7 @@
+<!--{}-->
+<template>
+  <div v-bind:a="a
+    ??
+    b
+  "/>
+</template>
diff --git a/tests/fixtures/html-indent/optional-chaining-01.vue b/tests/fixtures/html-indent/optional-chaining-01.vue
new file mode 100644
index 00000000..6a6078e9
--- /dev/null
+++ b/tests/fixtures/html-indent/optional-chaining-01.vue
@@ -0,0 +1,9 @@
+<!--{}-->
+<template>
+  <div v-bind:a="
+    obj
+      ?.aaa
+      ?.bbb
+      ?.ccc
+  "/>
+</template>
diff --git a/tests/fixtures/html-indent/optional-chaining-02.vue b/tests/fixtures/html-indent/optional-chaining-02.vue
new file mode 100644
index 00000000..3d53c601
--- /dev/null
+++ b/tests/fixtures/html-indent/optional-chaining-02.vue
@@ -0,0 +1,9 @@
+<!--{}-->
+<template>
+  <div v-bind:a="
+    obj?.
+      aaa?.
+      bbb?.
+      ccc
+  "/>
+</template>
diff --git a/tests/fixtures/html-indent/optional-chaining-03.vue b/tests/fixtures/html-indent/optional-chaining-03.vue
new file mode 100644
index 00000000..ff81bb46
--- /dev/null
+++ b/tests/fixtures/html-indent/optional-chaining-03.vue
@@ -0,0 +1,9 @@
+<!--{}-->
+<template>
+  <div v-bind:a="
+    obj?.
+      [aaa]?.
+      [bbb]
+      ?.[ccc]
+  "/>
+</template>
diff --git a/tests/fixtures/html-indent/optional-chaining-04.vue b/tests/fixtures/html-indent/optional-chaining-04.vue
new file mode 100644
index 00000000..f064b73c
--- /dev/null
+++ b/tests/fixtures/html-indent/optional-chaining-04.vue
@@ -0,0 +1,15 @@
+<!--{}-->
+<template>
+  <div v-bind:a="
+    obj?.
+      (
+        aaa
+      )?.
+      (
+        bbb
+      )
+      ?.(
+        ccc
+      )
+  "/>
+</template>
diff --git a/tests/fixtures/script-indent/bigint-01.vue b/tests/fixtures/script-indent/bigint-01.vue
new file mode 100644
index 00000000..4e8e6c20
--- /dev/null
+++ b/tests/fixtures/script-indent/bigint-01.vue
@@ -0,0 +1,7 @@
+<!--{}-->
+<script>
+var a =
+  10n
+  +
+  5n
+</script>
diff --git a/tests/fixtures/script-indent/export-all-declaration-02.vue b/tests/fixtures/script-indent/export-all-declaration-02.vue
new file mode 100644
index 00000000..b85213a6
--- /dev/null
+++ b/tests/fixtures/script-indent/export-all-declaration-02.vue
@@ -0,0 +1,16 @@
+<!--{}-->
+<script>
+export
+  *
+    as
+    foo
+  from
+  "mod"
+;
+export
+  *
+    as
+    bar
+  from
+  "mod"
+</script>
diff --git a/tests/fixtures/script-indent/for-await-of-01.vue b/tests/fixtures/script-indent/for-await-of-01.vue
new file mode 100644
index 00000000..b5c07e2c
--- /dev/null
+++ b/tests/fixtures/script-indent/for-await-of-01.vue
@@ -0,0 +1,15 @@
+<!--{}-->
+<script>
+async function fn() {
+  for
+  await
+    (
+      a
+        of
+        b
+    )
+  {
+    ;
+  }
+}
+</script>
diff --git a/tests/fixtures/script-indent/import-expression-01.vue b/tests/fixtures/script-indent/import-expression-01.vue
new file mode 100644
index 00000000..3f7c7cb6
--- /dev/null
+++ b/tests/fixtures/script-indent/import-expression-01.vue
@@ -0,0 +1,7 @@
+<!--{}-->
+<script>
+const fs = import
+  (
+    'fs'
+  )
+</script>
diff --git a/tests/fixtures/script-indent/import-expression-02.vue b/tests/fixtures/script-indent/import-expression-02.vue
new file mode 100644
index 00000000..8144c3ff
--- /dev/null
+++ b/tests/fixtures/script-indent/import-expression-02.vue
@@ -0,0 +1,8 @@
+<!--{}-->
+<script>
+const module = import
+  (
+    m.
+      n
+  )
+</script>
diff --git a/tests/fixtures/script-indent/import-expression-03.vue b/tests/fixtures/script-indent/import-expression-03.vue
new file mode 100644
index 00000000..e82a6df4
--- /dev/null
+++ b/tests/fixtures/script-indent/import-expression-03.vue
@@ -0,0 +1,11 @@
+<!--{}-->
+<script>
+async function fn() {
+  return await
+    import
+      (
+        m.
+          n
+      )
+}
+</script>
diff --git a/tests/fixtures/script-indent/nullish-coalescing-operator-01.vue b/tests/fixtures/script-indent/nullish-coalescing-operator-01.vue
new file mode 100644
index 00000000..22427c7f
--- /dev/null
+++ b/tests/fixtures/script-indent/nullish-coalescing-operator-01.vue
@@ -0,0 +1,6 @@
+<!--{}-->
+<script>
+var v = a
+  ??
+  b
+</script>
diff --git a/tests/fixtures/script-indent/object-expression-04.vue b/tests/fixtures/script-indent/object-expression-04.vue
new file mode 100644
index 00000000..25e8c207
--- /dev/null
+++ b/tests/fixtures/script-indent/object-expression-04.vue
@@ -0,0 +1,12 @@
+<!--{}-->
+<script>
+var obj = {
+  a:
+    1,
+  ...obj1,
+  b:
+    2,
+  ...
+    obj2,
+}
+</script>
diff --git a/tests/fixtures/script-indent/optional-chaining-01.vue b/tests/fixtures/script-indent/optional-chaining-01.vue
new file mode 100644
index 00000000..f0ea36ee
--- /dev/null
+++ b/tests/fixtures/script-indent/optional-chaining-01.vue
@@ -0,0 +1,7 @@
+<!--{}-->
+<script>
+obj
+  ?.aaa
+  ?.bbb
+  ?.ccc
+</script>
diff --git a/tests/fixtures/script-indent/optional-chaining-02.vue b/tests/fixtures/script-indent/optional-chaining-02.vue
new file mode 100644
index 00000000..11c77d51
--- /dev/null
+++ b/tests/fixtures/script-indent/optional-chaining-02.vue
@@ -0,0 +1,7 @@
+<!--{}-->
+<script>
+obj?.
+  aaa?.
+  bbb?.
+  ccc
+</script>
diff --git a/tests/fixtures/script-indent/optional-chaining-03.vue b/tests/fixtures/script-indent/optional-chaining-03.vue
new file mode 100644
index 00000000..01626acf
--- /dev/null
+++ b/tests/fixtures/script-indent/optional-chaining-03.vue
@@ -0,0 +1,7 @@
+<!--{}-->
+<script>
+obj?.
+  [aaa]?.
+  [bbb]
+  ?.[ccc]
+</script>
diff --git a/tests/fixtures/script-indent/optional-chaining-04.vue b/tests/fixtures/script-indent/optional-chaining-04.vue
new file mode 100644
index 00000000..65a42a74
--- /dev/null
+++ b/tests/fixtures/script-indent/optional-chaining-04.vue
@@ -0,0 +1,13 @@
+<!--{}-->
+<script>
+obj?.
+  (
+    aaa
+  )?.
+  (
+    bbb
+  )
+  ?.(
+    ccc
+  )
+</script>
diff --git a/tests/fixtures/script-indent/optional-chaining-05.vue b/tests/fixtures/script-indent/optional-chaining-05.vue
new file mode 100644
index 00000000..0e926b49
--- /dev/null
+++ b/tests/fixtures/script-indent/optional-chaining-05.vue
@@ -0,0 +1,11 @@
+<!--{}-->
+<script>
+// https://github.com/estree/estree/blob/master/es2020.md
+obj.aaa?.bbb;
+obj?.aaa.bbb;
+obj?.aaa?.bbb;
+(obj.aaa).bbb;
+(obj.aaa)?.bbb;
+(obj?.aaa).bbb;
+(obj?.aaa)?.bbb;
+</script>
diff --git a/tests/fixtures/script-indent/optional-chaining-06.vue b/tests/fixtures/script-indent/optional-chaining-06.vue
new file mode 100644
index 00000000..47898015
--- /dev/null
+++ b/tests/fixtures/script-indent/optional-chaining-06.vue
@@ -0,0 +1,25 @@
+<!--{}-->
+<script>
+// https://github.com/estree/estree/blob/master/es2020.md
+obj
+  .aaa
+  ?.bbb;
+obj
+  ?.aaa
+  .bbb;
+obj
+  ?.aaa
+  ?.bbb;
+(obj
+  .aaa)
+  .bbb;
+(obj
+  .aaa)
+  ?.bbb;
+(obj
+  ?.aaa)
+  .bbb;
+(obj
+  ?.aaa)
+  ?.bbb;
+</script>
diff --git a/tests/fixtures/script-indent/optional-chaining-07.vue b/tests/fixtures/script-indent/optional-chaining-07.vue
new file mode 100644
index 00000000..1ba9fb0f
--- /dev/null
+++ b/tests/fixtures/script-indent/optional-chaining-07.vue
@@ -0,0 +1,25 @@
+<!--{}-->
+<script>
+// https://github.com/estree/estree/blob/master/es2020.md
+obj.
+  aaa?.
+  bbb;
+obj?.
+  aaa.
+  bbb;
+obj?.
+  aaa?.
+  bbb;
+(obj.
+  aaa).
+  bbb;
+(obj.
+  aaa)?.
+  bbb;
+(obj?.
+  aaa).
+  bbb;
+(obj?.
+  aaa)?.
+  bbb;
+</script>
diff --git a/tests/fixtures/script-indent/optional-chaining-08.vue b/tests/fixtures/script-indent/optional-chaining-08.vue
new file mode 100644
index 00000000..61d07188
--- /dev/null
+++ b/tests/fixtures/script-indent/optional-chaining-08.vue
@@ -0,0 +1,12 @@
+<!--{}-->
+<script>
+// https://github.com/estree/estree/blob/master/es2020.md
+(obj.aaa
+).bbb;
+(obj.aaa
+)?.bbb;
+(obj?.aaa
+).bbb;
+(obj?.aaa
+)?.bbb;
+</script>
diff --git a/tests/fixtures/script-indent/optional-chaining-09.vue b/tests/fixtures/script-indent/optional-chaining-09.vue
new file mode 100644
index 00000000..c17562c0
--- /dev/null
+++ b/tests/fixtures/script-indent/optional-chaining-09.vue
@@ -0,0 +1,32 @@
+<!--{}-->
+<script>
+// https://github.com/estree/estree/blob/master/es2020.md
+(
+  obj
+    .
+    aaa
+)
+  .
+  bbb;
+(
+  obj
+    .
+    aaa
+)
+  ?.
+  bbb;
+(
+  obj
+    ?.
+    aaa
+)
+  .
+  bbb;
+(
+  obj
+    ?.
+    aaa
+)
+  ?.
+  bbb;
+</script>
diff --git a/tests/fixtures/script-indent/rest-properties-01.vue b/tests/fixtures/script-indent/rest-properties-01.vue
new file mode 100644
index 00000000..87ab2ab9
--- /dev/null
+++ b/tests/fixtures/script-indent/rest-properties-01.vue
@@ -0,0 +1,8 @@
+<!--{}-->
+<script>
+({
+  a,
+  ...
+    b
+} = {})
+</script>
diff --git a/tests/fixtures/script-indent/try-statement-04.vue b/tests/fixtures/script-indent/try-statement-04.vue
new file mode 100644
index 00000000..4b3d9e86
--- /dev/null
+++ b/tests/fixtures/script-indent/try-statement-04.vue
@@ -0,0 +1,14 @@
+<!--{}-->
+<script>
+try
+{
+}
+catch
+{
+  ;
+}
+finally
+{
+  ;
+}
+</script>
diff --git a/tests/lib/rules/html-indent.js b/tests/lib/rules/html-indent.js
index 875b5df7..d70b0395 100644
--- a/tests/lib/rules/html-indent.js
+++ b/tests/lib/rules/html-indent.js
@@ -40,6 +40,14 @@ function loadPatterns(additionalValid, additionalInvalid) {
     const code0 = fs.readFileSync(path.join(FIXTURE_ROOT, filename), 'utf8')
     const code = code0.replace(/^<!--(.+?)-->/, `<!--${filename}-->`)
     const baseObj = JSON.parse(/^<!--(.+?)-->/.exec(code0)[1])
+    if ('parser' in baseObj) {
+      baseObj.parser = require.resolve(baseObj.parser)
+    }
+    if ('parserOptions' in baseObj && 'parser' in baseObj.parserOptions) {
+      baseObj.parserOptions.parser = require.resolve(
+        baseObj.parserOptions.parser
+      )
+    }
     return Object.assign(baseObj, { code, filename })
   })
   const invalid = valid
@@ -104,7 +112,7 @@ function unIndent(strings) {
 const tester = new RuleTester({
   parser: require.resolve('vue-eslint-parser'),
   parserOptions: {
-    ecmaVersion: 2017,
+    ecmaVersion: 2020,
     ecmaFeatures: {
       globalReturn: true
     }
diff --git a/tests/lib/rules/prop-name-casing.js b/tests/lib/rules/prop-name-casing.js
index eb932b29..4815c4eb 100644
--- a/tests/lib/rules/prop-name-casing.js
+++ b/tests/lib/rules/prop-name-casing.js
@@ -131,18 +131,6 @@ ruleTester.run('prop-name-casing', rule, {
       `,
       parserOptions
     },
-    {
-      // TemplateLiteral computed property does not warn
-      filename: 'test.vue',
-      code: `
-        export default {
-          props: {
-            [\`greeting-text\`]: String
-          }
-        }
-      `,
-      parserOptions
-    },
     {
       // TemplateLiteral computed property does not warn
       filename: 'test.vue',
@@ -275,18 +263,6 @@ ruleTester.run('prop-name-casing', rule, {
       `,
       parserOptions
     },
-    {
-      // RegExp Literal computed property name
-      filename: 'test.vue',
-      code: `
-        export default {
-          props: {
-            [/greeting-text/]: String
-          }
-        }
-      `,
-      parserOptions
-    },
     {
       // Japanese characters
       filename: 'test.vue',
@@ -578,6 +554,32 @@ ruleTester.run('prop-name-casing', rule, {
       options: ['snake_case'],
       parserOptions,
       errors: ['Prop "_itemName" is not in snake_case.']
+    },
+    {
+      // TemplateLiteral computed property
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: {
+            [\`greeting-text\`]: String
+          }
+        }
+      `,
+      parserOptions,
+      errors: ['Prop "greeting-text" is not in camelCase.']
+    },
+    {
+      // RegExp Literal computed property name
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: {
+            [/greeting-text/]: String
+          }
+        }
+      `,
+      parserOptions,
+      errors: ['Prop "/greeting-text/" is not in camelCase.']
     }
   ]
 })
diff --git a/tests/lib/rules/script-indent.js b/tests/lib/rules/script-indent.js
index d74d41dd..42497d2d 100644
--- a/tests/lib/rules/script-indent.js
+++ b/tests/lib/rules/script-indent.js
@@ -41,6 +41,14 @@ function loadPatterns(additionalValid, additionalInvalid) {
     const code0 = fs.readFileSync(path.join(FIXTURE_ROOT, filename), 'utf8')
     const code = code0.replace(commentPattern, `$1${filename}$3`)
     const baseObj = JSON.parse(commentPattern.exec(code0)[2])
+    if ('parser' in baseObj) {
+      baseObj.parser = require.resolve(baseObj.parser)
+    }
+    if ('parserOptions' in baseObj && 'parser' in baseObj.parserOptions) {
+      baseObj.parserOptions.parser = require.resolve(
+        baseObj.parserOptions.parser
+      )
+    }
     return Object.assign(baseObj, { code, filename })
   })
   const invalid = valid
@@ -105,7 +113,7 @@ function unIndent(strings) {
 const tester = new RuleTester({
   parser: require.resolve('vue-eslint-parser'),
   parserOptions: {
-    ecmaVersion: 2017,
+    ecmaVersion: 2020,
     sourceType: 'module'
   }
 })