Skip to content

Commit fbf0194

Browse files
authored
Add vue/valid-define-props rule (#1560)
1 parent e5f0258 commit fbf0194

File tree

6 files changed

+475
-11
lines changed

6 files changed

+475
-11
lines changed

Diff for: docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ For example:
331331
| [vue/v-for-delimiter-style](./v-for-delimiter-style.md) | enforce `v-for` directive's delimiter style | :wrench: |
332332
| [vue/v-on-event-hyphenation](./v-on-event-hyphenation.md) | enforce v-on event naming style on custom components in template | :wrench: |
333333
| [vue/v-on-function-call](./v-on-function-call.md) | enforce or forbid parentheses after method calls without arguments in `v-on` directives | :wrench: |
334+
| [vue/valid-define-props](./valid-define-props.md) | enforce valid `defineProps` compiler macro | |
334335
| [vue/valid-next-tick](./valid-next-tick.md) | enforce valid `nextTick` function calls | :wrench: |
335336

336337
### Extension Rules

Diff for: docs/rules/valid-define-props.md

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/valid-define-props
5+
description: enforce valid `defineProps` compiler macro
6+
---
7+
# vue/valid-define-props
8+
9+
> enforce valid `defineProps` compiler macro
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+
13+
This rule checks whether `defineProps` compiler macro is valid.
14+
15+
## :book: Rule Details
16+
17+
This rule reports `defineProps` compiler macros in the following cases:
18+
19+
- `defineProps` are referencing locally declared variables.
20+
- `defineProps` has both a literal type and an argument. e.g. `defineProps<{/*props*/}>({/*props*/})`
21+
- `defineProps` has been called multiple times.
22+
- Props are defined in both `defineProps` and `export default {}`.
23+
- Props are not defined in either `defineProps` or `export default {}`.
24+
25+
<eslint-code-block :rules="{'vue/valid-define-props': ['error']}">
26+
27+
```vue
28+
<script setup>
29+
/* ✓ GOOD */
30+
defineProps({ msg: String })
31+
</script>
32+
```
33+
34+
</eslint-code-block>
35+
36+
<eslint-code-block :rules="{'vue/valid-define-props': ['error']}">
37+
38+
```vue
39+
<script setup>
40+
/* ✓ GOOD */
41+
defineProps(['msg'])
42+
</script>
43+
```
44+
45+
</eslint-code-block>
46+
47+
```vue
48+
<script setup lang="ts">
49+
/* ✓ GOOD */
50+
defineProps<{ msg?:string }>()
51+
</script>
52+
```
53+
54+
<eslint-code-block :rules="{'vue/valid-define-props': ['error']}">
55+
56+
```vue
57+
<script>
58+
const def = { msg: String }
59+
</script>
60+
<script setup>
61+
/* ✓ GOOD */
62+
defineProps(def)
63+
</script>
64+
```
65+
66+
</eslint-code-block>
67+
68+
<eslint-code-block :rules="{'vue/valid-define-props': ['error']}">
69+
70+
```vue
71+
<script setup>
72+
/* ✗ BAD */
73+
const def = { msg: String }
74+
defineProps(def)
75+
</script>
76+
```
77+
78+
</eslint-code-block>
79+
80+
```vue
81+
<script setup lang="ts">
82+
/* ✗ BAD */
83+
defineProps<{ msg?:string }>({ msg: String })
84+
</script>
85+
```
86+
87+
<eslint-code-block :rules="{'vue/valid-define-props': ['error']}">
88+
89+
```vue
90+
<script setup>
91+
/* ✗ BAD */
92+
defineProps({ msg: String })
93+
defineProps({ count: Number })
94+
</script>
95+
```
96+
97+
</eslint-code-block>
98+
99+
<eslint-code-block :rules="{'vue/valid-define-props': ['error']}">
100+
101+
```vue
102+
<script>
103+
export default {
104+
props: { msg: String }
105+
}
106+
</script>
107+
<script setup>
108+
/* ✗ BAD */
109+
defineProps({ count: Number })
110+
</script>
111+
```
112+
113+
</eslint-code-block>
114+
115+
<eslint-code-block :rules="{'vue/valid-define-props': ['error']}">
116+
117+
```vue
118+
<script setup>
119+
/* ✗ BAD */
120+
defineProps()
121+
</script>
122+
```
123+
124+
</eslint-code-block>
125+
126+
## :wrench: Options
127+
128+
Nothing.
129+
130+
## :mag: Implementation
131+
132+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/valid-define-props.js)
133+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/valid-define-props.js)

