Skip to content

Commit 2d6114b

Browse files
committed
Add new rule: vue/define-macros-order (#1855)
1 parent a473a0d commit 2d6114b

File tree

5 files changed

+598
-0
lines changed

5 files changed

+598
-0
lines changed

docs/rules/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ sidebarDepth: 0
1212
: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).
1313
:::
1414

15+
1516
## Base Rules (Enabling Correct ESLint Parsing)
1617

1718
Enforce all the rules in this category, as well as all higher priority rules, with:
@@ -312,6 +313,7 @@ For example:
312313
| [vue/component-name-in-template-casing](./component-name-in-template-casing.md) | enforce specific casing for the component naming style in template | :wrench: |
313314
| [vue/component-options-name-casing](./component-options-name-casing.md) | enforce the casing of component name in `components` options | :wrench::bulb: |
314315
| [vue/custom-event-name-casing](./custom-event-name-casing.md) | enforce specific casing for custom event name | |
316+
| [vue/define-macros-order](./define-macros-order.md) | enforce order of `defineEmits` and `defineProps` compiler macros | :wrench: |
315317
| [vue/html-button-has-type](./html-button-has-type.md) | disallow usage of button without an explicit type attribute | |
316318
| [vue/html-comment-content-newline](./html-comment-content-newline.md) | enforce unified line brake in HTML comments | :wrench: |
317319
| [vue/html-comment-content-spacing](./html-comment-content-spacing.md) | enforce unified spacing in HTML comments | :wrench: |

