Skip to content

Commit 783fcd6

Browse files
committed
Add support for defineModel
1 parent 26fc85e commit 783fcd6

17 files changed

+465
-30
lines changed

Diff for: docs/rules/define-macros-order.md

+8-5
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ This rule reports the `defineProps` and `defineEmits` compiler macros when they
2727
}
2828
```
2929

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

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

7070
</eslint-code-block>
7171

72-
### `{ "order": ["defineOptions", "defineProps", "defineEmits", "defineSlots"] }`
72+
### `{ "order": ["defineOptions", "defineModel", "defineProps", "defineEmits", "defineSlots"] }`
7373

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

7676
```vue
7777
<!-- ✓ GOOD -->
7878
<script setup>
7979
defineOptions({/* ... */})
80+
const model = defineModel()
8081
defineProps(/* ... */)
8182
defineEmits(/* ... */)
8283
const slots = defineSlots()
@@ -85,7 +86,7 @@ const slots = defineSlots()
8586

8687
</eslint-code-block>
8788

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

9091
```vue
9192
<!-- ✗ BAD -->
@@ -94,18 +95,20 @@ defineEmits(/* ... */)
9495
const slots = defineSlots()
9596
defineProps(/* ... */)
9697
defineOptions({/* ... */})
98+
const model = defineModel()
9799
</script>
98100
```
99101

100102
</eslint-code-block>
101103

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

