diff --git a/docs/rules/require-explicit-emits.md b/docs/rules/require-explicit-emits.md index f7ce60dd6..90cb03edb 100644 --- a/docs/rules/require-explicit-emits.md +++ b/docs/rules/require-explicit-emits.md @@ -112,6 +112,7 @@ export default { ## :couple: Related Rules - [vue/no-unused-emit-declarations](./no-unused-emit-declarations.md) +- [vue/require-explicit-slots](./require-explicit-slots.md) ## :books: Further Reading diff --git a/docs/rules/require-explicit-slots.md b/docs/rules/require-explicit-slots.md new file mode 100644 index 000000000..ac3050896 --- /dev/null +++ b/docs/rules/require-explicit-slots.md @@ -0,0 +1,68 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/require-explicit-slots +description: require slots to be explicitly defined with defineSlots +--- + +# vue/require-explicit-slots + +> require slots to be explicitly defined + +- :exclamation: ***This rule has not been released yet.*** + +## :book: Rule Details + +This rule enforces all slots used in the template to be defined once either in the `script setup` block with the [`defineSlots`](https://vuejs.org/api/sfc-script-setup.html) macro, or with the [`slots property`](https://vuejs.org/api/options-rendering.html#slots) in the Options API. + + + +```vue + + +``` + + + + + +```vue + + +``` + + + +## :wrench: Options + +Nothing. diff --git a/lib/rules/require-explicit-slots.js b/lib/rules/require-explicit-slots.js new file mode 100644 index 000000000..ed2d2edb9 --- /dev/null +++ b/lib/rules/require-explicit-slots.js @@ -0,0 +1,128 @@ +/** + * @author Mussin Benarbia + * See LICENSE file in root directory for full license. + */ +'use strict' + +const utils = require('../utils') + +/** + * @typedef {import('@typescript-eslint/types').TSESTree.TypeNode} TypeNode + */ + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'require slots to be explicitly defined', + categories: undefined, + url: 'https://eslint.vuejs.org/rules/require-explicit-slots.html' + }, + fixable: null, + schema: [], + messages: { + requireExplicitSlots: 'Slots must be explicitly defined.', + alreadyDefinedSlot: 'Slot {{slotName}} is already defined.' + } + }, + /** @param {RuleContext} context */ + create(context) { + const sourceCode = context.getSourceCode() + const documentFragment = + sourceCode.parserServices.getDocumentFragment && + sourceCode.parserServices.getDocumentFragment() + if (!documentFragment) { + return {} + } + const scripts = documentFragment.children.filter( + (element) => utils.isVElement(element) && element.name === 'script' + ) + if (scripts.every((script) => !utils.hasAttribute(script, 'lang', 'ts'))) { + return {} + } + const slotsDefined = new Set() + + return utils.compositingVisitors( + utils.defineScriptSetupVisitor(context, { + onDefineSlotsEnter(node) { + const typeArguments = + 'typeArguments' in node ? node.typeArguments : node.typeParameters + const param = /** @type {TypeNode|undefined} */ ( + typeArguments?.params[0] + ) + if (!param) return + + if (param.type === 'TSTypeLiteral') { + for (const memberNode of param.members) { + const slotName = memberNode.key.name + if (slotsDefined.has(slotName)) { + context.report({ + node: memberNode, + messageId: 'alreadyDefinedSlot', + data: { + slotName + } + }) + } else { + slotsDefined.add(slotName) + } + } + } + } + }), + utils.executeOnVue(context, (obj) => { + const slotsProperty = utils.findProperty(obj, 'slots') + if (!slotsProperty) return + + const slotsTypeHelper = + slotsProperty.value.typeAnnotation?.typeName.name === 'SlotsType' && + slotsProperty.value.typeAnnotation + if (!slotsTypeHelper) return + + const typeArguments = + 'typeArguments' in slotsTypeHelper + ? slotsTypeHelper.typeArguments + : slotsTypeHelper.typeParameters + const param = /** @type {TypeNode|undefined} */ ( + typeArguments?.params[0] + ) + if (!param) return + + if (param.type === 'TSTypeLiteral') { + for (const memberNode of param.members) { + const slotName = memberNode.key.name + if (slotsDefined.has(slotName)) { + context.report({ + node: memberNode, + messageId: 'alreadyDefinedSlot', + data: { + slotName + } + }) + } else { + slotsDefined.add(slotName) + } + } + } + }), + utils.defineTemplateBodyVisitor(context, { + "VElement[name='slot']"(node) { + let slotName = 'default' + + const slotNameAttr = utils.getAttribute(node, 'name') + + if (slotNameAttr) { + slotName = slotNameAttr.value.value + } + + if (!slotsDefined.has(slotName)) { + context.report({ + node, + messageId: 'requireExplicitSlots' + }) + } + } + }) + ) + } +} diff --git a/tests/lib/rules/require-explicit-slots.js b/tests/lib/rules/require-explicit-slots.js new file mode 100644 index 000000000..1849ea9a1 --- /dev/null +++ b/tests/lib/rules/require-explicit-slots.js @@ -0,0 +1,269 @@ +/** + * @author Mussin Benarbia + * See LICENSE file in root directory for full license. + */ +'use strict' + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/require-explicit-slots') + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + parser: require.resolve('@typescript-eslint/parser'), + ecmaVersion: 2020, + sourceType: 'module' + } +}) + +tester.run('require-explicit-slots', rule, { + valid: [ + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + // does not report any error if the script is not TS + { + filename: 'test.vue', + code: ` + + `, + parserOptions: { + parser: null + } + } + ], + invalid: [ + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: 'Slots must be explicitly defined.' + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: 'Slots must be explicitly defined.' + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: 'Slots must be explicitly defined.' + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: 'Slots must be explicitly defined.' + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: 'Slots must be explicitly defined.' + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: 'Slot foo is already defined.' + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: 'Slot foo is already defined.' + } + ] + } + ] +})