Repository: vuejs/eslint-plugin-vue
Pull request #1186 (2020-06-04 05:48:51)

Add `vue/no-useless-v-bind` rule
This PR adds `vue/no-useless-v-bind` rule that reports `v-bind` with a string literal value.


Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 8d3e9fb3..1e1a92a0 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -43,6 +43,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
 | [vue/no-async-in-computed-properties](./no-async-in-computed-properties.md) | disallow asynchronous actions in computed properties |  |
 | [vue/no-deprecated-data-object-declaration](./no-deprecated-data-object-declaration.md) | disallow using deprecated object declaration on data (in Vue.js 3.0.0+) | :wrench: |
 | [vue/no-deprecated-dollar-listeners-api](./no-deprecated-dollar-listeners-api.md) | disallow using deprecated `$listeners` (in Vue.js 3.0.0+) |  |
+| [vue/no-deprecated-dollar-scopedslots-api](./no-deprecated-dollar-scopedslots-api.md) | disallow using deprecated `$scopedSlots` (in Vue.js 3.0.0+) | :wrench: |
 | [vue/no-deprecated-events-api](./no-deprecated-events-api.md) | disallow using deprecated events api (in Vue.js 3.0.0+) |  |
 | [vue/no-deprecated-filter](./no-deprecated-filter.md) | disallow using deprecated filters syntax (in Vue.js 3.0.0+) |  |
 | [vue/no-deprecated-functional-template](./no-deprecated-functional-template.md) | disallow using deprecated the `functional` template (in Vue.js 3.0.0+) |  |
@@ -74,6 +75,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
 | [vue/require-component-is](./require-component-is.md) | require `v-bind:is` of `<component>` elements |  |
 | [vue/require-prop-type-constructor](./require-prop-type-constructor.md) | require prop type to be a constructor | :wrench: |
 | [vue/require-render-return](./require-render-return.md) | enforce render function to always return value |  |
+| [vue/require-slots-as-functions](./require-slots-as-functions.md) | enforce properties of `$slots` to be used as a function |  |
 | [vue/require-toggle-inside-transition](./require-toggle-inside-transition.md) | require control the display of the content inside `<transition>` |  |
 | [vue/require-v-for-key](./require-v-for-key.md) | require `v-bind:key` with `v-for` directives |  |
 | [vue/require-valid-default-prop](./require-valid-default-prop.md) | enforce props default values to be valid |  |
@@ -145,6 +147,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
 |:--------|:------------|:---|
 | [vue/attributes-order](./attributes-order.md) | enforce order of attributes | :wrench: |
 | [vue/component-tags-order](./component-tags-order.md) | enforce order of component top-level elements |  |
+| [vue/no-multiple-slot-args](./no-multiple-slot-args.md) | disallow to pass multiple arguments to scoped slots |  |
 | [vue/no-v-html](./no-v-html.md) | disallow use of v-html to prevent XSS attack |  |
 | [vue/order-in-components](./order-in-components.md) | enforce order of properties in components | :wrench: |
 | [vue/this-in-template](./this-in-template.md) | disallow usage of `this` in template |  |
@@ -252,6 +255,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
 |:--------|:------------|:---|
 | [vue/attributes-order](./attributes-order.md) | enforce order of attributes | :wrench: |
 | [vue/component-tags-order](./component-tags-order.md) | enforce order of component top-level elements |  |
+| [vue/no-multiple-slot-args](./no-multiple-slot-args.md) | disallow to pass multiple arguments to scoped slots |  |
 | [vue/no-v-html](./no-v-html.md) | disallow use of v-html to prevent XSS attack |  |
 | [vue/order-in-components](./order-in-components.md) | enforce order of properties in components | :wrench: |
 | [vue/this-in-template](./this-in-template.md) | disallow usage of `this` in template |  |
@@ -278,6 +282,7 @@ For example:
 | [vue/html-comment-content-spacing](./html-comment-content-spacing.md) | enforce unified spacing in HTML comments | :wrench: |
 | [vue/html-comment-indent](./html-comment-indent.md) | enforce consistent indentation in HTML comments | :wrench: |
 | [vue/match-component-file-name](./match-component-file-name.md) | require component name property to match its file name |  |
+| [vue/no-bare-strings-in-template](./no-bare-strings-in-template.md) | disallow the use of bare strings in `<template>` |  |
 | [vue/no-boolean-default](./no-boolean-default.md) | disallow boolean defaults | :wrench: |
 | [vue/no-duplicate-attr-inheritance](./no-duplicate-attr-inheritance.md) | enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"` |  |
 | [vue/no-potential-component-option-typo](./no-potential-component-option-typo.md) | disallow a potential typo in your component property |  |
