diff --git a/docs/rules/define-macros-order.md b/docs/rules/define-macros-order.md index 6fe48b20a..c4e36b253 100644 --- a/docs/rules/define-macros-order.md +++ b/docs/rules/define-macros-order.md @@ -25,7 +25,7 @@ This rule reports the `defineProps` and `defineEmits` compiler macros when they } ``` -- `order` (`string[]`) ... The order of defineEmits and defineProps macros +- `order` (`string[]`) ... The order of defineEmits and defineProps macros. You can also add `"defineOptions"` and `"defineSlots"`. ### `{ "order": ["defineProps", "defineEmits"] }` (default) @@ -66,6 +66,51 @@ defineEmits(/* ... */) +### `{ "order": ["defineOptions", "defineProps", "defineEmits", "defineSlots"] }` (default) + + + +```vue + + +``` + + + + + +```vue + + +``` + + + + + +```vue + + +``` + + + ## :rocket: Version This rule was introduced in eslint-plugin-vue v8.7.0 diff --git a/lib/rules/define-macros-order.js b/lib/rules/define-macros-order.js index baf03a40e..5a678db42 100644 --- a/lib/rules/define-macros-order.js +++ b/lib/rules/define-macros-order.js @@ -8,7 +8,9 @@ const utils = require('../utils') const MACROS_EMITS = 'defineEmits' const MACROS_PROPS = 'defineProps' -const ORDER = [MACROS_EMITS, MACROS_PROPS] +const MACROS_OPTIONS = 'defineOptions' +const MACROS_SLOTS = 'defineSlots' +const ORDER_SCHEMA = [MACROS_EMITS, MACROS_PROPS, MACROS_OPTIONS, MACROS_SLOTS] const DEFAULT_ORDER = [MACROS_PROPS, MACROS_EMITS] /** @@ -103,97 +105,69 @@ function create(context) { }, onDefineEmitsExit(node) { macrosNodes.set(MACROS_EMITS, getDefineMacrosStatement(node)) + }, + onDefineOptionsExit(node) { + macrosNodes.set(MACROS_OPTIONS, getDefineMacrosStatement(node)) + }, + onDefineSlotsExit(node) { + macrosNodes.set(MACROS_SLOTS, getDefineMacrosStatement(node)) } }), { 'Program:exit'(program) { - const shouldFirstNode = macrosNodes.get(order[0]) - const shouldSecondNode = macrosNodes.get(order[1]) + /** + * @typedef {object} OrderedData + * @property {string} name + * @property {ASTNode} node + */ const firstStatementIndex = getTargetStatementPosition( scriptSetup, program ) - const firstStatement = program.body[firstStatementIndex] + const orderedList = order + .map((name) => ({ name, node: macrosNodes.get(name) })) + .filter( + /** @returns {data is OrderedData} */ + (data) => utils.isDef(data.node) + ) - // have both defineEmits and defineProps - if (shouldFirstNode && shouldSecondNode) { - const secondStatement = program.body[firstStatementIndex + 1] + for (const [index, should] of orderedList.entries()) { + const targetStatement = program.body[firstStatementIndex + index] - // need move only first - if (firstStatement === shouldSecondNode) { - reportNotOnTop(order[0], shouldFirstNode, firstStatement) + if (should.node !== targetStatement) { + let moveTargetNodes = orderedList + .slice(index) + .map(({ node }) => node) + const targetStatementIndex = + moveTargetNodes.indexOf(targetStatement) + if (targetStatementIndex >= 0) { + moveTargetNodes = moveTargetNodes.slice(0, targetStatementIndex) + } + reportNotOnTop(should.name, moveTargetNodes, targetStatement) return } - - // need move both defineEmits and defineProps - if (firstStatement !== shouldFirstNode) { - reportBothNotOnTop( - shouldFirstNode, - shouldSecondNode, - firstStatement - ) - return - } - - // need move only second - if (secondStatement !== shouldSecondNode) { - reportNotOnTop(order[1], shouldSecondNode, secondStatement) - } - - return - } - - // have only first and need to move it - if (shouldFirstNode && firstStatement !== shouldFirstNode) { - reportNotOnTop(order[0], shouldFirstNode, firstStatement) - return - } - - // have only second and need to move it - if (shouldSecondNode && firstStatement !== shouldSecondNode) { - reportNotOnTop(order[1], shouldSecondNode, firstStatement) } } } ) - /** - * @param {ASTNode} shouldFirstNode - * @param {ASTNode} shouldSecondNode - * @param {ASTNode} before - */ - function reportBothNotOnTop(shouldFirstNode, shouldSecondNode, before) { - context.report({ - node: shouldFirstNode, - loc: shouldFirstNode.loc, - messageId: 'macrosNotOnTop', - data: { - macro: order[0] - }, - fix(fixer) { - return [ - ...moveNodeBefore(fixer, shouldFirstNode, before), - ...moveNodeBefore(fixer, shouldSecondNode, before) - ] - } - }) - } - /** * @param {string} macro - * @param {ASTNode} node + * @param {ASTNode[]} nodes * @param {ASTNode} before */ - function reportNotOnTop(macro, node, before) { + function reportNotOnTop(macro, nodes, before) { context.report({ - node, - loc: node.loc, + node: nodes[0], + loc: nodes[0].loc, messageId: 'macrosNotOnTop', data: { macro }, - fix(fixer) { - return moveNodeBefore(fixer, node, before) + *fix(fixer) { + for (const node of nodes) { + yield* moveNodeBefore(fixer, node, before) + } } }) } @@ -288,7 +262,7 @@ module.exports = { order: { type: 'array', items: { - enum: Object.values(ORDER) + enum: ORDER_SCHEMA }, uniqueItems: true, additionalItems: false diff --git a/lib/utils/index.js b/lib/utils/index.js index 1698b23ba..a63891bd9 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -1148,6 +1148,10 @@ module.exports = { * - `onDefinePropsExit` ... Event when defineProps visit ends. * - `onDefineEmitsEnter` ... Event when defineEmits is found. * - `onDefineEmitsExit` ... Event when defineEmits visit ends. + * - `onDefineOptionsEnter` ... Event when defineOptions is found. + * - `onDefineOptionsExit` ... Event when defineOptions visit ends. + * - `onDefineSlotsEnter` ... Event when defineSlots is found. + * - `onDefineSlotsExit` ... Event when defineSlots visit ends. * * @param {RuleContext} context The ESLint rule context object. * @param {ScriptSetupVisitor} visitor The visitor to traverse the AST nodes. @@ -1186,11 +1190,58 @@ module.exports = { scriptSetupVisitor[key] = (node) => callVisitor(key, node) } - const hasPropsEvent = - visitor.onDefinePropsEnter || visitor.onDefinePropsExit - const hasEmitsEvent = - visitor.onDefineEmitsEnter || visitor.onDefineEmitsExit - if (hasPropsEvent || hasEmitsEvent) { + class MacroListener { + /** + * @param {string} name + * @param {string} enterName + * @param {string} exitName + * @param {(candidateMacro: Expression | null, node: CallExpression) => boolean} isMacroNode + * @param {(context: RuleContext, node: CallExpression) => unknown} buildParam + */ + constructor(name, enterName, exitName, isMacroNode, buildParam) { + this.name = name + this.enterName = enterName + this.exitName = exitName + this.isMacroNode = isMacroNode + this.buildParam = buildParam + this.hasListener = Boolean( + visitor[this.enterName] || visitor[this.exitName] + ) + this.paramsMap = new Map() + } + } + const macroListenerList = [ + new MacroListener( + 'defineProps', + 'onDefinePropsEnter', + 'onDefinePropsExit', + (candidateMacro, node) => + candidateMacro === node || candidateMacro === getWithDefaults(node), + getComponentPropsFromDefineProps + ), + new MacroListener( + 'defineEmits', + 'onDefineEmitsEnter', + 'onDefineEmitsExit', + (candidateMacro, node) => candidateMacro === node, + getComponentEmitsFromDefineEmits + ), + new MacroListener( + 'defineOptions', + 'onDefineOptionsEnter', + 'onDefineOptionsExit', + (candidateMacro, node) => candidateMacro === node, + () => undefined + ), + new MacroListener( + 'defineSlots', + 'onDefineSlotsEnter', + 'onDefineSlotsExit', + (candidateMacro, node) => candidateMacro === node, + () => undefined + ) + ].filter((m) => m.hasListener) + if (macroListenerList.length > 0) { /** @type {Expression | null} */ let candidateMacro = null /** @param {VariableDeclarator|ExpressionStatement} node */ @@ -1213,8 +1264,6 @@ module.exports = { candidateMacro = null } } - const definePropsMap = new Map() - const defineEmitsMap = new Map() /** * @param {CallExpression} node */ @@ -1224,40 +1273,32 @@ module.exports = { inScriptSetup(node) && node.callee.type === 'Identifier' ) { - if ( - hasPropsEvent && - (candidateMacro === node || - candidateMacro === getWithDefaults(node)) && - node.callee.name === 'defineProps' - ) { - /** @type {ComponentProp[]} */ - const props = getComponentPropsFromDefineProps(context, node) - - callVisitor('onDefinePropsEnter', node, props) - definePropsMap.set(node, props) - } else if ( - hasEmitsEvent && - candidateMacro === node && - node.callee.name === 'defineEmits' - ) { - /** @type {ComponentEmit[]} */ - const emits = getComponentEmitsFromDefineEmits(context, node) - - callVisitor('onDefineEmitsEnter', node, emits) - defineEmitsMap.set(node, emits) + for (const macroListener of macroListenerList) { + if ( + node.callee.name !== macroListener.name || + !macroListener.isMacroNode(candidateMacro, node) + ) { + continue + } + const param = macroListener.buildParam(context, node) + callVisitor(macroListener.enterName, node, param) + macroListener.paramsMap.set(node, param) + break } } callVisitor('CallExpression', node) } scriptSetupVisitor['CallExpression:exit'] = (node) => { callVisitor('CallExpression:exit', node) - if (definePropsMap.has(node)) { - callVisitor('onDefinePropsExit', node, definePropsMap.get(node)) - definePropsMap.delete(node) - } - if (defineEmitsMap.has(node)) { - callVisitor('onDefineEmitsExit', node, defineEmitsMap.get(node)) - defineEmitsMap.delete(node) + for (const macroListener of macroListenerList) { + if (macroListener.paramsMap.has(node)) { + callVisitor( + macroListener.exitName, + node, + macroListener.paramsMap.get(node) + ) + macroListener.paramsMap.delete(node) + } } } } diff --git a/tests/lib/rules/define-macros-order.js b/tests/lib/rules/define-macros-order.js index bd46ac3d6..46e94eae5 100644 --- a/tests/lib/rules/define-macros-order.js +++ b/tests/lib/rules/define-macros-order.js @@ -148,6 +148,28 @@ tester.run('define-macros-order', rule, { ` + }, + { + filename: 'test.vue', + code: ` + + `, + options: [ + { + order: ['defineOptions', 'defineEmits', 'defineProps', 'defineSlots'] + } + ] } ], invalid: [ @@ -520,6 +542,86 @@ tester.run('define-macros-order', rule, { line: 5 } ] + }, + { + filename: 'test.vue', + code: ` + + `, + output: ` + + `, + options: [ + { + order: ['defineOptions', 'defineEmits', 'defineProps', 'defineSlots'] + } + ], + errors: [ + { + message: message('defineOptions'), + line: 12 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + output: ` + + `, + options: [ + { + order: ['defineOptions', 'defineEmits', 'defineProps', 'defineSlots'] + } + ], + errors: [ + { + message: message('defineOptions'), + line: 6 + } + ] } ] }) diff --git a/typings/eslint-plugin-vue/util-types/utils.ts b/typings/eslint-plugin-vue/util-types/utils.ts index d998761be..e355456dc 100644 --- a/typings/eslint-plugin-vue/util-types/utils.ts +++ b/typings/eslint-plugin-vue/util-types/utils.ts @@ -40,6 +40,10 @@ export interface ScriptSetupVisitor extends ScriptSetupVisitorBase { onDefinePropsExit?(node: CallExpression, props: ComponentProp[]): void onDefineEmitsEnter?(node: CallExpression, emits: ComponentEmit[]): void onDefineEmitsExit?(node: CallExpression, emits: ComponentEmit[]): void + onDefineOptionsEnter?(node: CallExpression): void + onDefineOptionsExit?(node: CallExpression): void + onDefineSlotsEnter?(node: CallExpression): void + onDefineSlotsExit?(node: CallExpression): void [query: string]: | ((node: VAST.ParamNode) => void) | ((node: CallExpression, props: ComponentProp[]) => void)