104106
```vue
105107
<!-- ✗ BAD -->
106108
<script setup>
107109
const bar = ref()
108110
defineOptions({/* ... */})
111+
const model = defineModel()
109112
defineProps(/* ... */)
110113
defineEmits(/* ... */)
111114
const slots = defineSlots()

Diff for: docs/rules/no-unsupported-features.md

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ This rule reports unsupported Vue.js syntax on the specified version.
3030
- `ignores` ... You can use this `ignores` option to ignore the given features.
3131
The `"ignores"` option accepts an array of the following strings.
3232
- Vue.js 3.4.0+
33+
- `"define-model"` ... `defineModel()` macro.
3334
- `"v-bind-same-name-shorthand"` ... `v-bind` same-name shorthand.
3435
- Vue.js 3.3.0+
3536
- `"define-slots"` ... `defineSlots()` macro.

Diff for: lib/rules/define-macros-order.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@ const MACROS_EMITS = 'defineEmits'
1010
const MACROS_PROPS = 'defineProps'
1111
const MACROS_OPTIONS = 'defineOptions'
1212
const MACROS_SLOTS = 'defineSlots'
13-
const ORDER_SCHEMA = [MACROS_EMITS, MACROS_PROPS, MACROS_OPTIONS, MACROS_SLOTS]
13+
const MACROS_MODEL = 'defineModel'
14+
const ORDER_SCHEMA = [
15+
MACROS_EMITS,
16+
MACROS_PROPS,
17+
MACROS_OPTIONS,
18+
MACROS_SLOTS,
19+
MACROS_MODEL
20+
]
1421
const DEFAULT_ORDER = [MACROS_PROPS, MACROS_EMITS]
1522

1623
/**
@@ -116,6 +123,9 @@ function create(context) {
116123
onDefineSlotsExit(node) {
117124
macrosNodes.set(MACROS_SLOTS, getDefineMacrosStatement(node))
118125
},
126+
onDefineModelExit(node) {
127+
macrosNodes.set(MACROS_MODEL, getDefineMacrosStatement(node))
128+
},
119129
onDefineExposeExit(node) {
120130
defineExposeNode = getDefineMacrosStatement(node)
121131
}

Diff for: lib/rules/no-undef-properties.js

+20
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,26 @@ module.exports = {
321321
const propertyReferences =
322322
propertyReferenceExtractor.extractFromPattern(pattern)
323323
ctx.verifyReferences(propertyReferences)
324+
},
325+
onDefineModelEnter(node, model) {
326+
const ctx = getVueComponentContext(programNode)
327+
328+
ctx.defineProperties.set(model.name.modelName, {
329+
isProps: true
330+
})
331+
332+
if (
333+
!node.parent ||
334+
node.parent.type !== 'VariableDeclarator' ||
335+
node.parent.init !== node
336+
) {
337+
return
338+
}
339+
340+
const pattern = node.parent.id
341+
const propertyReferences =
342+
propertyReferenceExtractor.extractFromPattern(pattern)
343+
ctx.verifyReferences(propertyReferences)
324344
}
325345
}),
326346
utils.defineVueVisitor(context, {

Diff for: lib/rules/no-unsupported-features.js

+3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const FEATURES = {
3737
'define-options': require('./syntaxes/define-options'),
3838
'define-slots': require('./syntaxes/define-slots'),
3939
// Vue.js 3.4.0+
40+
'define-model': require('./syntaxes/define-model'),
4041
'v-bind-same-name-shorthand': require('./syntaxes/v-bind-same-name-shorthand')
4142
}
4243

@@ -128,6 +129,8 @@ module.exports = {
128129
forbiddenDefineSlots:
129130
'`defineSlots()` macros are not supported until Vue.js "3.3.0".',
130131
// Vue.js 3.4.0+
132+
forbiddenDefineModel:
133+
'`defineModel()` macros are not supported until Vue.js "3.4.0".',
131134
forbiddenVBindSameNameShorthand:
132135
'`v-bind` same-name shorthand is not supported until Vue.js "3.4.0".'
133136
}

Diff for: lib/rules/no-unused-emit-declarations.js

+11
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,17 @@ module.exports = {
332332
emitReferenceIds
333333
})
334334
},
335+
onDefineModelEnter(node, model) {
336+
if (
337+
node.parent &&
338+
node.parent.type === 'VariableDeclarator' &&
339+
node.parent.init === node
340+
) {
341+
// If store the return of defineModel() in a variable, we can mark the 'update:modelName' event as used if use that variable.
342+
// If that variable is unused it will already be reported by `no-unused-var` rule.
343+
emitCalls.set(`update:${model.name.modelName}`, node)
344+
}
345+
},
335346
...callVisitor
336347
}),
337348
{

Diff for: lib/rules/no-unused-properties.js

+33-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const {
3030
* @typedef {object} ComponentNonObjectPropertyData
3131
* @property {string} name
3232
* @property {GroupName} groupName
33-
* @property {'array' | 'type' | 'infer-type'} type
33+
* @property {'array' | 'type' | 'infer-type' | 'model' } type
3434
* @property {ASTNode} node
3535
*
3636
* @typedef { ComponentNonObjectPropertyData | ComponentObjectPropertyData } ComponentPropertyData
@@ -406,7 +406,9 @@ module.exports = {
406406
if (!groups.has('props')) {
407407
return
408408
}
409-
const container = getVueComponentPropertiesContainer(node)
409+
const container = getVueComponentPropertiesContainer(
410+
context.getSourceCode().ast
411+
)
410412

411413
for (const prop of props) {
412414
if (!prop.propName) {
@@ -452,6 +454,35 @@ module.exports = {
452454
const propertyReferences =
453455
propertyReferenceExtractor.extractFromPattern(pattern)
454456
container.propertyReferencesForProps.push(propertyReferences)
457+
},
458+
onDefineModelEnter(node, model) {
459+
if (!groups.has('props')) {
460+
return
461+
}
462+
const container = getVueComponentPropertiesContainer(
463+
context.getSourceCode().ast
464+
)
465+
if (
466+
node.parent &&
467+
node.parent.type === 'VariableDeclarator' &&
468+
node.parent.init === node
469+
) {
470+
// If store the return of defineModel() in a variable, we can mark the model prop as used if use that variable.
471+
// If that variable is unused it will already be reported by `no-unused-var` rule.
472+
container.propertyReferences.push(
473+
propertyReferenceExtractor.extractFromName(
474+
model.name.modelName,
475+
model.name.node || node
476+
)
477+
)
478+
return
479+
}
480+
container.properties.push({
481+
type: 'model',
482+
name: model.name.modelName,
483+
groupName: 'props',
484+
node: model.name.node || node
485+
})
455486
}
456487
}),
457488
utils.defineVueVisitor(context, {

Diff for: lib/rules/require-prop-types.js

+38-19
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,29 @@ module.exports = {
2626
},
2727
/** @param {RuleContext} context */
2828
create(context) {
29+
/**
30+
* @param {Expression} node
31+
* @returns {boolean|null}
32+
*/
33+
function optionHasType(node) {
34+
switch (node.type) {
35+
case 'ObjectExpression': {
36+
// foo: {
37+
return objectHasType(node)
38+
}
39+
case 'ArrayExpression': {
40+
// foo: [
41+
return node.elements.length > 0
42+
}
43+
case 'FunctionExpression':
44+
case 'ArrowFunctionExpression': {
45+
return false
46+
}
47+
}
48+
49+
// Unknown
50+
return null
51+
}
2952
/**
3053
* @param {ObjectExpression} node
3154
* @returns {boolean}
@@ -44,37 +67,20 @@ module.exports = {
4467
)
4568
return Boolean(typeProperty || validatorProperty)
4669
}
47-
4870
/**
4971
* @param {ComponentProp} prop
5072
*/
5173
function checkProperty(prop) {
5274
if (prop.type !== 'object' && prop.type !== 'array') {
5375
return
5476
}
55-
let hasType = true
77+
let hasType
5678

5779
if (prop.type === 'array') {
5880
hasType = false
5981
} else {
6082
const { value } = prop
61-
switch (value.type) {
62-
case 'ObjectExpression': {
63-
// foo: {
64-
hasType = objectHasType(value)
65-
break
66-
}
67-
case 'ArrayExpression': {
68-
// foo: [
69-
hasType = value.elements.length > 0
70-
break
71-
}
72-
case 'FunctionExpression':
73-
case 'ArrowFunctionExpression': {
74-
hasType = false
75-
break
76-
}
77-
}
83+
hasType = optionHasType(value) ?? true
7884
}
7985

8086
if (!hasType) {
@@ -99,6 +105,19 @@ module.exports = {
99105
for (const prop of props) {
100106
checkProperty(prop)
101107
}
108+
},
109+
onDefineModelEnter(node, model) {
110+
if (model.typeNode) return
111+
if (model.options && (optionHasType(model.options) ?? true)) {
112+
return
113+
}
114+
context.report({
115+
node: model.options || node,
116+
messageId: 'requireType',
117+
data: {
118+
name: model.name.modelName
119+
}
120+
})
102121
}
103122
}),
104123
utils.executeOnVue(context, (obj) => {

Diff for: lib/rules/syntaxes/define-model.js

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* @author Yosuke Ota
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const utils = require('../../utils/index')
8+
9+
module.exports = {
10+
supported: '>=3.4.0',
11+
/** @param {RuleContext} context @returns {RuleListener} */
12+
createScriptVisitor(context) {
13+
return utils.defineScriptSetupVisitor(context, {
14+
onDefineModelEnter(node) {
15+
context.report({
16+
node,
17+
messageId: 'forbiddenDefineModel'
18+
})
19+
}
20+
})
21+
}
22+
}

0 commit comments

Comments
 (0)