diff --git a/docs/rules/index.md b/docs/rules/index.md
index abc3419a4..2580ecaeb 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -268,6 +268,7 @@ For example:
| [vue/static-class-names-order](./static-class-names-order.md) | enforce static class names order | :wrench: | :hammer: |
| [vue/v-for-delimiter-style](./v-for-delimiter-style.md) | enforce `v-for` directive's delimiter style | :wrench: | :lipstick: |
| [vue/v-on-handler-style](./v-on-handler-style.md) | enforce writing style for handlers in `v-on` directives | :wrench: | :hammer: |
+| [vue/valid-define-options](./valid-define-options.md) | enforce valid `defineOptions` compiler macro | | :warning: |
diff --git a/docs/rules/valid-define-emits.md b/docs/rules/valid-define-emits.md
index f7de8454c..0f52775ff 100644
--- a/docs/rules/valid-define-emits.md
+++ b/docs/rules/valid-define-emits.md
@@ -17,7 +17,7 @@ This rule checks whether `defineEmits` compiler macro is valid.
This rule reports `defineEmits` compiler macros in the following cases:
-- `defineEmits` are referencing locally declared variables.
+- `defineEmits` is referencing locally declared variables.
- `defineEmits` has both a literal type and an argument. e.g. `defineEmits<(e: 'foo')=>void>(['bar'])`
- `defineEmits` has been called multiple times.
- Custom events are defined in both `defineEmits` and `export default {}`.
@@ -139,6 +139,7 @@ Nothing.
## :couple: Related Rules
- [vue/define-emits-declaration](./define-emits-declaration.md)
+- [vue/valid-define-options](./valid-define-options.md)
- [vue/valid-define-props](./valid-define-props.md)
## :rocket: Version
diff --git a/docs/rules/valid-define-options.md b/docs/rules/valid-define-options.md
new file mode 100644
index 000000000..2cba98137
--- /dev/null
+++ b/docs/rules/valid-define-options.md
@@ -0,0 +1,119 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/valid-define-options
+description: enforce valid `defineOptions` compiler macro
+---
+# vue/valid-define-options
+
+> enforce valid `defineOptions` compiler macro
+
+- :exclamation: ***This rule has not been released yet.***
+
+This rule checks whether `defineOptions` compiler macro is valid.
+
+## :book: Rule Details
+
+This rule reports `defineOptions` compiler macros in the following cases:
+
+- `defineOptions` is referencing locally declared variables.
+- `defineOptions` has been called multiple times.
+- Options are not defined in `defineOptions`.
+- `defineOptions` has type arguments.
+- `defineOptions` has `props`, `emits`, `expose` or `slots` options.
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+## :wrench: Options
+
+Nothing.
+
+## :couple: Related Rules
+
+- [vue/valid-define-emits](./valid-define-emits.md)
+- [vue/valid-define-props](./valid-define-props.md)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/valid-define-options.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/valid-define-options.js)
diff --git a/docs/rules/valid-define-props.md b/docs/rules/valid-define-props.md
index 4345d2428..fc0f707fb 100644
--- a/docs/rules/valid-define-props.md
+++ b/docs/rules/valid-define-props.md
@@ -17,7 +17,7 @@ This rule checks whether `defineProps` compiler macro is valid.
This rule reports `defineProps` compiler macros in the following cases:
-- `defineProps` are referencing locally declared variables.
+- `defineProps` is referencing locally declared variables.
- `defineProps` has both a literal type and an argument. e.g. `defineProps<{/*props*/}>({/*props*/})`
- `defineProps` has been called multiple times.
- Props are defined in both `defineProps` and `export default {}`.
@@ -140,6 +140,7 @@ Nothing.
- [vue/define-props-declaration](./define-props-declaration.md)
- [vue/valid-define-emits](./valid-define-emits.md)
+- [vue/valid-define-options](./valid-define-options.md)
## :rocket: Version
diff --git a/lib/index.js b/lib/index.js
index c7b580868..f8230212e 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -210,6 +210,7 @@ module.exports = {
'v-slot-style': require('./rules/v-slot-style'),
'valid-attribute-name': require('./rules/valid-attribute-name'),
'valid-define-emits': require('./rules/valid-define-emits'),
+ 'valid-define-options': require('./rules/valid-define-options'),
'valid-define-props': require('./rules/valid-define-props'),
'valid-model-definition': require('./rules/valid-model-definition'),
'valid-next-tick': require('./rules/valid-next-tick'),
diff --git a/lib/rules/valid-define-emits.js b/lib/rules/valid-define-emits.js
index 239da270d..64d3fd15a 100644
--- a/lib/rules/valid-define-emits.js
+++ b/lib/rules/valid-define-emits.js
@@ -20,7 +20,7 @@ module.exports = {
messages: {
hasTypeAndArg: '`defineEmits` has both a type-only emit and an argument.',
referencingLocally:
- '`defineEmits` are referencing locally declared variables.',
+ '`defineEmits` is referencing locally declared variables.',
multiple: '`defineEmits` has been called multiple times.',
notDefined: 'Custom events are not defined.',
definedInBoth:
@@ -85,7 +85,7 @@ module.exports = {
if (utils.withinTypeNode(node)) {
continue
}
- //`defineEmits` are referencing locally declared variables.
+ //`defineEmits` is referencing locally declared variables.
context.report({
node,
messageId: 'referencingLocally'
diff --git a/lib/rules/valid-define-options.js b/lib/rules/valid-define-options.js
new file mode 100644
index 000000000..784ac3fbd
--- /dev/null
+++ b/lib/rules/valid-define-options.js
@@ -0,0 +1,127 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const { findVariable } = require('@eslint-community/eslint-utils')
+const utils = require('../utils')
+
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'enforce valid `defineOptions` compiler macro',
+ // TODO Switch in the next major version
+ // categories: ['vue3-essential', 'essential'],
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/valid-define-options.html'
+ },
+ fixable: null,
+ schema: [],
+ messages: {
+ referencingLocally:
+ '`defineOptions` is referencing locally declared variables.',
+ multiple: '`defineOptions` has been called multiple times.',
+ notDefined: 'Options are not defined.',
+ disallowProp:
+ '`defineOptions()` cannot be used to declare `{{propName}}`. Use `{{insteadMacro}}()` instead.',
+ typeArgs: '`defineOptions()` cannot accept type arguments.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ const scriptSetup = utils.getScriptSetupElement(context)
+ if (!scriptSetup) {
+ return {}
+ }
+
+ /** @type {Set} */
+ const optionsDefExpressions = new Set()
+ /** @type {CallExpression[]} */
+ const defineOptionsNodes = []
+
+ return utils.compositingVisitors(
+ utils.defineScriptSetupVisitor(context, {
+ onDefineOptionsEnter(node) {
+ defineOptionsNodes.push(node)
+
+ if (node.arguments.length > 0) {
+ const define = node.arguments[0]
+ if (define.type === 'ObjectExpression') {
+ for (const [propName, insteadMacro] of [
+ ['props', 'defineProps'],
+ ['emits', 'defineEmits'],
+ ['expose', 'defineExpose'],
+ ['slots', 'defineSlots']
+ ]) {
+ const prop = utils.findProperty(define, propName)
+ if (prop) {
+ context.report({
+ node,
+ messageId: 'disallowProp',
+ data: { propName, insteadMacro }
+ })
+ }
+ }
+ }
+
+ optionsDefExpressions.add(node.arguments[0])
+ } else {
+ context.report({
+ node,
+ messageId: 'notDefined'
+ })
+ }
+
+ if (node.typeParameters) {
+ context.report({
+ node: node.typeParameters,
+ messageId: 'typeArgs'
+ })
+ }
+ },
+ Identifier(node) {
+ for (const defineOptions of optionsDefExpressions) {
+ if (utils.inRange(defineOptions.range, node)) {
+ const variable = findVariable(context.getScope(), node)
+ if (
+ variable &&
+ variable.references.some((ref) => ref.identifier === node) &&
+ variable.defs.length > 0 &&
+ variable.defs.every(
+ (def) =>
+ def.type !== 'ImportBinding' &&
+ utils.inRange(scriptSetup.range, def.name) &&
+ !utils.inRange(defineOptions.range, def.name)
+ )
+ ) {
+ if (utils.withinTypeNode(node)) {
+ continue
+ }
+ //`defineOptions` is referencing locally declared variables.
+ context.report({
+ node,
+ messageId: 'referencingLocally'
+ })
+ }
+ }
+ }
+ }
+ }),
+ {
+ 'Program:exit'() {
+ if (defineOptionsNodes.length > 1) {
+ // `defineOptions` has been called multiple times.
+ for (const node of defineOptionsNodes) {
+ context.report({
+ node,
+ messageId: 'multiple'
+ })
+ }
+ }
+ }
+ }
+ )
+ }
+}
diff --git a/lib/rules/valid-define-props.js b/lib/rules/valid-define-props.js
index fe471dc15..43cce1d3c 100644
--- a/lib/rules/valid-define-props.js
+++ b/lib/rules/valid-define-props.js
@@ -21,7 +21,7 @@ module.exports = {
hasTypeAndArg:
'`defineProps` has both a type-only props and an argument.',
referencingLocally:
- '`defineProps` are referencing locally declared variables.',
+ '`defineProps` is referencing locally declared variables.',
multiple: '`defineProps` has been called multiple times.',
notDefined: 'Props are not defined.',
definedInBoth:
@@ -86,7 +86,7 @@ module.exports = {
if (utils.withinTypeNode(node)) {
continue
}
- //`defineProps` are referencing locally declared variables.
+ //`defineProps` is referencing locally declared variables.
context.report({
node,
messageId: 'referencingLocally'
diff --git a/tests/lib/rules/valid-define-emits.js b/tests/lib/rules/valid-define-emits.js
index 4dc70ca57..50c9047de 100644
--- a/tests/lib/rules/valid-define-emits.js
+++ b/tests/lib/rules/valid-define-emits.js
@@ -133,7 +133,7 @@ tester.run('valid-define-emits', rule, {
`,
errors: [
{
- message: '`defineEmits` are referencing locally declared variables.',
+ message: '`defineEmits` is referencing locally declared variables.',
line: 5
}
]
diff --git a/tests/lib/rules/valid-define-options.js b/tests/lib/rules/valid-define-options.js
new file mode 100644
index 000000000..58d31df68
--- /dev/null
+++ b/tests/lib/rules/valid-define-options.js
@@ -0,0 +1,201 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/valid-define-options')
+
+const tester = new RuleTester({
+ parser: require.resolve('vue-eslint-parser'),
+ parserOptions: { ecmaVersion: 2015, sourceType: 'module' }
+})
+
+tester.run('valid-define-options', rule, {
+ valid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ },
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ `
+ }
+ ],
+ invalid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ message: '`defineOptions` is referencing locally declared variables.',
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ message: '`defineOptions` has been called multiple times.',
+ line: 3
+ },
+ {
+ message: '`defineOptions` has been called multiple times.',
+ line: 4
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ message: 'Options are not defined.',
+ line: 3
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ parserOptions: { parser: require.resolve('@typescript-eslint/parser') },
+ errors: [
+ {
+ message: 'Options are not defined.',
+ line: 3
+ },
+ {
+ message: '`defineOptions()` cannot accept type arguments.',
+ line: 3
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ message:
+ '`defineOptions()` cannot be used to declare `props`. Use `defineProps()` instead.',
+ line: 3
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ message:
+ '`defineOptions()` cannot be used to declare `emits`. Use `defineEmits()` instead.',
+ line: 3
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ message:
+ '`defineOptions()` cannot be used to declare `expose`. Use `defineExpose()` instead.',
+ line: 3
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ message:
+ '`defineOptions()` cannot be used to declare `slots`. Use `defineSlots()` instead.',
+ line: 3
+ }
+ ]
+ }
+ ]
+})
diff --git a/tests/lib/rules/valid-define-props.js b/tests/lib/rules/valid-define-props.js
index a4c0d3ca2..511e46bbc 100644
--- a/tests/lib/rules/valid-define-props.js
+++ b/tests/lib/rules/valid-define-props.js
@@ -136,7 +136,7 @@ tester.run('valid-define-props', rule, {
`,
errors: [
{
- message: '`defineProps` are referencing locally declared variables.',
+ message: '`defineProps` is referencing locally declared variables.',
line: 5
}
]