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

Issue:
New rule `vue/require-macro-variable-name`
**What rule do you want to change?**
New config fields:
- Require using a defined name for the `define*` variable (e.g. `{ "name": "props" }`)
- Require name prefix in html (not for `vue/define-emits-declaration` e.g. `{ "prefix": true }`)

**Does this change cause the rule to produce more or fewer warnings?**
More (depends on configuration).

**How will the change be implemented? (New option, new default behavior, etc.)?**
The default behavior does not change the current behavior of the rule.

**Please provide some example code that this change will affect:**

`"vue/define-props-declaration": ["error", "type-based", { "name": "props" } ]`
```ts
interface Props {
  active: boolean;
}
// GOOD
const props = defineProps<Props>();
// BAD
const myProps = defineProps<Props>();
```
`"vue/define-props-declaration": ["error", "type-based", { "prefix": true } ]`
```html
<template>
  <!-- GOOD-->
  {{ props.active }}
  <!-- BAD-->
  {{ active }}
  {{ $props.active }}
</template>
```
`"vue/define-slots-declaration": ["error", "always", { "name": "slots", "prefix": true } ]`
```html
<template>
  <!-- GOOD-->
  {{ slots }}
  {{ slots.default }}
  <!-- BAD-->
  {{ $slots }}
  {{ $slots.default }}
</template>
```
`"vue/define-attrs-declaration": ["error", "always", { "name": "attrs", "prefix": true } ]`
```html
<template>
  <!-- GOOD-->
  {{ attrs }}
  <!-- BAD-->
  {{ $attrs}}
</template>
```

**What does the rule currently do for this code?**
https://eslint.vuejs.org/rules/define-props-declaration.html



Pull request description:
Add `vue/require-macro-variable-name` rule
Closes #2188 