@@ -287,6 +292,7 @@ For example:
 | [vue/no-unregistered-components](./no-unregistered-components.md) | disallow using components that are not registered inside templates |  |
 | [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: |
 | [vue/no-unused-properties](./no-unused-properties.md) | disallow unused properties |  |
+| [vue/no-useless-v-bind](./no-useless-v-bind.md) | disallow unnecessary `v-bind` directives | :wrench: |
 | [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: |
 | [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported |  |
 | [vue/require-explicit-emits](./require-explicit-emits.md) | require `emits` option with name triggered by `$emit()` |  |
diff --git a/docs/rules/no-bare-strings-in-template.md b/docs/rules/no-bare-strings-in-template.md
new file mode 100644
index 00000000..40041119
--- /dev/null
+++ b/docs/rules/no-bare-strings-in-template.md
@@ -0,0 +1,88 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-bare-strings-in-template
+description: disallow the use of bare strings in `<template>`
+---
+# vue/no-bare-strings-in-template
+> disallow the use of bare strings in `<template>`
+
+## :book: Rule Details
+
+This rule disallows the use of bare strings in `<template>`.  
+In order to be able to internationalize your application, you will need to avoid using plain strings in your templates. Instead, you would need to use a template helper specializing in translation.  
+
+This rule was inspired by [no-bare-strings rule in ember-template-lint](https://github.com/ember-template-lint/ember-template-lint/blob/master/docs/rule/no-bare-strings.md).
+
+
+<eslint-code-block :rules="{'vue/no-bare-strings-in-template': ['error']}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <h1>{{ $t('foo.bar') }}</h1>
+  <h1>{{ foo }}</h1>
+  <h1 v-t="'foo.bar'"></h1>
+
+  <!-- ✗ BAD -->
+  <h1>Lorem ipsum</h1>
+  <div
+    title="Lorem ipsum"
+    aria-label="Lorem ipsum"
+    aria-placeholder="Lorem ipsum"
+    aria-roledescription="Lorem ipsum"
+    aria-valuetext="Lorem ipsum"
+  />
+  <img alt="Lorem ipsum">
+  <input placeholder="Lorem ipsum">
+  <h1 v-text="'Lorem ipsum'" />
+
+  <!-- Does not check -->
+  <h1>{{ 'Lorem ipsum' }}</h1>
+  <div
+    v-bind:title="'Lorem ipsum'"
+  />
+</template>
+```
+
+</eslint-code-block>
+
+:::tip
+This rule does not check for string literals, in bindings and mustaches interpolation. This is because it looks like a conscious decision.  
+If you want to report these string literals, enable the [vue/no-useless-v-bind] and [vue/no-useless-mustaches] rules and fix the useless string literals.
+:::
+
+## :wrench: Options
+
+```js
+{
+  "vue/no-bare-strings-in-template": ["error", {
+    "whitelist": [
+      "(", ")", ",", ".", "&", "+", "-", "=", "*", "/", "#", "%", "!", "?", ":", "[", "]", "{", "}", "<", ">", "\u00b7", "\u2022", "\u2010", "\u2013", "\u2014", "\u2212", "|"
+    ],
+    "attributes": {
+      "/.+/": ["title", "aria-label", "aria-placeholder", "aria-roledescription", "aria-valuetext"],
+      "input": ["placeholder"],
+      "img": ["alt"]
+    },
+    "directives": ["v-text"]
+  }]
+}
+```
+
+- `whitelist` ... An array of whitelisted strings.
+- `attributes` ... An object whose keys are tag name or patterns and value is an array of attributes to check for that tag name.
+- `directives` ... An array of directive names to check literal value.
+
+## :couple: Related rules
+
+- [vue/no-useless-v-bind]
+- [vue/no-useless-mustaches]
+
+[vue/no-useless-v-bind]: ./no-useless-v-bind.md
+[vue/no-useless-mustaches]: ./no-useless-mustaches.md
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-bare-strings-in-template.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-bare-strings-in-template.js)
diff --git a/docs/rules/no-deprecated-dollar-scopedslots-api.md b/docs/rules/no-deprecated-dollar-scopedslots-api.md
new file mode 100644
index 00000000..9b1d577c
--- /dev/null
+++ b/docs/rules/no-deprecated-dollar-scopedslots-api.md
@@ -0,0 +1,47 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-deprecated-dollar-scopedslots-api
+description: disallow using deprecated `$scopedSlots` (in Vue.js 3.0.0+)
+---
+# vue/no-deprecated-dollar-scopedslots-api
+> disallow using deprecated `$scopedSlots` (in Vue.js 3.0.0+)
+
+- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
+## :book: Rule Details
+
+This rule reports use of deprecated `$scopedSlots`. (in Vue.js 3.0.0+).
+
+<eslint-code-block fix :rules="{'vue/no-deprecated-dollar-scopedslots-api': ['error']}">
+
+```vue
+<template>
+  <!-- ✗ BAD -->
+  <div v-if="$scopedSlots.default"><slot /></div>
+</template>
+<script>
+export default {
+  render() {
+    /* ✗ BAD */
+    return this.$scopedSlots.default()
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further reading
+
+- [Vue RFCs - 0006-slots-unification](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0006-slots-unification.md)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-deprecated-dollar-scopedslots-api.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-deprecated-dollar-scopedslots-api.js)
diff --git a/docs/rules/no-multiple-slot-args.md b/docs/rules/no-multiple-slot-args.md
new file mode 100644
index 00000000..d0d31945
--- /dev/null
+++ b/docs/rules/no-multiple-slot-args.md
@@ -0,0 +1,49 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-multiple-slot-args
+description: disallow to pass multiple arguments to scoped slots
+---
+# vue/no-multiple-slot-args
+> disallow to pass multiple arguments to scoped slots
+
+- :gear: This rule is included in `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
+
+## :book: Rule Details
+
+This rule disallows to pass multiple arguments to scoped slots.  
+In details, it reports call expressions if a call of `this.$scopedSlots` members has 2 or more arguments.
+
+<eslint-code-block :rules="{'vue/no-multiple-slot-args': ['error']}">
+
+```vue
+<script>
+export default {
+  render(h) {
+    /* ✓ GOOD */
+    var children = this.$scopedSlots.default()
+    var children = this.$scopedSlots.default(foo)
+    var children = this.$scopedSlots.default({ foo, bar })
+
+    /* ✗ BAD */
+    var children = this.$scopedSlots.default(foo, bar)
+    var children = this.$scopedSlots.default(...foo)
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further reading
+
+- [vuejs/vue#9468](https://github.com/vuejs/vue/issues/9468#issuecomment-462210146)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-multiple-slot-args.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-multiple-slot-args.js)
diff --git a/docs/rules/no-ref-as-operand.md b/docs/rules/no-ref-as-operand.md
index c983d669..609a0727 100644
--- a/docs/rules/no-ref-as-operand.md
+++ b/docs/rules/no-ref-as-operand.md
@@ -11,7 +11,8 @@ description: disallow use of value wrapped by `ref()` (Composition API) as an op
 
 ## :book: Rule Details
 
-This rule reports cases where a ref is used incorrectly as an operand.
+This rule reports cases where a ref is used incorrectly as an operand.  
+You must use `.value` to access the `Ref` value.
 
 <eslint-code-block :rules="{'vue/no-ref-as-operand': ['error']}">
 
diff --git a/docs/rules/no-useless-v-bind.md b/docs/rules/no-useless-v-bind.md
new file mode 100644
index 00000000..d202104e
--- /dev/null
+++ b/docs/rules/no-useless-v-bind.md
@@ -0,0 +1,87 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-useless-v-bind
+description: disallow unnecessary `v-bind` directives
+---
+# vue/no-useless-v-bind
+> disallow unnecessary `v-bind` directives
+
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
+## :book: Rule Details
+
+This rule reports `v-bind` with a string literal value.  
+The `v-bind` with a string literal value can be changed to a static attribute definition.
+
+<eslint-code-block fix :rules="{'vue/no-useless-v-bind': ['error']}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <div foo="bar"/>
+  <div :foo="bar"/>
+
+  <!-- ✗ BAD -->
+  <div v-bind:foo="'bar'"/>
+  <div :foo="'bar'"/>
+</template>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+```js
+{
+  "vue/no-useless-v-bind": ["error", {
+    "ignoreIncludesComment": false,
+    "ignoreStringEscape": false
+  }]
+}
+```
+
+- `ignoreIncludesComment` ... If `true`, do not report expressions containing comments. default `false`.
+- `ignoreStringEscape` ... If `true`, do not report string literals with useful escapes. default `false`.
+
+### `"ignoreIncludesComment": true`
+
+<eslint-code-block fix :rules="{'vue/no-useless-v-bind': ['error', {ignoreIncludesComment: true}]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <div v-bind:foo="'bar'/* comment */"/>
+
+  <!-- ✗ BAD -->
+  <div v-bind:foo="'bar'"/>
+</template>
+```
+
+</eslint-code-block>
+
+### `"ignoreStringEscape": true`
+
+<eslint-code-block fix :rules="{'vue/no-useless-v-bind': ['error', {ignoreStringEscape: true}]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <div v-bind:foo="'bar\nbaz'"/>
+</template>
+```
+
+</eslint-code-block>
+
+## :couple: Related rules
+
+- [vue/no-useless-mustaches]
+- [vue/no-useless-concat]
+
+[vue/no-useless-mustaches]: ./no-useless-mustaches.md
+[vue/no-useless-concat]: ./no-useless-concat.md
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-useless-v-bind.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-useless-v-bind.js)
diff --git a/docs/rules/require-slots-as-functions.md b/docs/rules/require-slots-as-functions.md
new file mode 100644
index 00000000..8b4c01f9
--- /dev/null
+++ b/docs/rules/require-slots-as-functions.md
@@ -0,0 +1,48 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/require-slots-as-functions
+description: enforce properties of `$slots` to be used as a function
+---
+# vue/require-slots-as-functions
+> enforce properties of `$slots` to be used as a function
+
+- :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 enforces the properties of `$slots` to be used as a function.  
+`this.$slots.default` was an array of VNode in Vue.js 2.x, but changed to a function that returns an array of VNode in Vue.js 3.x.
+
+<eslint-code-block :rules="{'vue/require-slots-as-functions': ['error']}">
+
+```vue
+<script>
+export default {
+  render(h) {
+    /* ✓ GOOD */
+    var children = this.$slots.default()
+    var children = this.$slots.default && this.$slots.default()
+
+    /* ✗ BAD */
+    var children = [...this.$slots.default]
+    var children = this.$slots.default.filter(test)
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further reading
+
+- [Vue RFCs - 0006-slots-unification](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0006-slots-unification.md)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/require-slots-as-functions.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/require-slots-as-functions.js)
diff --git a/lib/configs/recommended.js b/lib/configs/recommended.js
index e3ca8eba..e6388e15 100644
--- a/lib/configs/recommended.js
+++ b/lib/configs/recommended.js
@@ -8,6 +8,7 @@ module.exports = {
   rules: {
     'vue/attributes-order': 'warn',
     'vue/component-tags-order': 'warn',
+    'vue/no-multiple-slot-args': 'warn',
     'vue/no-v-html': 'warn',
     'vue/order-in-components': 'warn',
     'vue/this-in-template': 'warn'
diff --git a/lib/configs/vue3-essential.js b/lib/configs/vue3-essential.js
index b2aec42d..b802623d 100644
--- a/lib/configs/vue3-essential.js
+++ b/lib/configs/vue3-essential.js
@@ -11,6 +11,7 @@ module.exports = {
     'vue/no-async-in-computed-properties': 'error',
     'vue/no-deprecated-data-object-declaration': 'error',
     'vue/no-deprecated-dollar-listeners-api': 'error',
+    'vue/no-deprecated-dollar-scopedslots-api': 'error',
     'vue/no-deprecated-events-api': 'error',
     'vue/no-deprecated-filter': 'error',
     'vue/no-deprecated-functional-template': 'error',
@@ -42,6 +43,7 @@ module.exports = {
     'vue/require-component-is': 'error',
     'vue/require-prop-type-constructor': 'error',
     'vue/require-render-return': 'error',
+    'vue/require-slots-as-functions': 'error',
     'vue/require-toggle-inside-transition': 'error',
     'vue/require-v-for-key': 'error',
     'vue/require-valid-default-prop': 'error',
diff --git a/lib/configs/vue3-recommended.js b/lib/configs/vue3-recommended.js
index 700f0f8f..318690c3 100644
--- a/lib/configs/vue3-recommended.js
+++ b/lib/configs/vue3-recommended.js
@@ -8,6 +8,7 @@ module.exports = {
   rules: {
     'vue/attributes-order': 'warn',
     'vue/component-tags-order': 'warn',
+    'vue/no-multiple-slot-args': 'warn',
     'vue/no-v-html': 'warn',
     'vue/order-in-components': 'warn',
     'vue/this-in-template': 'warn'
diff --git a/lib/index.js b/lib/index.js
index d5fc2122..6504e05d 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -45,11 +45,13 @@ module.exports = {
     'name-property-casing': require('./rules/name-property-casing'),
     'no-arrow-functions-in-watch': require('./rules/no-arrow-functions-in-watch'),
     'no-async-in-computed-properties': require('./rules/no-async-in-computed-properties'),
+    'no-bare-strings-in-template': require('./rules/no-bare-strings-in-template'),
     'no-boolean-default': require('./rules/no-boolean-default'),
     'no-confusing-v-for-v-if': require('./rules/no-confusing-v-for-v-if'),
     'no-custom-modifiers-on-v-model': require('./rules/no-custom-modifiers-on-v-model'),
     'no-deprecated-data-object-declaration': require('./rules/no-deprecated-data-object-declaration'),
     'no-deprecated-dollar-listeners-api': require('./rules/no-deprecated-dollar-listeners-api'),
+    'no-deprecated-dollar-scopedslots-api': require('./rules/no-deprecated-dollar-scopedslots-api'),
     'no-deprecated-events-api': require('./rules/no-deprecated-events-api'),
     'no-deprecated-filter': require('./rules/no-deprecated-filter'),
     'no-deprecated-functional-template': require('./rules/no-deprecated-functional-template'),
@@ -70,6 +72,7 @@ module.exports = {
     'no-irregular-whitespace': require('./rules/no-irregular-whitespace'),
     'no-lifecycle-after-await': require('./rules/no-lifecycle-after-await'),
     'no-multi-spaces': require('./rules/no-multi-spaces'),
+    'no-multiple-slot-args': require('./rules/no-multiple-slot-args'),
     'no-multiple-template-root': require('./rules/no-multiple-template-root'),
     'no-mutating-props': require('./rules/no-mutating-props'),
     'no-parsing-error': require('./rules/no-parsing-error'),
@@ -94,6 +97,7 @@ module.exports = {
     'no-unused-vars': require('./rules/no-unused-vars'),
     'no-use-v-if-with-v-for': require('./rules/no-use-v-if-with-v-for'),
     'no-useless-concat': require('./rules/no-useless-concat'),
+    'no-useless-v-bind': require('./rules/no-useless-v-bind'),
     'no-v-html': require('./rules/no-v-html'),
     'no-v-model-argument': require('./rules/no-v-model-argument'),
     'no-watch-after-await': require('./rules/no-watch-after-await'),
@@ -111,6 +115,7 @@ module.exports = {
     'require-prop-type-constructor': require('./rules/require-prop-type-constructor'),
     'require-prop-types': require('./rules/require-prop-types'),
     'require-render-return': require('./rules/require-render-return'),
+    'require-slots-as-functions': require('./rules/require-slots-as-functions'),
     'require-toggle-inside-transition': require('./rules/require-toggle-inside-transition'),
     'require-v-for-key': require('./rules/require-v-for-key'),
     'require-valid-default-prop': require('./rules/require-valid-default-prop'),
diff --git a/lib/rules/no-bare-strings-in-template.js b/lib/rules/no-bare-strings-in-template.js
new file mode 100644
index 00000000..8fea1625
--- /dev/null
+++ b/lib/rules/no-bare-strings-in-template.js
@@ -0,0 +1,279 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+const regexp = require('../utils/regexp')
+const casing = require('../utils/casing')
+
+/**
+ * @typedef {import('vue-eslint-parser').AST.VAttribute} VAttribute
+ * @typedef {import('vue-eslint-parser').AST.VDirective} VDirective
+ * @typedef {import('vue-eslint-parser').AST.VElement} VElement
+ * @typedef {import('vue-eslint-parser').AST.VIdentifier} VIdentifier
+ * @typedef {import('vue-eslint-parser').AST.VExpressionContainer} VExpressionContainer
+ * @typedef {import('vue-eslint-parser').AST.VText} VText
+ */
+
+/**
+ * @typedef { { names: { [tagName in string]: Set<string> }, regexps: { name: RegExp, attrs: Set<string> }[], cache: { [tagName in string]: Set<string> } } } TargetAttrs
+ * @typedef { { upper: ElementStack, name: string, attrs: Set<string> } } ElementStack
+ */
+
+// ------------------------------------------------------------------------------
+// Constants
+// ------------------------------------------------------------------------------
+
+// https://dev.w3.org/html5/html-author/charref
+const DEFAULT_WHITELIST = [
+  '(',
+  ')',
+  ',',
+  '.',
+  '&',
+  '+',
+  '-',
+  '=',
+  '*',
+  '/',
+  '#',
+  '%',
+  '!',
+  '?',
+  ':',
+  '[',
+  ']',
+  '{',
+  '}',
+  '<',
+  '>',
+  '\u00b7', // "·"
+  '\u2022', // "•"
+  '\u2010', // "‐"
+  '\u2013', // "–"
+  '\u2014', // "—"
+  '\u2212', // "−"
+  '|'
+]
+
+const DEFAULT_ATTRIBUTES = {
+  '/.+/': [
+    'title',
+    'aria-label',
+    'aria-placeholder',
+    'aria-roledescription',
+    'aria-valuetext'
+  ],
+  input: ['placeholder'],
+  img: ['alt']
+}
+
+const DEFAULT_DIRECTIVES = ['v-text']
+
+// --------------------------------------------------------------------------
+// Helpers
+// --------------------------------------------------------------------------
+
+/**
+ * Parse attributes option
+ * @returns {TargetAttrs}
+ */
+function parseTargetAttrs(options) {
+  /** @type {TargetAttrs} */
+  const result = { names: {}, regexps: [], cache: {} }
+  for (const tagName of Object.keys(options)) {
+    /** @type { Set<string> } */
+    const attrs = new Set(options[tagName])
+    if (regexp.isRegExp(tagName)) {
+      result.regexps.push({
+        name: regexp.toRegExp(tagName),
+        attrs
+      })
+    } else {
+      result.names[tagName] = attrs
+    }
+  }
+  return result
+}
+
+/**
+ * Get a string from given expression container node
+ * @param {VExpressionContainer} node
+ * @returns { string | null }
+ */
+function getStringValue(value) {
+  const expression = value.expression
+  if (!expression) {
+    return null
+  }
+  if (expression.type !== 'Literal') {
+    return null
+  }
+  if (typeof expression.value === 'string') {
+    return expression.value
+  }
+  return null
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'disallow the use of bare strings in `<template>`',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/no-bare-strings-in-template.html'
+    },
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          whitelist: {
+            type: 'array',
+            items: { type: 'string' },
+            uniqueItems: true
+          },
+          attributes: {
+            type: 'object',
+            patternProperties: {
+              '^(?:\\S+|/.*/[a-z]*)$': {
+                type: 'array',
+                items: { type: 'string' },
+                uniqueItems: true
+              }
+            },
+            additionalProperties: false
+          },
+          directives: {
+            type: 'array',
+            items: { type: 'string', pattern: '^v-' },
+            uniqueItems: true
+          }
+        }
+      }
+    ],
+    messages: {
+      unexpected: 'Unexpected non-translated string used.',
+      unexpectedInAttr: 'Unexpected non-translated string used in `{{attr}}`.'
+    }
+  },
+  create(context) {
+    const opts = context.options[0] || {}
+    const whitelist = opts.whitelist || DEFAULT_WHITELIST
+    const attributes = parseTargetAttrs(opts.attributes || DEFAULT_ATTRIBUTES)
+    const directives = opts.directives || DEFAULT_DIRECTIVES
+
+    const whitelistRe = new RegExp(
+      whitelist.map((w) => regexp.escape(w)).join('|'),
+      'gu'
+    )
+
+    /** @type {ElementStack | null} */
+    let elementStack = null
+    /**
+     * Gets the bare string from given string
+     * @param {string} str
+     */
+    function getBareString(str) {
+      return str.trim().replace(whitelistRe, '').trim()
+    }
+
+    /**
+     * Get the attribute to be verified from the element name.
+     * @param {string} tagName
+     * @returns {Set<string>}
+     */
+    function getTargetAttrs(tagName) {
+      if (attributes.cache[tagName]) {
+        return attributes.cache[tagName]
+      }
+      /** @type {string[]} */
+      const result = []
+      if (attributes.names[tagName]) {
+        result.push(...attributes.names[tagName])
+      }
+      for (const { name, attrs } of attributes.regexps) {
+        name.lastIndex = 0
+        if (name.test(tagName)) {
+          result.push(...attrs)
+        }
+      }
+      if (casing.isKebabCase(tagName)) {
+        result.push(...getTargetAttrs(casing.pascalCase(tagName)))
+      }
+
+      return (attributes.cache[tagName] = new Set(result))
+    }
+
+    return utils.defineTemplateBodyVisitor(context, {
+      /** @param {VText} node */
+      VText(node) {
+        if (getBareString(node.value)) {
+          context.report({
+            node,
+            messageId: 'unexpected'
+          })
+        }
+      },
+      /**
+       * @param {VElement} node
+       */
+      VElement(node) {
+        elementStack = {
+          upper: elementStack,
+          name: node.rawName,
+          attrs: getTargetAttrs(node.rawName)
+        }
+      },
+      'VElement:exit'() {
+        elementStack = elementStack.upper
+      },
+      /** @param {VAttribute|VDirective} node */
+      VAttribute(node) {
+        if (!node.value) {
+          return
+        }
+        if (node.directive === false) {
+          const attrs = elementStack.attrs
+          if (!attrs.has(node.key.rawName)) {
+            return
+          }
+
+          if (getBareString(node.value.value)) {
+            context.report({
+              node: node.value,
+              messageId: 'unexpectedInAttr',
+              data: {
+                attr: node.key.rawName
+              }
+            })
+          }
+        } else {
+          const directive = `v-${node.key.name.name}`
+          if (!directives.includes(directive)) {
+            return
+          }
+          const str = getStringValue(node.value)
+          if (str && getBareString(str)) {
+            context.report({
+              node: node.value,
+              messageId: 'unexpectedInAttr',
+              data: {
+                attr: directive
+              }
+            })
+          }
+        }
+      }
+    })
+  }
+}
diff --git a/lib/rules/no-deprecated-dollar-scopedslots-api.js b/lib/rules/no-deprecated-dollar-scopedslots-api.js
new file mode 100644
index 00000000..f369c70f
--- /dev/null
+++ b/lib/rules/no-deprecated-dollar-scopedslots-api.js
@@ -0,0 +1,79 @@
+/**
+ * @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 using deprecated `$scopedSlots` (in Vue.js 3.0.0+)',
+      categories: ['vue3-essential'],
+      url:
+        'https://eslint.vuejs.org/rules/no-deprecated-dollar-scopedslots-api.html'
+    },
+    fixable: 'code',
+    schema: [],
+    messages: {
+      deprecated: 'The `$scopedSlots` is deprecated.'
+    }
+  },
+
+  create(context) {
+    return utils.defineTemplateBodyVisitor(
+      context,
+      {
+        VExpressionContainer(node) {
+          for (const reference of node.references) {
+            if (reference.variable != null) {
+              // Not vm reference
+              continue
+            }
+            if (reference.id.name === '$scopedSlots') {
+              context.report({
+                node: reference.id,
+                messageId: 'deprecated',
+                fix(fixer) {
+                  return fixer.replaceText(reference.id, '$slots')
+                }
+              })
+            }
+          }
+        }
+      },
+      utils.defineVueVisitor(context, {
+        MemberExpression(node) {
+          if (
+            node.property.type !== 'Identifier' ||
+            node.property.name !== '$scopedSlots'
+          ) {
+            return
+          }
+          if (!utils.isThis(node.object, context)) {
+            return
+          }
+
+          context.report({
+            node: node.property,
+            messageId: 'deprecated',
+            fix(fixer) {
+              return fixer.replaceText(node.property, '$slots')
+            }
+          })
+        }
+      })
+    )
+  }
+}
diff --git a/lib/rules/no-multiple-slot-args.js b/lib/rules/no-multiple-slot-args.js
new file mode 100644
index 00000000..e8bcae96
--- /dev/null
+++ b/lib/rules/no-multiple-slot-args.js
@@ -0,0 +1,127 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+const { findVariable } = require('eslint-utils')
+
+/**
+ * @typedef {import('vue-eslint-parser').AST.ESLintMemberExpression} MemberExpression
+ * @typedef {import('vue-eslint-parser').AST.ESLintIdentifier} Identifier
+ */
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'disallow to pass multiple arguments to scoped slots',
+      categories: ['vue3-recommended', 'recommended'],
+      url: 'https://eslint.vuejs.org/rules/no-multiple-slot-args.html'
+    },
+    fixable: null,
+    schema: [],
+    messages: {
+      unexpected: 'Unexpected multiple arguments.',
+      unexpectedSpread: 'Unexpected spread argument.'
+    }
+  },
+
+  create(context) {
+    /**
+     * Verify the given node
+     * @param {MemberExpression | Identifier} node The node to verify
+     */
+    function verify(node) {
+      const parent = node.parent
+
+      if (
+        parent.type === 'VariableDeclarator' &&
+        parent.id.type === 'Identifier'
+      ) {
+        // const foo = this.$scopedSlots.foo
+        verifyReferences(parent.id)
+        return
+      }
+
+      if (
+        parent.type === 'AssignmentExpression' &&
+        parent.right === node &&
+        parent.left.type === 'Identifier'
+      ) {
+        // foo = this.$scopedSlots.foo
+        verifyReferences(parent.left)
+        return
+      }
+
+      if (parent.type !== 'CallExpression' || parent.arguments.includes(node)) {
+        return
+      }
+
+      if (!parent.arguments.length) {
+        return
+      }
+      if (parent.arguments.length > 1) {
+        context.report({
+          node: parent.arguments[1],
+          messageId: 'unexpected'
+        })
+      }
+      if (parent.arguments[0].type === 'SpreadElement') {
+        context.report({
+          node: parent.arguments[0],
+          messageId: 'unexpectedSpread'
+        })
+      }
+    }
+    /**
+     * Verify the references of the given node.
+     * @param {Identifier} node The node to verify
+     */
+    function verifyReferences(node) {
+      // @ts-ignore
+      const variable = findVariable(context.getScope(), node)
+      if (!variable) {
+        return
+      }
+      for (const reference of variable.references) {
+        if (!reference.isRead()) {
+          continue
+        }
+        /** @type {Identifier} */
+        const id = reference.identifier
+        verify(id)
+      }
+    }
+
+    return utils.defineVueVisitor(context, {
+      /** @param {MemberExpression} node */
+      MemberExpression(node) {
+        const object = node.object
+        if (object.type !== 'MemberExpression') {
+          return
+        }
+        if (
+          object.property.type !== 'Identifier' ||
+          (object.property.name !== '$slots' &&
+            object.property.name !== '$scopedSlots')
+        ) {
+          return
+        }
+        if (!utils.isThis(object.object, context)) {
+          return
+        }
+        verify(node)
+      }
+    })
+  }
+}
diff --git a/lib/rules/no-ref-as-operand.js b/lib/rules/no-ref-as-operand.js
index 11873edf..cfd09921 100644
--- a/lib/rules/no-ref-as-operand.js
+++ b/lib/rules/no-ref-as-operand.js
@@ -4,6 +4,7 @@
  */
 'use strict'
 const { ReferenceTracker, findVariable } = require('eslint-utils')
+const utils = require('../utils')
 
 module.exports = {
   meta: {
@@ -18,19 +19,23 @@ module.exports = {
     schema: [],
     messages: {
       requireDotValue:
-        'Must use `.value` to read or write the value wrapped by `ref()`.'
+        'Must use `.value` to read or write the value wrapped by `{{method}}()`.'
     }
   },
   create(context) {
     const refReferenceIds = new Map()
 
     function reportIfRefWrapped(node) {
-      if (!refReferenceIds.has(node)) {
+      const data = refReferenceIds.get(node)
+      if (!data) {
         return
       }
       context.report({
         node,
-        messageId: 'requireDotValue'
+        messageId: 'requireDotValue',
+        data: {
+          method: data.method
+        }
       })
     }
     return {
@@ -41,11 +46,23 @@ module.exports = {
             [ReferenceTracker.ESM]: true,
             ref: {
               [ReferenceTracker.CALL]: true
+            },
+            computed: {
+              [ReferenceTracker.CALL]: true
+            },
+            toRef: {
+              [ReferenceTracker.CALL]: true
+            },
+            customRef: {
+              [ReferenceTracker.CALL]: true
+            },
+            shallowRef: {
+              [ReferenceTracker.CALL]: true
             }
           }
         }
 
-        for (const { node } of tracker.iterateEsmReferences(traceMap)) {
+        for (const { node, path } of tracker.iterateEsmReferences(traceMap)) {
           const variableDeclarator = node.parent
           if (
             !variableDeclarator ||
@@ -73,7 +90,8 @@ module.exports = {
 
             refReferenceIds.set(reference.identifier, {
               variableDeclarator,
-              variableDeclaration
+              variableDeclaration,
+              method: path[1]
             })
           }
         }
@@ -108,13 +126,13 @@ module.exports = {
           return
         }
         // Report only constants.
-        const info = refReferenceIds.get(node)
-        if (!info) {
+        const data = refReferenceIds.get(node)
+        if (!data) {
           return
         }
         if (
-          !info.variableDeclaration ||
-          info.variableDeclaration.kind !== 'const'
+          !data.variableDeclaration ||
+          data.variableDeclaration.kind !== 'const'
         ) {
           return
         }
@@ -126,6 +144,26 @@ module.exports = {
           return
         }
         reportIfRefWrapped(node)
+      },
+      // `${refValue}`
+      'TemplateLiteral>Identifier'(node) {
+        reportIfRefWrapped(node)
+      },
+      // refValue.x
+      'MemberExpression>Identifier'(node) {
+        if (node.parent.object !== node) {
+          return
+        }
+        const name = utils.getStaticPropertyName(node.parent)
+        if (
+          name === 'value' ||
+          name == null ||
+          // WritableComputedRef
+          name === 'effect'
+        ) {
+          return
+        }
+        reportIfRefWrapped(node)
       }
     }
   }
diff --git a/lib/rules/no-useless-v-bind.js b/lib/rules/no-useless-v-bind.js
new file mode 100644
index 00000000..2ffa7140
--- /dev/null
+++ b/lib/rules/no-useless-v-bind.js
@@ -0,0 +1,153 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+
+const DOUBLE_QUOTES_RE = /"/gu
+const SINGLE_QUOTES_RE = /'/gu
+
+/**
+ * @typedef {import('eslint').Rule.RuleContext} RuleContext
+ * @typedef {import('vue-eslint-parser').AST.VDirective} VDirective
+ */
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'disallow unnecessary `v-bind` directives',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/no-useless-v-bind.html'
+    },
+    fixable: 'code',
+    messages: {
+      unexpected: 'Unexpected `v-bind` with a string literal value.'
+    },
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          ignoreIncludesComment: {
+            type: 'boolean'
+          },
+          ignoreStringEscape: {
+            type: 'boolean'
+          }
+        }
+      }
+    ],
+    type: 'suggestion'
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    const opts = context.options[0] || {}
+    const ignoreIncludesComment = opts.ignoreIncludesComment
+    const ignoreStringEscape = opts.ignoreStringEscape
+    const sourceCode = context.getSourceCode()
+
+    /**
+     * Report if the value expression is string literals
+     * @param {VDirective} node the node to check
+     */
+    function verify(node) {
+      if (!node.value || node.key.modifiers.length) {
+        return
+      }
+      const { expression } = node.value
+      if (!expression) {
+        return
+      }
+      let strValue, rawValue
+      if (expression.type === 'Literal') {
+        if (typeof expression.value !== 'string') {
+          return
+        }
+        strValue = expression.value
+        rawValue = expression.raw.slice(1, -1)
+      } else if (expression.type === 'TemplateLiteral') {
+        if (expression.expressions.length > 0) {
+          return
+        }
+        strValue = expression.quasis[0].value.cooked
+        rawValue = expression.quasis[0].value.raw
+      } else {
+        return
+      }
+
+      const tokenStore = context.parserServices.getTemplateBodyTokenStore()
+      const hasComment = tokenStore
+        .getTokens(node.value, { includeComments: true })
+        .some((t) => t.type === 'Block' || t.type === 'Line')
+      if (ignoreIncludesComment && hasComment) {
+        return
+      }
+
+      let hasEscape = false
+      if (rawValue !== strValue) {
+        // check escapes
+        const chars = [...rawValue]
+        let c = chars.shift()
+        while (c) {
+          if (c === '\\') {
+            c = chars.shift()
+            if (
+              c == null ||
+              // ignore "\\", '"', "'", "`" and "$"
+              'nrvtbfux'.includes(c)
+            ) {
+              // has useful escape.
+              hasEscape = true
+              break
+            }
+          }
+          c = chars.shift()
+        }
+      }
+      if (ignoreStringEscape && hasEscape) {
+        return
+      }
+
+      context.report({
+        // @ts-ignore
+        node,
+        messageId: 'unexpected',
+        fix(fixer) {
+          if (hasComment || hasEscape) {
+            // cannot fix
+            return null
+          }
+          const text = sourceCode.getText(node.value)
+          const quoteChar = text[0]
+
+          const shorthand = node.key.name.rawName === ':'
+          /** @type { [number, number] } */
+          const keyDirectiveRange = [
+            node.key.name.range[0],
+            node.key.name.range[1] + (shorthand ? 0 : 1)
+          ]
+
+          let attrValue
+          if (quoteChar === '"') {
+            attrValue = strValue.replace(DOUBLE_QUOTES_RE, '&quot;')
+          } else if (quoteChar === "'") {
+            attrValue = strValue.replace(SINGLE_QUOTES_RE, '&apos;')
+          } else {
+            attrValue = strValue
+              .replace(DOUBLE_QUOTES_RE, '&quot;')
+              .replace(SINGLE_QUOTES_RE, '&apos;')
+          }
+          return [
+            fixer.removeRange(keyDirectiveRange),
+            fixer.replaceText(expression, attrValue)
+          ]
+        }
+      })
+    }
+
+    return utils.defineTemplateBodyVisitor(context, {
+      "VAttribute[directive=true][key.name.name='bind'][key.argument!=null]": verify
+    })
+  }
+}
diff --git a/lib/rules/order-in-components.js b/lib/rules/order-in-components.js
index d26fa7d1..2366cabb 100644
--- a/lib/rules/order-in-components.js
+++ b/lib/rules/order-in-components.js
@@ -64,7 +64,7 @@ function isComma (node) {
   return node.type === 'Punctuator' && node.value === ','
 }
 
