Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add vue/valid-define-options rule #2165

Merged
merged 4 commits into from
May 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/rules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: |

</rules-table>

Expand Down
3 changes: 2 additions & 1 deletion docs/rules/valid-define-emits.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}`.
Expand Down Expand Up @@ -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
Expand Down
119 changes: 119 additions & 0 deletions docs/rules/valid-define-options.md
Original file line number Diff line number Diff line change
@@ -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: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>

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.

<eslint-code-block :rules="{'vue/valid-define-options': ['error']}">

```vue
<script setup>
/* ✓ GOOD */
defineOptions({ name: 'foo' })
</script>
```

</eslint-code-block>

<eslint-code-block :rules="{'vue/valid-define-options': ['error']}">

```vue
<script>
const def = { name: 'foo' }
</script>
<script setup>
/* ✓ GOOD */
defineOptions(def)
</script>
```

</eslint-code-block>

<eslint-code-block :rules="{'vue/valid-define-options': ['error']}">

```vue
<script setup>
/* ✗ BAD */
const def = { name: 'foo' }
defineOptions(def)
</script>
```

</eslint-code-block>

<eslint-code-block :rules="{'vue/valid-define-options': ['error']}">

```vue
<script setup>
/* ✗ BAD */
defineOptions({ name: 'foo' })
defineOptions({ inheritAttrs: false })
</script>
```

</eslint-code-block>

<eslint-code-block :rules="{'vue/valid-define-options': ['error']}">

```vue
<script setup>
/* ✗ BAD */
defineOptions()
</script>
```

</eslint-code-block>

<eslint-code-block :rules="{'vue/valid-define-options': ['error']}">

```vue
<script setup lang="ts">
/* ✗ BAD */
defineOptions<{ name: 'Foo' }>()
</script>
```

</eslint-code-block>

<eslint-code-block :rules="{'vue/valid-define-options': ['error']}">

```vue
<script setup>
/* ✗ BAD */
defineOptions({ props: { msg: String } })
</script>
```

</eslint-code-block>

## :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)
3 changes: 2 additions & 1 deletion docs/rules/valid-define-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}`.
Expand Down Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
4 changes: 2 additions & 2 deletions lib/rules/valid-define-emits.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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'
Expand Down
127 changes: 127 additions & 0 deletions lib/rules/valid-define-options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* @author Yosuke Ota <https://github.com/ota-meshi>
* 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<Expression | SpreadElement>} */
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'
})
}
}
}
}
)
}
}
4 changes: 2 additions & 2 deletions lib/rules/valid-define-props.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion tests/lib/rules/valid-define-emits.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
]
Expand Down
Loading