diff --git a/docs/rules/index.md b/docs/rules/index.md index c3e4b8ed7..d6469f3e0 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 000000000..ef7400e1d --- /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 8ac5d9383..6ca29d49f 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 000000000..e52c1196c --- /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]) + } + } + ] + }) + } + } + } + }) + } +} 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 000000000..c54c224a5 --- /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' }] + } + ] +})