diff --git a/docs/rules/README.md b/docs/rules/README.md
index 3d2d113d7..e7ff5c423 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -12,6 +12,7 @@ sidebarDepth: 0
: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).
:::
+
## Base Rules (Enabling Correct ESLint Parsing)
Enforce all the rules in this category, as well as all higher priority rules, with:
@@ -312,6 +313,7 @@ For example:
| [vue/component-name-in-template-casing](./component-name-in-template-casing.md) | enforce specific casing for the component naming style in template | :wrench: |
| [vue/component-options-name-casing](./component-options-name-casing.md) | enforce the casing of component name in `components` options | :wrench::bulb: |
| [vue/custom-event-name-casing](./custom-event-name-casing.md) | enforce specific casing for custom event name | |
+| [vue/define-macros-order](./define-macros-order.md) | enforce order of `defineEmits` and `defineProps` compiler macros | :wrench: |
| [vue/html-button-has-type](./html-button-has-type.md) | disallow usage of button without an explicit type attribute | |
| [vue/html-comment-content-newline](./html-comment-content-newline.md) | enforce unified line brake in HTML comments | :wrench: |
| [vue/html-comment-content-spacing](./html-comment-content-spacing.md) | enforce unified spacing in HTML comments | :wrench: |
diff --git a/docs/rules/define-macros-order.md b/docs/rules/define-macros-order.md
new file mode 100644
index 000000000..fdb506914
--- /dev/null
+++ b/docs/rules/define-macros-order.md
@@ -0,0 +1,72 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/define-macros-order
+description: enforce order of `defineEmits` and `defineProps` compiler macros
+---
+# vue/define-macros-order
+
+> enforce order of `defineEmits` and `defineProps` compiler macros
+
+- :exclamation: ***This rule has not been released yet.***
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
+## :book: Rule Details
+
+This rule reports the situation when `defineProps` or `defineEmits` not on the top or have wrong order
+
+## :wrench: Options
+
+```json
+{
+ "vue/define-macros-order": ["error", {
+ "order": [ "defineEmits", "defineProps" ]
+ }]
+}
+```
+
+- `order` (`string[]`) ... The order of defineEmits and defineProps macros
+
+### `{ "order": [ "defineEmits", "defineProps" ] }` (default)
+
+
+
+```vue
+
+
+```
+
+
+
+
+
+```vue
+
+
+```
+
+
+
+
+
+```vue
+
+
+```
+
+
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/define-macros-order.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/define-macros-order.js)
diff --git a/lib/index.js b/lib/index.js
index d412a6bf1..39f91b515 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -27,6 +27,7 @@ module.exports = {
'component-options-name-casing': require('./rules/component-options-name-casing'),
'component-tags-order': require('./rules/component-tags-order'),
'custom-event-name-casing': require('./rules/custom-event-name-casing'),
+ 'define-macros-order': require('./rules/define-macros-order'),
'dot-location': require('./rules/dot-location'),
'dot-notation': require('./rules/dot-notation'),
eqeqeq: require('./rules/eqeqeq'),
diff --git a/lib/rules/define-macros-order.js b/lib/rules/define-macros-order.js
new file mode 100644
index 000000000..dca9571b3
--- /dev/null
+++ b/lib/rules/define-macros-order.js
@@ -0,0 +1,248 @@
+/**
+ * @author Eduard Deisling
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+const MACROS_EMITS = 'defineEmits'
+const MACROS_PROPS = 'defineProps'
+const ORDER = [MACROS_EMITS, MACROS_PROPS]
+const DEFAULT_ORDER = [MACROS_EMITS, MACROS_PROPS]
+
+/**
+ * Get an index of the first statement after imports in order to place
+ * defineEmits and defineProps before this statement
+ * @param {Program} program
+ */
+function getStatementAfterImportsIndex(program) {
+ let index = -1
+
+ program.body.some((item, i) => {
+ index = i
+ return item.type !== 'ImportDeclaration'
+ })
+
+ return index
+}
+
+/**
+ * We need to handle cases like "const props = defineProps(...)"
+ * Define macros must be used only on top, so we can look for "Program" type
+ * inside node.parent.type
+ * @param {CallExpression|ASTNode} node
+ * @return {ASTNode}
+ */
+function getDefineMacrosStatement(node) {
+ if (!node.parent) {
+ throw new Error('Macros has parent')
+ }
+
+ if (node.parent.type === 'Program') {
+ return node
+ }
+
+ return getDefineMacrosStatement(node.parent)
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+/** @param {RuleContext} context */
+function create(context) {
+ const scriptSetup = utils.getScriptSetupElement(context)
+
+ if (!scriptSetup) {
+ return {}
+ }
+
+ const sourceCode = context.getSourceCode()
+ const options = context.options
+ /** @type {[string, string]} */
+ const order = (options[0] && options[0].order) || DEFAULT_ORDER
+ /** @type {Map} */
+ const macrosNodes = new Map()
+
+ return utils.compositingVisitors(
+ utils.defineScriptSetupVisitor(context, {
+ onDefinePropsExit(node) {
+ macrosNodes.set(MACROS_PROPS, getDefineMacrosStatement(node))
+ },
+ onDefineEmitsExit(node) {
+ macrosNodes.set(MACROS_EMITS, getDefineMacrosStatement(node))
+ }
+ }),
+ {
+ 'Program:exit'(program) {
+ const shouldFirstNode = macrosNodes.get(order[0])
+ const shouldSecondNode = macrosNodes.get(order[1])
+ const firstStatementIndex = getStatementAfterImportsIndex(program)
+ const firstStatement = program.body[firstStatementIndex]
+
+ // have both defineEmits and defineProps
+ if (shouldFirstNode && shouldSecondNode) {
+ const secondStatement = program.body[firstStatementIndex + 1]
+
+ // need move only first
+ if (firstStatement === shouldSecondNode) {
+ reportNotOnTop(order[1], shouldFirstNode, firstStatement)
+ 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, shouldFirstNode)
+ }
+
+ 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} before
+ */
+ function reportNotOnTop(macro, node, before) {
+ context.report({
+ node,
+ loc: node.loc,
+ messageId: 'macrosNotOnTop',
+ data: {
+ macro
+ },
+ fix(fixer) {
+ return moveNodeBefore(fixer, node, before)
+ }
+ })
+ }
+
+ /**
+ * Move one newline with "node" to before the "beforeNode"
+ * @param {RuleFixer} fixer
+ * @param {ASTNode} node
+ * @param {ASTNode} beforeNode
+ */
+ function moveNodeBefore(fixer, node, beforeNode) {
+ const beforeNodeToken = sourceCode.getTokenBefore(node, {
+ includeComments: true
+ })
+ const beforeNodeIndex = getNewLineIndex(node)
+ const textNode = sourceCode.getText(node, node.range[0] - beforeNodeIndex)
+ /** @type {[number, number]} */
+ const removeRange = [beforeNodeToken.range[1], node.range[1]]
+ const index = getNewLineIndex(beforeNode)
+
+ return [
+ fixer.insertTextAfterRange([index, index], textNode),
+ fixer.removeRange(removeRange)
+ ]
+ }
+
+ /**
+ * Get index of first new line before the "node"
+ * @param {ASTNode} node
+ * @return {number}
+ */
+ function getNewLineIndex(node) {
+ const after = sourceCode.getTokenBefore(node, { includeComments: true })
+ const hasWhitespace = node.loc.start.line - after.loc.end.line > 1
+
+ if (!hasWhitespace) {
+ return after.range[1]
+ }
+
+ return sourceCode.getIndexFromLoc({
+ line: node.loc.start.line - 1,
+ column: 0
+ })
+ }
+}
+
+module.exports = {
+ meta: {
+ type: 'layout',
+ docs: {
+ description:
+ 'enforce order of `defineEmits` and `defineProps` compiler macros',
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/define-macros-order.html'
+ },
+ fixable: 'code',
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ order: {
+ type: 'array',
+ items: {
+ enum: Object.values(ORDER)
+ },
+ uniqueItems: true,
+ additionalItems: false
+ }
+ },
+ additionalProperties: false
+ }
+ ],
+ messages: {
+ macrosNotOnTop: '{{macro}} must be on top.'
+ }
+ },
+ create
+}
diff --git a/tests/lib/rules/define-macros-order.js b/tests/lib/rules/define-macros-order.js
new file mode 100644
index 000000000..fda88b7bc
--- /dev/null
+++ b/tests/lib/rules/define-macros-order.js
@@ -0,0 +1,276 @@
+/**
+ * @author *****your name*****
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/define-macros-order')
+
+const tester = new RuleTester({
+ parser: require.resolve('vue-eslint-parser'),
+ parserOptions: {
+ ecmaVersion: 2020,
+ sourceType: 'module'
+ }
+})
+
+const optionsEmitsFirst = [
+ {
+ order: ['defineEmits', 'defineProps']
+ }
+]
+
+const optionsPropsFirst = [
+ {
+ order: ['defineProps', 'defineEmits']
+ }
+]
+
+tester.run('define-macros-order', rule, {
+ valid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: optionsPropsFirst
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: optionsPropsFirst
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: optionsPropsFirst
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: optionsPropsFirst
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: optionsEmitsFirst
+ }
+ ],
+ invalid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ options: optionsEmitsFirst,
+ errors: [
+ {
+ message: 'defineEmits must be on top.',
+ line: 5
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ options: optionsPropsFirst,
+ errors: [
+ {
+ message: 'defineProps must be on top.',
+ line: 8
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ only: true,
+ code: `
+
+ `,
+ output: `
+
+ `,
+ options: optionsPropsFirst,
+ errors: [
+ {
+ message: 'defineEmits must be on top.',
+ line: 6
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ options: optionsEmitsFirst,
+ errors: [
+ {
+ message: 'defineEmits must be on top.',
+ line: 8
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ options: optionsEmitsFirst,
+ errors: [
+ {
+ message: 'defineEmits must be on top.',
+ line: 12
+ }
+ ]
+ }
+ ]
+})