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 support for defineModel #2360

Merged
merged 4 commits into from
Jan 10, 2024
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
13 changes: 8 additions & 5 deletions docs/rules/define-macros-order.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ This rule reports the `defineProps` and `defineEmits` compiler macros when they
}
```

- `order` (`string[]`) ... The order of defineEmits and defineProps macros. You can also add `"defineOptions"` and `"defineSlots"`.
- `order` (`string[]`) ... The order of defineEmits and defineProps macros. You can also add `"defineOptions"`, `"defineSlots"`, and `"defineModel"`.
- `defineExposeLast` (`boolean`) ... Force `defineExpose` at the end.

### `{ "order": ["defineProps", "defineEmits"] }` (default)
Expand Down Expand Up @@ -69,14 +69,15 @@ defineEmits(/* ... */)

</eslint-code-block>

### `{ "order": ["defineOptions", "defineProps", "defineEmits", "defineSlots"] }`
### `{ "order": ["defineOptions", "defineModel", "defineProps", "defineEmits", "defineSlots"] }`

<eslint-code-block fix :rules="{'vue/define-macros-order': ['error', {order: ['defineOptions', 'defineProps', 'defineEmits', 'defineSlots']}]}">
<eslint-code-block fix :rules="{'vue/define-macros-order': ['error', {order: ['defineOptions', 'defineModel', 'defineProps', 'defineEmits', 'defineSlots']}]}">

```vue
<!-- ✓ GOOD -->
<script setup>
defineOptions({/* ... */})
const model = defineModel()
defineProps(/* ... */)
defineEmits(/* ... */)
const slots = defineSlots()
Expand All @@ -85,7 +86,7 @@ const slots = defineSlots()

</eslint-code-block>

<eslint-code-block fix :rules="{'vue/define-macros-order': ['error', {order: ['defineOptions', 'defineProps', 'defineEmits', 'defineSlots']}]}">
<eslint-code-block fix :rules="{'vue/define-macros-order': ['error', {order: ['defineOptions', 'defineModel', 'defineProps', 'defineEmits', 'defineSlots']}]}">

```vue
<!-- ✗ BAD -->
Expand All @@ -94,18 +95,20 @@ defineEmits(/* ... */)
const slots = defineSlots()
defineProps(/* ... */)
defineOptions({/* ... */})
const model = defineModel()
</script>
```

</eslint-code-block>

<eslint-code-block fix :rules="{'vue/define-macros-order': ['error', {order: ['defineOptions', 'defineProps', 'defineEmits', 'defineSlots']}]}">
<eslint-code-block fix :rules="{'vue/define-macros-order': ['error', {order: ['defineOptions', 'defineModel', 'defineProps', 'defineEmits', 'defineSlots']}]}">

