From fd7589347f8b19e129670851c8b9d89c1613c4d4 Mon Sep 17 00:00:00 2001 From: KG <38478672+thesheppard@users.noreply.github.com> Date: Fri, 25 Apr 2025 13:14:14 +0200 Subject: [PATCH 1/2] feat: add prefer-define-component rule --- lib/rules/prefer-define-component.js | 114 ++++++++++ tests/lib/rules/prefer-define-component.js | 229 +++++++++++++++++++++ 2 files changed, 343 insertions(+) create mode 100644 lib/rules/prefer-define-component.js create mode 100644 tests/lib/rules/prefer-define-component.js diff --git a/lib/rules/prefer-define-component.js b/lib/rules/prefer-define-component.js new file mode 100644 index 000000000..7927de0ce --- /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: 'require 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 + } + ] + } + ] +}) From 58343cbedb46e0a9e87b1186f2a12b3971c0b031 Mon Sep 17 00:00:00 2001 From: KG <38478672+thesheppard@users.noreply.github.com> Date: Fri, 25 Apr 2025 13:16:01 +0200 Subject: [PATCH 2/2] docs: update docs --- docs/rules/index.md | 552 +++++++++++++------------- docs/rules/prefer-define-component.md | 96 +++++ lib/index.js | 1 + lib/rules/prefer-define-component.js | 2 +- 4 files changed, 375 insertions(+), 276 deletions(-) create mode 100644 docs/rules/prefer-define-component.md diff --git a/docs/rules/index.md b/docs/rules/index.md index 55c5b96c9..47d88ff33 100644 --- a/docs/rules/index.md +++ b/docs/rules/index.md @@ -8,9 +8,9 @@ pageClass: rule-list ::: tip Legend - :wrench: Indicates that the rule is fixable, and using `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fix-problems) can automatically fix some of the reported problems. +:wrench: Indicates that the rule is fixable, and using `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fix-problems) can automatically fix some of the reported problems. - :bulb: Indicates that some problems reported by the rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions). +:bulb: Indicates that some problems reported by the rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions). ::: Mark indicating rule type: @@ -25,10 +25,10 @@ Rules in this category are enabled for all presets provided by eslint-plugin-vue -| Rule ID | Description | | | -|:--------|:------------|:--:|:--:| -| [vue/comment-directive] | support comment-directives in `