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

Issue:
Rule Proposal: Warn on emitting camelCase events
**Please describe what the rule should do:**

Ensure that developers emit events that are compatible with specifying listeners in a DOM-based template.

**What category of rule is this? (place an "X" next to just one item)**

[ ] Enforces code style
[x] Warns about a potential error
[ ] Suggests an alternate way of doing something
[ ] Other (please specify:)

**Provide 2-3 code examples that this rule will warn about:**

```js
methods: {
	clicked() {
		this.$emit("userClickedButton");
	},
	clicked() {
		this.$emit("Clicked");
	},
	clicked() {
		this.$emit("BUTTON_CLICKED");
	},
},
```
...
```HTML
<body>
<div id="app">
     <child-comp @userClickedButton="doSomething"></child-comp>
</div>
</body>
```

**Why should this rule be included in ESLint (instead of a plugin)?**

If a component emits an event that includes a capital letter and you instantiate that component within an SFC or string template, you can use the `@eventName` / `v-on:eventName` attribute successfully to capture that event.

However, if you instantiate the component from within a DOM-based template, there is no way to capture the event. For props, Vue translates automatically between pascalCase and kebab-case, but it doesn't do so for events. Events simply won't be captured unless the listener attribute matches the emitted event string exactly.

As a result, component authors should be warned when they are creating events that can't be listened for from a DOM-based template. While I don't use DOM templates (I use SFCs almost exclusively), they are used quite often by new Vue developers, or developers just trying to play with a component.

Since this falls under the "silent failure" category, it's pretty frustrating both for the user and the component developer, but a rule to flag it during linting would be fairly simple to create.

Pull request:
Add `vue/custom-event-name-casing` rule
This PR adds `vue/custom-event-name-casing` rule.

The `vue/custom-event-name-casing` rule that enforces using kebab-case custom event names.

The `vue/custom-event-name-casing` rule is add to categories `essential` and `vue3-essential`.

close #356

Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 9b32f097..2df1e96a 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -38,6 +38,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
 
 | Rule ID | Description |    |
 |:--------|:------------|:---|
