Skip to content

Commit 213042c

Browse files
authored
Add new rule: vue/define-macros-order (#1855) (#1856)
* Add new rule: vue/define-macros-order (#1855) * Fix review comments * Add review case * Fix review comments * Fix review comments * Add semicolons * Add some newline heuristics * Fix slice review
1 parent 62e2620 commit 213042c

File tree

5 files changed

+754
-0
lines changed

5 files changed

+754
-0
lines changed

docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ For example:
312312
| [vue/component-name-in-template-casing](./component-name-in-template-casing.md) | enforce specific casing for the component naming style in template | :wrench: |
313313
| [vue/component-options-name-casing](./component-options-name-casing.md) | enforce the casing of component name in `components` options | :wrench::bulb: |
314314
| [vue/custom-event-name-casing](./custom-event-name-casing.md) | enforce specific casing for custom event name | |
315+
| [vue/define-macros-order](./define-macros-order.md) | enforce order of `defineEmits` and `defineProps` compiler macros | :wrench: |
315316
| [vue/html-button-has-type](./html-button-has-type.md) | disallow usage of button without an explicit type attribute | |
316317
| [vue/html-comment-content-newline](./html-comment-content-newline.md) | enforce unified line brake in HTML comments | :wrench: |
317318
| [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 `defineProps` and `defineEmits` compiler macros when they are not the first statements in `<script setup>` (after any potential import statements or type definitions) or when they are not in the correct order.
17+
18+
## :wrench: Options
19+
20+
```json
21+
{
22+
"vue/define-macros-order": ["error", {
23+
"order": ["defineProps", "defineEmits"]
24+
}]
25+
}
26+
```
27+
28+
- `order` (`string[]`) ... The order of defineEmits and defineProps macros
29+
30+
### `{ "order": ["defineProps", "defineEmits"] }` (default)
31+
32+
<eslint-code-block fix :rules="{'vue/define-macros-order': ['error']}">
33+
34+
```vue
35+
<!-- ✓ GOOD -->
36+
<script setup>
37+
defineProps(/* ... */)
38+
defineEmits(/* ... */)
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+
defineEmits(/* ... */)
50+
defineProps(/* ... */)
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+
defineProps(/* ... */)
63+
defineEmits(/* ... */)
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

+288
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
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_PROPS, MACROS_EMITS]
21+
22+
/**
23+
* @param {ASTNode} node
24+
*/
25+
function isUseStrictStatement(node) {
26+
return (
27+
node.type === 'ExpressionStatement' &&
28+
node.expression.type === 'Literal' &&
29+
node.expression.value === 'use strict'
30+
)
31+
}
32+
33+
/**
34+
* Get an index of the first statement after imports and interfaces in order
35+
* to place defineEmits and defineProps before this statement
36+
* @param {Program} program
37+
*/
38+
function getTargetStatementPosition(program) {
39+
const skipStatements = new Set([
40+
'ImportDeclaration',
41+
'TSInterfaceDeclaration',
42+
'TSTypeAliasDeclaration',
43+
'DebuggerStatement',
44+
'EmptyStatement'
45+
])
46+
47+
for (const [index, item] of program.body.entries()) {
48+
if (!skipStatements.has(item.type) && !isUseStrictStatement(item)) {
49+
return index
50+
}
51+
}
52+
53+
return -1
54+
}
55+
56+
/**
57+
* We need to handle cases like "const props = defineProps(...)"
58+
* Define macros must be used only on top, so we can look for "Program" type
59+
* inside node.parent.type
60+
* @param {CallExpression|ASTNode} node
61+
* @return {ASTNode}
62+
*/
63+
function getDefineMacrosStatement(node) {
64+
if (!node.parent) {
65+
throw new Error('Node has no parent')
66+
}
67+
68+
if (node.parent.type === 'Program') {
69+
return node
70+
}
71+
72+
return getDefineMacrosStatement(node.parent)
73+
}
74+
75+
// ------------------------------------------------------------------------------
76+
// Rule Definition
77+
// ------------------------------------------------------------------------------
78+
79+
/** @param {RuleContext} context */
80+
function create(context) {
81+
const scriptSetup = utils.getScriptSetupElement(context)
82+
83+
if (!scriptSetup) {
84+
return {}
85+
}
86+
87+
const sourceCode = context.getSourceCode()
88+
const options = context.options
89+
/** @type {[string, string]} */
90+
const order = (options[0] && options[0].order) || DEFAULT_ORDER
91+
/** @type {Map<string, ASTNode>} */
92+
const macrosNodes = new Map()
93+
94+
return utils.compositingVisitors(
95+
utils.defineScriptSetupVisitor(context, {
96+
onDefinePropsExit(node) {
97+
macrosNodes.set(MACROS_PROPS, getDefineMacrosStatement(node))
98+
},
99+
onDefineEmitsExit(node) {
100+
macrosNodes.set(MACROS_EMITS, getDefineMacrosStatement(node))
101+
}
102+
}),
103+
{
104+
'Program:exit'(program) {
105+
const shouldFirstNode = macrosNodes.get(order[0])
106+
const shouldSecondNode = macrosNodes.get(order[1])
107+
const firstStatementIndex = getTargetStatementPosition(program)
108+
const firstStatement = program.body[firstStatementIndex]
109+
110+
// have both defineEmits and defineProps
111+
if (shouldFirstNode && shouldSecondNode) {
112+
const secondStatement = program.body[firstStatementIndex + 1]
113+
114+
// need move only first
115+
if (firstStatement === shouldSecondNode) {
116+
reportNotOnTop(order[0], shouldFirstNode, firstStatement)
117+
return
118+
}
119+
120+
// need move both defineEmits and defineProps
121+
if (firstStatement !== shouldFirstNode) {
122+
reportBothNotOnTop(
123+
shouldFirstNode,
124+
shouldSecondNode,
125+
firstStatement
126+
)
127+
return
128+
}
129+
130+
// need move only second
131+
if (secondStatement !== shouldSecondNode) {
132+
reportNotOnTop(order[1], shouldSecondNode, shouldFirstNode)
133+
}
134+
135+
return
136+
}
137+
138+
// have only first and need to move it
139+
if (shouldFirstNode && firstStatement !== shouldFirstNode) {
140+
reportNotOnTop(order[0], shouldFirstNode, firstStatement)
141+
return
142+
}
143+
144+
// have only second and need to move it
145+
if (shouldSecondNode && firstStatement !== shouldSecondNode) {
146+
reportNotOnTop(order[1], shouldSecondNode, firstStatement)
147+
}
148+
}
149+
}
150+
)
151+
152+
/**
153+
* @param {ASTNode} shouldFirstNode
154+
* @param {ASTNode} shouldSecondNode
155+
* @param {ASTNode} before
156+
*/
157+
function reportBothNotOnTop(shouldFirstNode, shouldSecondNode, before) {
158+
context.report({
159+
node: shouldFirstNode,
160+
loc: shouldFirstNode.loc,
161+
messageId: 'macrosNotOnTop',
162+
data: {
163+
macro: order[0]
164+
},
165+
fix(fixer) {
166+
return [
167+
...moveNodeBefore(fixer, shouldFirstNode, before),
168+
...moveNodeBefore(fixer, shouldSecondNode, before)
169+
]
170+
}
171+
})
172+
}
173+
174+
/**
175+
* @param {string} macro
176+
* @param {ASTNode} node
177+
* @param {ASTNode} before
178+
*/
179+
function reportNotOnTop(macro, node, before) {
180+
context.report({
181+
node,
182+
loc: node.loc,
183+
messageId: 'macrosNotOnTop',
184+
data: {
185+
macro
186+
},
187+
fix(fixer) {
188+
return moveNodeBefore(fixer, node, before)
189+
}
190+
})
191+
}
192+
193+
/**
194+
* Move all lines of "node" with its comments to before the "target"
195+
* @param {RuleFixer} fixer
196+
* @param {ASTNode} node
197+
* @param {ASTNode} target
198+
*/
199+
function moveNodeBefore(fixer, node, target) {
200+
// get comments under tokens(if any)
201+
const beforeNodeToken = sourceCode.getTokenBefore(node)
202+
const nodeComment = sourceCode.getTokenAfter(beforeNodeToken, {
203+
includeComments: true
204+
})
205+
const nextNodeComment = sourceCode.getTokenAfter(node, {
206+
includeComments: true
207+
})
208+
// get positions of what we need to remove
209+
const cutStart = getLineStartIndex(nodeComment, beforeNodeToken)
210+
const cutEnd = getLineStartIndex(nextNodeComment, node)
211+
// get space before target
212+
const beforeTargetToken = sourceCode.getTokenBefore(target)
213+
const targetComment = sourceCode.getTokenAfter(beforeTargetToken, {
214+
includeComments: true
215+
})
216+
const textSpace = getTextBetweenTokens(beforeTargetToken, targetComment)
217+
// make insert text: comments + node + space before target
218+
const textNode = sourceCode.getText(
219+
node,
220+
node.range[0] - nodeComment.range[0]
221+
)
222+
const insertText = textNode + textSpace
223+
224+
return [
225+
fixer.insertTextBefore(targetComment, insertText),
226+
fixer.removeRange([cutStart, cutEnd])
227+
]
228+
}
229+
230+
/**
231+
* @param {ASTNode} tokenBefore
232+
* @param {ASTNode} tokenAfter
233+
*/
234+
function getTextBetweenTokens(tokenBefore, tokenAfter) {
235+
return sourceCode.text.slice(tokenBefore.range[1], tokenAfter.range[0])
236+
}
237+
238+
/**
239+
* Get position of the beginning of the token's line(or prevToken end if no line)
240+
* @param {ASTNode} token
241+
* @param {ASTNode} prevToken
242+
*/
243+
function getLineStartIndex(token, prevToken) {
244+
// if we have next token on the same line - get index right before that token
245+
if (token.loc.start.line === prevToken.loc.end.line) {
246+
return prevToken.range[1]
247+
}
248+
249+
return sourceCode.getIndexFromLoc({
250+
line: token.loc.start.line,
251+
column: 0
252+
})
253+
}
254+
}
255+
256+
module.exports = {
257+
meta: {
258+
type: 'layout',
259+
docs: {
260+
description:
261+
'enforce order of `defineEmits` and `defineProps` compiler macros',
262+
categories: undefined,
263+
url: 'https://eslint.vuejs.org/rules/define-macros-order.html'
264+
},
265+
fixable: 'code',
266+
schema: [
267+
{
268+
type: 'object',
269+
properties: {
270+
order: {
271+
type: 'array',
272+
items: {
273+
enum: Object.values(ORDER)
274+
},
275+
uniqueItems: true,
276+
additionalItems: false
277+
}
278+
},
279+
additionalProperties: false
280+
}
281+
],
282+
messages: {
283+
macrosNotOnTop:
284+
'{{macro}} should be the first statement in `<script setup>` (after any potential import statements or type definitions).'
285+
}
286+
},
287+
create
288+
}

0 commit comments

Comments
 (0)