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

Issue:
Rule Proposal: no undef component in template when using <script setup>
**Please describe what the rule should do:**
Warn against components that appear in the template but not defined/imported in script-setup

**What category should the rule belong to?**
[ ] Enforces code style (layout)
[X] Warns about a potential error (problem)
[ ] Suggests an alternate way of doing something (suggestion)
[ ] Other (please specify:)

**Provide 2-3 code examples that this rule should warn about:**

```vue
// Bad: OtherComponent not defined
<template>
  <div>
    <SomeComponent />
    <OtherComponent />
  </div>
</template>

<script setup>
import SomeComponent from '@/components/SomeComponent.vue'
</script>

//Good:
<template>
  <div>
    <SomeComponent />
    <OtherComponent />
  </div>
</template>

<script setup>
import SomeComponent from '@/components/SomeComponent.vue'
import OtherComponent from '@/components/OtherComponent.vue' // or: const OtherComponent = xxx.
</script>

//Also Good:
<template>
  <div>
    <some-component />
    <other-component />
  </div>
</template>

<script setup>
import SomeComponent from '@/components/SomeComponent.vue'
import OtherComponent from '@/components/OtherComponent.vue' // or: const OtherComponent = xxx.
</script>

```

**Additional context**
This is like the script-setup version of "no-unregistered components" rule



Pull request:
Add `vue/no-undef-components` rule and deprecate `vue/no-unregistered-components` rule 
This PR adds `vue/no-undef-components` rule and deprecates `vue/no-unregistered-components` rule .
The `vue/no-undef-components` rule warns that the component used in the `<template>` is not defined.  