-const ARITHMETIC_OPERATORS = ['+', '-', '*', '/', '%', '**']
+const ARITHMETIC_OPERATORS = ['+', '-', '*', '/', '%', '**'/* es2016 */]
 const BITWISE_OPERATORS = ['&', '|', '^', '~', '<<', '>>', '>>>']
 const COMPARISON_OPERATORS = ['==', '!=', '===', '!==', '>', '>=', '<', '<=']
 const RELATIONAL_OPERATORS = ['in', 'instanceof']
@@ -74,7 +74,7 @@ const ALL_BINARY_OPERATORS = [].concat(
   COMPARISON_OPERATORS,
   RELATIONAL_OPERATORS
 )
-const LOGICAL_OPERATORS = ['&&', '||']
+const LOGICAL_OPERATORS = ['&&', '||', '??'/* es2020 */]
 
 /*
  * Result `true` if the node is sure that there are no side effects
@@ -94,17 +94,15 @@ const LOGICAL_OPERATORS = ['&&', '||']
  */
 function isNotSideEffectsNode (node, visitorKeys) {
   let result = true
-  const noSideEffectsNodes = new Set()
+  let skipNode = false
   traverseNodes(node, {
     visitorKeys,
-    enterNode (node, parent) {
-      if (!result) {
+    enterNode (node) {
+      if (!result || skipNode) {
         return
       }
 
       if (
-        // parent has no side effects
-        noSideEffectsNodes.has(parent) ||
         // no side effects node
         node.type === 'FunctionExpression' ||
         node.type === 'Identifier' ||
@@ -113,7 +111,7 @@ function isNotSideEffectsNode (node, visitorKeys) {
         node.type === 'ArrowFunctionExpression' ||
         node.type === 'TemplateElement'
       ) {
-        noSideEffectsNodes.add(node)
+        skipNode = node
       } else if (
         node.type !== 'Property' &&
         node.type !== 'ObjectExpression' &&
@@ -131,7 +129,11 @@ function isNotSideEffectsNode (node, visitorKeys) {
         result = false
       }
     },
-    leaveNode () {}
+    leaveNode (node) {
+      if (skipNode === node) {
+        skipNode = null
+      }
+    }
   })
 
   return result
diff --git a/lib/rules/require-slots-as-functions.js b/lib/rules/require-slots-as-functions.js
new file mode 100644
index 00000000..07493ddb
--- /dev/null
+++ b/lib/rules/require-slots-as-functions.js
@@ -0,0 +1,124 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+const { findVariable } = require('eslint-utils')
+
+/**
+ * @typedef {import('vue-eslint-parser').AST.ESLintMemberExpression} MemberExpression
+ * @typedef {import('vue-eslint-parser').AST.ESLintIdentifier} Identifier
+ * @typedef {import('vue-eslint-parser').AST.ESLintExpression} Expression
+ */
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'enforce properties of `$slots` to be used as a function',
+      categories: ['vue3-essential'],
+      url: 'https://eslint.vuejs.org/rules/require-slots-as-functions.html'
+    },
+    fixable: null,
+    schema: [],
+    messages: {
+      unexpected: 'Property in `$slots` should be used as function.'
+    }
+  },
+
+  create(context) {
+    /**
+     * Verify the given node
+     * @param {MemberExpression | Identifier} node The node to verify
+     * @param {Expression} reportNode The node to report
+     */
+    function verify(node, reportNode) {
+      const parent = node.parent
+
+      if (
+        parent.type === 'VariableDeclarator' &&
+        parent.id.type === 'Identifier'
+      ) {
+        // const children = this.$slots.foo
+        verifyReferences(parent.id, reportNode)
+        return
+      }
+
+      if (
+        parent.type === 'AssignmentExpression' &&
+        parent.right === node &&
+        parent.left.type === 'Identifier'
+      ) {
+        // children = this.$slots.foo
+        verifyReferences(parent.left, reportNode)
+        return
+      }
+
+      if (
+        // this.$slots.foo.xxx
+        parent.type === 'MemberExpression' ||
+        // var [foo] = this.$slots.foo
+        parent.type === 'VariableDeclarator' ||
+        // [...this.$slots.foo]
+        parent.type === 'SpreadElement' ||
+        // [this.$slots.foo]
+        parent.type === 'ArrayExpression'
+      ) {
+        context.report({
+          node: reportNode,
+          messageId: 'unexpected'
+        })
+      }
+    }
+    /**
+     * Verify the references of the given node.
+     * @param {Identifier} node The node to verify
+     * @param {Expression} reportNode The node to report
+     */
+    function verifyReferences(node, reportNode) {
+      // @ts-ignore
+      const variable = findVariable(context.getScope(), node)
+      if (!variable) {
+        return
+      }
+      for (const reference of variable.references) {
+        if (!reference.isRead()) {
+          continue
+        }
+        /** @type {Identifier} */
+        const id = reference.identifier
+        verify(id, reportNode)
+      }
+    }
+
+    return utils.defineVueVisitor(context, {
+      /** @param {MemberExpression} node */
+      MemberExpression(node) {
+        const object = node.object
+        if (object.type !== 'MemberExpression') {
+          return
+        }
+        if (
+          object.property.type !== 'Identifier' ||
+          object.property.name !== '$slots'
+        ) {
+          return
+        }
+        if (!utils.isThis(object.object, context)) {
+          return
+        }
+        verify(node, node.property)
+      }
+    })
+  }
+}
diff --git a/lib/rules/valid-v-bind-sync.js b/lib/rules/valid-v-bind-sync.js
index 9d24442a..6b922530 100644
--- a/lib/rules/valid-v-bind-sync.js
+++ b/lib/rules/valid-v-bind-sync.js
@@ -37,10 +37,7 @@ function isValidElement(node) {
  * @returns {boolean} `true` if the node can be LHS.
  */
 function isLhs(node) {
-  return (
-    Boolean(node) &&
-    (node.type === 'Identifier' || node.type === 'MemberExpression')
-  )
+  return node.type === 'Identifier' || node.type === 'MemberExpression'
 }
 
 // ------------------------------------------------------------------------------
@@ -85,30 +82,32 @@ module.exports = {
           })
         }
 
-        if (node.value) {
-          if (!isLhs(node.value.expression)) {
+        if (!node.value || !node.value.expression) {
+          return
+        }
+
+        if (!isLhs(node.value.expression)) {
+          context.report({
+            node,
+            loc: node.loc,
+            messageId: 'unexpectedNonLhsExpression'
+          })
+        }
+
+        for (const reference of node.value.references) {
+          const id = reference.id
+          if (id.parent.type !== 'VExpressionContainer') {
+            continue
+          }
+          const variable = reference.variable
+          if (variable) {
             context.report({
               node,
               loc: node.loc,
-              messageId: 'unexpectedNonLhsExpression'
+              messageId: 'unexpectedUpdateIterationVariable',
+              data: { varName: id.name }
             })
           }
-
-          for (const reference of node.value.references) {
-            const id = reference.id
-            if (id.parent.type !== 'VExpressionContainer') {
-              continue
-            }
-            const variable = reference.variable
-            if (variable) {
-              context.report({
-                node,
-                loc: node.loc,
-                messageId: 'unexpectedUpdateIterationVariable',
-                data: { varName: id.name }
-              })
-            }
-          }
         }
       }
     })