docs/rules/define-macros-order.md

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/define-macros-order
5+
description: enforce order of `defineEmits` and `defineProps` compiler macros
6+
---
7+
# vue/define-macros-order
8+
9+
> enforce order of `defineEmits` and `defineProps` compiler macros
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
- :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.
13+
14+
## :book: Rule Details
15+
16+
This rule reports the situation when `defineProps` or `defineEmits` not on the top or have wrong order
17+
18+
## :wrench: Options
19+
20+
```json
21+
{
22+
"vue/define-macros-order": ["error", {
23+
"order": [ "defineEmits", "defineProps" ]
24+
}]
25+
}
26+
```
27+
28+
- `order` (`string[]`) ... The order of defineEmits and defineProps macros
29+
30+
### `{ "order": [ "defineEmits", "defineProps" ] }` (default)
31+
32+
<eslint-code-block fix :rules="{'vue/define-macros-order': ['error']}">
33+
34+
```vue
35+
<!-- ✓ GOOD -->
36+
<script setup>
37+
defineEmits(/* ... */)
38+
defineProps(/* ... */)
39+
</script>
40+
```
41+
42+
</eslint-code-block>
43+
44+
<eslint-code-block fix :rules="{'vue/define-macros-order': ['error']}">
45+
46+
```vue
47+
<!-- ✗ BAD -->
48+
<script setup>
49+
defineProps(/* ... */)
50+
defineEmits(/* ... */)
51+
</script>
52+
```
53+
54+
</eslint-code-block>
55+
56+
<eslint-code-block fix :rules="{'vue/define-macros-order': ['error']}">
57+
58+
```vue
59+
<!-- ✗ BAD -->
60+
<script setup>
61+
const bar = ref()
62+
defineEmits(/* ... */)
63+
defineProps(/* ... */)
64+
</script>
65+
```
66+
67+
</eslint-code-block>
68+
69+
## :mag: Implementation
70+
71+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/define-macros-order.js)
72+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/define-macros-order.js)

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ module.exports = {
2727
'component-options-name-casing': require('./rules/component-options-name-casing'),
2828
'component-tags-order': require('./rules/component-tags-order'),
2929
'custom-event-name-casing': require('./rules/custom-event-name-casing'),
30+
'define-macros-order': require('./rules/define-macros-order'),
3031
'dot-location': require('./rules/dot-location'),
3132
'dot-notation': require('./rules/dot-notation'),
3233
eqeqeq: require('./rules/eqeqeq'),

lib/rules/define-macros-order.js

+248
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
/**
2+
* @author Eduard Deisling
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const utils = require('../utils')
12+
13+
// ------------------------------------------------------------------------------
14+
// Helpers
15+
// ------------------------------------------------------------------------------
16+
17+
const MACROS_EMITS = 'defineEmits'
18+
const MACROS_PROPS = 'defineProps'
19+
const ORDER = [MACROS_EMITS, MACROS_PROPS]
20+
const DEFAULT_ORDER = [MACROS_EMITS, MACROS_PROPS]
21+
22+
/**
23+
* Get an index of the first statement after imports in order to place
24+
* defineEmits and defineProps before this statement
25+
* @param {Program} program
26+
*/
27+
function getStatementAfterImportsIndex(program) {
28+
let index = -1
29+
30+
program.body.some((item, i) => {
31+
index = i
32+
return item.type !== 'ImportDeclaration'
33+
})
34+
35+
return index
36+
}
37+
38+
/**
39+
* We need to handle cases like "const props = defineProps(...)"
40+
* Define macros must be used only on top, so we can look for "Program" type
41+
* inside node.parent.type
42+
* @param {CallExpression|ASTNode} node
43+
* @return {ASTNode}
44+
*/
45+
function getDefineMacrosStatement(node) {
46+
if (!node.parent) {
47+
throw new Error('Macros has parent')
48+
}
49+
50+
if (node.parent.type === 'Program') {
51+
return node
52+
}
53+
54+
return getDefineMacrosStatement(node.parent)
55+
}
56+
57+
// ------------------------------------------------------------------------------
58+
// Rule Definition
59+
// ------------------------------------------------------------------------------
60+
61+
/** @param {RuleContext} context */
62+
function create(context) {
63+
const scriptSetup = utils.getScriptSetupElement(context)
64+
65+
if (!scriptSetup) {
66+
return {}
67+
}
68+
69+
const sourceCode = context.getSourceCode()
70+
const options = context.options
71+
/** @type {[string, string]} */
72+
const order = (options[0] && options[0].order) || DEFAULT_ORDER
73+
/** @type {Map<string, ASTNode>} */
74+
const macrosNodes = new Map()
75+
76+
return utils.compositingVisitors(
77+
utils.defineScriptSetupVisitor(context, {
78+
onDefinePropsExit(node) {
79+
macrosNodes.set(MACROS_PROPS, getDefineMacrosStatement(node))
80+
},
81+
onDefineEmitsExit(node) {
82+
macrosNodes.set(MACROS_EMITS, getDefineMacrosStatement(node))
83+
}
84+
}),
85+
{
86+
'Program:exit'(program) {
87+
const shouldFirstNode = macrosNodes.get(order[0])
88+
const shouldSecondNode = macrosNodes.get(order[1])
89+
const firstStatementIndex = getStatementAfterImportsIndex(program)
90+
const firstStatement = program.body[firstStatementIndex]
91+
92+
// have both defineEmits and defineProps
93+
if (shouldFirstNode && shouldSecondNode) {
94+
const secondStatement = program.body[firstStatementIndex + 1]
95+
96+
// need move only first
97+
if (firstStatement === shouldSecondNode) {
98+
reportNotOnTop(order[1], shouldFirstNode, firstStatement)
99+
return
100+
}
101+
102+
// need move both defineEmits and defineProps
103+
if (firstStatement !== shouldFirstNode) {
104+
reportBothNotOnTop(
105+
shouldFirstNode,
106+
shouldSecondNode,
107+
firstStatement
108+
)
109+
return
110+
}
111+
112+
// need move only second
113+
if (secondStatement !== shouldSecondNode) {
114+
reportNotOnTop(order[1], shouldSecondNode, shouldFirstNode)
115+
}
116+
117+
return
118+
}
119+
120+
// have only first and need to move it
121+
if (shouldFirstNode && firstStatement !== shouldFirstNode) {
122+
reportNotOnTop(order[0], shouldFirstNode, firstStatement)
123+
return
124+
}
125+
126+
// have only second and need to move it
127+
if (shouldSecondNode && firstStatement !== shouldSecondNode) {
128+
reportNotOnTop(order[1], shouldSecondNode, firstStatement)
129+
}
130+
}
131+
}
132+
)
133+
134+
/**
135+
* @param {ASTNode} shouldFirstNode
136+
* @param {ASTNode} shouldSecondNode
137+
* @param {ASTNode} before
138+
*/
139+
function reportBothNotOnTop(shouldFirstNode, shouldSecondNode, before) {
140+
context.report({
141+
node: shouldFirstNode,
142+
loc: shouldFirstNode.loc,
143+
messageId: 'macrosNotOnTop',
144+
data: {
145+
macro: order[0]
146+
},
147+
fix(fixer) {
148+
return [
149+
...moveNodeBefore(fixer, shouldFirstNode, before),
150+
...moveNodeBefore(fixer, shouldSecondNode, before)
151+
]
152+
}
153+
})
154+
}
155+
156+
/**
157+
* @param {string} macro
158+
* @param {ASTNode} node
159+
* @param {ASTNode} before
160+
*/
161+
function reportNotOnTop(macro, node, before) {
162+
context.report({
163+
node,
164+
loc: node.loc,
165+
messageId: 'macrosNotOnTop',
166+
data: {
167+
macro
168+
},
169+
fix(fixer) {
170+
return moveNodeBefore(fixer, node, before)
171+
}
172+
})
173+
}
174+
175+
/**
176+
* Move one newline with "node" to before the "beforeNode"
177+
* @param {RuleFixer} fixer
178+
* @param {ASTNode} node
179+
* @param {ASTNode} beforeNode
180+
*/
181+
function moveNodeBefore(fixer, node, beforeNode) {
182+
const beforeNodeToken = sourceCode.getTokenBefore(node, {
183+
includeComments: true
184+
})
185+
const beforeNodeIndex = getNewLineIndex(node)
186+
const textNode = sourceCode.getText(node, node.range[0] - beforeNodeIndex)
187+
/** @type {[number, number]} */
188+
const removeRange = [beforeNodeToken.range[1], node.range[1]]
189+
const index = getNewLineIndex(beforeNode)
190+
191+
return [
192+
fixer.insertTextAfterRange([index, index], textNode),
193+
fixer.removeRange(removeRange)
194+
]
195+
}
196+
197+
/**
198+
* Get index of first new line before the "node"
199+
* @param {ASTNode} node
200+
* @return {number}
201+
*/
202+
function getNewLineIndex(node) {
203+
const after = sourceCode.getTokenBefore(node, { includeComments: true })
204+
const hasWhitespace = node.loc.start.line - after.loc.end.line > 1
205+
206+
if (!hasWhitespace) {
207+
return after.range[1]
208+
}
209+
210+
return sourceCode.getIndexFromLoc({
211+
line: node.loc.start.line - 1,
212+
column: 0
213+
})
214+
}
215+
}
216+
217+
module.exports = {
218+
meta: {
219+
type: 'layout',
220+
docs: {
221+
description:
222+
'enforce order of `defineEmits` and `defineProps` compiler macros',
223+
categories: undefined,
224+
url: 'https://eslint.vuejs.org/rules/define-macros-order.html'
225+
},
226+
fixable: 'code',
227+
schema: [
228+
{
229+
type: 'object',
230+
properties: {
231+
order: {
232+
type: 'array',
233+
items: {
234+
enum: Object.values(ORDER)
235+
},
236+
uniqueItems: true,
237+
additionalItems: false
238+
}
239+
},
240+
additionalProperties: false
241+
}
242+
],
243+
messages: {
244+
macrosNotOnTop: '{{macro}} must be on top.'
245+
}
246+
},
247+
create
248+
}

0 commit comments

Comments
 (0)