Skip to content

Commit 2ee0f4b

Browse files
Add vue/valid-define-options rule (#2165)
Co-authored-by: Flo Edelmann <[email protected]>
1 parent dd96780 commit 2ee0f4b

11 files changed

+459
-8
lines changed

Diff for: docs/rules/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ For example:
269269
| [vue/static-class-names-order](./static-class-names-order.md) | enforce static class names order | :wrench: | :hammer: |
270270
| [vue/v-for-delimiter-style](./v-for-delimiter-style.md) | enforce `v-for` directive's delimiter style | :wrench: | :lipstick: |
271271
| [vue/v-on-handler-style](./v-on-handler-style.md) | enforce writing style for handlers in `v-on` directives | :wrench: | :hammer: |
272+
| [vue/valid-define-options](./valid-define-options.md) | enforce valid `defineOptions` compiler macro | | :warning: |
272273

273274
</rules-table>
274275

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ This rule checks whether `defineEmits` compiler macro is valid.
1717

1818
This rule reports `defineEmits` compiler macros in the following cases:
1919

20-
- `defineEmits` are referencing locally declared variables.
20+
- `defineEmits` is referencing locally declared variables.
2121
- `defineEmits` has both a literal type and an argument. e.g. `defineEmits<(e: 'foo')=>void>(['bar'])`
2222
- `defineEmits` has been called multiple times.
2323
- Custom events are defined in both `defineEmits` and `export default {}`.
@@ -139,6 +139,7 @@ Nothing.
139139
## :couple: Related Rules
140140

141141
- [vue/define-emits-declaration](./define-emits-declaration.md)
142+
- [vue/valid-define-options](./valid-define-options.md)
142143
- [vue/valid-define-props](./valid-define-props.md)
143144

144145
## :rocket: Version

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

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

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ This rule checks whether `defineProps` compiler macro is valid.
1717

1818
This rule reports `defineProps` compiler macros in the following cases:
1919

20-
- `defineProps` are referencing locally declared variables.
20+
- `defineProps` is referencing locally declared variables.
2121
- `defineProps` has both a literal type and an argument. e.g. `defineProps<{/*props*/}>({/*props*/})`
2222
- `defineProps` has been called multiple times.
2323
- Props are defined in both `defineProps` and `export default {}`.
@@ -140,6 +140,7 @@ Nothing.
140140

141141
- [vue/define-props-declaration](./define-props-declaration.md)
142142
- [vue/valid-define-emits](./valid-define-emits.md)
143+
- [vue/valid-define-options](./valid-define-options.md)
143144

144145
## :rocket: Version
145146

Diff for: lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ module.exports = {
211211
'v-slot-style': require('./rules/v-slot-style'),
212212
'valid-attribute-name': require('./rules/valid-attribute-name'),
213213
'valid-define-emits': require('./rules/valid-define-emits'),
214+
'valid-define-options': require('./rules/valid-define-options'),
214215
'valid-define-props': require('./rules/valid-define-props'),
215216
'valid-model-definition': require('./rules/valid-model-definition'),
216217
'valid-next-tick': require('./rules/valid-next-tick'),

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ module.exports = {
2020
messages: {
2121
hasTypeAndArg: '`defineEmits` has both a type-only emit and an argument.',
2222
referencingLocally:
23-
'`defineEmits` are referencing locally declared variables.',
23+
'`defineEmits` is referencing locally declared variables.',
2424
multiple: '`defineEmits` has been called multiple times.',
2525
notDefined: 'Custom events are not defined.',
2626
definedInBoth:
@@ -85,7 +85,7 @@ module.exports = {
8585
if (utils.withinTypeNode(node)) {
8686
continue
8787
}
88-
//`defineEmits` are referencing locally declared variables.
88+
//`defineEmits` is referencing locally declared variables.
8989
context.report({
9090
node,
9191
messageId: 'referencingLocally'

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

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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-community/eslint-utils')
8+
const utils = require('../utils')
9+
10+
module.exports = {
11+
meta: {
12+
type: 'problem',
13+
docs: {
14+
description: 'enforce valid `defineOptions` compiler macro',
15+
// TODO Switch in the next major version
16+
// categories: ['vue3-essential', 'essential'],
17+
categories: undefined,
18+
url: 'https://eslint.vuejs.org/rules/valid-define-options.html'
19+
},
20+
fixable: null,
21+
schema: [],
22+
messages: {
23+
referencingLocally:
24+
'`defineOptions` is referencing locally declared variables.',
25+
multiple: '`defineOptions` has been called multiple times.',
26+
notDefined: 'Options are not defined.',
27+
disallowProp:
28+
'`defineOptions()` cannot be used to declare `{{propName}}`. Use `{{insteadMacro}}()` instead.',
29+
typeArgs: '`defineOptions()` cannot accept type arguments.'
30+
}
31+
},
32+
/** @param {RuleContext} context */
33+
create(context) {
34+
const scriptSetup = utils.getScriptSetupElement(context)
35+
if (!scriptSetup) {
36+
return {}
37+
}
38+
39+
/** @type {Set<Expression | SpreadElement>} */
40+
const optionsDefExpressions = new Set()
41+
/** @type {CallExpression[]} */
42+
const defineOptionsNodes = []
43+
44+
return utils.compositingVisitors(
45+
utils.defineScriptSetupVisitor(context, {
46+
onDefineOptionsEnter(node) {
47+
defineOptionsNodes.push(node)
48+
49+
if (node.arguments.length > 0) {
50+
const define = node.arguments[0]
51+
if (define.type === 'ObjectExpression') {
52+
for (const [propName, insteadMacro] of [
53+
['props', 'defineProps'],
54+
['emits', 'defineEmits'],
55+
['expose', 'defineExpose'],
56+
['slots', 'defineSlots']
57+
]) {
58+
const prop = utils.findProperty(define, propName)
59+
if (prop) {
60+
context.report({
61+
node,
62+
messageId: 'disallowProp',
63+
data: { propName, insteadMacro }
64+
})
65+
}
66+
}
67+
}
68+
69+
optionsDefExpressions.add(node.arguments[0])
70+
} else {
71+
context.report({
72+
node,
73+
messageId: 'notDefined'
74+
})
75+
}
76+
77+
if (node.typeParameters) {
78+
context.report({
79+
node: node.typeParameters,
80+
messageId: 'typeArgs'
81+
})
82+
}
83+
},
84+
Identifier(node) {
85+
for (const defineOptions of optionsDefExpressions) {
86+
if (utils.inRange(defineOptions.range, node)) {
87+
const variable = findVariable(context.getScope(), node)
88+
if (
89+
variable &&
90+
variable.references.some((ref) => ref.identifier === node) &&
91+
variable.defs.length > 0 &&
92+
variable.defs.every(
93+
(def) =>
94+
def.type !== 'ImportBinding' &&
95+
utils.inRange(scriptSetup.range, def.name) &&
96+
!utils.inRange(defineOptions.range, def.name)
97+
)
98+
) {
99+
if (utils.withinTypeNode(node)) {
100+
continue
101+
}
102+
//`defineOptions` is referencing locally declared variables.
103+
context.report({
104+
node,
105+
messageId: 'referencingLocally'
106+
})
107+
}
108+
}
109+
}
110+
}
111+
}),
112+
{
113+
'Program:exit'() {
114+
if (defineOptionsNodes.length > 1) {
115+
// `defineOptions` has been called multiple times.
116+
for (const node of defineOptionsNodes) {
117+
context.report({
118+
node,
119+
messageId: 'multiple'
120+
})
121+
}
122+
}
123+
}
124+
}
125+
)
126+
}
127+
}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ module.exports = {
2121
hasTypeAndArg:
2222
'`defineProps` has both a type-only props and an argument.',
2323
referencingLocally:
24-
'`defineProps` are referencing locally declared variables.',
24+
'`defineProps` is referencing locally declared variables.',
2525
multiple: '`defineProps` has been called multiple times.',
2626
notDefined: 'Props are not defined.',
2727
definedInBoth:
@@ -86,7 +86,7 @@ module.exports = {
8686
if (utils.withinTypeNode(node)) {
8787
continue
8888
}
89-
//`defineProps` are referencing locally declared variables.
89+
//`defineProps` is referencing locally declared variables.
9090
context.report({
9191
node,
9292
messageId: 'referencingLocally'

Diff for: tests/lib/rules/valid-define-emits.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ tester.run('valid-define-emits', rule, {
133133
`,
134134
errors: [
135135
{
136-
message: '`defineEmits` are referencing locally declared variables.',
136+
message: '`defineEmits` is referencing locally declared variables.',
137137
line: 5
138138
}
139139
]

0 commit comments

Comments
 (0)