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)