Repository: vuejs/eslint-plugin-vue
Pull request #1303 (2020-09-21 10:35:40)

Add `vue/experimental-script-setup-vars` rule


Patch:
diff --git a/docs/.vuepress/components/eslint-code-block.vue b/docs/.vuepress/components/eslint-code-block.vue
index 391ae549..46504375 100644
--- a/docs/.vuepress/components/eslint-code-block.vue
+++ b/docs/.vuepress/components/eslint-code-block.vue
@@ -62,6 +62,7 @@ export default {
     config() {
       return {
         globals: {
+          console: false,
           // ES2015 globals
           ArrayBuffer: false,
           DataView: false,
@@ -121,8 +122,13 @@ export default {
 
   async mounted() {
     // Load linter.
-    const [{ default: Linter }, { parseForESLint }] = await Promise.all([
+    const [
+      { default: Linter },
+      { default: noUndefRule },
+      { parseForESLint }
+    ] = await Promise.all([
       import('eslint4b/dist/linter'),
+      import('eslint/lib/rules/no-undef'),
       import('espree').then(() => import('vue-eslint-parser'))
     ])
 
@@ -131,6 +137,7 @@ export default {
     for (const ruleId of Object.keys(rules)) {
       linter.defineRule(`vue/${ruleId}`, rules[ruleId])
     }
+    linter.defineRule('no-undef', noUndefRule)
 
     linter.defineParser('vue-eslint-parser', { parseForESLint })
   }
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 9798e777..1ffb102f 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -24,6 +24,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
 | Rule ID | Description |    |
 |:--------|:------------|:---|
 | [vue/comment-directive](./comment-directive.md) | support comment-directives in `<template>` |  |
+| [vue/experimental-script-setup-vars](./experimental-script-setup-vars.md) | prevent variables defined in `<script setup>` to be marked as undefined |  |
 | [vue/jsx-uses-vars](./jsx-uses-vars.md) | prevent variables used in JSX to be marked as unused |  |
 
 ## Priority A: Essential (Error Prevention) <badge text="for Vue.js 3.x" vertical="middle">for Vue.js 3.x</badge>
@@ -50,6 +51,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
 | [vue/no-deprecated-functional-template](./no-deprecated-functional-template.md) | disallow using deprecated the `functional` template (in Vue.js 3.0.0+) |  |
 | [vue/no-deprecated-html-element-is](./no-deprecated-html-element-is.md) | disallow using deprecated the `is` attribute on HTML elements (in Vue.js 3.0.0+) |  |
 | [vue/no-deprecated-inline-template](./no-deprecated-inline-template.md) | disallow using deprecated `inline-template` attribute (in Vue.js 3.0.0+) |  |
+| [vue/no-deprecated-props-default-this](./no-deprecated-props-default-this.md) | disallow props default function `this` access |  |
 | [vue/no-deprecated-scope-attribute](./no-deprecated-scope-attribute.md) | disallow deprecated `scope` attribute (in Vue.js 2.5.0+) | :wrench: |
 | [vue/no-deprecated-slot-attribute](./no-deprecated-slot-attribute.md) | disallow deprecated `slot` attribute (in Vue.js 2.6.0+) | :wrench: |
 | [vue/no-deprecated-slot-scope-attribute](./no-deprecated-slot-scope-attribute.md) | disallow deprecated `slot-scope` attribute (in Vue.js 2.6.0+) | :wrench: |
diff --git a/docs/rules/experimental-script-setup-vars.md b/docs/rules/experimental-script-setup-vars.md
new file mode 100644
index 00000000..c4f76899
--- /dev/null
+++ b/docs/rules/experimental-script-setup-vars.md
@@ -0,0 +1,42 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/experimental-script-setup-vars
+description: prevent variables defined in `<script setup>` to be marked as undefined
+---
+# vue/experimental-script-setup-vars
+> prevent variables defined in `<script setup>` to be marked as undefined
+
+- :gear: This rule is included in all of `"plugin:vue/base"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-essential"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/recommended"` and `"plugin:vue/vue3-recommended"`.
+
+:::warning
+This rule is an experimental rule. It may be removed without notice.
+:::
+
+This rule will find variables defined in `<script setup="args">` and mark them as defined variables.
+
+This rule only has an effect when the `no-undef` rule is enabled.
+
+## :book: Rule Details
+
+Without this rule this code triggers warning:
+
+<eslint-code-block :rules="{'no-undef': ['error'], 'vue/experimental-script-setup-vars': ['error']}">
+
+```vue
+<script setup="props, { emit }">
+import { watchEffect } from 'vue'
+
+watchEffect(() => console.log(props.msg))
+emit('foo')
+</script>
+```
+
+</eslint-code-block>
+
+After turning on, `props` and `emit` are being marked as defined and `no-undef` rule doesn't report an issue.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/experimental-script-setup-vars.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/experimental-script-setup-vars.js)
diff --git a/docs/rules/no-deprecated-props-default-this.md b/docs/rules/no-deprecated-props-default-this.md
new file mode 100644
index 00000000..51ae7101
--- /dev/null
+++ b/docs/rules/no-deprecated-props-default-this.md
@@ -0,0 +1,70 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-deprecated-props-default-this
+description: disallow props default function `this` access
+---
+# vue/no-deprecated-props-default-this
+> disallow props default function `this` access
+
+- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
+
+## :book: Rule Details
+
+This rule reports the use of `this` within the props default value factory functions.
+In Vue.js 3.0.0+, props default value factory functions no longer have access to `this`.
+
+See [Migration Guide - Props Default Function `this` Access](https://v3.vuejs.org/guide/migration/props-default-this.html) for more details.
+
+<eslint-code-block :rules="{'vue/no-deprecated-props-default-this': ['error']}">
+
+```vue
+<script>
+export default {
+  props: {
+    a: String,
+    b: {
+      default () {
+        /* ✗ BAD */
+        return this.a
+      }
+    }
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/no-deprecated-props-default-this': ['error']}">
+
+```vue
+<script>
+export default {
+  props: {
+    a: String,
+    b: {
+      default (props) {
+        /* ✓ GOOD */
+        return props.a
+      }
+    }
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further Reading
+
+- [Migration Guide - Props Default Function `this` Access](https://v3.vuejs.org/guide/migration/props-default-this.html)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-deprecated-props-default-this.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-deprecated-props-default-this.js)
diff --git a/docs/rules/no-potential-component-option-typo.md b/docs/rules/no-potential-component-option-typo.md
index a3a9a6e7..2233361e 100644
--- a/docs/rules/no-potential-component-option-typo.md
+++ b/docs/rules/no-potential-component-option-typo.md
@@ -53,7 +53,7 @@ export default {
 
 </eslint-code-block>
 
-> we use editdistance to compare two string similarity, threshold is an option to control upper bound of editdistance to report
+> we use edit distance to compare two string similarity, threshold is an option to control upper bound of edit distance to report
 
 **Here is the another example about config option `threshold`**
 
@@ -75,15 +75,15 @@ export default {
   props: {
 
   },
-  /* ✓ GOOD, due to threshold is 5 */
-  method: {
+  /* ✗ BAD, due to threshold is 5 */
+  mehtod: {
 
   },
   /* ✓ GOOD, due to threshold is 5 */
   data: {
 
   },
-  /* ✗ BAD, due to we don't choose vue-router preset or add a custom option */
+  /* ✓ GOOD, due to we don't choose vue-router preset or add a custom option */
   beforeRouteEnteR() {
 
   }
@@ -97,7 +97,7 @@ export default {
 
 ```json
 {
-  "vue/no-unsed-vars": ["error", {
+  "vue/no-potential-component-option-typo": ["error", {
     "presets": ["vue"],
     "custom": [],
     "threshold": 1
@@ -111,11 +111,11 @@ export default {
 
 ## :rocket: Suggestion
 
-- We provide all the possible component option that editdistance between your vue component option and configuration options is greater than 0 and lessEqual than threshold
+- We provide all the possible component option that edit distance between your vue component option and configuration options is greater than 0 and less equal than threshold
 
 ## :books: Further Reading
 
-- [Edit_distance](https://en.wikipedia.org/wiki/Edit_distance)
+- [Edit distance](https://en.wikipedia.org/wiki/Edit_distance)
 
 ## :mag: Implementation
 
diff --git a/lib/configs/base.js b/lib/configs/base.js
index 2521a623..eb129643 100644
--- a/lib/configs/base.js
+++ b/lib/configs/base.js
@@ -16,6 +16,7 @@ module.exports = {
   plugins: ['vue'],
   rules: {
     'vue/comment-directive': 'error',
+    'vue/experimental-script-setup-vars': 'error',
     'vue/jsx-uses-vars': 'error'
   }
 }
diff --git a/lib/configs/vue3-essential.js b/lib/configs/vue3-essential.js
index 7d715c18..d1e2d176 100644
--- a/lib/configs/vue3-essential.js
+++ b/lib/configs/vue3-essential.js
@@ -18,6 +18,7 @@ module.exports = {
     'vue/no-deprecated-functional-template': 'error',
     'vue/no-deprecated-html-element-is': 'error',
     'vue/no-deprecated-inline-template': 'error',
+    'vue/no-deprecated-props-default-this': 'error',
     'vue/no-deprecated-scope-attribute': 'error',
     'vue/no-deprecated-slot-attribute': 'error',
     'vue/no-deprecated-slot-scope-attribute': 'error',
diff --git a/lib/index.js b/lib/index.js
index 556fadeb..eb6a4291 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -25,6 +25,7 @@ module.exports = {
     'dot-location': require('./rules/dot-location'),
     'dot-notation': require('./rules/dot-notation'),
     eqeqeq: require('./rules/eqeqeq'),
+    'experimental-script-setup-vars': require('./rules/experimental-script-setup-vars'),
     'func-call-spacing': require('./rules/func-call-spacing'),
     'html-closing-bracket-newline': require('./rules/html-closing-bracket-newline'),
     'html-closing-bracket-spacing': require('./rules/html-closing-bracket-spacing'),
@@ -59,6 +60,7 @@ module.exports = {
     'no-deprecated-functional-template': require('./rules/no-deprecated-functional-template'),
     'no-deprecated-html-element-is': require('./rules/no-deprecated-html-element-is'),
     'no-deprecated-inline-template': require('./rules/no-deprecated-inline-template'),
+    'no-deprecated-props-default-this': require('./rules/no-deprecated-props-default-this'),
     'no-deprecated-scope-attribute': require('./rules/no-deprecated-scope-attribute'),
     'no-deprecated-slot-attribute': require('./rules/no-deprecated-slot-attribute'),
     'no-deprecated-slot-scope-attribute': require('./rules/no-deprecated-slot-scope-attribute'),
diff --git a/lib/rules/experimental-script-setup-vars.js b/lib/rules/experimental-script-setup-vars.js
new file mode 100644
index 00000000..b209376d
--- /dev/null
+++ b/lib/rules/experimental-script-setup-vars.js
@@ -0,0 +1,228 @@
+/**
+ * @fileoverview prevent variables defined in `<script setup>` to be marked as undefined
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const Module = require('module')
+const path = require('path')
+const utils = require('../utils')
+const AST = require('vue-eslint-parser').AST
+
+const ecmaVersion = 2020
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description:
+        'prevent variables defined in `<script setup>` to be marked as undefined', // eslint-disable-line consistent-docs-description
+      categories: ['base'],
+      url: 'https://eslint.vuejs.org/rules/experimental-script-setup-vars.html'
+    },
+    schema: []
+  },
+  /**
+   * @param {RuleContext} context - The rule context.
+   * @returns {RuleListener} AST event handlers.
+   */
+  create(context) {
+    const documentFragment =
+      context.parserServices.getDocumentFragment &&
+      context.parserServices.getDocumentFragment()
+    if (!documentFragment) {
+      return {}
+    }
+    const sourceCode = context.getSourceCode()
+    const scriptElement = documentFragment.children
+      .filter(utils.isVElement)
+      .find(
+        (element) =>
+          element.name === 'script' &&
+          element.range[0] <= sourceCode.ast.range[0] &&
+          sourceCode.ast.range[1] <= element.range[1]
+      )
+    if (!scriptElement) {
+      return {}
+    }
+    const setupAttr = utils.getAttribute(scriptElement, 'setup')
+    if (!setupAttr || !setupAttr.value) {
+      return {}
+    }
+    const value = setupAttr.value.value
+
+    let eslintScope
+    try {
+      eslintScope = getESLintModule('eslint-scope', () =>
+        // @ts-ignore
+        require('eslint-scope')
+      )
+    } catch (_e) {
+      context.report({
+        node: setupAttr,
+        message: 'Can not be resolved eslint-scope.'
+      })
+      return {}
+    }
+    let espree
+    try {
+      espree = getESLintModule('espree', () =>
+        // @ts-ignore
+        require('espree')
+      )
+    } catch (_e) {
+      context.report({
+        node: setupAttr,
+        message: 'Can not be resolved espree.'
+      })
+      return {}
+    }
+
+    const globalScope = sourceCode.scopeManager.scopes[0]
+
+    /** @type {string[]} */
+    let vars
+    try {
+      vars = parseSetup(value, espree, eslintScope)
+    } catch (_e) {
+      context.report({
+        node: setupAttr.value,
+        message: 'Parsing error.'
+      })
+      return {}
+    }
+
+    // Define configured global variables.
+    for (const id of vars) {
+      const tempVariable = globalScope.set.get(id)
+
+      /** @type {Variable} */
+      let variable
+      if (!tempVariable) {
+        variable = new eslintScope.Variable(id, globalScope)
+
+        globalScope.variables.push(variable)
+        globalScope.set.set(id, variable)
+      } else {
+        variable = tempVariable
+      }
+
+      variable.eslintImplicitGlobalSetting = 'readonly'
+      variable.eslintExplicitGlobal = undefined
+      variable.eslintExplicitGlobalComments = undefined
+      variable.writeable = false
+    }
+
+    /*
+     * "through" contains all references which definitions cannot be found.
+     * Since we augment the global scope using configuration, we need to update
+     * references and remove the ones that were added by configuration.
+     */
+    globalScope.through = globalScope.through.filter((reference) => {
+      const name = reference.identifier.name
+      const variable = globalScope.set.get(name)
+
+      if (variable) {
+        /*
+         * Links the variable and the reference.
+         * And this reference is removed from `Scope#through`.
+         */
+        reference.resolved = variable
+        variable.references.push(reference)
+
+        return false
+      }
+
+      return true
+    })
+
+    return {}
+  }
+}
+
+/**
+ * @param {string} code
+ * @param {any} espree
+ * @param {any} eslintScope
+ * @returns {string[]}
+ */
+function parseSetup(code, espree, eslintScope) {
+  /** @type {Program} */
+  const ast = espree.parse(`(${code})=>{}`, { ecmaVersion })
+  const result = eslintScope.analyze(ast, {
+    ignoreEval: true,
+    nodejsScope: false,
+    ecmaVersion,
+    sourceType: 'script',
+    fallback: AST.getFallbackKeys
+  })
+
+  const variables = /** @type {Variable[]} */ (result.globalScope.childScopes[0]
+    .variables)
+
+  return variables.map((v) => v.name)
+}
+
+const createRequire =
+  // Added in v12.2.0
+  Module.createRequire ||
+  // Added in v10.12.0, but deprecated in v12.2.0.
+  Module.createRequireFromPath ||
+  // Polyfill - This is not executed on the tests on node@>=10.
+  /**
+   * @param {string} filename
+   */
+  function (filename) {
+    const mod = new Module(filename)
+
+    mod.filename = filename
+    // @ts-ignore
+    mod.paths = Module._nodeModulePaths(path.dirname(filename))
+    // @ts-ignore
+    mod._compile('module.exports = require;', filename)
+    return mod.exports
+  }
+
+/** @type { { 'espree'?: any, 'eslint-scope'?: any } } */
+const modulesCache = {}
+
+/**
+ * @param {string} p
+ */
+function isLinterPath(p) {
+  return (
+    // ESLint 6 and above
+    p.includes(`eslint${path.sep}lib${path.sep}linter${path.sep}linter.js`) ||
+    // ESLint 5
+    p.includes(`eslint${path.sep}lib${path.sep}linter.js`)
+  )
+}
+
+/**
+ * Load module from the loaded ESLint.
+ * If the loaded ESLint was not found, just returns `fallback()`.
+ * @param {'espree' | 'eslint-scope'} name
+ * @param { () => any } fallback
+ */
+function getESLintModule(name, fallback) {
+  if (!modulesCache[name]) {
+    // Lookup the loaded eslint
+    const linterPath = Object.keys(require.cache).find(isLinterPath)
+    if (linterPath) {
+      try {
+        modulesCache[name] = createRequire(linterPath)(name)
+      } catch (_e) {
+        // ignore
+      }
+    }
+    if (!modulesCache[name]) {
+      modulesCache[name] = fallback()
+    }
+  }
+
+  return modulesCache[name]
+}
diff --git a/lib/rules/no-deprecated-props-default-this.js b/lib/rules/no-deprecated-props-default-this.js
new file mode 100644
index 00000000..926476c1
--- /dev/null
+++ b/lib/rules/no-deprecated-props-default-this.js
@@ -0,0 +1,107 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'disallow props default function `this` access',
+      categories: ['vue3-essential'],
+      url:
+        'https://eslint.vuejs.org/rules/no-deprecated-props-default-this.html'
+    },
+    fixable: null,
+    schema: [],
+    messages: {
+      deprecated:
+        'Props default value factory functions no longer have access to `this`.'
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    /**
+     * @typedef {object} ScopeStack
+     * @property {ScopeStack | null} upper
+     * @property {FunctionExpression | FunctionDeclaration} node
+     * @property {boolean} propDefault
+     */
+    /** @type {Set<FunctionExpression>} */
+    const propsDefault = new Set()
+    /** @type {ScopeStack | null} */
+    let scopeStack = null
+
+    /**
+     * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
+     */
+    function onFunctionEnter(node) {
+      if (node.type === 'ArrowFunctionExpression') {
+        return
+      }
+      if (scopeStack) {
+        scopeStack = {
+          upper: scopeStack,
+          node,
+          propDefault: false
+        }
+      } else if (node.type === 'FunctionExpression' && propsDefault.has(node)) {
+        scopeStack = {
+          upper: scopeStack,
+          node,
+          propDefault: true
+        }
+      }
+    }
+
+    /**
+     * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
+     */
+    function onFunctionExit(node) {
+      if (scopeStack && scopeStack.node === node) {
+        scopeStack = scopeStack.upper
+      }
+    }
+    return utils.defineVueVisitor(context, {
+      onVueObjectEnter(node) {
+        for (const prop of utils.getComponentProps(node)) {
+          if (prop.type !== 'object') {
+            continue
+          }
+          if (prop.value.type !== 'ObjectExpression') {
+            continue
+          }
+          const def = utils.findProperty(prop.value, 'default')
+          if (!def) {
+            continue
+          }
+          if (def.value.type !== 'FunctionExpression') {
+            continue
+          }
+          propsDefault.add(def.value)
+        }
+      },
+      ':function': onFunctionEnter,
+      ':function:exit': onFunctionExit,
+      ThisExpression(node) {
+        if (scopeStack && scopeStack.propDefault) {
+          context.report({
+            node,
+            messageId: 'deprecated'
+          })
+        }
+      }
+    })
+  }
+}
diff --git a/typings/eslint/index.d.ts b/typings/eslint/index.d.ts
index 653abebd..1d793d24 100644
--- a/typings/eslint/index.d.ts
+++ b/typings/eslint/index.d.ts
@@ -49,6 +49,11 @@ export namespace Scope {
     identifiers: VAST.Identifier[]
     references: Reference[]
     defs: Definition[]
+
+    writeable?: boolean | undefined
+    eslintExplicitGlobal?: boolean | undefined
+    eslintExplicitGlobalComments?: Comment[] | undefined
+    eslintImplicitGlobalSetting?: 'readonly' | 'writable' | undefined
   }
   interface Reference {
     identifier: VAST.Identifier


Test patch:
diff --git a/tests/lib/rules/experimental-script-setup-vars.js b/tests/lib/rules/experimental-script-setup-vars.js
new file mode 100644
index 00000000..089019c7
--- /dev/null
+++ b/tests/lib/rules/experimental-script-setup-vars.js
@@ -0,0 +1,58 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const { RuleTester } = require('eslint')
+const rule = require('../../../lib/rules/experimental-script-setup-vars')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
+})
+
+tester.run('experimental-script-setup-vars', rule, {
+  valid: [
+    `
+    <script setup="props, { emit }">
+    import { watchEffect } from 'vue'
+
+    watchEffect(() => console.log(props.msg))
+    emit('foo')
+    </script>`,
+    `
+    <script setup>
+    export let count = 1
+    </script>`,
+    `
+    <script>
+    import { watchEffect } from 'vue'
+
+    export default {
+      setup (props, { emit }) {
+        watchEffect(() => console.log(props.msg))
+        emit('foo')
+        return {}
+      }
+    }
+    </script>`,
+    `
+    <template>
+      <div />
+    </template>`
+  ],
+  invalid: [
+    {
+      code: `
+      <script setup="a - b">
+      </script>
+      `,
+      errors: [
+        {
+          message: 'Parsing error.',
+          line: 2
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-deprecated-props-default-this.js b/tests/lib/rules/no-deprecated-props-default-this.js
new file mode 100644
index 00000000..0993e817
--- /dev/null
+++ b/tests/lib/rules/no-deprecated-props-default-this.js
@@ -0,0 +1,187 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/no-deprecated-props-default-this')
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
+})
+
+ruleTester.run('no-deprecated-props-default-this', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+        <template><div /></template>
+        <script>
+        export default {
+          props: {
+            a: String,
+            b: {
+              default (props) {
+                return props.a
+              }
+            }
+          }
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template><div /></template>
+        <script>
+        export default {
+          props: {
+            a: String,
+            b: {
+              default: () => {
+                return this.a
+              }
+            }
+          }
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template><div /></template>
+        <script>
+        export default {
+          props: {
+            a: String,
+            b: {
+              default () {
+                return function () {
+                  return this.a
+                }
+              }
+            }
+          }
+        }
+        </script>
+      `,
+      errors: [{}, {}]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template><div /></template>
+        <script>
+        const Foo = {
+          props: {
+            a: String,
+            b: {
+              default () {
+                return this.a
+              }
+            }
+          }
+        }
+        </script>
+      `
+    }
+  ],
+
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+        <template><div /></template>
+        <script>
+        export default {
+          props: {
+            a: String,
+            b: {
+              default () {
+                return this.a
+              }
+            }
+          }
+        }
+        </script>
+      `,
+      errors: [
+        {
+          message:
+            'Props default value factory functions no longer have access to `this`.',
+          line: 9,
+          column: 24,
+          endLine: 9,
+          endColumn: 28
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template><div /></template>
+        <script>
+        export default {
+          props: {
+            a: String,
+            b: {
+              default () {
+                return () => this.a
+              }
+            }
+          }
+        }
+        </script>
+      `,
+      errors: [
+        {
+          message:
+            'Props default value factory functions no longer have access to `this`.',
+          line: 9,
+          column: 30,
+          endLine: 9,
+          endColumn: 34
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template><div /></template>
+        <script>
+        export default {
+          props: {
+            a: String,
+            b: {
+              default () {
+                return this.a
+              }
+            },
+            c: {
+              default () {
+                return this.a
+              }
+            }
+          }
+        }
+        </script>
+      `,
+      errors: [
+        'Props default value factory functions no longer have access to `this`.',
+        'Props default value factory functions no longer have access to `this`.'
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/script-setup.js b/tests/lib/script-setup.js
new file mode 100644
index 00000000..ad7409dc
--- /dev/null
+++ b/tests/lib/script-setup.js
@@ -0,0 +1,49 @@
+/**
+ * @author Yosuke Ota <https://github.com/ota-meshi>
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const Linter = require('eslint').Linter
+const parser = require('vue-eslint-parser')
+const assert = require('assert')
+const experimentalScriptSetupVars = require('../../lib/rules/experimental-script-setup-vars')
+
+const baseConfig = {
+  parser: 'vue-eslint-parser',
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: 'module'
+  }
+}
+
+describe('script-setup test cases', () => {
+  const linter = new Linter()
+  linter.defineParser('vue-eslint-parser', parser)
+  linter.defineRule(
+    'vue/experimental-script-setup-vars',
+    experimentalScriptSetupVars
+  )
+
+  describe('temporary supports.', () => {
+    const config = Object.assign({}, baseConfig, {
+      globals: { console: false },
+      rules: {
+        'vue/experimental-script-setup-vars': 'error',
+        'no-undef': 'error'
+      }
+    })
+
+    it('should not be marked.', () => {
+      const code = `
+      <script setup="props, { emit }">
+      import { watchEffect } from 'vue'
+
+      watchEffect(() => console.log(props.msg))
+      emit('foo')
+      </script>`
+      const messages = linter.verify(code, config, 'test.vue')
+      assert.deepStrictEqual(messages, [])
+    })
+  })
+})