Repository: vuejs/eslint-plugin-vue (js)

Issue:
Rule proposal: `v-for-delimiter-style`
**Please describe what the rule should do:**
You can use both `v-for="x in xs"` and `v-for="x of xs"`. There should be a rule to enforce one of the styles (probably configurable).

**What category should the rule belong to?**
<!-- (place an "X" next to just one item) -->
- [X] Enforces code style
- [ ] Warns about a potential error
- [ ] Suggests an alternate way of doing something
- [ ] Other (please specify:)

**Provide 2-3 code examples that this rule should warn about:**
```vue
<!-- "vue/v-for-delimiter-style": ["warn", "in"] -->
<div v-for="x in xs" /><!-- GOOD -->
<div v-for="x of xs" /><!-- BAD -->

<!-- "vue/v-for-delimiter-style": ["warn", "of"] -->
<div v-for="x of xs" /><!-- GOOD -->
<div v-for="x in xs" /><!-- BAD -->
```

**Additional context**
See https://vuejs.org/v2/guide/list.html#Mapping-an-Array-to-Elements-with-v-for:

> You can also use `of` as the delimiter instead of `in`, so that it is closer to JavaScript’s syntax for iterators.

Pull request description:
Add `v-for-delimiter-style` rule
Closes #1228.

Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 7e0b1dd9..63d538e4 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -312,6 +312,7 @@ For example:
 | [vue/script-indent](./script-indent.md) | enforce consistent indentation in `<script>` | :wrench: |
 | [vue/sort-keys](./sort-keys.md) | enforce sort-keys in a manner that is compatible with order-in-components |  |
 | [vue/static-class-names-order](./static-class-names-order.md) | enforce static class names order | :wrench: |
+| [vue/v-for-delimiter-style](./v-for-delimiter-style.md) | enforce `v-for` directive's delimiter style | :wrench: |
 | [vue/v-on-function-call](./v-on-function-call.md) | enforce or forbid parentheses after method calls without arguments in `v-on` directives | :wrench: |
 
 ### Extension Rules
