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 + } + ] + } + ] +})