diff --git a/docs/rules/define-macros-order.md b/docs/rules/define-macros-order.md index 37074f05c..24d25c56e 100644 --- a/docs/rules/define-macros-order.md +++ b/docs/rules/define-macros-order.md @@ -20,12 +20,14 @@ This rule reports the `defineProps` and `defineEmits` compiler macros when they ```json { "vue/define-macros-order": ["error", { - "order": ["defineProps", "defineEmits"] + "order": ["defineProps", "defineEmits"], + "defineExposeLast": false }] } ``` - `order` (`string[]`) ... The order of defineEmits and defineProps macros. You can also add `"defineOptions"` and `"defineSlots"`. +- `defineExposeLast` (`boolean`) ... Force `defineExpose` at the end. ### `{ "order": ["defineProps", "defineEmits"] }` (default) @@ -111,6 +113,36 @@ const slots = defineSlots() +### `{ "defineExposeLast": true }` + + + +```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 5a678db42..b55c5e8c7 100644 --- a/lib/rules/define-macros-order.js +++ b/lib/rules/define-macros-order.js @@ -95,8 +95,12 @@ function create(context) { const options = context.options /** @type {[string, string]} */ const order = (options[0] && options[0].order) || DEFAULT_ORDER + /** @type {boolean} */ + const defineExposeLast = (options[0] && options[0].defineExposeLast) || false /** @type {Map} */ const macrosNodes = new Map() + /** @type {ASTNode} */ + let defineExposeNode return utils.compositingVisitors( utils.defineScriptSetupVisitor(context, { @@ -111,6 +115,9 @@ function create(context) { }, onDefineSlotsExit(node) { macrosNodes.set(MACROS_SLOTS, getDefineMacrosStatement(node)) + }, + onDefineExposeExit(node) { + defineExposeNode = getDefineMacrosStatement(node) } }), { @@ -131,6 +138,14 @@ function create(context) { (data) => utils.isDef(data.node) ) + // check last node + if (defineExposeLast) { + const lastNode = program.body[program.body.length - 1] + if (defineExposeNode && lastNode !== defineExposeNode) { + reportExposeNotOnBottom(defineExposeNode, lastNode) + } + } + for (const [index, should] of orderedList.entries()) { const targetStatement = program.body[firstStatementIndex + index] @@ -172,6 +187,58 @@ function create(context) { }) } + /** + * @param {ASTNode} node + * @param {ASTNode} lastNode + */ + function reportExposeNotOnBottom(node, lastNode) { + context.report({ + node, + loc: node.loc, + messageId: 'defineExposeNotTheLast', + suggest: [ + { + messageId: 'putExposeAtTheLast', + fix(fixer) { + return moveNodeToLast(fixer, node, lastNode) + } + } + ] + }) + } + + /** + * Move all lines of "node" with its comments to after the "target" + * @param {RuleFixer} fixer + * @param {ASTNode} node + * @param {ASTNode} target + */ + function moveNodeToLast(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 + }) + + // remove position: node (and comments) to next node (and comments) + const cutStart = getLineStartIndex(nodeComment, beforeNodeToken) + const cutEnd = getLineStartIndex(nextNodeComment, node) + + // insert text: comment + node + const textNode = sourceCode.getText( + node, + node.range[0] - beforeNodeToken.range[1] + ) + + return [ + fixer.insertTextAfter(target, textNode), + fixer.removeRange([cutStart, cutEnd]) + ] + } + /** * Move all lines of "node" with its comments to before the "target" * @param {RuleFixer} fixer @@ -255,6 +322,7 @@ module.exports = { url: 'https://eslint.vuejs.org/rules/define-macros-order.html' }, fixable: 'code', + hasSuggestions: true, schema: [ { type: 'object', @@ -266,6 +334,9 @@ module.exports = { }, uniqueItems: true, additionalItems: false + }, + defineExposeLast: { + type: 'boolean' } }, additionalProperties: false @@ -273,7 +344,11 @@ module.exports = { ], messages: { macrosNotOnTop: - '{{macro}} should be the first statement in ` + `, + options: optionsExposeLast + }, + { + filename: 'test.vue', + code: ` + + `, + options: [ + { + order: ['defineProps', 'defineEmits'], + defineExposeLast: true + } + ], + parserOptions: { + parser: require.resolve('@typescript-eslint/parser') + } } ], invalid: [ @@ -622,6 +676,108 @@ tester.run('define-macros-order', rule, { line: 6 } ] + }, + { + filename: 'test.vue', + code: ` + + `, + output: null, + options: optionsExposeLast, + errors: [ + { + message: defineExposeNotTheLast, + line: 6, + suggestions: [ + { + desc: putExposeAtBottom, + output: ` + + ` + } + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + output: ` + + `, + options: [ + { + order: ['defineOptions', 'defineEmits', 'defineProps'], + defineExposeLast: true + } + ], + errors: [ + { + message: defineExposeNotTheLast, + line: 6, + suggestions: [ + { + desc: putExposeAtBottom, + output: ` + + ` + } + ] + }, + { + message: message('defineOptions'), + line: 8 + } + ] } ] }) diff --git a/typings/eslint-plugin-vue/util-types/utils.ts b/typings/eslint-plugin-vue/util-types/utils.ts index b2704769f..4d8384d66 100644 --- a/typings/eslint-plugin-vue/util-types/utils.ts +++ b/typings/eslint-plugin-vue/util-types/utils.ts @@ -44,6 +44,8 @@ export interface ScriptSetupVisitor extends ScriptSetupVisitorBase { onDefineOptionsExit?(node: CallExpression): void onDefineSlotsEnter?(node: CallExpression): void onDefineSlotsExit?(node: CallExpression): void + onDefineExposeEnter?(node: CallExpression): void + onDefineExposeExit?(node: CallExpression): void [query: string]: | ((node: VAST.ParamNode) => void) | ((node: CallExpression, props: ComponentProp[]) => void)