diff --git a/lib/rules/valid-v-bind.js b/lib/rules/valid-v-bind.js
index dfb1f97e..7d88bad3 100644
--- a/lib/rules/valid-v-bind.js
+++ b/lib/rules/valid-v-bind.js
@@ -48,7 +48,7 @@ module.exports = {
           }
         }
 
-        if (!utils.hasAttributeValue(node)) {
+        if (!node.value || utils.isEmptyValueDirective(node, context)) {
           context.report({
             node,
             loc: node.loc,
diff --git a/lib/rules/valid-v-else-if.js b/lib/rules/valid-v-else-if.js
index b68caa74..59252855 100644
--- a/lib/rules/valid-v-else-if.js
+++ b/lib/rules/valid-v-else-if.js
@@ -70,7 +70,7 @@ module.exports = {
             message: "'v-else-if' directives require no modifier."
           })
         }
-        if (!utils.hasAttributeValue(node)) {
+        if (!node.value || utils.isEmptyValueDirective(node, context)) {
           context.report({
             node,
             loc: node.loc,
diff --git a/lib/rules/valid-v-else.js b/lib/rules/valid-v-else.js
index 3379fa32..2479f467 100644
--- a/lib/rules/valid-v-else.js
+++ b/lib/rules/valid-v-else.js
@@ -70,7 +70,7 @@ module.exports = {
             message: "'v-else' directives require no modifier."
           })
         }
-        if (utils.hasAttributeValue(node)) {
+        if (node.value) {
           context.report({
             node,
             loc: node.loc,
diff --git a/lib/rules/valid-v-for.js b/lib/rules/valid-v-for.js
index 681a5dcf..443afc2d 100644
--- a/lib/rules/valid-v-for.js
+++ b/lib/rules/valid-v-for.js
@@ -137,7 +137,7 @@ module.exports = {
             message: "'v-for' directives require no modifier."
           })
         }
-        if (!utils.hasAttributeValue(node)) {
+        if (!node.value || utils.isEmptyValueDirective(node, context)) {
           context.report({
             node,
             loc: node.loc,
diff --git a/lib/rules/valid-v-html.js b/lib/rules/valid-v-html.js
index 1bc1fcb2..575939a2 100644
--- a/lib/rules/valid-v-html.js
+++ b/lib/rules/valid-v-html.js
@@ -44,7 +44,7 @@ module.exports = {
             message: "'v-html' directives require no modifier."
           })
         }
-        if (!utils.hasAttributeValue(node)) {
+        if (!node.value || utils.isEmptyValueDirective(node, context)) {
           context.report({
             node,
             loc: node.loc,
diff --git a/lib/rules/valid-v-if.js b/lib/rules/valid-v-if.js
index 6c24cabc..c07ae2c8 100644
--- a/lib/rules/valid-v-if.js
+++ b/lib/rules/valid-v-if.js
@@ -62,7 +62,7 @@ module.exports = {
             message: "'v-if' directives require no modifier."
           })
         }
-        if (!utils.hasAttributeValue(node)) {
+        if (!node.value || utils.isEmptyValueDirective(node, context)) {
           context.report({
             node,
             loc: node.loc,
diff --git a/lib/rules/valid-v-model.js b/lib/rules/valid-v-model.js
index 7dc22a4d..0c7431c0 100644
--- a/lib/rules/valid-v-model.js
+++ b/lib/rules/valid-v-model.js
@@ -42,10 +42,7 @@ function isValidElement(node) {
  * @returns {boolean} `true` if the node can be LHS.
  */
 function isLhs(node) {
-  return (
-    node != null &&
-    (node.type === 'Identifier' || node.type === 'MemberExpression')
-  )
+  return node.type === 'Identifier' || node.type === 'MemberExpression'
 }
 
 /**
@@ -133,40 +130,43 @@ module.exports = {
           }
         }
 
-        if (!utils.hasAttributeValue(node)) {
+        if (!node.value || utils.isEmptyValueDirective(node, context)) {
           context.report({
             node,
             loc: node.loc,
             message: "'v-model' directives require that attribute value."
           })
+          return
+        }
+        if (!node.value.expression) {
+          // Parsing error
+          return
         }
-        if (node.value) {
-          if (!isLhs(node.value.expression)) {
+        if (!isLhs(node.value.expression)) {
+          context.report({
+            node,
+            loc: node.loc,
+            message:
+              "'v-model' directives require the attribute value which is valid as LHS."
+          })
+        }
+
+        for (const reference of node.value.references) {
+          const id = reference.id
+          if (id.parent.type !== 'VExpressionContainer') {
+            continue
+          }
+
+          const variable = getVariable(id.name, element)
+          if (variable != null) {
             context.report({
               node,
               loc: node.loc,
               message:
-                "'v-model' directives require the attribute value which is valid as LHS."
+                "'v-model' directives cannot update the iteration variable '{{varName}}' itself.",
+              data: { varName: id.name }
             })
           }
-
-          for (const reference of node.value.references) {
-            const id = reference.id
-            if (id.parent.type !== 'VExpressionContainer') {
-              continue
-            }
-
-            const variable = getVariable(id.name, element)
-            if (variable != null) {
-              context.report({
-                node,
-                loc: node.loc,
-                message:
-                  "'v-model' directives cannot update the iteration variable '{{varName}}' itself.",
-                data: { varName: id.name }
-              })
-            }
-          }
         }
       }
     })
diff --git a/lib/rules/valid-v-on.js b/lib/rules/valid-v-on.js
index 4e014e00..bcdb76db 100644
--- a/lib/rules/valid-v-on.js
+++ b/lib/rules/valid-v-on.js
@@ -108,20 +108,30 @@ module.exports = {
         }
 
         if (
-          !utils.hasAttributeValue(node) &&
+          (!node.value || !node.value.expression) &&
           !node.key.modifiers.some((modifier) =>
             VERB_MODIFIERS.has(modifier.name)
           )
         ) {
-          if (node.value && sourceCode.getText(node.value.expression)) {
-            const value = sourceCode.getText(node.value)
-            context.report({
-              node,
-              loc: node.loc,
-              message:
-                'Avoid using JavaScript keyword as "v-on" value: {{value}}.',
-              data: { value }
-            })
+          if (node.value && !utils.isEmptyValueDirective(node, context)) {
+            const valueText = sourceCode.getText(node.value)
+            let innerText = valueText
+            if (
+              (valueText[0] === '"' || valueText[0] === "'") &&
+              valueText[0] === valueText[valueText.length - 1]
+            ) {
+              // quoted
+              innerText = valueText.slice(1, -1)
+            }
+            if (/^\w+$/.test(innerText)) {
+              context.report({
+                node,
+                loc: node.loc,
+                message:
+                  'Avoid using JavaScript keyword as "v-on" value: {{value}}.',
+                data: { value: valueText }
+              })
+            }
           } else {
             context.report({
               node,
diff --git a/lib/rules/valid-v-show.js b/lib/rules/valid-v-show.js
index 51e4014f..71553722 100644
--- a/lib/rules/valid-v-show.js
+++ b/lib/rules/valid-v-show.js
@@ -44,7 +44,7 @@ module.exports = {
             message: "'v-show' directives require no modifier."
           })
         }
-        if (!utils.hasAttributeValue(node)) {
+        if (!node.value || utils.isEmptyValueDirective(node, context)) {
           context.report({
             node,
             loc: node.loc,
diff --git a/lib/rules/valid-v-slot.js b/lib/rules/valid-v-slot.js
index 79b05226..118dc846 100644
--- a/lib/rules/valid-v-slot.js
+++ b/lib/rules/valid-v-slot.js
@@ -264,7 +264,9 @@ module.exports = {
         if (
           ownerElement === element &&
           isDefaultSlot &&
-          !utils.hasAttributeValue(node)
+          (!node.value ||
+            utils.isEmptyValueDirective(node, context) ||
+            utils.isEmptyExpressionValueDirective(node, context))
         ) {
           context.report({
             node,
diff --git a/lib/rules/valid-v-text.js b/lib/rules/valid-v-text.js
index 2e1881ce..f0a3f09f 100644
--- a/lib/rules/valid-v-text.js
+++ b/lib/rules/valid-v-text.js
@@ -44,7 +44,7 @@ module.exports = {
             message: "'v-text' directives require no modifier."
           })
         }
-        if (!utils.hasAttributeValue(node)) {
+        if (!node.value || utils.isEmptyValueDirective(node, context)) {
           context.report({
             node,
             loc: node.loc,
diff --git a/lib/utils/index.js b/lib/utils/index.js
index 43c919f8..b61b5292 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -290,16 +290,73 @@ module.exports = {
   },
 
   /**
-   * Check whether the given attribute has their attribute value.
-   * @param {ASTNode} node The attribute node to check.
-   * @returns {boolean} `true` if the attribute has their value.
+   * Check whether the given directive attribute has their empty value (`=""`).
+   * @param {ASTNode} node The directive attribute node to check.
+   * @param {RuleContext} context The rule context to use parser services.
+   * @returns {boolean} `true` if the directive attribute has their empty value (`=""`).
    */
-  hasAttributeValue (node) {
+  isEmptyValueDirective (node, context) {
     assert(node && node.type === 'VAttribute')
-    return (
-      node.value != null &&
-      (node.value.expression != null || node.value.syntaxError != null)
-    )
+    if (node.value == null) {
+      return false
+    }
+    if (node.value.expression != null) {
+      return false
+    }
+
+    let valueText = context.getSourceCode().getText(node.value)
+    if ((valueText[0] === '"' || valueText[0] === "'") && valueText[0] === valueText[valueText.length - 1]) {
+      // quoted
+      valueText = valueText.slice(1, -1)
+    }
+    if (!valueText) {
+      // empty
+      return true
+    }
+    return false
+  },
+
+  /**
+   * Check whether the given directive attribute has their empty expression value (e.g. `=" "`, `="/* &ast;/"`).
+   * @param {ASTNode} node The directive attribute node to check.
+   * @param {RuleContext} context The rule context to use parser services.
+   * @returns {boolean} `true` if the directive attribute has their empty expression value.
+   */
+  isEmptyExpressionValueDirective (node, context) {
+    assert(node && node.type === 'VAttribute')
+    if (node.value == null) {
+      return false
+    }
+    if (node.value.expression != null) {
+      return false
+    }
+
+    const valueNode = node.value
+    const tokenStore = context.parserServices.getTemplateBodyTokenStore()
+    let quote1 = null
+    let quote2 = null
+    // `node.value` may be only comments, so cannot get the correct tokens with `tokenStore.getTokens(node.value)`.
+    for (const token of tokenStore.getTokens(node)) {
+      if (token.range[1] <= valueNode.range[0]) {
+        continue
+      }
+      if (valueNode.range[1] <= token.range[0]) {
+        // empty
+        return true
+      }
+      if (!quote1 && token.type === 'Punctuator' && (token.value === '"' || token.value === "'")) {
+        quote1 = token
+        continue
+      }
+      if (!quote2 && quote1 && token.type === 'Punctuator' && (token.value === quote1.value)) {
+        quote2 = token
+        continue
+      }
+      // not empty
+      return false
+    }
+    // empty
+    return true
   },
 
   /**
diff --git a/lib/utils/regexp.js b/lib/utils/regexp.js
index cdb4c3af..073a99f8 100644
--- a/lib/utils/regexp.js
+++ b/lib/utils/regexp.js
@@ -22,7 +22,7 @@ function escape(string) {
  * Strings like `"/^foo/i"` are converted to `/^foo/i` of `RegExp`.
  *
  * @param {string} string The string to convert.
- * @returns {string} Returns the `RegExp`.
+ * @returns {RegExp} Returns the `RegExp`.
  */
 function toRegExp(string) {
   const parts = RE_REGEXP_STR.exec(string)
@@ -32,7 +32,17 @@ function toRegExp(string) {
   return new RegExp(`^${escape(string)}$`)
 }
 
+/**
+ * Checks whether given string is regexp string
+ * @param {string} string
+ * @returns {boolean}
+ */
+function isRegExp(string) {
+  return Boolean(RE_REGEXP_STR.exec(string))
+}
+
 module.exports = {
   escape,
-  toRegExp
+  toRegExp,
+  isRegExp
 }
diff --git a/package.json b/package.json
index 1c6347af..18b2c893 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,8 @@
     "test:base": "mocha \"tests/lib/**/*.js\" --reporter dot",
     "test": "nyc npm run test:base -- \"tests/integrations/*.js\" --timeout 60000",
     "debug": "mocha --inspect-brk \"tests/lib/**/*.js\" --reporter dot --timeout 60000",
+    "cover": "npm run cover:test && npm run cover:report",
+    "cover:test": "nyc npm run test:base -- --timeout 60000",
     "cover:report": "nyc report --reporter=html",
     "lint": "eslint . --rulesdir eslint-internal-rules",
     "lint:fix": "eslint . --rulesdir eslint-internal-rules --fix",


Test patch:
diff --git a/tests/lib/rules/no-bare-strings-in-template.js b/tests/lib/rules/no-bare-strings-in-template.js
new file mode 100644
index 00000000..3fc4a432
--- /dev/null
+++ b/tests/lib/rules/no-bare-strings-in-template.js
@@ -0,0 +1,290 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-bare-strings-in-template')
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
+})
+
+tester.run('no-bare-strings-in-template', rule, {
+  valid: [
+    `
+    <template>
+      <h1>{{ $t('foo.bar') }}</h1>
+      <h1>{{ foo }}</h1>
+      <h1 v-t="'foo.bar'"></h1>
+    </template>
+    `,
+    `
+    <template>
+      <h1 v-text="'Lorem ipsum' + foo" />
+      <h1 v-text="foo" />
+      <h1 v-text="1" />
+      <h1 v-text="true" />
+      <h1 v-text />
+      <h1 v-text="" />
+    </template>
+    `,
+    `
+    <template>
+      <h1>#</h1>
+      <div title="#" />
+    </template>
+    `,
+    `
+    <template>
+      <div>(</div>
+      <div>)</div>
+      <div>,</div>
+      <div>.</div>
+      <div>&</div>
+      <div>+</div>
+      <div>-</div>
+      <div>=</div>
+      <div>*</div>
+      <div>/</div>
+      <div>#</div>
+      <div>%</div>
+      <div>!</div>
+      <div>?</div>
+      <div>:</div>
+      <div>[</div>
+      <div>]</div>
+      <div>{</div>
+      <div>}</div>
+      <div title="<"></div>
+      <div title=">"></div>
+      <div>•</div>
+      <div>—</div>
+      <div> </div>
+      <div>|</div>
+      <div>&lpar;</div>
+      <div>&rpar;</div>
+      <div>&comma;</div>
+      <div>&period;</div>
+      <div>&amp;</div>
+      <div>&AMP;</div>
+      <div>&plus;</div>
+      <div>&minus;</div>
+      <div>&equals;</div>
+      <div>&ast;</div>
+      <div>&midast;</div>
+      <div>&sol;</div>
+      <div>&num;</div>
+      <div>&percnt;</div>
+      <div>&excl;</div>
+      <div>&quest;</div>
+      <div>&colon;</div>
+      <div>&lsqb;</div>
+      <div>&lbrack;</div>
+      <div>&rsqb;</div>
+      <div>&rbrack;</div>
+      <div>&lcub;</div>
+      <div>&lbrace;</div>
+      <div>&rcub;</div>
+      <div>&rbrace;</div>
+      <div>&lt;</div>
+      <div>&LT;</div>
+      <div>&gt;</div>
+      <div>&GT;</div>
+      <div>&bull;</div>
+      <div>&bullet;</div>
+      <div>&mdash;</div>
+      <div>&ndash;</div>
+      <div>&nbsp;</div>
+      <div>&Tab;</div>
+      <div>&NewLine;</div>
+      <div>&verbar;</div>
+      <div>&vert;</div>
+      <div>&VerticalLine;</div>
+
+      <div>
+        ( ) , . & + - = * / # % ! ? : [ ] { }   • —   | &lpar; &rpar; &comma; &period; &amp; &AMP; &plus; &minus; &equals; &ast; &midast; &sol; &num; &percnt; &excl; &quest; &colon; &lsqb; &lbrack; &rsqb; &rbrack; &lcub; &lbrace; &rcub; &rbrace; &lt; &LT; &gt; &GT; &bull; &bullet; &mdash; &ndash; &nbsp; &Tab; &NewLine; &verbar; &vert; &VerticalLine;
+      </div>
+
+      <div
+        title="( ) , . & + - = * / # % ! ? : [ ] { } < > • —   | &lpar; &rpar; &comma; &period; &amp; &AMP; &plus; &minus; &equals; &ast; &midast; &sol; &num; &percnt; &excl; &quest; &colon; &lsqb; &lbrack; &rsqb; &rbrack; &lcub; &lbrace; &rcub; &rbrace; &lt; &LT; &gt; &GT; &bull; &bullet; &mdash; &ndash; &nbsp; &Tab; &NewLine; &verbar; &vert; &VerticalLine;"
+      />
+    </template>
+    `
+  ],
+  invalid: [
+    {
+      code: `
+      <template>
+        <h1>Lorem ipsum</h1>
+        <div
+          title="Lorem ipsum"
+          aria-label="Lorem ipsum"
+          aria-placeholder="Lorem ipsum"
+          aria-roledescription="Lorem ipsum"
+          aria-valuetext="Lorem ipsum"
+        />
+        <img alt="Lorem ipsum">
+        <input placeholder="Lorem ipsum">
+        <h1 v-text="'Lorem ipsum'" />
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Unexpected non-translated string used.',
+          line: 3,
+          column: 13,
+          endLine: 3,
+          endColumn: 24
+        },
+        {
+          message: 'Unexpected non-translated string used in `title`.',
+          line: 5,
+          column: 17,
+          endLine: 5,
+          endColumn: 30
+        },
+        {
+          message: 'Unexpected non-translated string used in `aria-label`.',
+          line: 6,
+          column: 22,
+          endLine: 6,
+          endColumn: 35
+        },
+        {
+          message:
+            'Unexpected non-translated string used in `aria-placeholder`.',
+          line: 7,
+          column: 28,
+          endLine: 7,
+          endColumn: 41
+        },
+        {
+          message:
+            'Unexpected non-translated string used in `aria-roledescription`.',
+          line: 8,
+          column: 32,
+          endLine: 8,
+          endColumn: 45
+        },
+        {
+          message: 'Unexpected non-translated string used in `aria-valuetext`.',
+          line: 9,
+          column: 26,
+          endLine: 9,
+          endColumn: 39
+        },
+        {
+          message: 'Unexpected non-translated string used in `alt`.',
+          line: 11,
+          column: 18,
+          endLine: 11,
+          endColumn: 31
+        },
+        {
+          message: 'Unexpected non-translated string used in `placeholder`.',
+          line: 12,
+          column: 28,
+          endLine: 12,
+          endColumn: 41
+        },
+        {
+          message: 'Unexpected non-translated string used in `v-text`.',
+          line: 13,
+          column: 20,
+          endLine: 13,
+          endColumn: 35
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+        <h1>Lorem</h1>
+        <h1>Lorem ipsum</h1>
+        <h1>ipsum</h1>
+      </template>
+      `,
+      options: [{ whitelist: ['Lorem'] }],
+      errors: [
+        {
+          line: 4,
+          column: 13,
+          messageId: 'unexpected',
+          endLine: 4,
+          endColumn: 24
+        },
+        {
+          line: 5,
+          column: 13,
+          messageId: 'unexpected',
+          endLine: 5,
+          endColumn: 18
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+        <h1 foo="Lorem ipsum" />
+        <h1 bar="Lorem ipsum" />
+        <h2 foo="Lorem ipsum" />
+        <h2 bar="Lorem ipsum" />
+        <div foo="Lorem ipsum" />
+        <div bar="Lorem ipsum" />
+      </template>
+      `,
+      options: [{ attributes: { '/^h\\d$/': ['foo'], h1: ['bar'] } }],
+      errors: [
+        {
+          message: 'Unexpected non-translated string used in `foo`.',
+          line: 3,
+          column: 17,
+          endLine: 3,
+          endColumn: 30
+        },
+        {
+          message: 'Unexpected non-translated string used in `bar`.',
+          line: 4,
+          column: 17,
+          endLine: 4,
+          endColumn: 30
+        },
+        {
+          message: 'Unexpected non-translated string used in `foo`.',
+          line: 5,
+          column: 17,
+          endLine: 5,
+          endColumn: 30
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+        <h1 v-foo="'Lorem ipsum'" />
+        <h1 v-bar="'Lorem ipsum'" />
+      </template>
+      `,
+      options: [{ directives: ['v-foo'] }],
+      errors: [
+        {
+          message: 'Unexpected non-translated string used in `v-foo`.',
+          line: 3,
+          column: 19,
+          endLine: 3,
+          endColumn: 34
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-deprecated-dollar-scopedslots-api.js b/tests/lib/rules/no-deprecated-dollar-scopedslots-api.js
new file mode 100644
index 00000000..303e5de9
--- /dev/null
+++ b/tests/lib/rules/no-deprecated-dollar-scopedslots-api.js
@@ -0,0 +1,288 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/no-deprecated-dollar-scopedslots-api')
+
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2018, sourceType: 'module' }
+})
+ruleTester.run('no-deprecated-dollar-scopedslots-api', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div v-bind="$attrs"/>
+        </template>
+        <script>
+        export default {
+          mounted () {
+            this.$emit('start')
+          }
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+          methods: {
+            click () {
+              this.$emit('click')
+            }
+          }
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+        }
+        const another = function () {
+          console.log(this.$scopedSlots)
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div foo="$scopedSlots"/>
+        </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div v-on="() => {
+            function click ($scopedSlots) {
+              fn(foo.$scopedSlots)
+              fn($scopedSlots)
+            }
+          }"/>
+          <div v-for="$scopedSlots in list">
+            <div v-on="$scopedSlots">
+          </div>
+          <VueComp>
+            <template v-slot="{$scopedSlots}">
+              <div v-on="$scopedSlots">
+            </template>
+          </VueComp>
+        </template>
+        <script>
+        export default {
+          methods: {
+            click ($scopedSlots) {
+              foo.$scopedSlots
+            }
+          }
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+          computed: {
+            foo () {
+              const {vm} = this
+              return vm.$scopedSlots
+            }
+          }
+        }
+        </script>
+      `
+    }
+  ],
+
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div v-if="$scopedSlots.default"/>
+        </template>
+        <script>
+        export default {
+          render() {
+            return this.$scopedSlots.foo('bar')
+          }
+        }
+        </script>
+      `,
+      output: `
+        <template>
+          <div v-if="$slots.default"/>
+        </template>
+        <script>
+        export default {
+          render() {
+            return this.$slots.foo('bar')
+          }
+        }
+        </script>
+      `,
+      errors: [
+        {
+          line: 3,
+          column: 22,
+          messageId: 'deprecated',
+          endLine: 3,
+          endColumn: 34
+        },
+        {
+          line: 8,
+          column: 25,
+          messageId: 'deprecated',
+          endLine: 8,
+          endColumn: 37
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div v-for="slot in $scopedSlots"/>
+          <div :foo="$scopedSlots"/>
+        </template>
+        <script>
+        export default {
+          computed: {
+            foo () {
+              fn(this.$scopedSlots)
+            }
+          }
+        }
+        </script>
+      `,
+      output: `
+        <template>
+          <div v-for="slot in $slots"/>
+          <div :foo="$slots"/>
+        </template>
+        <script>
+        export default {
+          computed: {
+            foo () {
+              fn(this.$slots)
+            }
+          }
+        }
+        </script>
+      `,
+      errors: [
+        {
+          line: 3,
+          column: 31,
+          messageId: 'deprecated',
+          endLine: 3,
+          endColumn: 43
+        },
+        {
+          line: 4,
+          column: 22,
+          messageId: 'deprecated',
+          endLine: 4,
+          endColumn: 34
+        },
+        {
+          line: 10,
+          column: 23,
+          messageId: 'deprecated',
+          endLine: 10,
+          endColumn: 35
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+          render() {
+            const vm = this
+            return vm.$scopedSlots.foo('bar')
+          }
+        }
+        </script>
+      `,
+      output: `
+        <script>
+        export default {
+          render() {
+            const vm = this
+            return vm.$slots.foo('bar')
+          }
+        }
+        </script>
+      `,
+      errors: [
+        {
+          line: 6,
+          column: 23,
+          messageId: 'deprecated'
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+          render() {
+            const vm = this
+            function fn() {
+              return vm.$scopedSlots
+            }
+            return fn().foo('bar')
+          }
+        }
+        </script>
+      `,
+      output: `
+        <script>
+        export default {
+          render() {
+            const vm = this
+            function fn() {
+              return vm.$slots
+            }
+            return fn().foo('bar')
+          }
+        }
+        </script>
+      `,
+      errors: [
+        {
+          line: 7,
+          column: 25,
+          messageId: 'deprecated'
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-multiple-slot-args.js b/tests/lib/rules/no-multiple-slot-args.js
new file mode 100644
index 00000000..fb42767b
--- /dev/null
+++ b/tests/lib/rules/no-multiple-slot-args.js
@@ -0,0 +1,164 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/no-multiple-slot-args')
+
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2018, sourceType: 'module' }
+})
+ruleTester.run('no-multiple-slot-args', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        render (h) {
+          var children = this.$scopedSlots.default()
+          var children = this.$scopedSlots.foo(foo)
+          const bar = this.$scopedSlots.bar
+          bar(foo)
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        render (h) {
+          unknown.$scopedSlots.default(foo, bar)
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        render (h) {
+          // for Vue3
+          var children = this.$slots.default()
+          var children = this.$slots.foo(foo)
+          const bar = this.$slots.bar
+          bar(foo)
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        render (h) {
+          this.$foo.default(foo, bar)
+        }
+      }
+      </script>
+      `
+    }
+  ],
+
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        render (h) {
+          this.$scopedSlots.default(foo, bar)
+          this.$scopedSlots.foo(foo, bar)
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message: 'Unexpected multiple arguments.',
+          line: 5,
+          column: 42,
+          endLine: 5,
+          endColumn: 45
+        },
+        {
+          message: 'Unexpected multiple arguments.',
+          line: 6,
+          column: 38,
+          endLine: 6,
+          endColumn: 41
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        render (h) {
+          let children
+
+          this.$scopedSlots.default(foo, { bar })
+
+          children = this.$scopedSlots.foo
+          if (children) children(...foo)
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message: 'Unexpected multiple arguments.',
+          line: 7,
+          column: 42,
+          endLine: 7,
+          endColumn: 49
+        },
+        {
+          message: 'Unexpected spread argument.',
+          line: 10,
+          column: 34,
+          endLine: 10,
+          endColumn: 40
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        render (h) {
+          // for Vue3
+          this.$slots.default(foo, bar)
+          this.$slots.foo(foo, bar)
+        }
+      }
+      </script>
+      `,
+      errors: [
+        'Unexpected multiple arguments.',
+        'Unexpected multiple arguments.'
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-ref-as-operand.js b/tests/lib/rules/no-ref-as-operand.js
index 6eeb11b5..5d53a557 100644
--- a/tests/lib/rules/no-ref-as-operand.js
+++ b/tests/lib/rules/no-ref-as-operand.js
@@ -103,7 +103,25 @@ tester.run('no-ref-as-operand', rule, {
     import { ref } from 'vue'
     const count = ref
     count++
-    `
+    `,
+    {
+      code: `
+      <script>
+        import { ref, computed, toRef, customRef, shallowRef } from 'vue'
+        const foo = shallowRef({})
+        foo[bar] = 123
+      </script>
+      `
+    },
+    {
+      code: `
+      <script>
+        import { ref, computed, toRef, customRef, shallowRef } from 'vue'
+        const foo = shallowRef({})
+        const isComp = foo.effect
+      </script>
+      `
+    }
   ],
   invalid: [
     {
@@ -332,6 +350,89 @@ tester.run('no-ref-as-operand', rule, {
           line: 9
         }
       ]
+    },
+    {
+      code: `
+      <script>
+        import { ref, computed, toRef, customRef, shallowRef } from 'vue'
+        let count = ref(0)
+        let cntcnt = computed(()=>count.value+count.value)
+
+        const state = reactive({
+          foo: 1,
+          bar: 2
+        })
+
+        const fooRef = toRef(state, 'foo')
+
+        let value = 'hello'
+        const cref = customRef((track, trigger) => {
+          return {
+            get() {
+              track()
+              return value
+            },
+            set(newValue) {
+              clearTimeout(timeout)
+              timeout = setTimeout(() => {
+                value = newValue
+                trigger()
+              }, delay)
+            }
+          }
+        })
+
+        const foo = shallowRef({})
+
+        count++ // error
+        cntcnt++ // error
+
+        const s = \`\${fooRef} : \${cref}\` // error x 2
+
+        const n = foo + 1 // error
+      </script>
+      `,
+      errors: [
+        {
+          message:
+            'Must use `.value` to read or write the value wrapped by `ref()`.',
+          line: 33
+        },
+        {
+          message:
+            'Must use `.value` to read or write the value wrapped by `computed()`.',
+          line: 34
+        },
+        {
+          message:
+            'Must use `.value` to read or write the value wrapped by `toRef()`.',
+          line: 36
+        },
+        {
+          message:
+            'Must use `.value` to read or write the value wrapped by `customRef()`.',
+          line: 36
+        },
+        {
+          message:
+            'Must use `.value` to read or write the value wrapped by `shallowRef()`.',
+          line: 38
+        }
+      ]
+    },
+    {
+      code: `
+      <script>
+        import { ref, computed, toRef, customRef, shallowRef } from 'vue'
+        const foo = shallowRef({})
+        foo.bar = 123
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'requireDotValue'
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/no-useless-v-bind.js b/tests/lib/rules/no-useless-v-bind.js
new file mode 100644
index 00000000..8362aa0b
--- /dev/null
+++ b/tests/lib/rules/no-useless-v-bind.js
@@ -0,0 +1,149 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-useless-v-bind.js')
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: 'module'
+  }
+})
+
+tester.run('no-useless-v-bind', rule, {
+  valid: [
+    `
+    <template>
+      <div id="foo" />
+      <div id='foo' />
+      <div id=foo />
+      <div :id="foo" />
+      <div :id="'foo' || 'bar'" />
+      <div :id="1" />
+      <div :id />
+      <div :id="{" />
+      <div :id="null" />
+    </template>`,
+    {
+      code: `
+      <template>
+        <div :id="'comment'/*comment*/" />
+        <div :id="'comment'//comment
+        " />
+      </template>
+      `,
+      options: [{ ignoreIncludesComment: true }]
+    },
+    {
+      code: `
+      <template>
+        <div :id="'\\n'" />
+        <div :id="'\\r'" />
+      </template>`,
+      options: [{ ignoreStringEscape: true }]
+    }
+  ],
+  invalid: [
+    {
+      code: `
+      <template>
+        <div :id="'foo'" />
+        <div v-bind:id="'foo'" />
+      </template>`,
+      output: `
+      <template>
+        <div id="foo" />
+        <div id="foo" />
+      </template>`,
+      errors: [
+        {
+          message: 'Unexpected `v-bind` with a string literal value.',
+          line: 3,
+          column: 14,
+          endLine: 3,
+          endColumn: 25
+        },
+        {
+          message: 'Unexpected `v-bind` with a string literal value.',
+          line: 4,
+          column: 14,
+          endLine: 4,
+          endColumn: 31
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+        <div :id="'comment'/*comment*/" />
+        <div :id="'comment'//comment
+        " />
+      </template>
+      `,
+      output: null,
+      errors: [
+        'Unexpected `v-bind` with a string literal value.',
+        'Unexpected `v-bind` with a string literal value.'
+      ]
+    },
+    {
+      code: `
+      <template>
+        <div :id="'\\n'" />
+        <div :id="'\\r'" />
+      </template>`,
+      output: null,
+      errors: [
+        'Unexpected `v-bind` with a string literal value.',
+        'Unexpected `v-bind` with a string literal value.'
+      ]
+    },
+    {
+      code: `
+      <template>
+        <div :id="'&quot;'" />
+        <div :id=\`&quot;&apos;\` />
+        <div :id="'\\\\'" />
+        <div :id="'\\\\r'" />
+        <div :id="'\\'" />
+        <div :id="\`foo\`" />
+        <div :id="\`foo\${bar}\`" />
+        <div :id='"&apos;"' />
+        <div :id=\`foo\` />
+      </template>`,
+      output: `
+      <template>
+        <div id="&quot;" />
+        <div id=&quot;&apos; />
+        <div id="\\" />
+        <div id="\\r" />
+        <div :id="'\\'" />
+        <div id="foo" />
+        <div :id="\`foo\${bar}\`" />
+        <div id='&apos;' />
+        <div id=foo />
+      </template>`,
+      errors: [
+        'Unexpected `v-bind` with a string literal value.',
+        'Unexpected `v-bind` with a string literal value.',
+        'Unexpected `v-bind` with a string literal value.',
+        'Unexpected `v-bind` with a string literal value.',
+        'Unexpected `v-bind` with a string literal value.',
+        'Unexpected `v-bind` with a string literal value.',
+        'Unexpected `v-bind` with a string literal value.'
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/order-in-components.js b/tests/lib/rules/order-in-components.js
index f94b4ec6..53823494 100644
--- a/tests/lib/rules/order-in-components.js
+++ b/tests/lib/rules/order-in-components.js
@@ -10,7 +10,7 @@ const RuleTester = require('eslint').RuleTester
 const ruleTester = new RuleTester()
 
 const parserOptions = {
-  ecmaVersion: 2018,
+  ecmaVersion: 2020,
   sourceType: 'module'
 }
 
@@ -751,6 +751,7 @@ ruleTester.run('order-in-components', rule, {
           testConditional: a ? b : c,
           testYield: function* () {},
           testTemplate: \`a:\${a},b:\${b},c:\${c}.\`,
+          testNullish: a ?? b,
           name: 'burger',
         };
       `,
@@ -768,11 +769,12 @@ ruleTester.run('order-in-components', rule, {
           testConditional: a ? b : c,
           testYield: function* () {},
           testTemplate: \`a:\${a},b:\${b},c:\${c}.\`,
+          testNullish: a ?? b,
         };
       `,
       errors: [{
         message: 'The "name" property should be above the "data" property on line 3.',
-        line: 13
+        line: 14
       }]
     }
   ]
diff --git a/tests/lib/rules/require-slots-as-functions.js b/tests/lib/rules/require-slots-as-functions.js
new file mode 100644
index 00000000..9d79a1ec
--- /dev/null
+++ b/tests/lib/rules/require-slots-as-functions.js
@@ -0,0 +1,115 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/require-slots-as-functions')
+
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2018, sourceType: 'module' }
+})
+ruleTester.run('require-slots-as-functions', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        render (h) {
+          var children = this.$slots.default()
+          var children = this.$slots.default && this.$slots.default()
+
+          return h('div', this.$slots.default)
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        render (h) {
+          var children = unknown.$slots.default
+          var children = unknown.$slots.default.filter(test)
+
+          return h('div', [...children])
+        }
+      }
+      </script>
+      `
+    }
+  ],
+
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        render (h) {
+          var children = this.$slots.default
+          var children = this.$slots.default.filter(test)
+
+          return h('div', [...children])
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message: 'Property in `$slots` should be used as function.',
+          line: 5,
+          column: 38,
+          endLine: 5,
+          endColumn: 45
+        },
+        {
+          message: 'Property in `$slots` should be used as function.',
+          line: 6,
+          column: 38,
+          endLine: 6,
+          endColumn: 45
+        }
+      ]
+    },
+
+    {
+      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))
+        }
+      }
+      </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.'
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/valid-v-bind-sync.js b/tests/lib/rules/valid-v-bind-sync.js
index 385f5d99..ea8bdf33 100644
--- a/tests/lib/rules/valid-v-bind-sync.js
+++ b/tests/lib/rules/valid-v-bind-sync.js
@@ -130,6 +130,21 @@ tester.run('valid-v-bind-sync', rule, {
     {
       filename: 'test.vue',
       code: '<template><MyComponent :foo.sync.unknown="foo" /></template>'
+    },
+    // parsing error
+    {
+      filename: 'parsing-error.vue',
+      code: '<template><MyComponent :foo.sync="." /></template>'
+    },
+    // comment value (parsing error)
+    {
+      filename: 'comment-value.vue',
+      code: '<template><MyComponent :foo.sync="/**/" /></template>'
+    },
+    // empty value (valid-v-bind)
+    {
+      filename: 'empty-value.vue',
+      code: '<template><MyComponent :foo.sync="" /></template>'
     }
   ],
   invalid: [
diff --git a/tests/lib/rules/valid-v-bind.js b/tests/lib/rules/valid-v-bind.js
index d98ff34d..b5e71af7 100644
--- a/tests/lib/rules/valid-v-bind.js
+++ b/tests/lib/rules/valid-v-bind.js
@@ -74,6 +74,16 @@ tester.run('valid-v-bind', rule, {
     {
       filename: 'test.vue',
       code: "<template><input v-bind='$attrs' /></template>"
+    },
+    // parsing error
+    {
+      filename: 'parsing-error.vue',
+      code: '<template><MyComponent :foo="." /></template>'
+    },
+    // comment value (parsing error)
+    {
+      filename: 'comment-value.vue',
+      code: '<template><MyComponent :foo="/**/" /></template>'
     }
   ],
   invalid: [
@@ -91,6 +101,12 @@ tester.run('valid-v-bind', rule, {
       filename: 'test.vue',
       code: "<template><div :aaa.unknown='bbb'></div></template>",
       errors: ["'v-bind' directives don't support the modifier 'unknown'."]
+    },
+    // empty value
+    {
+      filename: 'empty-value.vue',
+      code: '<template><MyComponent :foo="" /></template>',
+      errors: ["'v-bind' directives require an attribute value."]
     }
   ]
 })
diff --git a/tests/lib/rules/valid-v-cloak.js b/tests/lib/rules/valid-v-cloak.js
index 8a8a0aca..abecacba 100644
--- a/tests/lib/rules/valid-v-cloak.js
+++ b/tests/lib/rules/valid-v-cloak.js
@@ -47,6 +47,24 @@ tester.run('valid-v-cloak', rule, {
       filename: 'test.vue',
       code: '<template><div v-cloak="aaa"></div></template>',
       errors: ["'v-cloak' directives require no attribute value."]
+    },
+    // parsing error
+    {
+      filename: 'parsing-error.vue',
+      code: '<template><div v-cloak="."></div></template>',
+      errors: ["'v-cloak' directives require no attribute value."]
+    },
+    // comment value
+    {
+      filename: 'comment-value.vue',
+      code: '<template><div v-cloak="/**/" /></template>',
+      errors: ["'v-cloak' directives require no attribute value."]
+    },
+    // empty value
+    {
+      filename: 'empty-value.vue',
+      code: '<template><div v-cloak="" /></template>',
+      errors: ["'v-cloak' directives require no attribute value."]
     }
   ]
 })
diff --git a/tests/lib/rules/valid-v-else-if.js b/tests/lib/rules/valid-v-else-if.js
index 778d0733..e9f05d1e 100644
--- a/tests/lib/rules/valid-v-else-if.js
+++ b/tests/lib/rules/valid-v-else-if.js
@@ -40,6 +40,18 @@ tester.run('valid-v-else-if', rule, {
     {
       filename: 'test.vue',
       code: `<template>\n    <c1 v-if="1" />\n    <c2 v-else-if="1" />\n    <c3 v-else />\n</template>`
+    },
+    // parsing error
+    {
+      filename: 'parsing-error.vue',
+      code:
+        '<template><div v-if="foo"></div><div v-else-if="."></div></template>'
+    },
+    // comment value (parsing error)
+    {
+      filename: 'comment-value.vue',
+      code:
+        '<template><div v-if="foo"></div><div v-else-if="/**/"></div></template>'
     }
   ],
   invalid: [
@@ -122,6 +134,13 @@ tester.run('valid-v-else-if', rule, {
       code:
         '<template><div><div v-if="foo"></div><div v-else-if></div></div></template>',
       errors: ["'v-else-if' directives require that attribute value."]
+    },
+    // empty value
+    {
+      filename: 'empty-value.vue',
+      code:
+        '<template><div v-if="foo"></div><div v-else-if=""></div></template>',
+      errors: ["'v-else-if' directives require that attribute value."]
     }
   ]
 })
diff --git a/tests/lib/rules/valid-v-else.js b/tests/lib/rules/valid-v-else.js
index 1e235f9b..31b06786 100644
--- a/tests/lib/rules/valid-v-else.js
+++ b/tests/lib/rules/valid-v-else.js
@@ -120,6 +120,25 @@ tester.run('valid-v-else', rule, {
       code:
         '<template><div><div v-if="foo"></div><div v-else="foo"></div></div></template>',
       errors: ["'v-else' directives require no attribute value."]
+    },
+    // parsing error
+    {
+      filename: 'parsing-error.vue',
+      code: '<template><div v-if="foo"></div><div v-else="."></div></template>',
+      errors: ["'v-else' directives require no attribute value."]
+    },
+    // comment value
+    {
+      filename: 'comment-value.vue',
+      code:
+        '<template><div v-if="foo"></div><div v-else="/**/"></div></template>',
+      errors: ["'v-else' directives require no attribute value."]
+    },
+    // empty value
+    {
+      filename: 'empty-value.vue',
+      code: '<template><div v-if="foo"></div><div v-else=""></div></template>',
+      errors: ["'v-else' directives require no attribute value."]
     }
   ]
 })
diff --git a/tests/lib/rules/valid-v-for.js b/tests/lib/rules/valid-v-for.js
index 74a1ea61..94fa4346 100644
--- a/tests/lib/rules/valid-v-for.js
+++ b/tests/lib/rules/valid-v-for.js
@@ -127,6 +127,21 @@ tester.run('valid-v-for', rule, {
           </template>
         </template>
       `
+    },
+    // parsing error
+    {
+      filename: 'parsing-error.vue',
+      code: '<template><div v-for="."></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="xin list"><div></div></template></div></template>'
+    },
+    // comment value (parsing error)
+    {
+      filename: 'comment-value.vue',
+      code: '<template><div v-for="/**/"></div></template>'
     }
   ],
   invalid: [
@@ -245,12 +260,6 @@ tester.run('valid-v-for', rule, {
         '<template><div><template v-for="x in list" :key="x"><custom-component></custom-component></template></div></template>',
       errors: ["Custom elements in iteration require 'v-bind:key' directives."]
     },
-    {
-      filename: 'test.vue',
-      code:
-        '<template><div><template v-for="xin list"><div></div></template></div></template>',
-      errors: ["'v-for' directives require that attribute value."]
-    },
     {
       filename: 'test.vue',
       code:
@@ -309,6 +318,12 @@ tester.run('valid-v-for', rule, {
           </template>
         </template>
       `
+    },
+    // empty value
+    {
+      filename: 'empty-value.vue',
+      code: '<template><div v-for=""></div></template>',
+      errors: ["'v-for' directives require that attribute value."]
     }
   ]
 })
diff --git a/tests/lib/rules/valid-v-html.js b/tests/lib/rules/valid-v-html.js
index 9a30bcf4..afb312f7 100644
--- a/tests/lib/rules/valid-v-html.js
+++ b/tests/lib/rules/valid-v-html.js
@@ -30,6 +30,16 @@ tester.run('valid-v-html', rule, {
     {
       filename: 'test.vue',
       code: '<template><div v-html="foo"></div></template>'
+    },
+    // parsing error
+    {
+      filename: 'parsing-error.vue',
+      code: '<template><div v-html="."></div></template>'
+    },
+    // comment value (parsing error)
+    {
+      filename: 'comment-value.vue',
+      code: '<template><div v-html="/**/"></div></template>'
     }
   ],
   invalid: [
@@ -47,6 +57,12 @@ tester.run('valid-v-html', rule, {
       filename: 'test.vue',
       code: '<template><div v-html></div></template>',
       errors: ["'v-html' directives require that attribute value."]
+    },
+    // empty value
+    {
+      filename: 'empty-value.vue',
+      code: '<template><div v-html=""></div></template>',
+      errors: ["'v-html' directives require that attribute value."]
     }
   ]
 })
diff --git a/tests/lib/rules/valid-v-if.js b/tests/lib/rules/valid-v-if.js
index effe0483..d3afe578 100644
--- a/tests/lib/rules/valid-v-if.js
+++ b/tests/lib/rules/valid-v-if.js
@@ -30,6 +30,16 @@ tester.run('valid-v-if', rule, {
     {
       filename: 'test.vue',
       code: '<template><div><div v-if="foo"></div></div></template>'
+    },
+    // parsing error
+    {
+      filename: 'parsing-error.vue',
+      code: '<template><div v-if="."></div></template>'
+    },
+    // comment value (parsing error)
+    {
+      filename: 'comment-value.vue',
+      code: '<template><div v-if="/**/"></div></template>'
     }
   ],
   invalid: [
@@ -62,6 +72,12 @@ tester.run('valid-v-if', rule, {
       filename: 'test.vue',
       code: '<template><div><div v-if></div></div></template>',
       errors: ["'v-if' directives require that attribute value."]
+    },
+    // empty value
+    {
+      filename: 'empty-value.vue',
+      code: '<template><div><div v-if=""></div></div></template>',
+      errors: ["'v-if' directives require that attribute value."]
     }
   ]
 })
diff --git a/tests/lib/rules/valid-v-model.js b/tests/lib/rules/valid-v-model.js
index c9c28585..9e78ef47 100644
--- a/tests/lib/rules/valid-v-model.js
+++ b/tests/lib/rules/valid-v-model.js
@@ -150,6 +150,16 @@ tester.run('valid-v-model', rule, {
       filename: 'test.vue',
       code:
         '<template><MyComponent v-model.modifier.modifierTwo="a"></MyComponent></template>'
+    },
+    // parsing error
+    {
+      filename: 'parsing-error.vue',
+      code: '<template><MyComponent v-model="." /></template>'
+    },
+    // comment value (parsing error)
+    {
+      filename: 'comment-value.vue',
+      code: '<template><MyComponent v-model="/**/" /></template>'
     }
   ],
   invalid: [
@@ -216,6 +226,12 @@ tester.run('valid-v-model', rule, {
       errors: [
         "'v-model' directives cannot update the iteration variable 'e' itself."
       ]
+    },
+    // empty value
+    {
+      filename: 'empty-value.vue',
+      code: '<template><MyComponent v-model="" /></template>',
+      errors: ["'v-model' directives require that attribute value."]
     }
   ]
 })
diff --git a/tests/lib/rules/valid-v-on.js b/tests/lib/rules/valid-v-on.js
index 911039ea..6f162e1d 100644
--- a/tests/lib/rules/valid-v-on.js
+++ b/tests/lib/rules/valid-v-on.js
@@ -96,6 +96,33 @@ tester.run('valid-v-on', rule, {
       filename: 'test.vue',
       code: '<template><div v-on:keydown.bar.aaa="foo"></div></template>',
       options: [{ modifiers: ['bar', 'aaa'] }]
+    },
+    // parsing error
+    {
+      filename: 'parsing-error.vue',
+      code: '<template><MyComponent v-on:keydown="." /></template>'
+    },
+    // comment value (valid)
+    {
+      filename: 'comment-value.vue',
+      code: '<template><MyComponent v-on:keydown="/**/" /></template>'
+    },
+    {
+      filename: 'comment-value.vue',
+      code: '<template><MyComponent v-on:keydown=/**/ /></template>'
+    },
+    {
+      filename: 'comment-value.vue',
+      code: '<template><MyComponent v-on:keydown.stop="/**/" /></template>'
+    },
+    {
+      filename: 'comment-value.vue',
+      code: '<template><MyComponent v-on:keydown.stop=/**/ /></template>'
+    },
+    // empty value
+    {
+      filename: 'empty-value.vue',
+      code: '<template><MyComponent v-on:keydown.stop="" /></template>'
     }
   ],
   invalid: [
@@ -139,6 +166,14 @@ tester.run('valid-v-on', rule, {
       filename: 'test.vue',
       code: '<template><div @click="delete"></div></template>',
       errors: ['Avoid using JavaScript keyword as "v-on" value: "delete".']
+    },
+    // empty value
+    {
+      filename: 'empty-value.vue',
+      code: '<template><MyComponent v-on:keydown="" /></template>',
+      errors: [
+        "'v-on' directives require a value or verb modifier (like 'stop' or 'prevent')."
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/valid-v-once.js b/tests/lib/rules/valid-v-once.js
index 0c6a71a2..c9f3750c 100644
--- a/tests/lib/rules/valid-v-once.js
+++ b/tests/lib/rules/valid-v-once.js
@@ -47,6 +47,24 @@ tester.run('valid-v-once', rule, {
       filename: 'test.vue',
       code: '<template><div v-once="aaa"></div></template>',
       errors: ["'v-once' directives require no attribute value."]
+    },
+    // parsing error
+    {
+      filename: 'parsing-error.vue',
+      code: '<template><MyComponent v-once="." /></template>',
+      errors: ["'v-once' directives require no attribute value."]
+    },
+    // comment value
+    {
+      filename: 'comment-value.vue',
+      code: '<template><MyComponent v-once="/**/" /></template>',
+      errors: ["'v-once' directives require no attribute value."]
+    },
+    // empty value
+    {
+      filename: 'comment-value.vue',
+      code: '<template><MyComponent v-once="" /></template>',
+      errors: ["'v-once' directives require no attribute value."]
     }
   ]
 })
diff --git a/tests/lib/rules/valid-v-pre.js b/tests/lib/rules/valid-v-pre.js
index ae9d38e6..3d9c9d88 100644
--- a/tests/lib/rules/valid-v-pre.js
+++ b/tests/lib/rules/valid-v-pre.js
@@ -47,6 +47,24 @@ tester.run('valid-v-pre', rule, {
       filename: 'test.vue',
       code: '<template><div v-pre="aaa"></div></template>',
       errors: ["'v-pre' directives require no attribute value."]
+    },
+    // parsing error
+    {
+      filename: 'parsing-error.vue',
+      code: '<template><div v-pre="." /></template>',
+      errors: ["'v-pre' directives require no attribute value."]
+    },
+    // comment value
+    {
+      filename: 'comment-value.vue',
+      code: '<template><div v-pre="/**/" /></template>',
+      errors: ["'v-pre' directives require no attribute value."]
+    },
+    // empty value
+    {
+      filename: 'empty-value.vue',
+      code: '<template><div v-pre="" /></template>',
+      errors: ["'v-pre' directives require no attribute value."]
     }
   ]
 })
diff --git a/tests/lib/rules/valid-v-show.js b/tests/lib/rules/valid-v-show.js
index 0471ccd9..4220e874 100644
--- a/tests/lib/rules/valid-v-show.js
+++ b/tests/lib/rules/valid-v-show.js
@@ -30,6 +30,16 @@ tester.run('valid-v-show', rule, {
     {
       filename: 'test.vue',
       code: '<template><div v-show="foo"></div></template>'
+    },
+    // parsing error
+    {
+      filename: 'parsing-error.vue',
+      code: '<template><MyComponent v-show="." /></template>'
+    },
+    // comment value (parsing error)
+    {
+      filename: 'comment-value.vue',
+      code: '<template><MyComponent v-show="/**/" /></template>'
     }
   ],
   invalid: [
@@ -48,8 +58,9 @@ tester.run('valid-v-show', rule, {
       code: '<template><div v-show></div></template>',
       errors: ["'v-show' directives require that attribute value."]
     },
+    // empty value
     {
-      filename: 'test.vue',
+      filename: 'empty-value.vue',
       code: '<template><div v-show=""></div></template>',
       errors: ["'v-show' directives require that attribute value."]
     }
diff --git a/tests/lib/rules/valid-v-slot.js b/tests/lib/rules/valid-v-slot.js
index ab8307fe..0043756f 100644
--- a/tests/lib/rules/valid-v-slot.js
+++ b/tests/lib/rules/valid-v-slot.js
@@ -83,7 +83,13 @@ tester.run('valid-v-slot', rule, {
       <MyComponent>
         <template v-for="(key, value) in xxxx" #[key]>{{value}}</template>
       </MyComponent>
-    </template>`
+    </template>`,
+    // parsing error
+    {
+      filename: 'parsing-error.vue',
+      code:
+        '<template><MyComponent v-slot="." ><div /></MyComponent></template>'
+    }
   ],
   invalid: [
     // Verify location.
@@ -294,6 +300,26 @@ tester.run('valid-v-slot', rule, {
         </template>
       `,
       errors: [{ messageId: 'requireAttributeValue' }]
+    },
+    // comment value
+    {
+      filename: 'comment-value1.vue',
+      code:
+        '<template><MyComponent v-slot="/**/" ><div /></MyComponent></template>',
+      errors: [{ messageId: 'requireAttributeValue' }]
+    },
+    {
+      filename: 'comment-value2.vue',
+      code:
+        '<template><MyComponent v-slot=/**/ ><div /></MyComponent></template>',
+      errors: [{ messageId: 'requireAttributeValue' }]
+    },
+    // empty value
+    {
+      filename: 'empty-value.vue',
+      code:
+        '<template><MyComponent v-slot="" ><div /></MyComponent></template>',
+      errors: [{ messageId: 'requireAttributeValue' }]
     }
   ]
 })
diff --git a/tests/lib/rules/valid-v-text.js b/tests/lib/rules/valid-v-text.js
index 3956462c..611b1a31 100644
--- a/tests/lib/rules/valid-v-text.js
+++ b/tests/lib/rules/valid-v-text.js
@@ -34,6 +34,16 @@ tester.run('valid-v-text', rule, {
     {
       filename: 'test.vue',
       code: '<template><div v-text="foo"></div></template>'
+    },
+    // parsing error
+    {
+      filename: 'parsing-error.vue',
+      code: '<template><div v-text="." /></template>'
+    },
+    // comment value (parsing error)
+    {
+      filename: 'parsing-error.vue',
+      code: '<template><div v-text="/**/" /></template>'
     }
   ],
   invalid: [
@@ -51,6 +61,12 @@ tester.run('valid-v-text', rule, {
       filename: 'test.vue',
       code: '<template><div v-text></div></template>',
       errors: ["'v-text' directives require that attribute value."]
+    },
+    // empty value
+    {
+      filename: 'empty-value.vue',
+      code: '<template><div v-text="" /></template>',
+      errors: ["'v-text' directives require that attribute value."]
     }
   ]
 })