diff --git a/docs/rules/index.md b/docs/rules/index.md index 7828b58bb..e9be6d753 100644 --- a/docs/rules/index.md +++ b/docs/rules/index.md @@ -546,6 +546,7 @@ The following rules extend the rules provided by ESLint itself and apply them to [vue/padding-line-between-blocks]: ./padding-line-between-blocks.md [vue/padding-line-between-tags]: ./padding-line-between-tags.md [vue/padding-lines-in-component-definition]: ./padding-lines-in-component-definition.md +[vue/prefer-define-component]: ./prefer-define-component.md [vue/prefer-define-options]: ./prefer-define-options.md [vue/prefer-import-from-vue]: ./prefer-import-from-vue.md [vue/prefer-prop-type-boolean-first]: ./prefer-prop-type-boolean-first.md @@ -644,4 +645,4 @@ The following rules extend the rules provided by ESLint itself and apply them to [v9.0.0]: https://github.com/vuejs/eslint-plugin-vue/releases/tag/v9.0.0 [v9.16.0]: https://github.com/vuejs/eslint-plugin-vue/releases/tag/v9.16.0 [v9.17.0]: https://github.com/vuejs/eslint-plugin-vue/releases/tag/v9.17.0 -[v9.7.0]: https://github.com/vuejs/eslint-plugin-vue/releases/tag/v9.7.0 +[v9.7.0]: https://github.com/vuejs/eslint-plugin-vue/releases/tag/v9.7.0 \ No newline at end of file diff --git a/docs/rules/prefer-define-component.md b/docs/rules/prefer-define-component.md new file mode 100644 index 000000000..03f02bf41 --- /dev/null +++ b/docs/rules/prefer-define-component.md @@ -0,0 +1,96 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/prefer-define-component +description: require components to be defined using `defineComponent` +--- + +# vue/prefer-define-component + +> require components to be defined using `defineComponent` + +## :book: Rule Details + +This rule enforces the use of `defineComponent` when defining Vue components. Using `defineComponent` provides proper typing in Vue 3 and IDE support for object properties. + + + +```vue + +``` + + + + + +```vue + +``` + + + + + +```vue + +``` + + + +This rule doesn't report components using ` +``` + + + +## :wrench: Options + +Nothing. + +## :couple: Related Rules + +- [vue/require-default-export](./require-default-export.md) +- [vue/require-direct-export](./require-direct-export.md) + +## :books: Further Reading + +- [Vue.js Guide - TypeScript with Composition API](https://vuejs.org/guide/typescript/composition-api.html#typing-component-props) + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/prefer-define-component.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/prefer-define-component.js) diff --git a/lib/index.js b/lib/index.js index e511536fa..0ce94365c 100644 --- a/lib/index.js +++ b/lib/index.js @@ -205,6 +205,7 @@ const plugin = { 'padding-line-between-blocks': require('./rules/padding-line-between-blocks'), 'padding-line-between-tags': require('./rules/padding-line-between-tags'), 'padding-lines-in-component-definition': require('./rules/padding-lines-in-component-definition'), + 'prefer-define-component': require('./rules/prefer-define-component'), 'prefer-define-options': require('./rules/prefer-define-options'), 'prefer-import-from-vue': require('./rules/prefer-import-from-vue'), 'prefer-prop-type-boolean-first': require('./rules/prefer-prop-type-boolean-first'), diff --git a/lib/rules/prefer-define-component.js b/lib/rules/prefer-define-component.js new file mode 100644 index 000000000..2ef6246db --- /dev/null +++ b/lib/rules/prefer-define-component.js @@ -0,0 +1,114 @@ +/** + * @author Kamogelo Moalusi + * See LICENSE file in root directory for full license. + */ +'use strict' + +// @ts-nocheck +const utils = require('../utils') + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'enforce components to be defined using `defineComponent`', + categories: ['vue3-recommended', 'vue2-recommended'], + url: 'https://eslint.vuejs.org/rules/prefer-define-component.html' + }, + fixable: null, + schema: [], + messages: { + 'prefer-define-component': 'Use `defineComponent` to define a component.' + } + }, + /** @param {RuleContext} context */ + create(context) { + const filePath = context.getFilename() + if (!utils.isVueFile(filePath)) return {} + + const sourceCode = context.getSourceCode() + const documentFragment = sourceCode.parserServices.getDocumentFragment?.() + + // Check if there's a non-setup script tag + const hasNormalScript = + documentFragment && + documentFragment.children.some( + (e) => + utils.isVElement(e) && + e.name === 'script' && + (!e.startTag.attributes || + !e.startTag.attributes.some((attr) => attr.key.name === 'setup')) + ) + + // If no regular script tag, we don't need to check + if (!hasNormalScript) return {} + + // Skip checking if there's only a setup script (no normal script) + if (utils.isScriptSetup(context) && !hasNormalScript) return {} + + let hasDefineComponent = false + /** @type {ExportDefaultDeclaration | null} */ + let exportDefaultNode = null + let hasVueExtend = false + + return utils.compositingVisitors(utils.defineVueVisitor(context, {}), { + /** @param {ExportDefaultDeclaration} node */ + 'Program > ExportDefaultDeclaration'(node) { + exportDefaultNode = node + }, + + /** @param {CallExpression} node */ + 'Program > ExportDefaultDeclaration > CallExpression'(node) { + if ( + node.callee.type === 'Identifier' && + node.callee.name === 'defineComponent' + ) { + hasDefineComponent = true + return + } + + // Support aliased imports + if (node.callee.type === 'Identifier') { + const variable = utils.findVariableByIdentifier(context, node.callee) + if ( + variable && + variable.defs && + variable.defs.length > 0 && + variable.defs[0].node.type === 'ImportSpecifier' && + variable.defs[0].node.imported && + variable.defs[0].node.imported.name === 'defineComponent' + ) { + hasDefineComponent = true + return + } + } + + // Check for Vue.extend case + if ( + node.callee.type === 'MemberExpression' && + node.callee.object && + node.callee.object.type === 'Identifier' && + node.callee.object.name === 'Vue' && + node.callee.property && + node.callee.property.type === 'Identifier' && + node.callee.property.name === 'extend' + ) { + hasVueExtend = true + } + }, + + 'Program > ExportDefaultDeclaration > ObjectExpression'() { + hasDefineComponent = false + }, + + 'Program:exit'() { + if (exportDefaultNode && (hasVueExtend || !hasDefineComponent)) { + context.report({ + node: exportDefaultNode, + messageId: 'prefer-define-component' + }) + } + } + }) + } +} diff --git a/tests/lib/rules/prefer-define-component.js b/tests/lib/rules/prefer-define-component.js new file mode 100644 index 000000000..800c561c8 --- /dev/null +++ b/tests/lib/rules/prefer-define-component.js @@ -0,0 +1,229 @@ +/** + * @author Kamogelo Moalusi + * See LICENSE file in root directory for full license. + */ +'use strict' + +const RuleTester = require('../../eslint-compat').RuleTester +const rule = require('../../../lib/rules/prefer-define-component') + +const tester = new RuleTester({ + languageOptions: { + parser: require('vue-eslint-parser'), + ecmaVersion: 2020, + sourceType: 'module' + } +}) + +tester.run('prefer-define-component', 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: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + } + ], + invalid: [ + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: 'Use `defineComponent` to define a component.', + line: 3, + column: 7 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: 'Use `defineComponent` to define a component.', + line: 3, + column: 7 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: 'Use `defineComponent` to define a component.', + line: 3, + column: 7 + } + ] + }, + { + filename: 'test.vue', + code: ` + + + `, + errors: [ + { + message: 'Use `defineComponent` to define a component.', + line: 3, + column: 7 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: 'Use `defineComponent` to define a component.', + line: 7, + column: 7 + } + ] + }, + { + filename: 'test.vue', + code: ` + + + `, + errors: [ + { + message: 'Use `defineComponent` to define a component.', + line: 3, + column: 7 + } + ] + } + ] +})