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-props rule #1560

Merged
merged 2 commits into from Jul 6, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -331,6 +331,7 @@ For example:
| [vue/v-for-delimiter-style](./v-for-delimiter-style.md) | enforce `v-for` directive's delimiter style | :wrench: |
| [vue/v-on-event-hyphenation](./v-on-event-hyphenation.md) | enforce v-on event naming style on custom components in template | :wrench: |
| [vue/v-on-function-call](./v-on-function-call.md) | enforce or forbid parentheses after method calls without arguments in `v-on` directives | :wrench: |
| [vue/valid-define-props](./valid-define-props.md) | enforce valid `defineProps` compiler macro | |
| [vue/valid-next-tick](./valid-next-tick.md) | enforce valid `nextTick` function calls | :wrench: |

### Extension Rules
Expand Down
133 changes: 133 additions & 0 deletions docs/rules/valid-define-props.md
@@ -0,0 +1,133 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/valid-define-props
description: enforce valid `defineProps` compiler macro
---
# vue/valid-define-props

> enforce valid `defineProps` 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 `defineProps` compiler macro is valid.

## :book: Rule Details

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

- `defineProps` are 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 {}`.
- Props are not defined in either `defineProps` or `export default {}`.

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

```vue
<script setup>
/* ✓ GOOD */
defineProps({ msg: String })
</script>
```

</eslint-code-block>

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

```vue
<script setup>
/* ✓ GOOD */
defineProps(['msg'])
</script>
```

</eslint-code-block>

```vue
<script setup lang="ts">
/* ✓ GOOD */
defineProps<{ msg?:string }>()
</script>
```

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

```vue
<script>
const def = { msg: String }
</script>
<script setup>
/* ✓ GOOD */
defineProps(def)
</script>
```

</eslint-code-block>

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

```vue
<script setup>
/* ✗ BAD */
const def = { msg: String }
defineProps(def)
</script>
```

</eslint-code-block>

```vue
<script setup lang="ts">
/* ✗ BAD */
defineProps<{ msg?:string }>({ msg: String })
</script>
```

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

```vue
<script setup>
/* ✗ BAD */
defineProps({ msg: String })
defineProps({ count: Number })
</script>
```

</eslint-code-block>

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

```vue
<script>
export default {
props: { msg: String }
}
</script>
<script setup>
/* ✗ BAD */
defineProps({ count: Number })
</script>
```

</eslint-code-block>

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

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

</eslint-code-block>

## :wrench: Options

Nothing.

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/valid-define-props.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/valid-define-props.js)
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -171,6 +171,7 @@ module.exports = {
'v-on-function-call': require('./rules/v-on-function-call'),
'v-on-style': require('./rules/v-on-style'),
'v-slot-style': require('./rules/v-slot-style'),
'valid-define-props': require('./rules/valid-define-props'),
'valid-next-tick': require('./rules/valid-next-tick'),
'valid-template-root': require('./rules/valid-template-root'),
'valid-v-bind-sync': require('./rules/valid-v-bind-sync'),
Expand Down
145 changes: 145 additions & 0 deletions lib/rules/valid-define-props.js
@@ -0,0 +1,145 @@
/**
* @author Yosuke Ota <https://github.com/ota-meshi>
* See LICENSE file in root directory for full license.
*/
'use strict'

const { findVariable } = require('eslint-utils')
const utils = require('../utils')

module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce valid `defineProps` compiler macro',
// TODO Switch in the major version.
// categories: ['vue3-essential'],
categories: undefined,
url: 'https://eslint.vuejs.org/rules/valid-define-props.html'
},
fixable: null,
schema: [],
messages: {
hasTypeAndArg:
'`defineProps` has both a type-only props and an argument.',
referencingLocally:
'`defineProps` are referencing locally declared variables.',
multiple: '`defineProps` has been called multiple times.',
notDefined: 'Props are not defined.',
definedInBoth:
'Props are defined in both `defineProps` and `export default {}`.'
}
},
/** @param {RuleContext} context */
create(context) {
const scriptSetup = utils.getScriptSetupElement(context)
if (!scriptSetup) {
return {}
}

/** @type {Set<Expression | SpreadElement>} */
const propsDefExpressions = new Set()
let hasDefaultExport = false
/** @type {CallExpression[]} */
const definePropsNodes = []
/** @type {CallExpression | null} */
let emptyDefineProps = null

return utils.compositingVisitors(
utils.defineScriptSetupVisitor(context, {
onDefinePropsEnter(node) {
definePropsNodes.push(node)

if (node.arguments.length >= 1) {
if (node.typeParameters && node.typeParameters.params.length >= 1) {
// `defineProps` has both a literal type and an argument.
context.report({
node,
messageId: 'hasTypeAndArg'
})
return
}

propsDefExpressions.add(node.arguments[0])
} else {
if (
!node.typeParameters ||
node.typeParameters.params.length === 0
) {
emptyDefineProps = node
}
}
},
Identifier(node) {
for (const def of propsDefExpressions) {
if (utils.inRange(def.range, node)) {
const variable = findVariable(context.getScope(), node)
if (
variable &&
variable.references.some((ref) => ref.identifier === node)
) {
if (
variable.defs.length &&
variable.defs.every((def) =>
utils.inRange(scriptSetup.range, def.name)
)
) {
//`defineProps` are referencing locally declared variables.
context.report({
node,
messageId: 'referencingLocally'
})
}
}
}
}
}
}),
utils.defineVueVisitor(context, {
onVueObjectEnter(node, { type }) {
if (type !== 'export' || utils.inRange(scriptSetup.range, node)) {
return
}

hasDefaultExport = Boolean(utils.findProperty(node, 'props'))
}
}),
{
'Program:exit'() {
if (!definePropsNodes.length) {
return
}
if (definePropsNodes.length > 1) {
// `defineProps` has been called multiple times.
for (const node of definePropsNodes) {
context.report({
node,
messageId: 'multiple'
})
}
return
}
if (emptyDefineProps) {
if (!hasDefaultExport) {
// Props are not defined.
context.report({
node: emptyDefineProps,
messageId: 'notDefined'
})
}
} else {
if (hasDefaultExport) {
// Props are defined in both `defineProps` and `export default {}`.
for (const node of definePropsNodes) {
context.report({
node,
messageId: 'definedInBoth'
})
}
}
}
}
}
)
}
}
50 changes: 39 additions & 11 deletions lib/utils/index.js
Expand Up @@ -1058,16 +1058,26 @@ module.exports = {
const hasEmitsEvent =
visitor.onDefineEmitsEnter || visitor.onDefineEmitsExit
if (hasPropsEvent || hasEmitsEvent) {
/** @type {ESNode | null} */
let nested = null
scriptSetupVisitor[':function, BlockStatement'] = (node) => {
if (!nested) {
nested = node
/** @type {Expression | null} */
let candidateMacro = null
/** @param {VariableDeclarator|ExpressionStatement} node */
scriptSetupVisitor[
'Program > VariableDeclaration > VariableDeclarator, Program > ExpressionStatement'
] = (node) => {
if (!candidateMacro) {
candidateMacro =
node.type === 'VariableDeclarator' ? node.init : node.expression
}
}
scriptSetupVisitor[':function, BlockStatement:exit'] = (node) => {
if (nested === node) {
nested = null
/** @param {VariableDeclarator|ExpressionStatement} node */
scriptSetupVisitor[
'Program > VariableDeclaration > VariableDeclarator, Program > ExpressionStatement:exit'
] = (node) => {
if (
candidateMacro ===
(node.type === 'VariableDeclarator' ? node.init : node.expression)
) {
candidateMacro = null
}
}
const definePropsMap = new Map()
Expand All @@ -1077,11 +1087,16 @@ module.exports = {
*/
scriptSetupVisitor.CallExpression = (node) => {
if (
!nested &&
candidateMacro &&
inScriptSetup(node) &&
node.callee.type === 'Identifier'
) {
if (hasPropsEvent && node.callee.name === 'defineProps') {
if (
hasPropsEvent &&
(candidateMacro === node ||
candidateMacro === getWithDefaults(node)) &&
node.callee.name === 'defineProps'
) {
/** @type {(ComponentArrayProp | ComponentObjectProp | ComponentTypeProp)[]} */
let props = []
if (node.arguments.length >= 1) {
Expand All @@ -1100,7 +1115,11 @@ module.exports = {
}
callVisitor('onDefinePropsEnter', node, props)
definePropsMap.set(node, props)
} else if (hasEmitsEvent && node.callee.name === 'defineEmits') {
} else if (
hasEmitsEvent &&
candidateMacro === node &&
node.callee.name === 'defineEmits'
) {
/** @type {(ComponentArrayEmit | ComponentObjectEmit | ComponentTypeEmit)[]} */
let emits = []
if (node.arguments.length >= 1) {
Expand Down Expand Up @@ -2395,6 +2414,15 @@ function hasWithDefaults(node) {
)
}

/**
* Get the withDefaults call node from given defineProps call node.
* @param {CallExpression} node The node of defineProps
* @returns {CallExpression | null}
*/
function getWithDefaults(node) {
return hasWithDefaults(node) ? node.parent : null
}

/**
* Gets a map of the property nodes defined in withDefaults.
* @param {CallExpression} node The node of defineProps
Expand Down