Diff for: lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ module.exports = {
171171
'v-on-function-call': require('./rules/v-on-function-call'),
172172
'v-on-style': require('./rules/v-on-style'),
173173
'v-slot-style': require('./rules/v-slot-style'),
174+
'valid-define-props': require('./rules/valid-define-props'),
174175
'valid-next-tick': require('./rules/valid-next-tick'),
175176
'valid-template-root': require('./rules/valid-template-root'),
176177
'valid-v-bind-sync': require('./rules/valid-v-bind-sync'),

Diff for: lib/rules/valid-define-props.js

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/**
2+
* @author Yosuke Ota <https://github.com/ota-meshi>
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const { findVariable } = require('eslint-utils')
8+
const utils = require('../utils')
9+
10+
module.exports = {
11+
meta: {
12+
type: 'problem',
13+
docs: {
14+
description: 'enforce valid `defineProps` compiler macro',
15+
// TODO Switch in the major version.
16+
// categories: ['vue3-essential'],
17+
categories: undefined,
18+
url: 'https://eslint.vuejs.org/rules/valid-define-props.html'
19+
},
20+
fixable: null,
21+
schema: [],
22+
messages: {
23+
hasTypeAndArg:
24+
'`defineProps` has both a type-only props and an argument.',
25+
referencingLocally:
26+
'`defineProps` are referencing locally declared variables.',
27+
multiple: '`defineProps` has been called multiple times.',
28+
notDefined: 'Props are not defined.',
29+
definedInBoth:
30+
'Props are defined in both `defineProps` and `export default {}`.'
31+
}
32+
},
33+
/** @param {RuleContext} context */
34+
create(context) {
35+
const scriptSetup = utils.getScriptSetupElement(context)
36+
if (!scriptSetup) {
37+
return {}
38+
}
39+
40+
/** @type {Set<Expression | SpreadElement>} */
41+
const propsDefExpressions = new Set()
42+
let hasDefaultExport = false
43+
/** @type {CallExpression[]} */
44+
const definePropsNodes = []
45+
/** @type {CallExpression | null} */
46+
let emptyDefineProps = null
47+
48+
return utils.compositingVisitors(
49+
utils.defineScriptSetupVisitor(context, {
50+
onDefinePropsEnter(node) {
51+
definePropsNodes.push(node)
52+
53+
if (node.arguments.length >= 1) {
54+
if (node.typeParameters && node.typeParameters.params.length >= 1) {
55+
// `defineProps` has both a literal type and an argument.
56+
context.report({
57+
node,
58+
messageId: 'hasTypeAndArg'
59+
})
60+
return
61+
}
62+
63+
propsDefExpressions.add(node.arguments[0])
64+
} else {
65+
if (
66+
!node.typeParameters ||
67+
node.typeParameters.params.length === 0
68+
) {
69+
emptyDefineProps = node
70+
}
71+
}
72+
},
73+
Identifier(node) {
74+
for (const def of propsDefExpressions) {
75+
if (utils.inRange(def.range, node)) {
76+
const variable = findVariable(context.getScope(), node)
77+
if (
78+
variable &&
79+
variable.references.some((ref) => ref.identifier === node)
80+
) {
81+
if (
82+
variable.defs.length &&
83+
variable.defs.every((def) =>
84+
utils.inRange(scriptSetup.range, def.name)
85+
)
86+
) {
87+
//`defineProps` are referencing locally declared variables.
88+
context.report({
89+
node,
90+
messageId: 'referencingLocally'
91+
})
92+
}
93+
}
94+
}
95+
}
96+
}
97+
}),
98+
utils.defineVueVisitor(context, {
99+
onVueObjectEnter(node, { type }) {
100+
if (type !== 'export' || utils.inRange(scriptSetup.range, node)) {
101+
return
102+
}
103+
104+
hasDefaultExport = Boolean(utils.findProperty(node, 'props'))
105+
}
106+
}),
107+
{
108+
'Program:exit'() {
109+
if (!definePropsNodes.length) {
110+
return
111+
}
112+
if (definePropsNodes.length > 1) {
113+
// `defineProps` has been called multiple times.
114+
for (const node of definePropsNodes) {
115+
context.report({
116+
node,
117+
messageId: 'multiple'
118+
})
119+
}
120+
return
121+
}
122+
if (emptyDefineProps) {
123+
if (!hasDefaultExport) {
124+
// Props are not defined.
125+
context.report({
126+
node: emptyDefineProps,
127+
messageId: 'notDefined'
128+
})
129+
}
130+
} else {
131+
if (hasDefaultExport) {
132+
// Props are defined in both `defineProps` and `export default {}`.
133+
for (const node of definePropsNodes) {
134+
context.report({
135+
node,
136+
messageId: 'definedInBoth'
137+
})
138+
}
139+
}
140+
}
141+
}
142+
}
143+
)
144+
}
145+
}