+| [vue/custom-event-name-casing](./custom-event-name-casing.md) | enforce custom event names always use "kebab-case" |  |
 | [vue/no-arrow-functions-in-watch](./no-arrow-functions-in-watch.md) | disallow using arrow functions to define watcher |  |
 | [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: |
@@ -160,6 +161,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
 
 | Rule ID | Description |    |
 |:--------|:------------|:---|
+| [vue/custom-event-name-casing](./custom-event-name-casing.md) | enforce custom event names always use "kebab-case" |  |
 | [vue/no-arrow-functions-in-watch](./no-arrow-functions-in-watch.md) | disallow using arrow functions to define watcher |  |
 | [vue/no-async-in-computed-properties](./no-async-in-computed-properties.md) | disallow asynchronous actions in computed properties |  |
 | [vue/no-custom-modifiers-on-v-model](./no-custom-modifiers-on-v-model.md) | disallow custom modifiers on v-model used on the component |  |
diff --git a/docs/rules/custom-event-name-casing.md b/docs/rules/custom-event-name-casing.md
new file mode 100644
index 00000000..cc0153c8
--- /dev/null
+++ b/docs/rules/custom-event-name-casing.md
@@ -0,0 +1,63 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/custom-event-name-casing
+description: enforce custom event names always use "kebab-case"
+---
+# vue/custom-event-name-casing
+> enforce custom event names always use "kebab-case"
+
+- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
+
+## :book: Rule Details
+
+This rule enforces using kebab-case custom event names.
+
+> Event names will never be used as variable or property names in JavaScript, so there’s no reason to use camelCase or PascalCase. Additionally, `v-on` event listeners inside DOM templates will be automatically transformed to lowercase (due to HTML’s case-insensitivity), so `v-on:myEvent` would become `v-on:myevent` – making `myEvent` impossible to listen to.
+>
+> For these reasons, we recommend you **always use kebab-case for event names**.
+
+See [Guide - Custom Events] for more details.
+
+<eslint-code-block :rules="{'vue/custom-event-name-casing': ['error']}">
+
+```vue
+<template>
+  <!-- ✔ GOOD -->
+  <button @click="$emit('my-event')" />
+
+  <!-- ✘ BAD -->
+  <button @click="$emit('myEvent')" />
+</template>
+<script>
+export default {
+  methods: {
+    onClick () {
+      /* ✔ GOOD */
+      this.$emit('my-event')
+      this.$emit('update:myProp', myProp)
+
+      /* ✘ BAD */
+      this.$emit('myEvent')
+    }
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further Reading
+
+- [Guide - Custom Events]
+
+[Guide - Custom Events]: https://vuejs.org/v2/guide/components-custom-events.html
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/custom-event-name-casing.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/custom-event-name-casing.js)
diff --git a/docs/rules/html-indent.md b/docs/rules/html-indent.md
index 81e22585..d82ff86e 100644
--- a/docs/rules/html-indent.md
+++ b/docs/rules/html-indent.md
@@ -76,7 +76,11 @@ This rule enforces a consistent indentation style in `<template>`. The default s
 - `type` (`number | "tab"`) ... The type of indentation. Default is `2`. If this is a number, it's the number of spaces for one indent. If this is `"tab"`, it uses one tab for one indent.
 - `attribute` (`integer`) ... The multiplier of indentation for attributes. Default is `1`.
 - `baseIndent` (`integer`) ... The multiplier of indentation for top-level statements. Default is `1`.
-- `closeBracket` (`integer`) ... The multiplier of indentation for right brackets. Default is `0`.
+- `closeBracket` (`integer | object`) ... The multiplier of indentation for right brackets. Default is `0`.  
+  You can apply all of the following by setting a number value.
+  - `closeBracket.startTag` (`integer`) ... The multiplier of indentation for right brackets of start tags (`<div>`). Default is `0`.
+  - `closeBracket.endTag` (`integer`) ... The multiplier of indentation for right brackets of end tags (`</div>`). Default is `0`.
+  - `closeBracket.selfClosingTag` (`integer`) ... The multiplier of indentation for right brackets of start tags (`<div/>`). Default is `0`.
 - `alignAttributesVertically` (`boolean`) ... Condition for whether attributes should be vertically aligned to the first attribute in multiline case or not. Default is `true`
 - `ignores` (`string[]`) ... The selector to ignore nodes. The AST spec is [here](https://github.com/mysticatea/vue-eslint-parser/blob/master/docs/ast.md). You can use [esquery](https://github.com/estools/esquery#readme) to select nodes. Default is an empty array.
 
diff --git a/lib/configs/essential.js b/lib/configs/essential.js
index 92a9e740..9005982b 100644
--- a/lib/configs/essential.js
+++ b/lib/configs/essential.js
@@ -6,6 +6,7 @@
 module.exports = {
   extends: require.resolve('./base'),
   rules: {
+    'vue/custom-event-name-casing': 'error',
     'vue/no-arrow-functions-in-watch': 'error',
     'vue/no-async-in-computed-properties': 'error',
     'vue/no-custom-modifiers-on-v-model': 'error',
diff --git a/lib/configs/vue3-essential.js b/lib/configs/vue3-essential.js
index 9ae00e5e..b2aec42d 100644
--- a/lib/configs/vue3-essential.js
+++ b/lib/configs/vue3-essential.js
@@ -6,6 +6,7 @@
 module.exports = {
   extends: require.resolve('./base'),
   rules: {
+    'vue/custom-event-name-casing': 'error',
     'vue/no-arrow-functions-in-watch': 'error',
     'vue/no-async-in-computed-properties': 'error',
     'vue/no-deprecated-data-object-declaration': 'error',
diff --git a/lib/index.js b/lib/index.js
index 6b5ea201..8f6e7f4d 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -21,6 +21,7 @@ module.exports = {
     'component-definition-name-casing': require('./rules/component-definition-name-casing'),
     'component-name-in-template-casing': require('./rules/component-name-in-template-casing'),
     'component-tags-order': require('./rules/component-tags-order'),
+    'custom-event-name-casing': require('./rules/custom-event-name-casing'),
     'dot-location': require('./rules/dot-location'),
     eqeqeq: require('./rules/eqeqeq'),
     'html-closing-bracket-newline': require('./rules/html-closing-bracket-newline'),
diff --git a/lib/rules/custom-event-name-casing.js b/lib/rules/custom-event-name-casing.js
new file mode 100644
index 00000000..cec6f55d
--- /dev/null
+++ b/lib/rules/custom-event-name-casing.js
@@ -0,0 +1,221 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+/**
+ * @typedef {import('vue-eslint-parser').AST.ESLintLiteral} Literal
+ * @typedef {import('vue-eslint-parser').AST.ESLintCallExpression} CallExpression
+ */
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const { findVariable } = require('eslint-utils')
+const utils = require('../utils')
+const { isKebabCase } = require('../utils/casing')
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+/**
+ * Check whether the given event name is valid.
+ * @param {string} name The name to check.
+ * @returns {boolean} `true` if the given event name is valid.
+ */
+function isValidEventName(name) {
+  return isKebabCase(name) || name.startsWith('update:')
+}
+
+/**
+ * Get the name param node from the given CallExpression
+ * @param {CallExpression} node CallExpression
+ * @returns { Literal & { value: string } }
+ */
+function getNameParamNode(node) {
+  const nameLiteralNode = node.arguments[0]
+  if (
+    !nameLiteralNode ||
+    nameLiteralNode.type !== 'Literal' ||
+    typeof nameLiteralNode.value !== 'string'
+  ) {
+    // cannot check
+    return null
+  }
+
+  return nameLiteralNode
+}
+/**
+ * Get the callee member node from the given CallExpression
+ * @param {CallExpression} node CallExpression
+ */
+function getCalleeMemberNode(node) {
+  const callee = node.callee
+
+  if (callee.type === 'MemberExpression') {
+    const name = utils.getStaticPropertyName(callee)
+    if (name) {
+      return { name, member: callee }
+    }
+  }
+  return null
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'enforce custom event names always use "kebab-case"',
+      categories: ['vue3-essential', 'essential'],
+      url: 'https://eslint.vuejs.org/rules/custom-event-name-casing.html'
+    },
+    fixable: null,
+    schema: [],
+    messages: {
+      unexpected: "Custom event name '{{name}}' must be kebab-case."
+    }
+  },
+
+  create(context) {
+    const setupContexts = new Map()
+
+    /**
+     * @param { Literal & { value: string } } nameLiteralNode
+     */
+    function verify(nameLiteralNode) {
+      const name = nameLiteralNode.value
+      if (isValidEventName(name)) {
+        return
+      }
+      context.report({
+        node: nameLiteralNode,
+        messageId: 'unexpected',
+        data: {
+          name
+        }
+      })
+    }
+
+    return utils.defineTemplateBodyVisitor(
+      context,
+      {
+        CallExpression(node) {
+          const callee = node.callee
+          const nameLiteralNode = getNameParamNode(node)
+          if (!nameLiteralNode) {
+            // cannot check
+            return
+          }
+          if (callee.type === 'Identifier' && callee.name === '$emit') {
+            verify(nameLiteralNode)
+          }
+        }
+      },
+      utils.compositingVisitors(
+        utils.defineVueVisitor(context, {
+          onSetupFunctionEnter(node, { node: vueNode }) {
+            const contextParam = node.params[1]
+            if (!contextParam) {
+              // no arguments
+              return
+            }
+            if (contextParam.type === 'RestElement') {
+              // cannot check
+              return
+            }
+            if (contextParam.type === 'ArrayPattern') {
+              // cannot check
+              return
+            }
+            const contextReferenceIds = new Set()
+            const emitReferenceIds = new Set()
+            if (contextParam.type === 'ObjectPattern') {
+              const emitProperty = contextParam.properties.find(
+                (p) =>
+                  p.type === 'Property' &&
+                  utils.getStaticPropertyName(p) === 'emit'
+              )
+              if (!emitProperty) {
+                return
+              }
+              const emitParam = emitProperty.value
+              // `setup(props, {emit})`
+              const variable = findVariable(context.getScope(), emitParam)
+              if (!variable) {
+                return
+              }
+              for (const reference of variable.references) {
+                emitReferenceIds.add(reference.identifier)
+              }
+            } else {
+              // `setup(props, context)`
+              const variable = findVariable(context.getScope(), contextParam)
+              if (!variable) {
+                return
+              }
+              for (const reference of variable.references) {
+                contextReferenceIds.add(reference.identifier)
+              }
+            }
+            setupContexts.set(vueNode, {
+              contextReferenceIds,
+              emitReferenceIds
+            })
+          },
+          CallExpression(node, { node: vueNode }) {
+            const nameLiteralNode = getNameParamNode(node)
+            if (!nameLiteralNode) {
+              // cannot check
+              return
+            }
+
+            // verify setup context
+            const setupContext = setupContexts.get(vueNode)
+            if (setupContext) {
+              const { contextReferenceIds, emitReferenceIds } = setupContext
+              if (emitReferenceIds.has(node.callee)) {
+                // verify setup(props,{emit}) {emit()}
+                verify(nameLiteralNode)
+              } else {
+                const emit = getCalleeMemberNode(node)
+                if (
+                  emit &&
+                  emit.name === 'emit' &&
+                  contextReferenceIds.has(emit.member.object)
+                ) {
+                  // verify setup(props,context) {context.emit()}
+                  verify(nameLiteralNode)
+                }
+              }
+            }
+          },
+          onVueObjectExit(node) {
+            setupContexts.delete(node)
+          }
+        }),
+        {
+          CallExpression(node) {
+            const nameLiteralNode = getNameParamNode(node)
+            if (!nameLiteralNode) {
+              // cannot check
+              return
+            }
+            const emit = getCalleeMemberNode(node)
+            // verify $emit
+            if (emit && emit.name === '$emit') {
+              // verify this.$emit()
+              verify(nameLiteralNode)
+            }
+          }
+        }
+      )
+    )
+  }
+}
diff --git a/lib/rules/html-indent.js b/lib/rules/html-indent.js
index d62f6f3a..9cbea60c 100644
--- a/lib/rules/html-indent.js
+++ b/lib/rules/html-indent.js
@@ -44,7 +44,20 @@ module.exports = {
         properties: {
           attribute: { type: 'integer', minimum: 0 },
           baseIndent: { type: 'integer', minimum: 0 },
-          closeBracket: { type: 'integer', minimum: 0 },
+          closeBracket: {
+            anyOf: [
+              { type: 'integer', minimum: 0 },
+              {
+                type: 'object',
+                properties: {
+                  startTag: { type: 'integer', minimum: 0 },
+                  endTag: { type: 'integer', minimum: 0 },
+                  selfClosingTag: { type: 'integer', minimum: 0 }
+                },
+                additionalProperties: false
+              }
+            ]
+          },
           switchCase: { type: 'integer', minimum: 0 },
           alignAttributesVertically: { type: 'boolean' },
           ignores: {
diff --git a/lib/rules/no-unused-vars.js b/lib/rules/no-unused-vars.js
index 37356c9c..e808ee7f 100644
--- a/lib/rules/no-unused-vars.js
+++ b/lib/rules/no-unused-vars.js
@@ -6,6 +6,58 @@
 
 const utils = require('../utils')
 
+/**
+ * @typedef {import('vue-eslint-parser').AST.Node} Node
+ * @typedef {import('vue-eslint-parser').AST.VElement} VElement
+ * @typedef {import('vue-eslint-parser').AST.Variable} Variable
+ */
+/**
+ * @typedef {Variable['kind']} VariableKind
+ */
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+/**
+ * Groups variables by directive kind.
+ * @param {VElement} node The element node
+ * @returns { { [kind in VariableKind]?: Variable[] } } The variables of grouped by directive kind.
+ */
+function groupingVariables(node) {
+  /** @type { { [kind in VariableKind]?: Variable[] } } */
+  const result = {}
+  for (const variable of node.variables) {
+    const vars = result[variable.kind] || (result[variable.kind] = [])
+    vars.push(variable)
+  }
+  return result
+}
+
+/**
+ * Checks if the given variable was defined by destructuring.
+ * @param {Variable} variable the given variable to check
+ * @returns {boolean} `true` if the given variable was defined by destructuring.
+ */
+function isDestructuringVar(variable) {
+  const node = variable.id
+  /** @type {Node} */
+  let parent = node.parent
+  while (parent) {
+    if (
+      parent.type === 'VForExpression' ||
+      parent.type === 'VSlotScopeExpression'
+    ) {
+      return false
+    }
+    if (parent.type === 'Property' || parent.type === 'ArrayPattern') {
+      return true
+    }
+    parent = parent.parent
+  }
+  return false
+}
+
 // ------------------------------------------------------------------------------
 // Rule Definition
 // ------------------------------------------------------------------------------
@@ -41,38 +93,52 @@ module.exports = {
       ignoreRegEx = new RegExp(ignorePattern, 'u')
     }
     return utils.defineTemplateBodyVisitor(context, {
+      /**
+       * @param {VElement} node
+       */
       VElement(node) {
-        const variables = node.variables
-        for (let i = variables.length - 1; i >= 0; i--) {
-          const variable = variables[i]
+        const vars = groupingVariables(node)
+        for (const variables of Object.values(vars)) {
+          let hasAfterUsed = false
 
-          if (variable.references.length) {
-            break
-          }
+          for (let i = variables.length - 1; i >= 0; i--) {
+            const variable = variables[i]
 
-          if (ignoreRegEx != null && ignoreRegEx.test(variable.id.name)) {
-            continue
-          }
-          context.report({
-            node: variable.id,
-            loc: variable.id.loc,
-            message: `'{{name}}' is defined but never used.`,
-            data: variable.id,
-            suggest:
-              ignorePattern === '^_'
-                ? [
-                    {
-                      desc: `Replace the ${variable.id.name} with _${variable.id.name}`,
-                      fix(fixer) {
-                        return fixer.replaceText(
-                          variable.id,
-                          `_${variable.id.name}`
-                        )
+            if (variable.references.length) {
+              hasAfterUsed = true
+              continue
+            }
+
+            if (ignoreRegEx != null && ignoreRegEx.test(variable.id.name)) {
+              continue
+            }
+
+            if (hasAfterUsed && !isDestructuringVar(variable)) {
+              // If a variable after the variable is used, it will be skipped.
+              continue
+            }
+
+            context.report({
+              node: variable.id,
+              loc: variable.id.loc,
+              message: `'{{name}}' is defined but never used.`,
+              data: variable.id,
+              suggest:
+                ignorePattern === '^_'
+                  ? [
+                      {
+                        desc: `Replace the ${variable.id.name} with _${variable.id.name}`,
+                        fix(fixer) {
+                          return fixer.replaceText(
+                            variable.id,
+                            `_${variable.id.name}`
+                          )
+                        }
                       }
-                    }
-                  ]
-                : []
-          })
+                    ]
+                  : []
+            })
+          }
         }
       }
     })
diff --git a/lib/utils/indent-common.js b/lib/utils/indent-common.js
index 1c8c3838..a69d7a5d 100644
--- a/lib/utils/indent-common.js
+++ b/lib/utils/indent-common.js
@@ -21,12 +21,26 @@ const BLOCK_COMMENT_PREFIX = /^\s*\*/
 const ITERATION_OPTS = Object.freeze({ includeComments: true, filter: isNotWhitespace })
 const PREFORMATTED_ELEMENT_NAMES = ['pre', 'textarea']
 
+/**
+ * @typedef {object} IndentOptions
+ * @property { " " | "\t" } IndentOptions.indentChar
+ * @property {number} IndentOptions.indentSize
+ * @property {number} IndentOptions.baseIndent
+ * @property {number} IndentOptions.attribute
+ * @property {object} IndentOptions.closeBracket
+ * @property {number} IndentOptions.closeBracket.startTag
+ * @property {number} IndentOptions.closeBracket.endTag
+ * @property {number} IndentOptions.closeBracket.selfClosingTag
+ * @property {number} IndentOptions.switchCase
+ * @property {boolean} IndentOptions.alignAttributesVertically
+ * @property {string[]} IndentOptions.ignores
+ */
 /**
  * Normalize options.
  * @param {number|"tab"|undefined} type The type of indentation.
  * @param {Object} options Other options.
  * @param {Object} defaultOptions The default value of options.
- * @returns {{indentChar:" "|"\t",indentSize:number,baseIndent:number,attribute:number,closeBracket:number,switchCase:number,alignAttributesVertically:boolean,ignores:string[]}} Normalized options.
+ * @returns {IndentOptions} Normalized options.
  */
 function parseOptions (type, options, defaultOptions) {
   const ret = Object.assign({
@@ -34,7 +48,11 @@ function parseOptions (type, options, defaultOptions) {
     indentSize: 2,
     baseIndent: 0,
     attribute: 1,
-    closeBracket: 0,
+    closeBracket: {
+      startTag: 0,
+      endTag: 0,
+      selfClosingTag: 0
+    },
     switchCase: 0,
     alignAttributesVertically: true,
     ignores: []
@@ -54,7 +72,21 @@ function parseOptions (type, options, defaultOptions) {
     ret.attribute = options.attribute
   }
   if (Number.isSafeInteger(options.closeBracket)) {
-    ret.closeBracket = options.closeBracket
+    const num = options.closeBracket
+    ret.closeBracket = {
+      startTag: num,
+      endTag: num,
+      selfClosingTag: num
+    }
+  } else if (options.closeBracket) {
+    ret.closeBracket = Object.assign(
+      {
+        startTag: 0,
+        endTag: 0,
+        selfClosingTag: 0
+      },
+      options.closeBracket
+    )
   }
   if (Number.isSafeInteger(options.switchCase)) {
     ret.switchCase = options.switchCase
@@ -919,11 +951,12 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
     },
 
     VEndTag (node) {
-      const openToken = tokenStore.getFirstToken(node)
+      const element = node.parent
+      const startTagOpenToken = tokenStore.getFirstToken(element.startTag)
       const closeToken = tokenStore.getLastToken(node)
 
       if (closeToken.type.endsWith('TagClose')) {
-        setOffset(closeToken, options.closeBracket, openToken)
+        setOffset(closeToken, options.closeBracket.endTag, startTagOpenToken)
       }
     },
 
@@ -996,7 +1029,10 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
         options.alignAttributesVertically
       )
       if (closeToken != null && closeToken.type.endsWith('TagClose')) {
-        setOffset(closeToken, options.closeBracket, openToken)
+        const offset = closeToken.type !== 'HTMLSelfClosingTagClose'
+          ? options.closeBracket.startTag
+          : options.closeBracket.selfClosingTag
+        setOffset(closeToken, offset, openToken)
       }
     },
 


Test patch:
diff --git a/tests/fixtures/html-indent/close-bracket-01.vue b/tests/fixtures/html-indent/close-bracket-01.vue
new file mode 100644
index 00000000..3307b145
--- /dev/null
+++ b/tests/fixtures/html-indent/close-bracket-01.vue
@@ -0,0 +1,16 @@
+<!--{}-->
+<template>
+  <a
+    href="#"
+  >Link 1</a
+  >
+  <a
+    href="#"
+  >
+    Link 2</a
+  >
+  <div>Content</div
+  >
+  <div
+  />
+</template>
diff --git a/tests/fixtures/html-indent/close-bracket-02.vue b/tests/fixtures/html-indent/close-bracket-02.vue
new file mode 100644
index 00000000..3f171a69
--- /dev/null
+++ b/tests/fixtures/html-indent/close-bracket-02.vue
@@ -0,0 +1,16 @@
+<!--{"options":[2, { "closeBracket": 1 }]}-->
+<template>
+  <a
+    href="#"
+    >Link 1</a
+    >
+  <a
+    href="#"
+    >
+    Link 2</a
+    >
+  <div>Content</div
+    >
+  <div
+    />
+</template>
diff --git a/tests/fixtures/html-indent/close-bracket-03.vue b/tests/fixtures/html-indent/close-bracket-03.vue
new file mode 100644
index 00000000..518423c4
--- /dev/null
+++ b/tests/fixtures/html-indent/close-bracket-03.vue
@@ -0,0 +1,16 @@
+<!--{"options":[2, { "closeBracket": { "startTag": 1 } }]}-->
+<template>
+  <a
+    href="#"
+    >Link 1</a
+  >
+  <a
+    href="#"
+    >
+    Link 2</a
+  >
+  <div>Content</div
+  >
+  <div
+  />
+</template>
diff --git a/tests/fixtures/html-indent/close-bracket-04.vue b/tests/fixtures/html-indent/close-bracket-04.vue
new file mode 100644
index 00000000..f90ffc4d
--- /dev/null
+++ b/tests/fixtures/html-indent/close-bracket-04.vue
@@ -0,0 +1,16 @@
+<!--{"options":[2, { "closeBracket": { "endTag": 1 } }]}-->
+<template>
+  <a
+    href="#"
+  >Link 1</a
+    >
+  <a
+    href="#"
+  >
+    Link 2</a
+    >
+  <div>Content</div
+    >
+  <div
+  />
+</template>
diff --git a/tests/fixtures/html-indent/close-bracket-05.vue b/tests/fixtures/html-indent/close-bracket-05.vue
new file mode 100644
index 00000000..b5d48275
--- /dev/null
+++ b/tests/fixtures/html-indent/close-bracket-05.vue
@@ -0,0 +1,16 @@
+<!--{"options":[2, { "closeBracket": { "selfClosingTag": 1 } }]}-->
+<template>
+  <a
+    href="#"
+  >Link 1</a
+  >
+  <a
+    href="#"
+  >
+    Link 2</a
+  >
+  <div>Content</div
+  >
+  <div
+    />
+</template>
diff --git a/tests/fixtures/html-indent/close-bracket-06.vue b/tests/fixtures/html-indent/close-bracket-06.vue
new file mode 100644
index 00000000..d5bae8ed
--- /dev/null
+++ b/tests/fixtures/html-indent/close-bracket-06.vue
@@ -0,0 +1,16 @@
+<!--{"options":[2, { "closeBracket": {  "startTag": 1, "endTag": 2, "selfClosingTag": 3 } }]}-->
+<template>
+  <a
+    href="#"
+    >Link 1</a
+      >
+  <a
+    href="#"
+    >
+    Link 2</a
+      >
+  <div>Content</div
+      >
+  <div
+        />
+</template>
diff --git a/tests/lib/rules/custom-event-name-casing.js b/tests/lib/rules/custom-event-name-casing.js
new file mode 100644
index 00000000..b767223e
--- /dev/null
+++ b/tests/lib/rules/custom-event-name-casing.js
@@ -0,0 +1,221 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/custom-event-name-casing')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2019,
+    sourceType: 'module'
+  }
+})
+
+tester.run('custom-event-name-casing', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <input
+          @click="$emit('foo-bar')">
+      </template>
+      <script>
+      export default {
+        setup(props, context) {
+          return {
+            onInput(value) {
+              context.emit('update:fooBar', value)
+              context.emit('foo-bar')
+            }
+          }
+        },
+        methods: {
+          onClick() {
+            this.$emit('foo-bar')
+          }
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <input
+          @click="$emit('update:fooBar', value)">
+      </template>
+      <script>
+      export default {
+        setup(props, {emit}) {
+          return {
+            onInput(value) {
+              emit('update:fooBar', value)
+              emit('foo-bar')
+            }
+          }
+        },
+        methods: {
+          onClick() {
+            this.$emit('update:fooBar', value)
+          }
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <input
+          @click="emit('fooBar')">
+      </template>
+      <script>
+      export default {
+        setup(context) {
+          return {
+            onInput(value) {
+              context.emit('barBaz')
+            }
+          }
+        },
+        methods: {
+          onClick() {
+            $emit('bazQux')
+          }
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <input
+          @click="$emit(fooBar)">
+      </template>
+      <script>
+      export default {
+        setup(props, context) {
+          return {
+            onInput(value) {
+              context.emit(barBaz)
+            }
+          }
+        },
+        methods: {
+          onClick() {
+            this.$emit(bazQux)
+          }
+        }
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup(prop, ...context) {
+          return {
+            onInput(value) {
+              context.emit('barBaz')
+            }
+          }
+        },
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup(prop, [context]) {
+          return {
+            onInput(value) {
+              context.emit('barBaz')
+            }
+          }
+        },
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        setup(prop, {e}) {
+          return {
+            onInput(value) {
+              e('barBaz')
+            }
+          }
+        },
+      }
+      </script>
+      `
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <input
+          @click="$emit('fooBar')">
+      </template>
+      <script>
+      export default {
+        setup(props, context) {
+          return {
+            onInput(value) {
+              context.emit('barBaz')
+            }
+          }
+        },
+        methods: {
+          onClick() {
+            this.$emit('bazQux')
+          }
+        }
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message: "Custom event name 'fooBar' must be kebab-case.",
+          line: 4,
+          column: 25,
+          endLine: 4,
+          endColumn: 33
+        },
+        {
+          message: "Custom event name 'barBaz' must be kebab-case.",
+          line: 11,
+          column: 28,
+          endLine: 11,
+          endColumn: 36
+        },
+        {
+          message: "Custom event name 'bazQux' must be kebab-case.",
+          line: 17,
+          column: 24,
+          endLine: 17,
+          endColumn: 32
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-unused-vars.js b/tests/lib/rules/no-unused-vars.js
index 0c1b85e5..81df799b 100644
--- a/tests/lib/rules/no-unused-vars.js
+++ b/tests/lib/rules/no-unused-vars.js
@@ -162,6 +162,58 @@ tester.run('no-unused-vars', rule, {
       code: '<template><div v-for="(a, _i) in foo" ></div></template>',
       options: [{ ignorePattern: '^_' }],
       errors: ["'a' is defined but never used."]
+    },
+    {
+      code:
+        '<template><my-component v-slot="a" >{{d}}</my-component></template>',
+      errors: ["'a' is defined but never used."]
+    },
+    {
+      code:
+        '<template><my-component v-for="i in foo" v-slot="a" >{{a}}</my-component></template>',
+      errors: ["'i' is defined but never used."]
+    },
+    {
+      code:
+        '<template><my-component v-for="i in foo" v-slot="a" >{{i}}</my-component></template>',
+      errors: ["'a' is defined but never used."]
+    },
+    {
+      code:
+        '<template><div v-for="({a, b}, [c, d], e, f) in foo" >{{f}}</div></template>',
+      errors: [
+        "'a' is defined but never used.",
+        "'b' is defined but never used.",
+        "'c' is defined but never used.",
+        "'d' is defined but never used."
+      ]
+    },
+    {
+      code:
+        '<template><div v-for="({a, b}, c, [d], e, f) in foo" >{{f}}</div></template>',
+      errors: [
+        "'a' is defined but never used.",
+        "'b' is defined but never used.",
+        "'d' is defined but never used."
+      ]
+    },
+    {
+      code:
+        '<template><my-component v-slot="{a, b, c, d}" >{{d}}</my-component></template>',
+      errors: [
+        "'a' is defined but never used.",
+        "'b' is defined but never used.",
+        "'c' is defined but never used."
+      ]
+    },
+    {
+      code:
+        '<template><div v-for="({a, b: bar}, c = 1, [d], e, f) in foo" >{{f}}</div></template>',
+      errors: [
+        "'a' is defined but never used.",
+        "'bar' is defined but never used.",
+        "'d' is defined but never used."
+      ]
     }
   ]
 })