close #1693

Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 49a6c638..a4f8c871 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -341,8 +341,8 @@ For example:
 | [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes |  |
 | [vue/no-template-target-blank](./no-template-target-blank.md) | disallow target="_blank" attribute without rel="noopener noreferrer" |  |
 | [vue/no-this-in-before-route-enter](./no-this-in-before-route-enter.md) | disallow `this` usage in a `beforeRouteEnter` method |  |
+| [vue/no-undef-components](./no-undef-components.md) | disallow use of undefined components in `<template>` |  |
 | [vue/no-undef-properties](./no-undef-properties.md) | disallow undefined properties |  |
-| [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-unused-refs](./no-unused-refs.md) | disallow unused refs |  |
@@ -396,6 +396,7 @@ The following rules extend the rules provided by ESLint itself and apply them to
 | [vue/object-curly-newline](./object-curly-newline.md) | enforce consistent line breaks after opening and before closing braces | :wrench: |
 | [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: |
 | [vue/object-property-newline](./object-property-newline.md) | enforce placing object properties on separate lines | :wrench: |
+| [vue/object-shorthand](./object-shorthand.md) | require or disallow method and property shorthand syntax for object literals | :wrench: |
 | [vue/operator-linebreak](./operator-linebreak.md) | enforce consistent linebreak style for operators | :wrench: |
 | [vue/prefer-template](./prefer-template.md) | require template literals instead of string concatenation | :wrench: |
 | [vue/space-in-parens](./space-in-parens.md) | enforce consistent spacing inside parentheses | :wrench: |
@@ -413,3 +414,4 @@ The following rules extend the rules provided by ESLint itself and apply them to
 | [vue/experimental-script-setup-vars](./experimental-script-setup-vars.md) | (no replacement) |
 | [vue/name-property-casing](./name-property-casing.md) | [vue/component-definition-name-casing](./component-definition-name-casing.md) |
 | [vue/no-confusing-v-for-v-if](./no-confusing-v-for-v-if.md) | [vue/no-use-v-if-with-v-for](./no-use-v-if-with-v-for.md) |
+| [vue/no-unregistered-components](./no-unregistered-components.md) | [vue/no-undef-components](./no-undef-components.md) |
diff --git a/docs/rules/no-undef-components.md b/docs/rules/no-undef-components.md
new file mode 100644
index 00000000..b4f75750
--- /dev/null
+++ b/docs/rules/no-undef-components.md
@@ -0,0 +1,178 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-undef-components
+description: disallow use of undefined components in `<template>`
+---
+# vue/no-undef-components
+
+> disallow use of undefined components in `<template>`
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+
+This rule warns reports component that are used in the `<template>`, but that are not defined in the `<script setup>` or the Options API's `components` section.
+
+Undefined components will be resolved from globally registered components. However, if you are not using global components, you can use this rule to prevent run-time errors.
+
+::: warning Note
+This rule cannot check globally registered components and components registered in mixins
+unless you add them as part of the ignored patterns.
+:::
+
+<eslint-code-block :rules="{'vue/no-undef-components': ['error']}">
+
+```vue
+<script setup>
+import Foo from './Foo.vue'
+</script>
+
+<template>
+  <!-- ✓ GOOD -->
+  <Foo />
+
+  <!-- ✗ BAD -->
+  <Bar />
+</template>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/no-undef-components': ['error']}">
+
+```vue
+<!-- ✓ GOOD -->
+<template>
+  <div>
+    <h2>Lorem ipsum</h2>
+    <the-modal>
+      <component is="TheInput" />
+      <component :is="'TheDropdown'" />
+      <TheButton>CTA</TheButton>
+    </the-modal>
+  </div>
+</template>
+
+<script>
+import TheButton from 'components/TheButton.vue'
+import TheModal from 'components/TheModal.vue'
+import TheInput from 'components/TheInput.vue'
+import TheDropdown from 'components/TheDropdown.vue'
+
+export default {
+  components: {
+    TheButton,
+    TheModal,
+    TheInput,
+    TheDropdown,
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/no-undef-components': ['error']}">
+
+```vue
+<!-- ✗ BAD -->
+<template>
+  <div>
+    <h2>Lorem ipsum</h2>
+    <TheModal />
+  </div>
+</template>
+
+<script>
+export default {
+  components: {
+
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+```json
+{
+  "vue/no-undef-components": ["error", {
+    "ignorePatterns": []
+  }]
+}
+```
+
+- `ignorePatterns` Suppresses all errors if component name matches one or more patterns.
+
+### `ignorePatterns: ['custom(\\-\\w+)+']`
+
+<eslint-code-block :rules="{'vue/no-undef-components': ['error', { 'ignorePatterns': ['custom(\\-\\w+)+'] }]}">
+
+```vue
+<script setup>
+</script>
+
+<template>
+  <!-- ✓ GOOD -->
+  <CustomComponent />
+
+  <!-- ✗ BAD -->
+  <Bar />
+</template>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/no-undef-components': ['error', { 'ignorePatterns': ['custom(\\-\\w+)+'] }]}">
+
+```vue
+<!-- ✓ GOOD -->
+<template>
+  <div>
+    <h2>Lorem ipsum</h2>
+    <CustomComponent />
+  </div>
+</template>
+
+<script>
+  export default {
+    components: {
+
+    },
+  }
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/no-undef-components': ['error', { 'ignorePatterns': ['custom(\\-\\w+)+'] }]}">
+
+```vue
+<!-- ✗ BAD -->
+<template>
+  <div>
+    <h2>Lorem ipsum</h2>
+    <WarmButton />
+  </div>
+</template>
+
+<script>
+  export default {
+    components: {
+
+    },
+  }
+</script>
+```
+
+</eslint-code-block>
+
+## :couple: Related Rules
+
+- [vue/no-undef-properties](./no-undef-properties.md)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-undef-components.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-undef-components.js)
diff --git a/docs/rules/no-unregistered-components.md b/docs/rules/no-unregistered-components.md
index 20e2867b..761431cf 100644
--- a/docs/rules/no-unregistered-components.md
+++ b/docs/rules/no-unregistered-components.md
@@ -9,6 +9,8 @@ since: v7.0.0
 
 > disallow using components that are not registered inside templates
 
+- :warning: This rule was **deprecated** and replaced by [vue/no-undef-components](no-undef-components.md) rule.
+
 ## :book: Rule Details
 
 This rule reports components that haven't been registered and are being used in the template.
diff --git a/docs/rules/object-shorthand.md b/docs/rules/object-shorthand.md
new file mode 100644
index 00000000..e2e9249b
--- /dev/null
+++ b/docs/rules/object-shorthand.md
@@ -0,0 +1,27 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/object-shorthand
+description: require or disallow method and property shorthand syntax for object literals
+---
+# vue/object-shorthand
+
+> require or disallow method and property shorthand syntax for object literals
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+- :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.
+
+This rule is the same rule as core [object-shorthand] rule but it applies to the expressions in `<template>`.
+
+## :books: Further Reading
+
+- [object-shorthand]
+
+[object-shorthand]: https://eslint.org/docs/rules/object-shorthand
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/object-shorthand.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/object-shorthand.js)
+
+<sup>Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/object-shorthand)</sup>
diff --git a/lib/index.js b/lib/index.js
index 02171098..c04b18ca 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -128,6 +128,7 @@ module.exports = {
     'no-template-target-blank': require('./rules/no-template-target-blank'),
     'no-textarea-mustache': require('./rules/no-textarea-mustache'),
     'no-this-in-before-route-enter': require('./rules/no-this-in-before-route-enter'),
+    'no-undef-components': require('./rules/no-undef-components'),
     'no-undef-properties': require('./rules/no-undef-properties'),
     'no-unregistered-components': require('./rules/no-unregistered-components'),
     'no-unsupported-features': require('./rules/no-unsupported-features'),
@@ -151,6 +152,7 @@ module.exports = {
     'object-curly-newline': require('./rules/object-curly-newline'),
     'object-curly-spacing': require('./rules/object-curly-spacing'),
     'object-property-newline': require('./rules/object-property-newline'),
+    'object-shorthand': require('./rules/object-shorthand'),
     'one-component-per-file': require('./rules/one-component-per-file'),
     'operator-linebreak': require('./rules/operator-linebreak'),
     'order-in-components': require('./rules/order-in-components'),
diff --git a/lib/rules/no-undef-components.js b/lib/rules/no-undef-components.js
new file mode 100644
index 00000000..aa9ddb0d
--- /dev/null
+++ b/lib/rules/no-undef-components.js
@@ -0,0 +1,270 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+const casing = require('../utils/casing')
+
+// ------------------------------------------------------------------------------
+// Rule helpers
+// ------------------------------------------------------------------------------
+
+/**
+ * `casing.camelCase()` converts the beginning to lowercase,
+ * but does not convert the case of the beginning character when converting with Vue3.
+ * @see https://github.com/vuejs/core/blob/ae4b0783d78670b6e942ae2a4e3ec6efbbffa158/packages/shared/src/index.ts#L105
+ * @param {string} str
+ */
+function camelize(str) {
+  return str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : ''))
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'disallow use of undefined components in `<template>`',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/no-undef-components.html'
+    },
+    fixable: null,
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          ignorePatterns: {
+            type: 'array'
+          }
+        },
+        additionalProperties: false
+      }
+    ],
+    messages: {
+      undef: "The '<{{name}}>' component has been used, but not defined."
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    const options = context.options[0] || {}
+    /** @type {string[]} */
+    const ignorePatterns = options.ignorePatterns || []
+
+    /**
+     * Check whether the given element name is a verify target or not.
+     *
+     * @param {string} rawName The element name.
+     * @returns {boolean}
+     */
+    function isVerifyTargetComponent(rawName) {
+      const kebabCaseName = casing.kebabCase(rawName)
+
+      if (
+        utils.isHtmlWellKnownElementName(rawName) ||
+        utils.isSvgWellKnownElementName(rawName) ||
+        utils.isBuiltInComponentName(kebabCaseName)
+      ) {
+        return false
+      }
+      const pascalCaseName = casing.pascalCase(rawName)
+      // Check ignored patterns
+      if (
+        ignorePatterns.some((pattern) => {
+          const regExp = new RegExp(pattern)
+          return (
+            regExp.test(rawName) ||
+            regExp.test(kebabCaseName) ||
+            regExp.test(pascalCaseName)
+          )
+        })
+      ) {
+        return false
+      }
+      return true
+    }
+
+    /** @type { (rawName:string, reportNode: ASTNode) => void } */
+    let verifyName
+    /** @type {RuleListener} */
+    let scriptVisitor = {}
+    /** @type {TemplateListener} */
+    const templateBodyVisitor = {
+      VElement(node) {
+        if (!utils.isHtmlElementNode(node) && !utils.isSvgElementNode(node)) {
+          return
+        }
+        verifyName(node.rawName, node.startTag)
+      },
+      /** @param {VAttribute} node */
+      "VAttribute[directive=false][key.name='is']"(node) {
+        if (
+          !node.value // `<component is />`
+        )
+          return
+        const value = node.value.value.startsWith('vue:') // Usage on native elements 3.1+
+          ? node.value.value.slice(4)
+          : node.value.value
+        verifyName(value, node)
+      }
+    }
+
+    if (utils.isScriptSetup(context)) {
+      // For <script setup>
+      /** @type {Set<string>} */
+      const scriptVariableNames = new Set()
+      const globalScope = context.getSourceCode().scopeManager.globalScope
+      if (globalScope) {
+        for (const variable of globalScope.variables) {
+          scriptVariableNames.add(variable.name)
+        }
+        const moduleScope = globalScope.childScopes.find(
+          (scope) => scope.type === 'module'
+        )
+        for (const variable of (moduleScope && moduleScope.variables) || []) {
+          scriptVariableNames.add(variable.name)
+        }
+      }
+      /**
+       * @see https://github.com/vuejs/core/blob/ae4b0783d78670b6e942ae2a4e3ec6efbbffa158/packages/compiler-core/src/transforms/transformElement.ts#L334
+       * @param {string} name
+       */
+      const existsSetupReference = (name) => {
+        if (scriptVariableNames.has(name)) {
+          return true
+        }
+        const camelName = camelize(name)
+        if (scriptVariableNames.has(camelName)) {
+          return true
+        }
+        const pascalName = casing.capitalize(camelName)
+        if (scriptVariableNames.has(pascalName)) {
+          return true
+        }
+        return false
+      }
+      verifyName = (rawName, reportNode) => {
+        if (!isVerifyTargetComponent(rawName)) {
+          return
+        }
+        if (existsSetupReference(rawName)) {
+          return
+        }
+        // Check namespace
+        // https://github.com/vuejs/core/blob/ae4b0783d78670b6e942ae2a4e3ec6efbbffa158/packages/compiler-core/src/transforms/transformElement.ts#L305
+        const dotIndex = rawName.indexOf('.')
+        if (dotIndex > 0) {
+          if (existsSetupReference(rawName.slice(0, dotIndex))) {
+            return
+          }
+        }
+
+        context.report({
+          node: reportNode,
+          messageId: 'undef',
+          data: {
+            name: rawName
+          }
+        })
+      }
+    } else {
+      // For Options API
+
+      /**
+       * All registered components
+       * @type {string[]}
+       */
+      const registeredComponentNames = []
+      /**
+       * All registered components, transformed to kebab-case
+       * @type {string[]}
+       */
+      const registeredComponentKebabCaseNames = []
+
+      /**
+       * All registered components using kebab-case syntax
+       * @type {string[]}
+       */
+      const componentsRegisteredAsKebabCase = []
+
+      scriptVisitor = utils.executeOnVue(context, (obj) => {
+        registeredComponentNames.push(
+          ...utils.getRegisteredComponents(obj).map(({ name }) => name)
+        )
+
+        const nameProperty = utils.findProperty(obj, 'name')
+
+        if (nameProperty) {
+          if (nameProperty.value.type === 'Literal') {
+            registeredComponentNames.push(`${nameProperty.value.value}`)
+          }
+        }
+
+        registeredComponentKebabCaseNames.push(
+          ...registeredComponentNames.map((name) => casing.kebabCase(name))
+        )
+        componentsRegisteredAsKebabCase.push(
+          ...registeredComponentNames.filter(
+            (name) => name === casing.kebabCase(name)
+          )
+        )
+      })
+
+      verifyName = (rawName, reportNode) => {
+        if (!isVerifyTargetComponent(rawName)) {
+          return
+        }
+        if (registeredComponentNames.includes(rawName)) {
+          return
+        }
+        const kebabCaseName = casing.kebabCase(rawName)
+        if (registeredComponentKebabCaseNames.includes(kebabCaseName)) {
+          if (
+            // Component registered as `foo-bar` cannot be used as `FooBar`
+            !casing.isPascalCase(rawName)
+          ) {
+            return
+          }
+        }
+
+        context.report({
+          node: reportNode,
+          messageId: 'undef',
+          data: {
+            name: rawName
+          }
+        })
+      }
+
+      /** @param {VDirective} node */
+      templateBodyVisitor[
+        "VAttribute[directive=true][key.name.name='bind'][key.argument.name='is'], VAttribute[directive=true][key.name.name='is']"
+      ] = (node) => {
+        if (
+          !node.value ||
+          node.value.type !== 'VExpressionContainer' ||
+          !node.value.expression
+        )
+          return
+
+        if (node.value.expression.type === 'Literal') {
+          verifyName(`${node.value.expression.value}`, node)
+        }
+      }
+    }
+
+    return utils.defineTemplateBodyVisitor(
+      context,
+      templateBodyVisitor,
+      scriptVisitor
+    )
+  }
+}
diff --git a/lib/rules/no-unregistered-components.js b/lib/rules/no-unregistered-components.js
index 5cc22a9f..efd265e5 100644
--- a/lib/rules/no-unregistered-components.js
+++ b/lib/rules/no-unregistered-components.js
@@ -44,8 +44,10 @@ module.exports = {
         'disallow using components that are not registered inside templates',
       categories: null,
       recommended: false,
-      url: 'https://eslint.vuejs.org/rules/no-unregistered-components.html'
+      url: 'https://eslint.vuejs.org/rules/no-unregistered-components.html',
+      replacedBy: ['no-undef-components']
     },
+    deprecated: true,
     fixable: null,
     schema: [
       {
diff --git a/lib/rules/object-shorthand.js b/lib/rules/object-shorthand.js
new file mode 100644
index 00000000..79a3c8a3
--- /dev/null
+++ b/lib/rules/object-shorthand.js
@@ -0,0 +1,12 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const { wrapCoreRule } = require('../utils')
+
+// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
+module.exports = wrapCoreRule('object-shorthand', {
+  skipDynamicArguments: true
+})
diff --git a/lib/utils/property-references.js b/lib/utils/property-references.js
index 37709ee0..5170240d 100644
--- a/lib/utils/property-references.js
+++ b/lib/utils/property-references.js
@@ -96,6 +96,12 @@ function definePropertyReferenceExtractor(context) {
   /** @type {{ toRefNodes: Set<ESNode>, toRefsNodes: Set<ESNode>} | null} */
   let toRefSet = null
 
+  let isFunctionalTemplate = false
+  const templateBody = context.getSourceCode().ast.templateBody
+  if (templateBody) {
+    isFunctionalTemplate = utils.hasAttribute(templateBody, 'functional')
+  }
+
   function getToRefSet() {
     if (toRefSet) {
       return toRefSet
@@ -578,6 +584,7 @@ function definePropertyReferenceExtractor(context) {
 
       ignoreRef = (name) => globalScope.set.has(name)
     }
+    /** @type {IPropertyReferences[]} */
     const references = []
     for (const id of node.references
       .filter((ref) => ref.variable == null)
@@ -585,9 +592,15 @@ function definePropertyReferenceExtractor(context) {
       if (ignoreRef(id.name)) {
         continue
       }
-      references.push(
-        extractFromName(id.name, id, () => extractFromExpression(id, true))
-      )
+      if (!isFunctionalTemplate) {
+        references.push(
+          extractFromName(id.name, id, () => extractFromExpression(id, true))
+        )
+      } else {
+        if (id.name === 'props') {
+          references.push(extractFromExpression(id, true))
+        }
+      }
     }
     return mergePropertyReferences(references)
   }


Test patch:
diff --git a/tests/lib/rules/no-undef-components.js b/tests/lib/rules/no-undef-components.js
new file mode 100644
index 00000000..8bc637f4
--- /dev/null
+++ b/tests/lib/rules/no-undef-components.js
@@ -0,0 +1,923 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-undef-components')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: 'module'
+  }
+})
+
+tester.run('no-undef-components', rule, {
+  valid: [
+    // <script setup>
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        import Foo from './Foo.vue'
+      </script>
+
+      <template>
+        <Foo />
+        <Teleport />
+        <template v-if="foo"></template>
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        import FooPascalCase from './component.vue'
+        import BarPascalCase from './component.vue'
+        import BazPascalCase from './component.vue'
+      </script>
+
+      <template>
+        <FooPascalCase />
+        <bar-pascal-case />
+        <bazPascalCase />
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      import fooCamelCase from './component.vue'
+      import barCamelCase from './component.vue'
+      </script>
+
+      <template>
+        <fooCamelCase />
+        <bar-camel-case />
+      </template>
+      `
+    },
+    // namespace
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      import * as Form from './form-components'
+      </script>
+
+      <template>
+        <Form.Input>
+          <Form.Label>label</Form.Label>
+        </Form.Input>
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      import Bar from './Bar.vue'
+      </script>
+
+      <template>
+        <Foo />
+      </template>
+      `,
+      options: [{ ignorePatterns: ['Foo'] }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      import Bar from './Bar.vue'
+      </script>
+
+      <template>
+        <CustomComponent />
+      </template>
+      `,
+      options: [
+        {
+          ignorePatterns: ['custom(\\-\\w+)+']
+        }
+      ]
+    },
+
+    // options API
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div>Lorem ipsum</div>
+        </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <CustomComponent />
+        </template>
+      `,
+      options: [
+        {
+          ignorePatterns: ['custom(\\-\\w+)+']
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <CustomComponent>
+            Some text
+          </CustomComponent>
+        </template>
+      `,
+      options: [
+        {
+          ignorePatterns: ['custom(\\-\\w+)+']
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <custom-component>
+            Some text
+          </custom-component>
+        </template>
+      `,
+      options: [
+        {
+          ignorePatterns: ['custom(\\-\\w+)+']
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <custom-component>
+            Some text
+          </custom-component>
+        </template>
+      `,
+      options: [
+        {
+          ignorePatterns: ['Custom(\\w+)+']
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <custom_component>
+            Some text
+          </custom_component>
+        </template>
+      `,
+      options: [
+        {
+          ignorePatterns: ['Custom(\\w+)+']
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <customComponent>
+            Some text
+          </customComponent>
+        </template>
+      `,
+      options: [
+        {
+          ignorePatterns: ['Custom(\\w+)+']
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <customComponent>
+            <warm-code>Text</warm-code>
+            <InfoBtnPrimary value="text" />
+          </customComponent>
+        </template>
+      `,
+      options: [
+        {
+          ignorePatterns: ['Custom(\\w+)+', 'Warm(\\w+)+', 'InfoBtn(\\w+)+']
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <CustomComponent />
+        </template>
+        <script>
+        export default {
+          components: {
+            CustomComponent
+          }
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <CustomComponent>
+            Some text
+          </CustomComponent>
+        </template>
+        <script>
+        export default {
+          components: {
+            CustomComponent
+          }
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component :is="'CustomComponent'" />
+        </template>
+        <script>
+        export default {
+          components: {
+            CustomComponent
+          }
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component :is="'CustomComponent'">
+            Some text
+          </component>
+        </template>
+        <script>
+        export default {
+          components: {
+            CustomComponent
+          }
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component v-if="showComponent" :is="'CustomComponent'" />
+        </template>
+        <script>
+        export default {
+          data: () => ({
+            showComponent: true
+          }),
+          components: {
+            CustomComponent
+          }
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component v-if="showComponent" :is="'CustomComponent'" />
+        </template>
+        <script>
+        export default {
+          data: () => ({
+            showComponent: false
+          }),
+          components: {
+            CustomComponent
+          }
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component :is="'div'" />
+        </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component is="div" />
+        </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component is="span">
+            Text
+          </component>
+        </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <Component is="div" />
+        </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <Component is="span">
+            Text
+          </Component>
+        </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component :is="name" />
+        </template>
+        <script>
+        export default {
+          data: () => ({ name: 'div' })
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component :is="name" />
+        </template>
+        <script>
+        export default {
+          data: () => ({ name: 'warm-button' }),
+          components: { WarmButton }
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <teleport />
+          <suspense />
+          <suspense>
+            <div>Text</div>
+          </suspense>
+          <Teleport />
+          <Suspense />
+          <Suspense>
+            <div>Text</div>
+          </Suspense>
+        </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <transition />
+          <transition-group />
+          <keep-alive />
+          <transition>
+            <div>Text</div>
+          </transition>
+          <TransitionGroup />
+          <KeepAlive />
+          <Transition>
+            <div>Text</div>
+          </Transition>
+          <Slot />
+          <slot />
+          <slot>
+            foo
+          </slot>
+        </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <Custom-Component />
+        </template>
+        <script>
+        export default {
+          components: {
+            'custom-component': InfoPrimaryWrapper
+          }
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component is />
+        </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <Component is />
+        </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div v-is="'CustomComponent'" />
+        </template>
+        <script>
+        export default {
+          components: {
+            CustomComponent
+          }
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <CustomComponent />
+        </template>
+        <script>
+        export default {
+          name: 'CustomComponent'
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <custom-component />
+        </template>
+        <script>
+        export default {
+          name: 'CustomComponent'
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component :is="'CustomComponent'" />
+        </template>
+        <script>
+        export default {
+          name: 'CustomComponent'
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component is="CustomComponent" />
+        </template>
+        <script>
+        export default {
+          name: 'CustomComponent'
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component is="vue:CustomComponent" />
+        </template>
+        <script>
+        export default {
+          name: 'CustomComponent'
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div v-is="'CustomComponent'" />
+        </template>
+        <script>
+        export default {
+          name: 'CustomComponent'
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <CustomComponentWithNamedSlots>
+            <template #slotA>
+              <div>Text</div>
+            </template>
+          </CustomComponentWithNamedSlots>
+        </template>
+        <script>
+        export default {
+          components: {
+            CustomComponentWithNamedSlots
+          }
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <TheModal />
+      </template>
+
+      <script setup>
+      import TheModal from 'foo'
+      </script>`
+    }
+  ],
+  invalid: [
+    // <script setup>
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      import Bar from './Bar.vue'
+      </script>
+
+      <template>
+        <Foo />
+      </template>
+      `,
+      errors: [
+        {
+          message: "The '<Foo>' component has been used, but not defined.",
+          line: 7,
+          column: 9
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      import PascalCase from './component.vue'
+      </script>
+
+      <template>
+        <pascal_case />
+      </template>
+      `,
+      errors: [
+        {
+          message:
+            "The '<pascal_case>' component has been used, but not defined.",
+          line: 7,
+          column: 9
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      import * as Form from './form-components'
+      </script>
+
+      <template>
+        <Foo.Input />
+      </template>
+      `,
+      errors: [
+        {
+          message:
+            "The '<Foo.Input>' component has been used, but not defined.",
+          line: 7,
+          column: 9
+        }
+      ]
+    },
+
+    // options API
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <CustomComponent />
+        </template>
+      `,
+      errors: [
+        {
+          message:
+            "The '<CustomComponent>' component has been used, but not defined.",
+          line: 3
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <WarmButton />
+        </template>
+      `,
+      options: [
+        {
+          ignorePatterns: ['custom(\\-\\w+)+']
+        }
+      ],
+      errors: [
+        {
+          message:
+            "The '<WarmButton>' component has been used, but not defined.",
+          line: 3
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <CustomComponent>
+            Some text
+          </CustomComponent>
+        </template>
+      `,
+      errors: [
+        {
+          message:
+            "The '<CustomComponent>' component has been used, but not defined.",
+          line: 3
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <WarmButton>
+            Some text
+          </WarmButton>
+        </template>
+      `,
+      options: [
+        {
+          ignorePatterns: ['custom(\\-\\w+)+']
+        }
+      ],
+      errors: [
+        {
+          message:
+            "The '<WarmButton>' component has been used, but not defined.",
+          line: 3
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <CustomComponent />
+        </template>
+        <script>
+        export default {
+          components: {
+            WarmButton
+          }
+        }
+        </script>
+      `,
+      errors: [
+        {
+          message:
+            "The '<CustomComponent>' component has been used, but not defined.",
+          line: 3
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <CustomComponent>
+            Some text
+          </CustomComponent>
+        </template>
+        <script>
+        export default {
+          components: {
+            WarmButton
+          }
+        }
+        </script>
+      `,
+      errors: [
+        {
+          message:
+            "The '<CustomComponent>' component has been used, but not defined.",
+          line: 3
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component :is="'CustomComponent'" />
+        </template>
+        <script>
+        export default {
+          components: {
+            WarmButton
+          }
+        }
+        </script>
+      `,
+      errors: [
+        {
+          message:
+            "The '<CustomComponent>' component has been used, but not defined.",
+          line: 3
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component :is="'CustomComponent'">
+            Some text
+          </component>
+        </template>
+        <script>
+        export default {
+          components: {
+            WarmButton
+          }
+        }
+        </script>
+      `,
+      errors: [
+        {
+          message:
+            "The '<CustomComponent>' component has been used, but not defined.",
+          line: 3
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <CustomComponent />
+        </template>
+        <script>
+        export default {
+          components: {
+            'custom-component': InfoPrimaryWrapper
+          }
+        }
+        </script>
+      `,
+      errors: [
+        {
+          message:
+            "The '<CustomComponent>' component has been used, but not defined.",
+          line: 3
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <CustomComponent />
+        </template>
+        <script>
+        export default {
+          name: CustomComponent
+        }
+        </script>
+      `,
+      errors: [
+        {
+          message:
+            "The '<CustomComponent>' component has been used, but not defined.",
+          line: 3
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <CustomComponentWithNamedSlots>
+            <template #slotA>
+              <div>Text</div>
+            </template>
+          </CustomComponentWithNamedSlots>
+        </template>
+      `,
+      errors: [
+        {
+          message:
+            "The '<CustomComponentWithNamedSlots>' component has been used, but not defined.",
+          line: 3
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div is="vue:foo" />
+        </template>
+        <script>
+        export default {
+          components: {
+            bar
+          }
+        }
+        </script>
+      `,
+      errors: [
+        {
+          message: "The '<foo>' component has been used, but not defined.",
+          line: 3
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div v-is="'foo'" />
+        </template>
+        <script>
+        export default {
+          components: {
+            bar
+          }
+        }
+        </script>
+      `,
+      errors: [
+        {
+          message: "The '<foo>' component has been used, but not defined.",
+          line: 3
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-undef-properties.js b/tests/lib/rules/no-undef-properties.js
index f432a8d0..8b8ebbb1 100644
--- a/tests/lib/rules/no-undef-properties.js
+++ b/tests/lib/rules/no-undef-properties.js
@@ -517,6 +517,24 @@ tester.run('no-undef-properties', rule, {
         };
       </script>
       `
+    },
+
+    // Vue2 functional component
+    {
+      filename: 'test.vue',
+      code: `
+      <template functional>
+        <div>{{props.a}} {{props.b}}</div>
+      </template>
+
+      <script>
+      export default {
+        props: {
+          a: String,
+          b: String,
+        },
+      };
+      </script>`
     }
   ],
 
@@ -1086,6 +1104,31 @@ tester.run('no-undef-properties', rule, {
           line: 16
         }
       ]
+    },
+
+    // Vue2 functional component
+    {
+      filename: 'test.vue',
+      code: `
+      <template functional>
+        <div>{{props.a}} {{props.b}} {{props.c}}</div>
+      </template>
+
+      <script>
+      export default {
+        props: {
+          a: String,
+          b: String,
+        },
+      };
+      </script>`,
+      errors: [
+        {
+          message: "'c' is not defined.",
+          line: 3,
+          column: 46
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/no-unused-properties.js b/tests/lib/rules/no-unused-properties.js
index ecd41243..0023443c 100644
--- a/tests/lib/rules/no-unused-properties.js
+++ b/tests/lib/rules/no-unused-properties.js
@@ -1678,6 +1678,24 @@ tester.run('no-unused-properties', rule, {
       <style lang="scss" scoped>
       //
       </style>`
+    },
+
+    // Vue2 functional component
+    {
+      filename: 'test.vue',
+      code: `
+      <template functional>
+        <div>{{props.a}} {{props.b}}</div>
+      </template>
+
+      <script>
+      export default {
+        props: {
+          a: String,
+          b: String,
+        },
+      };
+      </script>`
     }
   ],
 
@@ -2756,6 +2774,34 @@ tester.run('no-unused-properties', rule, {
         "'foo.bar.b' of data found, but never used.",
         "'foo.baz' of data found, but never used."
       ]
+    },
+
+    // Vue2 functional component
+    {
+      filename: 'test.vue',
+      code: `
+      <template functional>
+        <div>{{a}} {{b}} {{props.c}}</div>
+      </template>
+
+      <script>
+      export default {
+        props: {
+          a: String,
+          b: String,
+        },
+      };
+      </script>`,
+      errors: [
+        {
+          message: "'a' of property found, but never used.",
+          line: 9
+        },
+        {
+          message: "'b' of property found, but never used.",
+          line: 10
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/object-shorthand.js b/tests/lib/rules/object-shorthand.js
new file mode 100644
index 00000000..62a9a9ef
--- /dev/null
+++ b/tests/lib/rules/object-shorthand.js
@@ -0,0 +1,81 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/object-shorthand')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: 'module'
+  }
+})
+
+tester.run('object-shorthand', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div :style="{height}"></div>
+      </template>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div :style="{height: height}"></div>
+      </template>
+      `,
+      options: ['never']
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div :style="{height: height}"></div>
+      </template>
+      `,
+      output: `
+      <template>
+        <div :style="{height}"></div>
+      </template>
+      `,
+      errors: [
+        {
+          message: 'Expected property shorthand.',
+          line: 3,
+          column: 23
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div :style="{height}"></div>
+      </template>
+      `,
+      output: `
+      <template>
+        <div :style="{height: height}"></div>
+      </template>
+      `,
+      options: ['never'],
+      errors: [
+        {
+          message: 'Expected longform property syntax.',
+          line: 3,
+          column: 23
+        }
+      ]
+    }
+  ]
+})