Patch:
diff --git a/docs/rules/index.md b/docs/rules/index.md
index c3e4b8ed..d6469f3e 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -262,6 +262,7 @@ For example:
 | [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported |  | :hammer: |
 | [vue/require-emit-validator](./require-emit-validator.md) | require type definitions in emits | :bulb: | :hammer: |
 | [vue/require-expose](./require-expose.md) | require declare public properties using `expose` | :bulb: | :hammer: |
+| [vue/require-macro-variable-name](./require-macro-variable-name.md) | require a certain macro variable name | :bulb: | :hammer: |
 | [vue/require-name-property](./require-name-property.md) | require a name property in Vue components | :bulb: | :hammer: |
 | [vue/require-prop-comment](./require-prop-comment.md) | require props to have a comment |  | :hammer: |
 | [vue/script-indent](./script-indent.md) | enforce consistent indentation in `<script>` | :wrench: | :lipstick: |
diff --git a/docs/rules/require-macro-variable-name.md b/docs/rules/require-macro-variable-name.md
new file mode 100644
index 00000000..ef7400e1
--- /dev/null
+++ b/docs/rules/require-macro-variable-name.md
@@ -0,0 +1,84 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/require-macro-variable-name
+description: require a certain macro variable name
+---
+# vue/require-macro-variable-name
+
+> require a certain macro variable name
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
+
+## :book: Rule Details
+
+This rule reports macro variables not corresponding to the specified name.
+
+<eslint-code-block :rules="{'vue/require-macro-variable-name': ['error']}">
+
+```vue
+<!-- ✓ GOOD -->
+<script setup>
+const props = defineProps({ msg: String })
+const emit = defineEmits(['update:msg'])
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/require-macro-variable-name': ['error']}">
+
+```vue
+<!-- ✗ BAD  -->
+<script setup>
+const propsDefined = defineProps({ msg: String })
+const emitsDefined = defineEmits(['update:msg'])
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+```json
+{
+  "vue/require-macro-variable-name": ["error", {
+    "defineProps": "props",
+    "defineEmits": "emit",
+    "defineSlots": "slots",
+    "useSlots": "slots",
+    "useAttrs": "attrs"
+  }]
+}
+```
+
+- `defineProps` - The name of the macro variable for `defineProps`. default: `props`
+- `defineEmits` - The name of the macro variable for `defineEmits`. default: `emit`
+- `defineSlots` - The name of the macro variable for `defineSlots`. default: `slots`
+- `useSlots` - The name of the macro variable for `useSlots`. default: `slots`
+- `useAttrs` - The name of the macro variable for `useAttrs`. default: `attrs`
+
+### With custom macro variable names
+
+<eslint-code-block :rules="{'vue/require-macro-variable-name': ['error', {
+    'defineProps': 'propsCustom',
+    'defineEmits': 'emitCustom',
+    'defineSlots': 'slotsCustom',
+    'useSlots': 'slotsCustom',
+    'useAttrs': 'attrsCustom'
+  }]}">
+
+```vue
+<script setup>
+const slotsCustom = defineSlots()
+const attrsCustom = useAttrs()
+</script>
+```
+
+</eslint-code-block>
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/require-macro-variable-name.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/require-macro-variable-name.js)
diff --git a/lib/index.js b/lib/index.js
index 8ac5d938..6ca29d49 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -181,6 +181,7 @@ module.exports = {
     'require-emit-validator': require('./rules/require-emit-validator'),
     'require-explicit-emits': require('./rules/require-explicit-emits'),
     'require-expose': require('./rules/require-expose'),
+    'require-macro-variable-name': require('./rules/require-macro-variable-name'),
     'require-name-property': require('./rules/require-name-property'),
     'require-prop-comment': require('./rules/require-prop-comment'),
     'require-prop-type-constructor': require('./rules/require-prop-type-constructor'),
diff --git a/lib/rules/require-macro-variable-name.js b/lib/rules/require-macro-variable-name.js
new file mode 100644
index 00000000..e52c1196
--- /dev/null
+++ b/lib/rules/require-macro-variable-name.js
@@ -0,0 +1,111 @@
+/**
+ * @author ItMaga
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+
+const DEFAULT_OPTIONS = {
+  defineProps: 'props',
+  defineEmits: 'emit',
+  defineSlots: 'slots',
+  useSlots: 'slots',
+  useAttrs: 'attrs'
+}
+
+module.exports = {
+  meta: {
+    hasSuggestions: true,
+    type: 'suggestion',
+    docs: {
+      description: 'require a certain macro variable name',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/require-macro-variable-name.html'
+    },
+    fixable: null,
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          defineProps: {
+            type: 'string',
+            default: DEFAULT_OPTIONS.defineProps
+          },
+          defineEmits: {
+            type: 'string',
+            default: DEFAULT_OPTIONS.defineEmits
+          },
+          defineSlots: {
+            type: 'string',
+            default: DEFAULT_OPTIONS.defineSlots
+          },
+          useSlots: {
+            type: 'string',
+            default: DEFAULT_OPTIONS.useSlots
+          },
+          useAttrs: {
+            type: 'string',
+            default: DEFAULT_OPTIONS.useAttrs
+          }
+        },
+        additionalProperties: false
+      }
+    ],
+    messages: {
+      requireName:
+        'The variable name of "{{macroName}}" must be "{{variableName}}".',
+      changeName: 'Change the variable name to "{{variableName}}".'
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    const options = context.options[0] || DEFAULT_OPTIONS
+    const relevantMacros = new Set([
+      ...Object.keys(DEFAULT_OPTIONS),
+      'withDefaults'
+    ])
+
+    return utils.defineScriptSetupVisitor(context, {
+      VariableDeclarator(node) {
+        if (
+          node.init &&
+          node.init.type === 'CallExpression' &&
+          node.init.callee.type === 'Identifier' &&
+          relevantMacros.has(node.init.callee.name)
+        ) {
+          const macroName =
+            node.init.callee.name === 'withDefaults'
+              ? 'defineProps'
+              : node.init.callee.name
+
+          if (
+            node.id.type === 'Identifier' &&
+            node.id.name !== options[macroName]
+          ) {
+            context.report({
+              node: node.id,
+              loc: node.id.loc,
+              messageId: 'requireName',
+              data: {
+                macroName,
+                variableName: options[macroName]
+              },
+              suggest: [
+                {
+                  messageId: 'changeName',
+                  data: {
+                    variableName: options[macroName]
+                  },
+                  fix(fixer) {
+                    return fixer.replaceText(node.id, options[macroName])
+                  }
+                }
+              ]
+            })
+          }
+        }
+      }
+    })
+  }
+}


Test patch:
diff --git a/tests/lib/rules/require-macro-variable-name.js b/tests/lib/rules/require-macro-variable-name.js
new file mode 100644
index 00000000..c54c224a
--- /dev/null
+++ b/tests/lib/rules/require-macro-variable-name.js
@@ -0,0 +1,324 @@
+/**
+ * @author ItMaga
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/require-macro-variable-name')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: 'module'
+  }
+})
+
+const customOptions = {
+  defineProps: 'customProps',
+  defineEmits: 'customEmits',
+  defineSlots: 'customSlots',
+  useSlots: 'customUseSlots',
+  useAttrs: 'customUseAttrs'
+}
+
+tester.run('require-macro-variable-name', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        const props = defineProps({})
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        const { foo, bar } = defineProps(['foo', 'bar'])
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        const { foo = 42, bar = 'abc' } = defineProps(['foo', 'bar'])
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        import { toRef } from 'vue'
+
+        const props = defineProps(['foo', 'bar'])
+        const foo = toRef(props, 'foo')
+        const bar = toRef(props, 'bar')
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        const props = withDefaults(defineProps(['foo', 'bar']), {
+          foo: 42,
+          bar: 'abc'
+        })
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        const ${customOptions.defineProps} = defineProps(['foo', 'bar'])
+        const ${customOptions.defineEmits} = defineEmits(['baz'])
+      </script>
+      `,
+      options: [customOptions]
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        const customName = defineProps({})
+      </script>
+      `,
+      errors: [
+        {
+          message: 'The variable name of "defineProps" must be "props".',
+          line: 3,
+          column: 15,
+          suggestions: [
+            {
+              desc: 'Change the variable name to "props".',
+              output: `
+      <script setup>
+        const props = defineProps({})
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        const emitsWrong = defineEmits({})
+        const slotsWrong = defineSlots({})
+        const attrsWrong = useAttrs({})
+      </script>
+      `,
+      errors: [
+        {
+          message: 'The variable name of "defineEmits" must be "emit".',
+          line: 3,
+          column: 15,
+          suggestions: [
+            {
+              desc: 'Change the variable name to "emit".',
+              output: `
+      <script setup>
+        const emit = defineEmits({})
+        const slotsWrong = defineSlots({})
+        const attrsWrong = useAttrs({})
+      </script>
+      `
+            }
+          ]
+        },
+        {
+          message: 'The variable name of "defineSlots" must be "slots".',
+          line: 4,
+          column: 15,
+          suggestions: [
+            {
+              desc: 'Change the variable name to "slots".',
+              output: `
+      <script setup>
+        const emitsWrong = defineEmits({})
+        const slots = defineSlots({})
+        const attrsWrong = useAttrs({})
+      </script>
+      `
+            }
+          ]
+        },
+        {
+          message: 'The variable name of "useAttrs" must be "attrs".',
+          line: 5,
+          column: 15,
+          suggestions: [
+            {
+              desc: 'Change the variable name to "attrs".',
+              output: `
+      <script setup>
+        const emitsWrong = defineEmits({})
+        const slotsWrong = defineSlots({})
+        const attrs = useAttrs({})
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        const slotsWrong = useSlots({})
+      </script>
+      `,
+      errors: [
+        {
+          message: 'The variable name of "useSlots" must be "slots".',
+          line: 3,
+          column: 15,
+          suggestions: [
+            {
+              desc: 'Change the variable name to "slots".',
+              output: `
+      <script setup>
+        const slots = useSlots({})
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        const propsWrong = withDefaults(defineProps(['foo', 'bar']), {
+          foo: 42,
+          bar: 'abc'
+        })
+      </script>
+      `,
+      errors: [
+        {
+          message: 'The variable name of "defineProps" must be "props".',
+          line: 3,
+          column: 15,
+          suggestions: [
+            {
+              desc: 'Change the variable name to "props".',
+              output: `
+      <script setup>
+        const props = withDefaults(defineProps(['foo', 'bar']), {
+          foo: 42,
+          bar: 'abc'
+        })
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        const slots = defineSlots({})
+        const useSlots = useSlots({})
+        const attrs = useAttrs({})
+      </script>
+      `,
+      errors: [
+        {
+          message: `The variable name of "defineSlots" must be "${customOptions.defineSlots}".`,
+          line: 3,
+          column: 15,
+          suggestions: [
+            {
+              desc: `Change the variable name to "${customOptions.defineSlots}".`,
+              output: `
+      <script setup>
+        const ${customOptions.defineSlots} = defineSlots({})
+        const useSlots = useSlots({})
+        const attrs = useAttrs({})
+      </script>
+      `
+            }
+          ]
+        },
+        {
+          message: `The variable name of "useSlots" must be "${customOptions.useSlots}".`,
+          line: 4,
+          column: 15,
+          suggestions: [
+            {
+              desc: `Change the variable name to "${customOptions.useSlots}".`,
+              output: `
+      <script setup>
+        const slots = defineSlots({})
+        const ${customOptions.useSlots} = useSlots({})
+        const attrs = useAttrs({})
+      </script>
+      `
+            }
+          ]
+        },
+        {
+          message: `The variable name of "useAttrs" must be "${customOptions.useAttrs}".`,
+          line: 5,
+          column: 15,
+          suggestions: [
+            {
+              desc: `Change the variable name to "${customOptions.useAttrs}".`,
+              output: `
+      <script setup>
+        const slots = defineSlots({})
+        const useSlots = useSlots({})
+        const ${customOptions.useAttrs} = useAttrs({})
+      </script>
+      `
+            }
+          ]
+        }
+      ],
+      options: [customOptions]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+        const slotsCustom = defineSlots({})
+        const attrsCustom = useAttrs({})
+      </script>
+      `,
+      errors: [
+        {
+          message: `The variable name of "useAttrs" must be "attrs".`,
+          line: 4,
+          column: 15,
+          suggestions: [
+            {
+              desc: `Change the variable name to "attrs".`,
+              output: `
+      <script setup>
+        const slotsCustom = defineSlots({})
+        const attrs = useAttrs({})
+      </script>
+      `
+            }
+          ]
+        }
+      ],
+      options: [{ defineSlots: 'slotsCustom' }]
+    }
+  ]
+})