diff --git a/docs/rules/README.md b/docs/rules/README.md
index 3d2d113d7..26ce0e59d 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -312,6 +312,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..d928e003a
--- /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 `defineProps` and `defineEmits` compiler macros when they are not the first statements in `
+```
+
+
+
+
+
+```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..9c8ea45e6
--- /dev/null
+++ b/lib/rules/define-macros-order.js
@@ -0,0 +1,288 @@
+/**
+ * @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_PROPS, MACROS_EMITS]
+
+/**
+ * @param {ASTNode} node
+ */
+function isUseStrictStatement(node) {
+ return (
+ node.type === 'ExpressionStatement' &&
+ node.expression.type === 'Literal' &&
+ node.expression.value === 'use strict'
+ )
+}
+
+/**
+ * Get an index of the first statement after imports and interfaces in order
+ * to place defineEmits and defineProps before this statement
+ * @param {Program} program
+ */
+function getTargetStatementPosition(program) {
+ const skipStatements = new Set([
+ 'ImportDeclaration',
+ 'TSInterfaceDeclaration',
+ 'TSTypeAliasDeclaration',
+ 'DebuggerStatement',
+ 'EmptyStatement'
+ ])
+
+ for (const [index, item] of program.body.entries()) {
+ if (!skipStatements.has(item.type) && !isUseStrictStatement(item)) {
+ return index
+ }
+ }
+
+ return -1
+}
+
+/**
+ * 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('Node has no 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 = getTargetStatementPosition(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[0], 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 all lines of "node" with its comments to before the "target"
+ * @param {RuleFixer} fixer
+ * @param {ASTNode} node
+ * @param {ASTNode} target
+ */
+ function moveNodeBefore(fixer, node, target) {
+ // get comments under tokens(if any)
+ const beforeNodeToken = sourceCode.getTokenBefore(node)
+ const nodeComment = sourceCode.getTokenAfter(beforeNodeToken, {
+ includeComments: true
+ })
+ const nextNodeComment = sourceCode.getTokenAfter(node, {
+ includeComments: true
+ })
+ // get positions of what we need to remove
+ const cutStart = getLineStartIndex(nodeComment, beforeNodeToken)
+ const cutEnd = getLineStartIndex(nextNodeComment, node)
+ // get space before target
+ const beforeTargetToken = sourceCode.getTokenBefore(target)
+ const targetComment = sourceCode.getTokenAfter(beforeTargetToken, {
+ includeComments: true
+ })
+ const textSpace = getTextBetweenTokens(beforeTargetToken, targetComment)
+ // make insert text: comments + node + space before target
+ const textNode = sourceCode.getText(
+ node,
+ node.range[0] - nodeComment.range[0]
+ )
+ const insertText = textNode + textSpace
+
+ return [
+ fixer.insertTextBefore(targetComment, insertText),
+ fixer.removeRange([cutStart, cutEnd])
+ ]
+ }
+
+ /**
+ * @param {ASTNode} tokenBefore
+ * @param {ASTNode} tokenAfter
+ */
+ function getTextBetweenTokens(tokenBefore, tokenAfter) {
+ return sourceCode.text.slice(tokenBefore.range[1], tokenAfter.range[0])
+ }
+
+ /**
+ * Get position of the beginning of the token's line(or prevToken end if no line)
+ * @param {ASTNode} token
+ * @param {ASTNode} prevToken
+ */
+ function getLineStartIndex(token, prevToken) {
+ // if we have next token on the same line - get index right before that token
+ if (token.loc.start.line === prevToken.loc.end.line) {
+ return prevToken.range[1]
+ }
+
+ return sourceCode.getIndexFromLoc({
+ line: token.loc.start.line,
+ 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}} should be the first statement in `
+ `,
+ 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: message('defineEmits'),
+ line: 5
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ options: optionsPropsFirst,
+ errors: [
+ {
+ message: message('defineProps'),
+ line: 8
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ options: optionsPropsFirst,
+ errors: [
+ {
+ message: message('defineProps'),
+ line: 6
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ options: optionsEmitsFirst,
+ errors: [
+ {
+ message: message('defineEmits'),
+ line: 8
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ options: optionsEmitsFirst,
+ errors: [
+ {
+ message: message('defineEmits'),
+ line: 12
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ errors: [
+ {
+ message: message('defineProps'),
+ line: 10
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ options: optionsEmitsFirst,
+ errors: [
+ {
+ message: message('defineEmits'),
+ line: 16
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ options: optionsEmitsFirst,
+ errors: [
+ {
+ message: message('defineEmits'),
+ line: 3
+ }
+ ]
+ }
+ ]
+})