Skip to content

Commit 27f9627

Browse files
authored
fix(language-core): validate v-model variable against model type (#5214)
1 parent 6eebff5 commit 27f9627

File tree

6 files changed

+80
-36
lines changed

6 files changed

+80
-36
lines changed

packages/language-core/lib/codegen/template/elementEvents.ts

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,17 @@ export function* generateElementEvents(
2020
let emitVar: string | undefined;
2121
let eventsVar: string | undefined;
2222
let propsVar: string | undefined;
23+
2324
for (const prop of node.props) {
2425
if (
2526
prop.type === CompilerDOM.NodeTypes.DIRECTIVE
26-
&& prop.name === 'on'
27-
&& prop.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION
28-
&& !prop.arg.loc.source.startsWith('[')
29-
&& !prop.arg.loc.source.endsWith(']')
27+
&& (
28+
prop.name === 'model'
29+
|| prop.name === 'on'
30+
&& prop.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION
31+
&& !prop.arg.loc.source.startsWith('[')
32+
&& !prop.arg.loc.source.endsWith(']')
33+
)
3034
) {
3135
ctx.currentComponent!.used = true;
3236
if (!emitVar) {
@@ -37,21 +41,33 @@ export function* generateElementEvents(
3741
yield `let ${eventsVar}!: __VLS_NormalizeEmits<typeof ${emitVar}>${endOfLine}`;
3842
yield `let ${propsVar}!: __VLS_FunctionalComponentProps<typeof ${componentFunctionalVar}, typeof ${componentVNodeVar}>${endOfLine}`;
3943
}
40-
let source = prop.arg.loc.source;
41-
let start = prop.arg.loc.start.offset;
42-
let propPrefix = 'on';
44+
45+
let source = prop.arg?.loc.source ?? 'model-value';
46+
let start = prop.arg?.loc.start.offset;
47+
let propPrefix = 'on-';
4348
let emitPrefix = '';
44-
if (source.startsWith('vue:')) {
49+
if (prop.name === 'model') {
50+
propPrefix = 'onUpdate:';
51+
emitPrefix = 'update:';
52+
}
53+
else if (source.startsWith('vue:')) {
4554
source = source.slice('vue:'.length);
46-
start = start + 'vue:'.length;
47-
propPrefix = 'onVnode';
55+
start = start! + 'vue:'.length;
56+
propPrefix = 'onVnode-';
4857
emitPrefix = 'vnode-';
4958
}
50-
yield `const ${ctx.getInternalVariable()}: __VLS_NormalizeComponentEvent<typeof ${propsVar}, typeof ${eventsVar}, '${camelize(propPrefix + '-' + source)}', '${emitPrefix}${source}', '${camelize(emitPrefix + source)}'> = {${newLine}`;
51-
yield* generateEventArg(ctx, source, start, propPrefix);
52-
yield `: `;
53-
yield* generateEventExpression(options, ctx, prop);
54-
yield `}${endOfLine}`;
59+
60+
yield `(): __VLS_NormalizeComponentEvent<typeof ${propsVar}, typeof ${eventsVar}, '${camelize(propPrefix + source)}', '${emitPrefix + source}', '${camelize(emitPrefix + source)}'> => ({${newLine}`;
61+
if (prop.name === 'on') {
62+
yield* generateEventArg(ctx, source, start!, propPrefix.slice(0, -1));
63+
yield `: `;
64+
yield* generateEventExpression(options, ctx, prop);
65+
}
66+
else {
67+
yield `'${camelize(propPrefix + source)}': `;
68+
yield* generateModelEventExpression(options, ctx, prop);
69+
}
70+
yield `})${endOfLine}`;
5571
}
5672
}
5773
}
@@ -159,6 +175,29 @@ export function* generateEventExpression(
159175
}
160176
}
161177

178+
export function* generateModelEventExpression(
179+
options: TemplateCodegenOptions,
180+
ctx: TemplateCodegenContext,
181+
prop: CompilerDOM.DirectiveNode
182+
): Generator<Code> {
183+
if (prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION) {
184+
yield `(...[$event]) => (`;
185+
yield* generateInterpolation(
186+
options,
187+
ctx,
188+
'template',
189+
ctx.codeFeatures.verification,
190+
prop.exp.content,
191+
prop.exp.loc.start.offset,
192+
prop.exp.loc
193+
)
194+
yield ` = $event)`;
195+
}
196+
else {
197+
yield `() => {}`;
198+
}
199+
}
200+
162201
export function isCompoundExpression(ts: typeof import('typescript'), ast: ts.SourceFile) {
163202
let result = true;
164203
if (ast.statements.length === 0) {
Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
<template>{{ msg }}</template>
2-
31
<script lang="ts" setup>
2+
import type { PropType } from 'vue';
43
import { exactType } from '../../shared';
5-
import {PropType} from 'vue'
64
75
const msg = defineModel('msg', {
86
type: String as PropType<string | null>,
97
required: true,
10-
})
8+
});
119
12-
exactType(msg.value, {} as string | null)
10+
exactType(msg.value, {} as string | null);
1311
</script>
12+
13+
<template>
14+
{{ msg }}
15+
</template>
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
<template>{{ other }}</template>
2-
31
<script lang="ts" setup>
4-
const other = defineModel<string | null>('other')
2+
const other = defineModel<string | null>('other');
53
</script>
4+
5+
<template>
6+
{{ other }}
7+
</template>
Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
<template>
2-
<child v-model:msg="msg" />
3-
<child :msg="msg" @update:msg="v => exactType(v, {} as string|null)" />
4-
5-
<child2 v-model:other="other" />
6-
</template>
71
<script lang="ts" setup>
2+
import { ref } from 'vue';
83
import { exactType } from '../../shared';
9-
import { ref } from 'vue'
10-
import child from './child.vue'
11-
import child2 from './child2.vue'
4+
import child from './child.vue';
5+
import child2 from './child2.vue';
126
13-
const msg = ref<string | null>('test')
14-
const other = ref<string | null>('test2')
7+
const msg = ref<string | null>('test');
8+
const other = ref<string | null | undefined>('test2');
159
</script>
10+
11+
<template>
12+
<child v-model:msg="msg" />
13+
<child :msg="msg" @update:msg="v => exactType(v, {} as string | null)" />
14+
15+
<child2 v-model:other="other" />
16+
</template>

test-workspace/tsc/passedFixtures/vue3/#4822/main.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export default defineComponent({
1111
</script>
1212

1313
<script setup lang="ts">
14-
let foo!: number;
14+
let foo!: number | undefined;
1515
</script>
1616

1717
<template>

test-workspace/tsc/passedFixtures/vue3/defineModelModifiers/main.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script setup lang="ts">
22
import Comp from './comp.vue';
33
4-
const fooBar = 'fooBar';
4+
let fooBar!: string;
55
</script>
66

77
<template>

0 commit comments

Comments
 (0)