```vue
<!-- ✗ BAD -->
<script setup>
const bar = ref()
defineOptions({/* ... */})
const model = defineModel()
defineProps(/* ... */)
defineEmits(/* ... */)
const slots = defineSlots()
Expand Down
1 change: 1 addition & 0 deletions docs/rules/no-unsupported-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ This rule reports unsupported Vue.js syntax on the specified version.
- `ignores` ... You can use this `ignores` option to ignore the given features.
The `"ignores"` option accepts an array of the following strings.
- Vue.js 3.4.0+
- `"define-model"` ... `defineModel()` macro.
- `"v-bind-same-name-shorthand"` ... `v-bind` same-name shorthand.
- Vue.js 3.3.0+
- `"define-slots"` ... `defineSlots()` macro.
Expand Down
12 changes: 11 additions & 1 deletion lib/rules/define-macros-order.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ const MACROS_EMITS = 'defineEmits'
const MACROS_PROPS = 'defineProps'
const MACROS_OPTIONS = 'defineOptions'
const MACROS_SLOTS = 'defineSlots'
const ORDER_SCHEMA = [MACROS_EMITS, MACROS_PROPS, MACROS_OPTIONS, MACROS_SLOTS]
const MACROS_MODEL = 'defineModel'
const ORDER_SCHEMA = [
MACROS_EMITS,
MACROS_PROPS,
MACROS_OPTIONS,
MACROS_SLOTS,
MACROS_MODEL
]
const DEFAULT_ORDER = [MACROS_PROPS, MACROS_EMITS]

/**
Expand Down Expand Up @@ -116,6 +123,9 @@ function create(context) {
onDefineSlotsExit(node) {
macrosNodes.set(MACROS_SLOTS, getDefineMacrosStatement(node))
},
onDefineModelExit(node) {
macrosNodes.set(MACROS_MODEL, getDefineMacrosStatement(node))
},
onDefineExposeExit(node) {
defineExposeNode = getDefineMacrosStatement(node)
}
Expand Down
20 changes: 20 additions & 0 deletions lib/rules/no-undef-properties.js
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,26 @@ module.exports = {
const propertyReferences =
propertyReferenceExtractor.extractFromPattern(pattern)
ctx.verifyReferences(propertyReferences)
},
onDefineModelEnter(node, model) {
const ctx = getVueComponentContext(programNode)

ctx.defineProperties.set(model.name.modelName, {
isProps: true
})

if (
!node.parent ||
node.parent.type !== 'VariableDeclarator' ||
node.parent.init !== node
) {
return
}

const pattern = node.parent.id
const propertyReferences =
propertyReferenceExtractor.extractFromPattern(pattern)
ctx.verifyReferences(propertyReferences)
}
}),
utils.defineVueVisitor(context, {
Expand Down
3 changes: 3 additions & 0 deletions lib/rules/no-unsupported-features.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const FEATURES = {
'define-options': require('./syntaxes/define-options'),
'define-slots': require('./syntaxes/define-slots'),
// Vue.js 3.4.0+
'define-model': require('./syntaxes/define-model'),
'v-bind-same-name-shorthand': require('./syntaxes/v-bind-same-name-shorthand')
}

Expand Down Expand Up @@ -128,6 +129,8 @@ module.exports = {
forbiddenDefineSlots:
'`defineSlots()` macros are not supported until Vue.js "3.3.0".',
// Vue.js 3.4.0+
forbiddenDefineModel:
'`defineModel()` macros are not supported until Vue.js "3.4.0".',
forbiddenVBindSameNameShorthand:
'`v-bind` same-name shorthand is not supported until Vue.js "3.4.0".'
}
Expand Down
11 changes: 11 additions & 0 deletions lib/rules/no-unused-emit-declarations.js
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,17 @@ module.exports = {
emitReferenceIds
})
},
onDefineModelEnter(node, model) {
if (
node.parent &&
node.parent.type === 'VariableDeclarator' &&
node.parent.init === node
) {
// If the return value of defineModel() is stored in a variable, we can mark the 'update:modelName' event as used if that that variable is used.
// If that variable is unused, it will already be reported by `no-unused-var` rule.
emitCalls.set(`update:${model.name.modelName}`, node)
}
},
...callVisitor
}),
{
Expand Down
35 changes: 33 additions & 2 deletions lib/rules/no-unused-properties.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const {
* @typedef {object} ComponentNonObjectPropertyData
* @property {string} name
* @property {GroupName} groupName
* @property {'array' | 'type' | 'infer-type'} type
* @property {'array' | 'type' | 'infer-type' | 'model' } type
* @property {ASTNode} node
*
* @typedef { ComponentNonObjectPropertyData | ComponentObjectPropertyData } ComponentPropertyData
Expand Down Expand Up @@ -406,7 +406,9 @@ module.exports = {
if (!groups.has('props')) {
return
}
const container = getVueComponentPropertiesContainer(node)
const container = getVueComponentPropertiesContainer(
context.getSourceCode().ast
)

for (const prop of props) {
if (!prop.propName) {
Expand Down Expand Up @@ -452,6 +454,35 @@ module.exports = {
const propertyReferences =
propertyReferenceExtractor.extractFromPattern(pattern)
container.propertyReferencesForProps.push(propertyReferences)
},
onDefineModelEnter(node, model) {
if (!groups.has('props')) {
return
}
const container = getVueComponentPropertiesContainer(
context.getSourceCode().ast
)
if (
node.parent &&
node.parent.type === 'VariableDeclarator' &&
node.parent.init === node
) {
// If the return value of defineModel() is stored in a variable, we can mark the model prop as used if that that variable is used.
// If that variable is unused, it will already be reported by `no-unused-var` rule.
container.propertyReferences.push(
propertyReferenceExtractor.extractFromName(
model.name.modelName,
model.name.node || node
)
)
return
}
container.properties.push({
type: 'model',
name: model.name.modelName,
groupName: 'props',
node: model.name.node || node
})
}
}),
utils.defineVueVisitor(context, {
Expand Down
63 changes: 38 additions & 25 deletions lib/rules/require-prop-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,29 @@ module.exports = {
},
/** @param {RuleContext} context */
create(context) {
/**
* @param {Expression} node
* @returns {boolean|null}
*/
function optionHasType(node) {
switch (node.type) {
case 'ObjectExpression': {
// foo: {
return objectHasType(node)
}
case 'ArrayExpression': {
// foo: [
return node.elements.length > 0
}
case 'FunctionExpression':
case 'ArrowFunctionExpression': {
return false
}
}

// Unknown
return null
}
/**
* @param {ObjectExpression} node
* @returns {boolean}
Expand All @@ -44,38 +67,15 @@ module.exports = {
)
return Boolean(typeProperty || validatorProperty)
}

/**
* @param {ComponentProp} prop
*/
function checkProperty(prop) {
if (prop.type !== 'object' && prop.type !== 'array') {
return
}
let hasType = true

if (prop.type === 'array') {
hasType = false
} else {
const { value } = prop
switch (value.type) {
case 'ObjectExpression': {
// foo: {
hasType = objectHasType(value)
break
}
case 'ArrayExpression': {
// foo: [
hasType = value.elements.length > 0
break
}
case 'FunctionExpression':
case 'ArrowFunctionExpression': {
hasType = false
break
}
}
}
const hasType =
prop.type === 'array' ? false : optionHasType(prop.value) ?? true

if (!hasType) {
const { node, propName } = prop
Expand All @@ -99,6 +99,19 @@ module.exports = {
for (const prop of props) {
checkProperty(prop)
}
},
onDefineModelEnter(node, model) {
if (model.typeNode) return
if (model.options && (optionHasType(model.options) ?? true)) {
return
}
context.report({
node: model.options || node,
messageId: 'requireType',
data: {
name: model.name.modelName
}
})
}
}),
utils.executeOnVue(context, (obj) => {
Expand Down
22 changes: 22 additions & 0 deletions lib/rules/syntaxes/define-model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'

const utils = require('../../utils/index')

module.exports = {
supported: '>=3.4.0',
/** @param {RuleContext} context @returns {RuleListener} */
createScriptVisitor(context) {
return utils.defineScriptSetupVisitor(context, {
onDefineModelEnter(node) {
context.report({
node,
messageId: 'forbiddenDefineModel'
})
}
})
}
}
Loading
Loading