diff --git a/docs/rules/require-explicit-emits.md b/docs/rules/require-explicit-emits.md
index 2a165ec1..ba33eadf 100644
--- a/docs/rules/require-explicit-emits.md
+++ b/docs/rules/require-explicit-emits.md
@@ -74,7 +74,37 @@ export default {
 
 ## :wrench: Options
 
-Nothing.
+```json
+{
+  "vue/require-explicit-emits": ["error", {
+    "allowProps": false
+  }]
+}
+```
+
+- `"allowProps"` ... If `true`, allow event names defined in `props`. default `false`
+
+### `"allowProps": true`
+
+<eslint-code-block fix :rules="{'vue/require-explicit-emits': ['error', {allowProps: true}]}">
+
+```vue
+<script>
+export default {
+  props: ['onGood', 'bad'],
+  methods: {
+    foo () {
+      // ✓ GOOD
+      this.$emit('good')
+      // ✗ BAD
+      this.$emit('bad')
+    }
+  }
+}
+</script>
+```
+
+</eslint-code-block>
 
 ## :books: Further Reading
 
diff --git a/docs/rules/v-for-delimiter-style.md b/docs/rules/v-for-delimiter-style.md
new file mode 100644
index 00000000..2b90aca2
--- /dev/null
+++ b/docs/rules/v-for-delimiter-style.md
@@ -0,0 +1,65 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/v-for-delimiter-style
+description: enforce `v-for` directive's delimiter style
+---
+# vue/v-for-delimiter-style
+> enforce `v-for` directive's delimiter style
+
+- :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 enforces which delimiter (`in` or `of`) should be used in `v-for` directives.
+
+<eslint-code-block fix :rules="{'vue/v-for-delimiter-style': ['error']}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <div v-for="x in xs" />
+
+  <!-- ✗ BAD -->
+  <div v-for="x of xs" />
+</template>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+Default is set to `in`.
+
+```json
+{
+  "vue/v-for-delimiter-style": ["error", "in" | "of"]
+}
+```
+
+- `"in"` (default) ... requires using `in`.
+- `"of"` ... requires using `of`.
+
+### `"of"`
+
+<eslint-code-block fix :rules="{'vue/v-for-delimiter-style': ['error', 'of']}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <div v-for="x of xs" />
+
+  <!-- ✗ BAD -->
+  <div v-for="x in xs" />
+</template>
+```
+
+</eslint-code-block>
+
+## :books: Further Reading
+
+- [Guide - List Rendering](https://v3.vuejs.org/guide/list.html)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/v-for-delimiter-style.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/v-for-delimiter-style.js)
diff --git a/lib/configs/no-layout-rules.js b/lib/configs/no-layout-rules.js
index 3d2cb5d3..be534fd3 100644
--- a/lib/configs/no-layout-rules.js
+++ b/lib/configs/no-layout-rules.js
@@ -41,6 +41,7 @@ module.exports = {
     'vue/space-in-parens': 'off',
     'vue/space-infix-ops': 'off',
     'vue/space-unary-ops': 'off',
-    'vue/template-curly-spacing': 'off'
+    'vue/template-curly-spacing': 'off',
+    'vue/v-for-delimiter-style': 'off'
   }
 }
diff --git a/lib/index.js b/lib/index.js
index 5f285be6..71c922f4 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -146,6 +146,7 @@ module.exports = {
     'this-in-template': require('./rules/this-in-template'),
     'use-v-on-exact': require('./rules/use-v-on-exact'),
     'v-bind-style': require('./rules/v-bind-style'),
+    'v-for-delimiter-style': require('./rules/v-for-delimiter-style'),
     'v-on-function-call': require('./rules/v-on-function-call'),
     'v-on-style': require('./rules/v-on-style'),
     'v-slot-style': require('./rules/v-slot-style'),
diff --git a/lib/rules/no-multi-spaces.js b/lib/rules/no-multi-spaces.js
index ef1fee67..aa8b70a2 100644
--- a/lib/rules/no-multi-spaces.js
+++ b/lib/rules/no-multi-spaces.js
@@ -4,6 +4,8 @@
  */
 'use strict'
 
+const path = require('path')
+
 // ------------------------------------------------------------------------------
 // Rule Definition
 // ------------------------------------------------------------------------------
@@ -50,11 +52,14 @@ module.exports = {
     return {
       Program(node) {
         if (context.parserServices.getTemplateBodyTokenStore == null) {
-          context.report({
-            loc: { line: 1, column: 0 },
-            message:
-              'Use the latest vue-eslint-parser. See also https://eslint.vuejs.org/user-guide/#what-is-the-use-the-latest-vue-eslint-parser-error.'
-          })
+          const filename = context.getFilename()
+          if (path.extname(filename) === '.vue') {
+            context.report({
+              loc: { line: 1, column: 0 },
+              message:
+                'Use the latest vue-eslint-parser. See also https://eslint.vuejs.org/user-guide/#what-is-the-use-the-latest-vue-eslint-parser-error.'
+            })
+          }
           return
         }
         if (!node.templateBody) {
diff --git a/lib/rules/no-reserved-component-names.js b/lib/rules/no-reserved-component-names.js
index e21bf7cd..aecbe602 100644
--- a/lib/rules/no-reserved-component-names.js
+++ b/lib/rules/no-reserved-component-names.js
@@ -37,14 +37,10 @@ const vue3BuiltInComponents = ['teleport', 'suspense']
 function isLowercase(word) {
   return /^[a-z]*$/.test(word)
 }
-/** @param {string} word  */
-function capitalizeFirstLetter(word) {
-  return word[0].toUpperCase() + word.substring(1, word.length)
-}
 
 const RESERVED_NAMES_IN_HTML = new Set([
   ...htmlElements,
-  ...htmlElements.map(capitalizeFirstLetter)
+  ...htmlElements.map(casing.capitalize)
 ])
 const RESERVED_NAMES_IN_VUE = new Set([
   ...vueBuiltInComponents,
@@ -57,11 +53,11 @@ const RESERVED_NAMES_IN_VUE3 = new Set([
 ])
 const RESERVED_NAMES_IN_OTHERS = new Set([
   ...deprecatedHtmlElements,
-  ...deprecatedHtmlElements.map(capitalizeFirstLetter),
+  ...deprecatedHtmlElements.map(casing.capitalize),
   ...kebabCaseElements,
   ...kebabCaseElements.map(casing.pascalCase),
   ...svgElements,
-  ...svgElements.filter(isLowercase).map(capitalizeFirstLetter)
+  ...svgElements.filter(isLowercase).map(casing.capitalize)
 ])
 
 // ------------------------------------------------------------------------------
diff --git a/lib/rules/no-unsupported-features.js b/lib/rules/no-unsupported-features.js
index 6f7a87ab..9eb00be0 100644
--- a/lib/rules/no-unsupported-features.js
+++ b/lib/rules/no-unsupported-features.js
@@ -9,7 +9,7 @@ const utils = require('../utils')
 
 /**
  * @typedef {object} SyntaxRule
- * @property {string | ((range: semver.Range) => boolean)} supported
+ * @property {string} supported
  * @property { (context: RuleContext) => TemplateListener } [createTemplateBodyVisitor]
  * @property { (context: RuleContext) => RuleListener } [createScriptVisitor]
  */
@@ -28,6 +28,8 @@ const FEATURES = {
   'v-is': require('./syntaxes/v-is')
 }
 
+const SYNTAX_NAMES = /** @type {(keyof FEATURES)[]} */ (Object.keys(FEATURES))
+
 const cache = new Map()
 /**
  * Get the `semver.Range` object of a given range text.
@@ -71,7 +73,7 @@ module.exports = {
           ignores: {
             type: 'array',
             items: {
-              enum: Object.keys(FEATURES)
+              enum: SYNTAX_NAMES
             },
             uniqueItems: true
           }
@@ -82,7 +84,7 @@ module.exports = {
     messages: {
       // Vue.js 2.5.0+
       forbiddenSlotScopeAttribute:
-        '`slot-scope` are not supported until Vue.js "2.5.0".',
+        '`slot-scope` are not supported except Vue.js ">=2.5.0 <3.0.0".',
       // Vue.js 2.6.0+
       forbiddenDynamicDirectiveArguments:
         'Dynamic arguments are not supported until Vue.js "2.6.0".',
@@ -119,22 +121,15 @@ module.exports = {
      * @returns {boolean} `true` if it's supporting.
      */
     function isNotSupportingVersion(aCase) {
-      if (typeof aCase.supported === 'function') {
-        return !aCase.supported(versionRange)
-      }
-      return versionRange.intersects(getSemverRange(`<${aCase.supported}`))
+      return !semver.subset(versionRange, getSemverRange(aCase.supported))
     }
 
-    const syntaxNames = /** @type {(keyof FEATURES)[]} */ (Object.keys(
-      FEATURES
-    ))
-
     /** @type {TemplateListener} */
     let templateBodyVisitor = {}
     /** @type {RuleListener} */
     let scriptVisitor = {}
 
-    for (const syntaxName of syntaxNames) {
+    for (const syntaxName of SYNTAX_NAMES) {
       /** @type {SyntaxRule} */
       const syntax = FEATURES[syntaxName]
       if (ignores.includes(syntaxName) || !isNotSupportingVersion(syntax)) {
diff --git a/lib/rules/require-explicit-emits.js b/lib/rules/require-explicit-emits.js
index fdadc233..ec356364 100644
--- a/lib/rules/require-explicit-emits.js
+++ b/lib/rules/require-explicit-emits.js
@@ -7,6 +7,8 @@
 /**
  * @typedef {import('../utils').ComponentArrayEmit} ComponentArrayEmit
  * @typedef {import('../utils').ComponentObjectEmit} ComponentObjectEmit
+ * @typedef {import('../utils').ComponentArrayProp} ComponentArrayProp
+ * @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp
  * @typedef {import('../utils').VueObjectData} VueObjectData
  */
 
@@ -16,6 +18,7 @@
 
 const { findVariable } = require('eslint-utils')
 const utils = require('../utils')
+const { capitalize } = require('../utils/casing')
 
 // ------------------------------------------------------------------------------
 // Helpers
@@ -89,7 +92,17 @@ module.exports = {
       url: 'https://eslint.vuejs.org/rules/require-explicit-emits.html'
     },
     fixable: null,
-    schema: [],
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          allowProps: {
+            type: 'boolean'
+          }
+        },
+        additionalProperties: false
+      }
+    ],
     messages: {
       missing:
         'The "{{name}}" event has been triggered but not declared on `emits` option.',
@@ -102,49 +115,49 @@ module.exports = {
   },
   /** @param {RuleContext} context */
   create(context) {
-    /** @typedef { { node: Literal, name: string } } EmitCellName */
+    const options = context.options[0] || {}
+    const allowProps = !!options.allowProps
     /** @type {Map<ObjectExpression, { contextReferenceIds: Set<Identifier>, emitReferenceIds: Set<Identifier> }>} */
     const setupContexts = new Map()
     /** @type {Map<ObjectExpression, (ComponentArrayEmit | ComponentObjectEmit)[]>} */
     const vueEmitsDeclarations = new Map()
-
-    /** @type {EmitCellName[]} */
-    const templateEmitCellNames = []
-    /** @type { { type: 'export' | 'mark' | 'definition', object: ObjectExpression, emits: (ComponentArrayEmit | ComponentObjectEmit)[] } | null } */
-    let vueObjectData = null
+    /** @type {Map<ObjectExpression, (ComponentArrayProp | ComponentObjectProp)[]>} */
+    const vuePropsDeclarations = new Map()
 
     /**
-     * @param {Literal} nameLiteralNode
+     * @typedef {object} VueTemplateObjectData
+     * @property {'export' | 'mark' | 'definition'} type
+     * @property {ObjectExpression} object
+     * @property {(ComponentArrayEmit | ComponentObjectEmit)[]} emits
+     * @property {(ComponentArrayProp | ComponentObjectProp)[]} props
      */
-    function addTemplateEmitCellName(nameLiteralNode) {
-      templateEmitCellNames.push({
-        node: nameLiteralNode,
-        name: `${nameLiteralNode.value}`
-      })
-    }
+    /** @type {VueTemplateObjectData | null} */
+    let vueTemplateObjectData = null
 
     /**
-     * @param {(ComponentArrayEmit | ComponentObjectEmit)[]} emitsDeclarations
+     * @param {(ComponentArrayEmit | ComponentObjectEmit)[]} emits
+     * @param {(ComponentArrayProp | ComponentObjectProp)[]} props
      * @param {Literal} nameLiteralNode
      * @param {ObjectExpression} vueObjectNode
      */
-    function verify(emitsDeclarations, nameLiteralNode, vueObjectNode) {
+    function verifyEmit(emits, props, nameLiteralNode, vueObjectNode) {
       const name = `${nameLiteralNode.value}`
-      if (emitsDeclarations.some((e) => e.emitName === name)) {
+      if (emits.some((e) => e.emitName === name)) {
         return
       }
+      if (allowProps) {
+        const key = `on${capitalize(name)}`
+        if (props.some((e) => e.propName === key)) {
+          return
+        }
+      }
       context.report({
         node: nameLiteralNode,
         messageId: 'missing',
         data: {
           name
         },
-        suggest: buildSuggest(
-          vueObjectNode,
-          emitsDeclarations,
-          nameLiteralNode,
-          context
-        )
+        suggest: buildSuggest(vueObjectNode, emits, nameLiteralNode, context)
       })
     }
 
@@ -153,47 +166,31 @@ module.exports = {
       {
         /** @param { CallExpression & { argument: [Literal, ...Expression] } } node */
         'CallExpression[arguments.0.type=Literal]'(node) {
-          const callee = node.callee
+          const callee = utils.skipChainExpression(node.callee)
           const nameLiteralNode = /** @type {Literal} */ (node.arguments[0])
           if (!nameLiteralNode || typeof nameLiteralNode.value !== 'string') {
             // cannot check
             return
           }
-          if (callee.type === 'Identifier' && callee.name === '$emit') {
-            addTemplateEmitCellName(nameLiteralNode)
-          }
-        },
-        "VElement[parent.type!='VElement']:exit"() {
-          if (!vueObjectData) {
+          if (!vueTemplateObjectData) {
             return
           }
-          const emitsDeclarationNames = new Set(
-            vueObjectData.emits.map((e) => e.emitName)
-          )
-
-          for (const { name, node } of templateEmitCellNames) {
-            if (emitsDeclarationNames.has(name)) {
-              continue
-            }
-            context.report({
-              node,
-              messageId: 'missing',
-              data: {
-                name
-              },
-              suggest: buildSuggest(
-                vueObjectData.object,
-                vueObjectData.emits,
-                node,
-                context
-              )
-            })
+          if (callee.type === 'Identifier' && callee.name === '$emit') {
+            verifyEmit(
+              vueTemplateObjectData.emits,
+              vueTemplateObjectData.props,
+              nameLiteralNode,
+              vueTemplateObjectData.object
+            )
           }
         }
       },
       utils.defineVueVisitor(context, {
         onVueObjectEnter(node) {
           vueEmitsDeclarations.set(node, utils.getComponentEmits(node))
+          if (allowProps) {
+            vuePropsDeclarations.set(node, utils.getComponentProps(node))
+          }
         },
         onSetupFunctionEnter(node, { node: vueNode }) {
           const contextParam = node.params[1]
@@ -286,7 +283,12 @@ module.exports = {
             const { contextReferenceIds, emitReferenceIds } = setupContext
             if (callee.type === 'Identifier' && emitReferenceIds.has(callee)) {
               // verify setup(props,{emit}) {emit()}
-              verify(emitsDeclarations, nameLiteralNode, vueNode)
+              verifyEmit(
+                emitsDeclarations,
+                vuePropsDeclarations.get(vueNode) || [],
+                nameLiteralNode,
+                vueNode
+              )
             } else if (emit && emit.name === 'emit') {
               const memObject = utils.skipChainExpression(emit.member.object)
               if (
@@ -294,7 +296,12 @@ module.exports = {
                 contextReferenceIds.has(memObject)
               ) {
                 // verify setup(props,context) {context.emit()}
-                verify(emitsDeclarations, nameLiteralNode, vueNode)
+                verifyEmit(
+                  emitsDeclarations,
+                  vuePropsDeclarations.get(vueNode) || [],
+                  nameLiteralNode,
+                  vueNode
+                )
               }
             }
           }
@@ -304,26 +311,36 @@ module.exports = {
             const memObject = utils.skipChainExpression(emit.member.object)
             if (utils.isThis(memObject, context)) {
               // verify this.$emit()
-              verify(emitsDeclarations, nameLiteralNode, vueNode)
+              verifyEmit(
+                emitsDeclarations,
+                vuePropsDeclarations.get(vueNode) || [],
+                nameLiteralNode,
+                vueNode
+              )
             }
           }
         },
         onVueObjectExit(node, { type }) {
           const emits = vueEmitsDeclarations.get(node)
-          if (!vueObjectData || vueObjectData.type !== 'export') {
+          if (
+            !vueTemplateObjectData ||
+            vueTemplateObjectData.type !== 'export'
+          ) {
             if (
               emits &&
               (type === 'mark' || type === 'export' || type === 'definition')
             ) {
-              vueObjectData = {
+              vueTemplateObjectData = {
                 type,
                 object: node,
-                emits
+                emits,
+                props: vuePropsDeclarations.get(node) || []
               }
             }
           }
           setupContexts.delete(node)
           vueEmitsDeclarations.delete(node)
+          vuePropsDeclarations.delete(node)
         }
       })
     )
diff --git a/lib/rules/require-valid-default-prop.js b/lib/rules/require-valid-default-prop.js
index ffebef07..ef9453b2 100644
--- a/lib/rules/require-valid-default-prop.js
+++ b/lib/rules/require-valid-default-prop.js
@@ -4,6 +4,7 @@
  */
 'use strict'
 const utils = require('../utils')
+const { capitalize } = require('../utils/casing')
 
 /**
  * @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp
@@ -68,13 +69,6 @@ function getTypes(node) {
   return []
 }
 
-/**
- * @param {string} text
- */
-function capitalize(text) {
-  return text[0].toUpperCase() + text.slice(1)
-}
-
 // ------------------------------------------------------------------------------
 // Rule Definition
 // ------------------------------------------------------------------------------
diff --git a/lib/rules/syntaxes/dynamic-directive-arguments.js b/lib/rules/syntaxes/dynamic-directive-arguments.js
index 595f70fb..9a790e9f 100644
--- a/lib/rules/syntaxes/dynamic-directive-arguments.js
+++ b/lib/rules/syntaxes/dynamic-directive-arguments.js
@@ -4,7 +4,7 @@
  */
 'use strict'
 module.exports = {
-  supported: '2.6.0',
+  supported: '>=2.6.0',
   /** @param {RuleContext} context @returns {TemplateListener} */
   createTemplateBodyVisitor(context) {
     /**
diff --git a/lib/rules/syntaxes/scope-attribute.js b/lib/rules/syntaxes/scope-attribute.js
index c1673c3c..c356a675 100644
--- a/lib/rules/syntaxes/scope-attribute.js
+++ b/lib/rules/syntaxes/scope-attribute.js
@@ -5,6 +5,7 @@
 'use strict'
 module.exports = {
   deprecated: '2.5.0',
+  supported: '<3.0.0',
   /** @param {RuleContext} context @returns {TemplateListener} */
   createTemplateBodyVisitor(context) {
     /**
diff --git a/lib/rules/syntaxes/slot-attribute.js b/lib/rules/syntaxes/slot-attribute.js
index 6f38166e..1ce27484 100644
--- a/lib/rules/syntaxes/slot-attribute.js
+++ b/lib/rules/syntaxes/slot-attribute.js
@@ -5,6 +5,7 @@
 'use strict'
 module.exports = {
   deprecated: '2.6.0',
+  supported: '<3.0.0',
   /** @param {RuleContext} context @returns {TemplateListener} */
   createTemplateBodyVisitor(context) {
     const sourceCode = context.getSourceCode()
diff --git a/lib/rules/syntaxes/slot-scope-attribute.js b/lib/rules/syntaxes/slot-scope-attribute.js
index 2afd790b..902b5ded 100644
--- a/lib/rules/syntaxes/slot-scope-attribute.js
+++ b/lib/rules/syntaxes/slot-scope-attribute.js
@@ -5,7 +5,7 @@
 'use strict'
 module.exports = {
   deprecated: '2.6.0',
-  supported: '2.5.0',
+  supported: '>=2.5.0 <3.0.0',
   /**
    * @param {RuleContext} context
    * @param {object} option
diff --git a/lib/rules/syntaxes/v-bind-prop-modifier-shorthand.js b/lib/rules/syntaxes/v-bind-prop-modifier-shorthand.js
index 4038c81a..219d2b3c 100644
--- a/lib/rules/syntaxes/v-bind-prop-modifier-shorthand.js
+++ b/lib/rules/syntaxes/v-bind-prop-modifier-shorthand.js
@@ -3,15 +3,9 @@
  * See LICENSE file in root directory for full license.
  */
 'use strict'
-const semver = require('semver')
-const unsupported = new semver.Range('<=2.5 || >=2.6.0')
 
 module.exports = {
-  // >=2.6.0-beta.1 <=2.6.0-beta.3
-  /** @param {semver.Range} versionRange */
-  supported: (versionRange) => {
-    return !versionRange.intersects(unsupported)
-  },
+  supported: '>=2.6.0-beta.1 <=2.6.0-beta.3',
   /** @param {RuleContext} context @returns {TemplateListener} */
   createTemplateBodyVisitor(context) {
     /**
diff --git a/lib/rules/syntaxes/v-is.js b/lib/rules/syntaxes/v-is.js
index a974b820..9aeeb8d7 100644
--- a/lib/rules/syntaxes/v-is.js
+++ b/lib/rules/syntaxes/v-is.js
@@ -4,7 +4,7 @@
  */
 'use strict'
 module.exports = {
-  supported: '3.0.0',
+  supported: '>=3.0.0',
   /** @param {RuleContext} context @returns {TemplateListener} */
   createTemplateBodyVisitor(context) {
     /**
diff --git a/lib/rules/syntaxes/v-model-argument.js b/lib/rules/syntaxes/v-model-argument.js
index bba028e1..d1bd5973 100644
--- a/lib/rules/syntaxes/v-model-argument.js
+++ b/lib/rules/syntaxes/v-model-argument.js
@@ -5,7 +5,7 @@
 'use strict'
 
 module.exports = {
-  supported: '3.0.0',
+  supported: '>=3.0.0',
   /** @param {RuleContext} context @returns {TemplateListener} */
   createTemplateBodyVisitor(context) {
     return {
diff --git a/lib/rules/syntaxes/v-model-custom-modifiers.js b/lib/rules/syntaxes/v-model-custom-modifiers.js
index 5630f9ad..d11116b2 100644
--- a/lib/rules/syntaxes/v-model-custom-modifiers.js
+++ b/lib/rules/syntaxes/v-model-custom-modifiers.js
@@ -11,7 +11,7 @@
 const BUILTIN_MODIFIERS = new Set(['lazy', 'number', 'trim'])
 
 module.exports = {
-  supported: '3.0.0',
+  supported: '>=3.0.0',
   /** @param {RuleContext} context @returns {TemplateListener} */
   createTemplateBodyVisitor(context) {
     return {
diff --git a/lib/rules/syntaxes/v-slot.js b/lib/rules/syntaxes/v-slot.js
index ae71dc9d..9a9ba1fb 100644
--- a/lib/rules/syntaxes/v-slot.js
+++ b/lib/rules/syntaxes/v-slot.js
@@ -4,7 +4,7 @@
  */
 'use strict'
 module.exports = {
-  supported: '2.6.0',
+  supported: '>=2.6.0',
   /** @param {RuleContext} context @returns {TemplateListener} */
   createTemplateBodyVisitor(context) {
     const sourceCode = context.getSourceCode()
diff --git a/lib/rules/v-for-delimiter-style.js b/lib/rules/v-for-delimiter-style.js
new file mode 100644
index 00000000..ef0086cf
--- /dev/null
+++ b/lib/rules/v-for-delimiter-style.js
@@ -0,0 +1,69 @@
+/**
+ * @fileoverview enforce `v-for` directive's delimiter style
+ * @author Flo Edelmann
+ * @copyright 2020 Flo Edelmann. All rights reserved.
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'layout',
+    docs: {
+      description: "enforce `v-for` directive's delimiter style",
+      categories: undefined,
+      recommended: false,
+      url: 'https://eslint.vuejs.org/rules/v-for-delimiter-style.html'
+    },
+    fixable: 'code',
+    schema: [{ enum: ['in', 'of'] }]
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    const preferredDelimiter =
+      /** @type {string|undefined} */ (context.options[0]) || 'in'
+
+    return utils.defineTemplateBodyVisitor(context, {
+      /** @param {VForExpression} node */
+      VForExpression(node) {
+        const tokenStore =
+          context.parserServices.getTemplateBodyTokenStore &&
+          context.parserServices.getTemplateBodyTokenStore()
+
+        const delimiterToken = /** @type {Token} */ (tokenStore.getTokenAfter(
+          node.left.length
+            ? node.left[node.left.length - 1]
+            : tokenStore.getFirstToken(node),
+          (token) => token.type !== 'Punctuator' || token.value !== ')'
+        ))
+
+        if (delimiterToken.value === preferredDelimiter) {
+          return
+        }
+
+        context.report({
+          node,
+          loc: node.loc,
+          message: `Expected '{{preferredDelimiter}}' instead of '{{usedDelimiter}}' in 'v-for'.`,
+          data: {
+            preferredDelimiter,
+            usedDelimiter: delimiterToken.value
+          },
+          *fix(fixer) {
+            yield fixer.replaceText(delimiterToken, preferredDelimiter)
+          }
+        })
+      }
+    })
+  }
+}
diff --git a/lib/utils/casing.js b/lib/utils/casing.js
index f6dea6d6..b2f2e89b 100644
--- a/lib/utils/casing.js
+++ b/lib/utils/casing.js
@@ -197,5 +197,7 @@ module.exports = {
   isCamelCase,
   isPascalCase,
   isKebabCase,
-  isSnakeCase
+  isSnakeCase,
+
+  capitalize
 }
diff --git a/lib/utils/index.js b/lib/utils/index.js
index 5abb635e..814517e3 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -1531,11 +1531,14 @@ function defineTemplateBodyVisitor(
   scriptVisitor
 ) {
   if (context.parserServices.defineTemplateBodyVisitor == null) {
-    context.report({
-      loc: { line: 1, column: 0 },
-      message:
-        'Use the latest vue-eslint-parser. See also https://eslint.vuejs.org/user-guide/#what-is-the-use-the-latest-vue-eslint-parser-error'
-    })
+    const filename = context.getFilename()
+    if (path.extname(filename) === '.vue') {
+      context.report({
+        loc: { line: 1, column: 0 },
+        message:
+          'Use the latest vue-eslint-parser. See also https://eslint.vuejs.org/user-guide/#what-is-the-use-the-latest-vue-eslint-parser-error.'
+      })
+    }
     return {}
   }
   return context.parserServices.defineTemplateBodyVisitor(


Test patch:
diff --git a/tests/lib/rules-without-vue-eslint-parser.js b/tests/lib/rules-without-vue-eslint-parser.js
index cd91f791..b7c58513 100644
--- a/tests/lib/rules-without-vue-eslint-parser.js
+++ b/tests/lib/rules-without-vue-eslint-parser.js
@@ -7,6 +7,7 @@
 const Linter = require('eslint').Linter
 const parser = require('babel-eslint')
 const rules = require('../..').rules
+const assert = require('assert')
 
 describe("Don't crash even if without vue-eslint-parser.", () => {
   const code = '<template><div>TEST</div></template>'
@@ -25,7 +26,16 @@ describe("Don't crash even if without vue-eslint-parser.", () => {
       }
       linter.defineParser('babel-eslint', parser)
       linter.defineRule(ruleId, rules[key])
-      linter.verifyAndFix(code, config, 'test.vue')
+      const resultVue = linter.verifyAndFix(code, config, 'test.vue')
+      for (const { message } of resultVue.messages) {
+        assert.strictEqual(
+          message,
+          'Use the latest vue-eslint-parser. See also https://eslint.vuejs.org/user-guide/#what-is-the-use-the-latest-vue-eslint-parser-error.'
+        )
+      }
+
+      const resultJs = linter.verifyAndFix(code, config, 'test.js')
+      assert.strictEqual(resultJs.messages.length, 0)
     })
   }
 })
diff --git a/tests/lib/rules/no-unsupported-features/dynamic-directive-arguments.js b/tests/lib/rules/no-unsupported-features/dynamic-directive-arguments.js
index 5dd9bba7..61b39e73 100644
--- a/tests/lib/rules/no-unsupported-features/dynamic-directive-arguments.js
+++ b/tests/lib/rules/no-unsupported-features/dynamic-directive-arguments.js
@@ -54,7 +54,7 @@ tester.run('no-unsupported-features/dynamic-directive-arguments', rule, {
       <template>
         <a :[href]="'/xxx'" />
       </template>`,
-      options: buildOptions({ version: '2.6.0-beta.2' })
+      options: buildOptions({ version: '^3.0.0' })
     }
   ],
   invalid: [
diff --git a/tests/lib/rules/no-unsupported-features/slot-scope-attribute.js b/tests/lib/rules/no-unsupported-features/slot-scope-attribute.js
index f9be7a00..5307aa61 100644
--- a/tests/lib/rules/no-unsupported-features/slot-scope-attribute.js
+++ b/tests/lib/rules/no-unsupported-features/slot-scope-attribute.js
@@ -70,7 +70,8 @@ tester.run('no-unsupported-features/slot-scope-attribute', rule, {
       output: null,
       errors: [
         {
-          message: '`slot-scope` are not supported until Vue.js "2.5.0".',
+          message:
+            '`slot-scope` are not supported except Vue.js ">=2.5.0 <3.0.0".',
           line: 4
         }
       ]
@@ -86,7 +87,25 @@ tester.run('no-unsupported-features/slot-scope-attribute', rule, {
       output: null,
       errors: [
         {
-          message: '`slot-scope` are not supported until Vue.js "2.5.0".',
+          message:
+            '`slot-scope` are not supported except Vue.js ">=2.5.0 <3.0.0".',
+          line: 4
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+        <LinkList>
+          <a slot-scope />
+        </LinkList>
+      </template>`,
+      options: buildOptions({ version: '^3.0.0' }),
+      output: null,
+      errors: [
+        {
+          message:
+            '`slot-scope` are not supported except Vue.js ">=2.5.0 <3.0.0".',
           line: 4
         }
       ]
diff --git a/tests/lib/rules/no-unsupported-features/v-slot.js b/tests/lib/rules/no-unsupported-features/v-slot.js
index 6874731b..66b3b0b9 100644
--- a/tests/lib/rules/no-unsupported-features/v-slot.js
+++ b/tests/lib/rules/no-unsupported-features/v-slot.js
@@ -70,7 +70,7 @@ tester.run('no-unsupported-features/v-slot', rule, {
           <template v-slot:name ><a /></template>
         </LinkList>
       </template>`,
-      options: buildOptions({ version: '2.6.0-beta.2' })
+      options: buildOptions({ version: '^3.0.0' })
     }
   ],
   invalid: [
diff --git a/tests/lib/rules/require-explicit-emits.js b/tests/lib/rules/require-explicit-emits.js
index d59d7860..4f27563c 100644
--- a/tests/lib/rules/require-explicit-emits.js
+++ b/tests/lib/rules/require-explicit-emits.js
@@ -359,6 +359,27 @@ tester.run('require-explicit-emits', rule, {
       }
       </script>
       `
+    },
+    // allowProps
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <button @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        props: ['onFoo'],
+        methods: {
+          fn() { this.$emit('foo') }
+        },
+        setup(p, ctx) {
+          ctx.emit('foo')
+        }
+      }
+      </script>
+      `,
+      options: [{ allowProps: true }]
     }
   ],
   invalid: [
@@ -1551,6 +1572,41 @@ emits: {'foo': null}
         'The "foo" event has been triggered but not declared on `emits` option.',
         'The "bar" event has been triggered but not declared on `emits` option.'
       ]
+    },
+    // allowProps
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <button @click="$emit('foo')"/>
+      </template>
+      <script>
+      export default {
+        props: ['foo'],
+        methods: {
+          fn() { this.$emit('foo') }
+        },
+        setup(p, ctx) {
+          ctx.emit('foo')
+        }
+      }
+      </script>
+      `,
+      options: [{ allowProps: true }],
+      errors: [
+        {
+          line: 3,
+          messageId: 'missing'
+        },
+        {
+          line: 9,
+          messageId: 'missing'
+        },
+        {
+          line: 12,
+          messageId: 'missing'
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/v-for-delimiter-style.js b/tests/lib/rules/v-for-delimiter-style.js
new file mode 100644
index 00000000..4b232a29
--- /dev/null
+++ b/tests/lib/rules/v-for-delimiter-style.js
@@ -0,0 +1,128 @@
+/**
+ * @fileoverview enforce `v-for` directive's delimiter style
+ * @author Flo Edelmann
+ * @copyright 2020 Flo Edelmann. All rights reserved.
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/v-for-delimiter-style')
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2015 }
+})
+
+tester.run('v-for-delimiter-style', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: ''
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div v-for="x in xs"></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div v-for="x    in xs"></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div v-for="x in    xs"></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div v-for="x    in    xs"></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div v-for="x in xs"></div></template>',
+      options: ['in']
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div v-for="x of xs"></div></template>',
+      options: ['of']
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: '<template><div v-for="x of xs"></div></template>',
+      output: '<template><div v-for="x in xs"></div></template>',
+      errors: [
+        {
+          message: "Expected 'in' instead of 'of' in 'v-for'.",
+          column: 23
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div v-for="x    of xs"></div></template>',
+      output: '<template><div v-for="x    in xs"></div></template>',
+      errors: [
+        {
+          message: "Expected 'in' instead of 'of' in 'v-for'.",
+          column: 23
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div v-for="x of    xs"></div></template>',
+      output: '<template><div v-for="x in    xs"></div></template>',
+      errors: [
+        {
+          message: "Expected 'in' instead of 'of' in 'v-for'.",
+          column: 23
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div v-for="x    of    xs"></div></template>',
+      output: '<template><div v-for="x    in    xs"></div></template>',
+      errors: [
+        {
+          message: "Expected 'in' instead of 'of' in 'v-for'.",
+          column: 23
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      options: ['in'],
+      code: '<template><div v-for="x of xs"></div></template>',
+      output: '<template><div v-for="x in xs"></div></template>',
+      errors: [
+        {
+          message: "Expected 'in' instead of 'of' in 'v-for'.",
+          column: 23
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      options: ['of'],
+      code: '<template><div v-for="x in xs"></div></template>',
+      output: '<template><div v-for="x of xs"></div></template>',
+      errors: [
+        {
+          message: "Expected 'of' instead of 'in' in 'v-for'.",
+          column: 23
+        }
+      ]
+    }
+  ]
+})