Skip to content

Commit 7ad4a94

Browse files
fix(language-core): support calling template bindings without .value (#6181)
1 parent e68e7ea commit 7ad4a94

11 files changed

Lines changed: 129 additions & 27 deletions

File tree

‎packages/language-core/lib/codegen/script/template.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export function* generateTemplate(
1414
yield* generateTemplateDirectives(options);
1515

1616
for (const name of options.withDotValueBindings) {
17-
yield `${names.withDotValue}(${name}, {} as import('${options.vueCompilerOptions.lib}').Ref)${endOfLine}`;
17+
yield `${names.withDotValue}(${name}, {} as import('${options.vueCompilerOptions.lib}').Ref<unknown>)${endOfLine}`;
1818
}
1919

2020
if (options.templateAndStyleCodes.length) {

‎packages/language-core/lib/codegen/template/context.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ export function createTemplateCodegenContext() {
161161
// context accesses -----------------------------------------------------------
162162

163163
const contextAccesses = new Map<string, Map<string, Set<number>>>();
164+
const callAccessedBindings = new Set<string>();
164165

165166
function accessVariable(source: string, name: string, offset?: number) {
166167
let map = contextAccesses.get(name);
@@ -176,6 +177,10 @@ export function createTemplateCodegenContext() {
176177
}
177178
}
178179

180+
function accessVariableAsCall(name: string) {
181+
callAccessedBindings.add(name);
182+
}
183+
179184
function* generateAutoImport(): Generator<Code> {
180185
const all = [...contextAccesses.entries()];
181186
if (!all.some(([, offsets]) => offsets.size)) {
@@ -268,6 +273,8 @@ export function createTemplateCodegenContext() {
268273
scope,
269274
contextAccesses,
270275
accessVariable,
276+
callAccessedBindings,
277+
accessVariableAsCall,
271278
generateAutoImport,
272279
conditions,
273280
generateConditionGuards,

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

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,13 +168,31 @@ export function* generateEventExpression(
168168
const scope = ctx.scope();
169169
scope.declare('$event');
170170
yield* ctx.generateConditionGuards();
171+
172+
// The top-level `__VLS_withDotValue` assertion does not narrow imported
173+
// bindings inside this closure (TS control-flow limitation), so re-assert
174+
// the setup bindings that are called by this handler right here. Only
175+
// called bindings are re-asserted: for local bindings the top-level
176+
// assertion already flows into this closure, and re-asserting non-call
177+
// accesses would double-narrow them.
178+
ctx.callAccessedBindings.clear();
179+
const codes: Code[] = [];
180+
for (const code of interpolation) {
181+
codes.push(code);
182+
}
183+
for (const name of ctx.callAccessedBindings) {
184+
if (options.setupBindings.has(name)) {
185+
yield `${names.withDotValue}(${name}, {} as import('${options.vueCompilerOptions.lib}').Ref<unknown>)${endOfLine}`;
186+
}
187+
}
188+
171189
if (isSingleExpression(options.typescript, ast)) {
172190
yield `return (`;
173-
yield* interpolation;
191+
yield* codes;
174192
yield `)`;
175193
}
176194
else {
177-
yield* interpolation;
195+
yield* codes;
178196
}
179197
yield endOfLine;
180198
yield* scope.end();

‎packages/language-core/lib/codegen/template/interpolation.ts‎

Lines changed: 65 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export function* generateInterpolation(
3434

3535
let prevEnd = 0;
3636
for (
37-
const [name, offset, isShorthand] of forEachIdentifiers(
37+
const [name, offset, isShorthand, isCall] of forEachIdentifiers(
3838
typescript,
3939
ctx,
4040
block,
@@ -89,6 +89,9 @@ export function* generateInterpolation(
8989
}
9090
else if (setupBindings.has(name)) {
9191
ctx.accessVariable(block.name, name, start + offset);
92+
if (isCall) {
93+
ctx.accessVariableAsCall(name);
94+
}
9295
yield [
9396
name,
9497
block.name,
@@ -97,7 +100,9 @@ export function* generateInterpolation(
97100
? { ...data, __shorthandExpression: 'js' }
98101
: data,
99102
];
100-
yield `.value`;
103+
if (!isCall) {
104+
yield `.value`;
105+
}
101106
}
102107
else {
103108
// #1205, #1264
@@ -179,20 +184,20 @@ function* forEachIdentifiers(
179184
code: string,
180185
prefix: string,
181186
suffix: string,
182-
): Generator<[string, number, boolean]> {
187+
): Generator<[string, number, boolean, boolean]> {
183188
if (identifierRE.test(code) && !shouldIdentifierSkipped(ctx, code)) {
184-
yield [code, 0, false];
189+
yield [code, 0, false, false];
185190
return;
186191
}
187192

188193
const scope = ctx.scope();
189194
const ast = getTypeScriptAST(ts, block, prefix + code + suffix);
190-
for (const [id, isShorthand] of forEachDeclarations(ts, ast, ast, ctx, scope)) {
195+
for (const [id, isShorthand, isCall] of forEachDeclarations(ts, ast, ast, ctx, scope, false)) {
191196
const text = getNodeText(ts, id, ast);
192197
if (shouldIdentifierSkipped(ctx, text)) {
193198
continue;
194199
}
195-
yield [text, getStartEnd(ts, id, ast).start - prefix.length, isShorthand];
200+
yield [text, getStartEnd(ts, id, ast).start - prefix.length, isShorthand, isCall];
196201
}
197202
scope.end();
198203
}
@@ -203,15 +208,52 @@ function* forEachDeclarations(
203208
ast: ts.SourceFile,
204209
ctx: TemplateCodegenContext,
205210
scope: ReturnType<TemplateCodegenContext['scope']>,
206-
): Generator<[ts.Identifier, boolean]> {
211+
inCallPosition: boolean,
212+
): Generator<[ts.Identifier, boolean, boolean]> {
207213
if (ts.isIdentifier(node)) {
208-
yield [node, false];
214+
yield [node, false, inCallPosition];
209215
}
210216
else if (ts.isShorthandPropertyAssignment(node)) {
211-
yield [node.name, true];
217+
yield [node.name, true, inCallPosition];
212218
}
213219
else if (ts.isPropertyAccessExpression(node)) {
214-
yield* forEachDeclarations(ts, node.expression, ast, ctx, scope);
220+
yield* forEachDeclarations(ts, node.expression, ast, ctx, scope, false);
221+
}
222+
else if (ts.isCallExpression(node)) {
223+
yield* forEachDeclarations(ts, node.expression, ast, ctx, scope, true);
224+
for (const arg of node.arguments) {
225+
yield* forEachDeclarations(ts, arg, ast, ctx, scope, false);
226+
}
227+
}
228+
else if (ts.isTaggedTemplateExpression(node)) {
229+
yield* forEachDeclarations(ts, node.tag, ast, ctx, scope, true);
230+
yield* forEachDeclarations(ts, node.template, ast, ctx, scope, false);
231+
}
232+
else if (ts.isParenthesizedExpression(node)) {
233+
yield* forEachDeclarations(ts, node.expression, ast, ctx, scope, inCallPosition);
234+
}
235+
else if (ts.isNonNullExpression(node)) {
236+
yield* forEachDeclarations(ts, node.expression, ast, ctx, scope, inCallPosition);
237+
}
238+
else if (ts.isTypeAssertionExpression(node)) {
239+
yield* forEachDeclarationsInTypeNode(ts, node.type);
240+
yield* forEachDeclarations(ts, node.expression, ast, ctx, scope, inCallPosition);
241+
}
242+
else if (ts.isAsExpression(node) || ts.isSatisfiesExpression(node)) {
243+
yield* forEachDeclarations(ts, node.expression, ast, ctx, scope, inCallPosition);
244+
yield* forEachDeclarationsInTypeNode(ts, node.type);
245+
}
246+
else if (ts.isBinaryExpression(node)) {
247+
const isLogical = node.operatorToken.kind === ts.SyntaxKind.BarBarToken
248+
|| node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken
249+
|| node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken;
250+
yield* forEachDeclarations(ts, node.left, ast, ctx, scope, isLogical ? inCallPosition : false);
251+
yield* forEachDeclarations(ts, node.right, ast, ctx, scope, isLogical ? inCallPosition : false);
252+
}
253+
else if (ts.isConditionalExpression(node)) {
254+
yield* forEachDeclarations(ts, node.condition, ast, ctx, scope, false);
255+
yield* forEachDeclarations(ts, node.whenTrue, ast, ctx, scope, inCallPosition);
256+
yield* forEachDeclarations(ts, node.whenFalse, ast, ctx, scope, inCallPosition);
215257
}
216258
else if (ts.isVariableDeclaration(node)) {
217259
scope.declare(...collectBindingNames(ts, node.name, ast));
@@ -232,18 +274,18 @@ function* forEachDeclarations(
232274
if (ts.isPropertyAssignment(prop)) {
233275
// fix https://github.com/vuejs/language-tools/issues/1176
234276
if (ts.isComputedPropertyName(prop.name)) {
235-
yield* forEachDeclarations(ts, prop.name.expression, ast, ctx, scope);
277+
yield* forEachDeclarations(ts, prop.name.expression, ast, ctx, scope, false);
236278
}
237-
yield* forEachDeclarations(ts, prop.initializer, ast, ctx, scope);
279+
yield* forEachDeclarations(ts, prop.initializer, ast, ctx, scope, false);
238280
}
239281
// fix https://github.com/vuejs/language-tools/issues/1156
240282
else if (ts.isShorthandPropertyAssignment(prop)) {
241-
yield* forEachDeclarations(ts, prop, ast, ctx, scope);
283+
yield* forEachDeclarations(ts, prop, ast, ctx, scope, false);
242284
}
243285
// fix https://github.com/vuejs/language-tools/issues/1148#issuecomment-1094378126
244286
else if (ts.isSpreadAssignment(prop)) {
245287
// TODO: cannot report "Spread types may only be created from object types.ts(2698)"
246-
yield* forEachDeclarations(ts, prop.expression, ast, ctx, scope);
288+
yield* forEachDeclarations(ts, prop.expression, ast, ctx, scope, false);
247289
}
248290
// fix https://github.com/vuejs/language-tools/issues/4604
249291
else if (ts.isFunctionLike(prop) && prop.body) {
@@ -258,13 +300,13 @@ function* forEachDeclarations(
258300
else if (ts.isBlock(node)) {
259301
const scope = ctx.scope();
260302
for (const child of forEachNode(ts, node)) {
261-
yield* forEachDeclarations(ts, child, ast, ctx, scope);
303+
yield* forEachDeclarations(ts, child, ast, ctx, scope, false);
262304
}
263305
scope.end();
264306
}
265307
else {
266308
for (const child of forEachNode(ts, node)) {
267-
yield* forEachDeclarations(ts, child, ast, ctx, scope);
309+
yield* forEachDeclarations(ts, child, ast, ctx, scope, false);
268310
}
269311
}
270312
}
@@ -275,15 +317,15 @@ function* forEachDeclarationsInBinding(
275317
ast: ts.SourceFile,
276318
ctx: TemplateCodegenContext,
277319
scope: ReturnType<TemplateCodegenContext['scope']>,
278-
): Generator<[ts.Identifier, boolean]> {
320+
): Generator<[ts.Identifier, boolean, boolean]> {
279321
if ('type' in node && node.type) {
280322
yield* forEachDeclarationsInTypeNode(ts, node.type);
281323
}
282324
if (!ts.isIdentifier(node.name)) {
283-
yield* forEachDeclarations(ts, node.name, ast, ctx, scope);
325+
yield* forEachDeclarations(ts, node.name, ast, ctx, scope, false);
284326
}
285327
if (node.initializer) {
286-
yield* forEachDeclarations(ts, node.initializer, ast, ctx, scope);
328+
yield* forEachDeclarations(ts, node.initializer, ast, ctx, scope, false);
287329
}
288330
}
289331

@@ -292,28 +334,28 @@ function* forEachDeclarationsInFunction(
292334
node: ts.ArrowFunction | ts.FunctionExpression | ts.AccessorDeclaration | ts.MethodDeclaration,
293335
ast: ts.SourceFile,
294336
ctx: TemplateCodegenContext,
295-
): Generator<[ts.Identifier, boolean]> {
337+
): Generator<[ts.Identifier, boolean, boolean]> {
296338
const scope = ctx.scope();
297339
for (const param of node.parameters) {
298340
scope.declare(...collectBindingNames(ts, param.name, ast));
299341
yield* forEachDeclarationsInBinding(ts, param, ast, ctx, scope);
300342
}
301343
if (node.body) {
302-
yield* forEachDeclarations(ts, node.body, ast, ctx, scope);
344+
yield* forEachDeclarations(ts, node.body, ast, ctx, scope, false);
303345
}
304346
scope.end();
305347
}
306348

307349
function* forEachDeclarationsInTypeNode(
308350
ts: typeof import('typescript'),
309351
node: ts.Node,
310-
): Generator<[ts.Identifier, boolean]> {
352+
): Generator<[ts.Identifier, boolean, boolean]> {
311353
if (ts.isTypeQueryNode(node)) {
312354
let id = node.exprName;
313355
while (!ts.isIdentifier(id)) {
314356
id = id.left;
315357
}
316-
yield [id, false];
358+
yield [id, false, false];
317359
}
318360
else {
319361
for (const child of forEachNode(ts, node)) {

‎packages/language-core/types/template-helpers.d.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,11 @@ declare global {
135135
function __VLS_asFunctionalSlot<S>(slot: S): S extends () => infer R ? (props: {}) => R : NonNullable<S>;
136136
function __VLS_omit<T, K>(target: T, props: K): Omit<T, keyof K>;
137137
function __VLS_tryAsConstant<const T>(t: T): T;
138-
function __VLS_withDotValue<T, Ref>(t: T, ref: Ref): asserts t is T extends Ref ? T : T & { value: T };
138+
function __VLS_withDotValue<T, Ref>(
139+
t: T,
140+
ref: Ref,
141+
): asserts t is T extends Ref ? T extends { value: infer V } ? V extends (...args: any) => any ? T & V : T : T
142+
: T & { value: T };
139143
}
140144

141145
export {};

‎packages/tsc/tests/typecheck.spec.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ test(`vue-tsc`, () => {
4343
"test-workspace/tsc/useTemplateRef-dynamic-arg/main.vue(3,7): error TS2464: A computed property name must be of type 'string', 'number', 'symbol', or 'any'.",
4444
"test-workspace/tsc/useTemplateRef-nullable/main.vue(3,5): error TS18047: 'foo.value' is possibly 'null'.",
4545
"test-workspace/tsc/useTemplateRef-nullable/main.vue(3,9): error TS2339: Property 'bar' does not exist on type 'HTMLInputElement'.",
46+
"test-workspace/tsc/withDotValue/main.vue(10,19): error TS2740: Type 'TemplateStringsArray' is missing the following properties from type 'Event': bubbles, cancelBubble, cancelable, composed, and 18 more.",
47+
"test-workspace/tsc/withDotValue/main.vue(2,25): error TS2345: Argument of type 'number' is not assignable to parameter of type 'Event'.",
48+
"test-workspace/tsc/withDotValue/main.vue(3,17): error TS2345: Argument of type 'number' is not assignable to parameter of type 'Event'.",
49+
"test-workspace/tsc/withDotValue/main.vue(4,28): error TS2345: Argument of type 'number' is not assignable to parameter of type 'Event'.",
50+
"test-workspace/tsc/withDotValue/main.vue(5,28): error TS2345: Argument of type 'number' is not assignable to parameter of type 'Event'.",
51+
"test-workspace/tsc/withDotValue/main.vue(6,20): error TS2345: Argument of type 'number' is not assignable to parameter of type 'Event'.",
52+
"test-workspace/tsc/withDotValue/main.vue(7,24): error TS2740: Type 'TemplateStringsArray' is missing the following properties from type 'Event': bubbles, cancelBubble, cancelable, composed, and 18 more.",
53+
"test-workspace/tsc/withDotValue/main.vue(8,16): error TS2740: Type 'TemplateStringsArray' is missing the following properties from type 'Event': bubbles, cancelBubble, cancelable, composed, and 18 more.",
54+
"test-workspace/tsc/withDotValue/main.vue(9,27): error TS2740: Type 'TemplateStringsArray' is missing the following properties from type 'Event': bubbles, cancelBubble, cancelable, composed, and 18 more.",
4655
]
4756
`);
4857
});

‎test-workspace/tsc/tsconfig.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,9 @@
675675
},
676676
{
677677
"path": "./withDefaults/tsconfig.json"
678+
},
679+
{
680+
"path": "./withDotValue/tsconfig.json"
678681
}
679682
]
680683
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export function helper(_e: Event) {}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<template>
2+
<button @click="helper(123)"></button>
3+
<div>{{ helper(123) }}</div>
4+
<button @click="refHelper(123)"></button>
5+
<button @click="refHelper(456)"></button>
6+
<div>{{ refHelper(123) }}</div>
7+
<button @click="helper`foo`"></button>
8+
<div>{{ helper`foo` }}</div>
9+
<button @click="refHelper`foo`"></button>
10+
<div>{{ refHelper`foo` }}</div>
11+
</template>
12+
<script setup lang="ts">
13+
import { helper } from './helper';
14+
import { refHelper } from './refHelper';
15+
</script>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import { ref } from 'vue';
2+
export const refHelper = ref((_e: Event) => {});

0 commit comments

Comments
 (0)