Diff for: lib/utils/index.js

+39-11
Original file line numberDiff line numberDiff line change
@@ -1058,16 +1058,26 @@ module.exports = {
10581058
const hasEmitsEvent =
10591059
visitor.onDefineEmitsEnter || visitor.onDefineEmitsExit
10601060
if (hasPropsEvent || hasEmitsEvent) {
1061-
/** @type {ESNode | null} */
1062-
let nested = null
1063-
scriptSetupVisitor[':function, BlockStatement'] = (node) => {
1064-
if (!nested) {
1065-
nested = node
1061+
/** @type {Expression | null} */
1062+
let candidateMacro = null
1063+
/** @param {VariableDeclarator|ExpressionStatement} node */
1064+
scriptSetupVisitor[
1065+
'Program > VariableDeclaration > VariableDeclarator, Program > ExpressionStatement'
1066+
] = (node) => {
1067+
if (!candidateMacro) {
1068+
candidateMacro =
1069+
node.type === 'VariableDeclarator' ? node.init : node.expression
10661070
}
10671071
}
1068-
scriptSetupVisitor[':function, BlockStatement:exit'] = (node) => {
1069-
if (nested === node) {
1070-
nested = null
1072+
/** @param {VariableDeclarator|ExpressionStatement} node */
1073+
scriptSetupVisitor[
1074+
'Program > VariableDeclaration > VariableDeclarator, Program > ExpressionStatement:exit'
1075+
] = (node) => {
1076+
if (
1077+
candidateMacro ===
1078+
(node.type === 'VariableDeclarator' ? node.init : node.expression)
1079+
) {
1080+
candidateMacro = null
10711081
}
10721082
}
10731083
const definePropsMap = new Map()
@@ -1077,11 +1087,16 @@ module.exports = {
10771087
*/
10781088
scriptSetupVisitor.CallExpression = (node) => {
10791089
if (
1080-
!nested &&
1090+
candidateMacro &&
10811091
inScriptSetup(node) &&
10821092
node.callee.type === 'Identifier'
10831093
) {
1084-
if (hasPropsEvent && node.callee.name === 'defineProps') {
1094+
if (
1095+
hasPropsEvent &&
1096+
(candidateMacro === node ||
1097+
candidateMacro === getWithDefaults(node)) &&
1098+
node.callee.name === 'defineProps'
1099+
) {
10851100
/** @type {(ComponentArrayProp | ComponentObjectProp | ComponentTypeProp)[]} */
10861101
let props = []
10871102
if (node.arguments.length >= 1) {
@@ -1100,7 +1115,11 @@ module.exports = {
11001115
}
11011116
callVisitor('onDefinePropsEnter', node, props)
11021117
definePropsMap.set(node, props)
1103-
} else if (hasEmitsEvent && node.callee.name === 'defineEmits') {
1118+
} else if (
1119+
hasEmitsEvent &&
1120+
candidateMacro === node &&
1121+
node.callee.name === 'defineEmits'
1122+
) {
11041123
/** @type {(ComponentArrayEmit | ComponentObjectEmit | ComponentTypeEmit)[]} */
11051124
let emits = []
11061125
if (node.arguments.length >= 1) {
@@ -2395,6 +2414,15 @@ function hasWithDefaults(node) {
23952414
)
23962415
}
23972416

2417+
/**
2418+
* Get the withDefaults call node from given defineProps call node.
2419+
* @param {CallExpression} node The node of defineProps
2420+
* @returns {CallExpression | null}
2421+
*/
2422+
function getWithDefaults(node) {
2423+
return hasWithDefaults(node) ? node.parent : null
2424+
}
2425+
23982426
/**
23992427
* Gets a map of the property nodes defined in withDefaults.
24002428
* @param {CallExpression} node The node of defineProps

0 commit comments

Comments
 (0)