diff --git a/packages/language-core/lib/codegen/common.ts b/packages/language-core/lib/codegen/common.ts index 5c8814310e..a2d925b5d3 100644 --- a/packages/language-core/lib/codegen/common.ts +++ b/packages/language-core/lib/codegen/common.ts @@ -45,11 +45,10 @@ export function collectVars( ts: typeof import('typescript'), node: ts.Node, ast: ts.SourceFile, - results: string[] = [], - includesRest = true + results: string[] = [] ) { - const identifiers = collectIdentifiers(ts, node, [], includesRest); - for (const id of identifiers) { + const identifiers = collectIdentifiers(ts, node, []); + for (const [id] of identifiers) { results.push(getNodeText(ts, id, ast)); } return results; @@ -58,28 +57,26 @@ export function collectVars( export function collectIdentifiers( ts: typeof import('typescript'), node: ts.Node, - results: ts.Identifier[] = [], - includesRest = true + results: [id: ts.Identifier, isRest: boolean][] = [], + isRest = false ) { if (ts.isIdentifier(node)) { - results.push(node); + results.push([node, isRest]); } else if (ts.isObjectBindingPattern(node)) { for (const el of node.elements) { - if (includesRest || !el.dotDotDotToken) { - collectIdentifiers(ts, el.name, results, includesRest); - } + collectIdentifiers(ts, el.name, results, !!el.dotDotDotToken); } } else if (ts.isArrayBindingPattern(node)) { for (const el of node.elements) { if (ts.isBindingElement(el)) { - collectIdentifiers(ts, el.name, results, includesRest); + collectIdentifiers(ts, el.name, results, !!el.dotDotDotToken); } } } else { - ts.forEachChild(node, node => collectIdentifiers(ts, node, results, includesRest)); + ts.forEachChild(node, node => collectIdentifiers(ts, node, results, false)); } return results; } diff --git a/packages/language-core/lib/codegen/script/template.ts b/packages/language-core/lib/codegen/script/template.ts index 6debf6c809..7ad0f73e53 100644 --- a/packages/language-core/lib/codegen/script/template.ts +++ b/packages/language-core/lib/codegen/script/template.ts @@ -231,6 +231,7 @@ function* generateCssVars(options: ScriptCodegenOptions, ctx: TemplateCodegenCon for (const [segment, offset, onlyError] of forEachInterpolationSegment( options.ts, undefined, + undefined, ctx, cssBind.text, cssBind.offset, diff --git a/packages/language-core/lib/codegen/template/index.ts b/packages/language-core/lib/codegen/template/index.ts index f7a55b2caf..feac4f497a 100644 --- a/packages/language-core/lib/codegen/template/index.ts +++ b/packages/language-core/lib/codegen/template/index.ts @@ -17,6 +17,7 @@ export interface TemplateCodegenOptions { edited: boolean; scriptSetupBindingNames: Set; scriptSetupImportComponentNames: Set; + destructuredPropNames: Set; templateRefNames: Set; hasDefineSlots?: boolean; slotsAssignName?: string; diff --git a/packages/language-core/lib/codegen/template/interpolation.ts b/packages/language-core/lib/codegen/template/interpolation.ts index fb6f4611cb..f2b35d5f12 100644 --- a/packages/language-core/lib/codegen/template/interpolation.ts +++ b/packages/language-core/lib/codegen/template/interpolation.ts @@ -25,6 +25,7 @@ export function* generateInterpolation( }[] = []; for (let [section, offset, onlyError] of forEachInterpolationSegment( options.ts, + options.destructuredPropNames, options.templateRefNames, ctx, code, @@ -72,6 +73,7 @@ export function* generateInterpolation( export function* forEachInterpolationSegment( ts: typeof import('typescript'), + destructuredPropNames: Set | undefined, templateRefNames: Set | undefined, ctx: TemplateCodegenContext, code: string, @@ -101,6 +103,9 @@ export function* forEachInterpolationSegment( isShorthand: isShorthand, offset: getStartEnd(ts, id, ast).start, }); + if (destructuredPropNames?.has(text)) { + return; + } if (offset !== undefined) { ctx.accessExternalVariable(text, offset + getStartEnd(ts, id, ast).start); } @@ -127,7 +132,7 @@ export function* forEachInterpolationSegment( const curVar = ctxVars[i]; const nextVar = ctxVars[i + 1]; - yield* generateVar(code, templateRefNames, curVar, nextVar); + yield* generateVar(code, destructuredPropNames, templateRefNames, curVar, nextVar); if (nextVar.isShorthand) { yield [code.substring(curVar.offset + curVar.text.length, nextVar.offset + nextVar.text.length), curVar.offset + curVar.text.length]; @@ -139,7 +144,7 @@ export function* forEachInterpolationSegment( } const lastVar = ctxVars.at(-1)!; - yield* generateVar(code, templateRefNames, lastVar); + yield* generateVar(code, destructuredPropNames, templateRefNames, lastVar); yield [code.substring(lastVar.offset + lastVar.text.length), lastVar.offset + lastVar.text.length]; } else { @@ -149,6 +154,7 @@ export function* forEachInterpolationSegment( function* generateVar( code: string, + destructuredPropNames: Set | undefined, templateRefNames: Set | undefined, curVar: { text: string, @@ -165,6 +171,7 @@ function* generateVar( // fix https://github.com/vuejs/language-tools/issues/1264 yield ['', nextVar.offset, true]; + const isDestructuredProp = destructuredPropNames?.has(curVar.text) ?? false; const isTemplateRef = templateRefNames?.has(curVar.text) ?? false; if (isTemplateRef) { yield [`__VLS_unref(`, undefined]; @@ -172,7 +179,9 @@ function* generateVar( yield [`)`, undefined]; } else { - yield [`__VLS_ctx.`, undefined]; + if (!isDestructuredProp) { + yield [`__VLS_ctx.`, undefined]; + } yield [code.substring(curVar.offset, curVar.offset + curVar.text.length), curVar.offset]; } } diff --git a/packages/language-core/lib/parsers/scriptSetupRanges.ts b/packages/language-core/lib/parsers/scriptSetupRanges.ts index 077bc4bdc9..4feccbde61 100644 --- a/packages/language-core/lib/parsers/scriptSetupRanges.ts +++ b/packages/language-core/lib/parsers/scriptSetupRanges.ts @@ -1,6 +1,6 @@ import type * as ts from 'typescript'; import type { VueCompilerOptions, TextRange } from '../types'; -import { collectVars } from '../codegen/common'; +import { collectIdentifiers } from '../codegen/common'; export interface ScriptSetupRanges extends ReturnType { } @@ -15,7 +15,8 @@ export function parseScriptSetupRanges( const props: { name?: string; - destructured?: string[]; + destructured?: Set; + destructuredRest?: string; define?: ReturnType & { statement: TextRange; }; @@ -308,7 +309,17 @@ export function parseScriptSetupRanges( else if (vueCompilerOptions.macros.defineProps.includes(callText)) { if (ts.isVariableDeclaration(parent)) { if (ts.isObjectBindingPattern(parent.name)) { - props.destructured = collectVars(ts, parent.name, ast, [], false); + props.destructured = new Set(); + const identifiers = collectIdentifiers(ts, parent.name, []); + for (const [id, isRest] of identifiers) { + const name = getNodeText(ts, id, ast); + if (isRest) { + props.destructuredRest = name; + } + else { + props.destructured.add(name); + } + } } else { props.name = getNodeText(ts, parent.name, ast); diff --git a/packages/language-core/lib/plugins/vue-tsx.ts b/packages/language-core/lib/plugins/vue-tsx.ts index c529addb79..5bbea9aec1 100644 --- a/packages/language-core/lib/plugins/vue-tsx.ts +++ b/packages/language-core/lib/plugins/vue-tsx.ts @@ -104,6 +104,7 @@ function createTsx( edited: ctx.vueCompilerOptions.__test || (fileEditTimes.get(fileName) ?? 0) >= 2, scriptSetupBindingNames: scriptSetupBindingNames(), scriptSetupImportComponentNames: scriptSetupImportComponentNames(), + destructuredPropNames: destructuredPropNames(), templateRefNames: templateRefNames(), hasDefineSlots: hasDefineSlots(), slotsAssignName: slotsAssignName(), @@ -144,6 +145,17 @@ function createTsx( } return newNames; }); + const destructuredPropNames = computed>(oldNames => { + const newNames = scriptSetupRanges()?.props.destructured ?? new Set(); + const rest = scriptSetupRanges()?.props.destructuredRest; + if (rest) { + newNames.add(rest); + } + if (oldNames && twoSetsEqual(newNames, oldNames)) { + return oldNames; + } + return newNames; + }); const templateRefNames = computed>(oldNames => { const newNames = new Set( scriptSetupRanges()?.templateRefs diff --git a/packages/language-service/lib/plugins/vue-inlayhints.ts b/packages/language-service/lib/plugins/vue-inlayhints.ts index c17fbb7692..1e0f28f2c3 100644 --- a/packages/language-service/lib/plugins/vue-inlayhints.ts +++ b/packages/language-service/lib/plugins/vue-inlayhints.ts @@ -105,7 +105,7 @@ type Scope = Record; export function findDestructuredProps( ts: typeof import('typescript'), ast: ts.SourceFile, - props: string[] + props: Set ) { const rootScope: Scope = {}; const scopeStack: Scope[] = [rootScope]; @@ -180,7 +180,7 @@ export function findDestructuredProps( && ts.isCallExpression(initializer) && initializer.expression.getText(ast) === 'defineProps'; - for (const id of collectIdentifiers(ts, name)) { + for (const [id] of collectIdentifiers(ts, name)) { if (isDefineProps) { excludedIds.add(id); } else { @@ -196,7 +196,7 @@ export function findDestructuredProps( } for (const p of parameters) { - for (const id of collectIdentifiers(ts, p)) { + for (const [id] of collectIdentifiers(ts, p)) { registerLocalBinding(id); } } diff --git a/test-workspace/tsc/passedFixtures/vue2/tsconfig.json b/test-workspace/tsc/passedFixtures/vue2/tsconfig.json index 156a2a05a2..cc86334474 100644 --- a/test-workspace/tsc/passedFixtures/vue2/tsconfig.json +++ b/test-workspace/tsc/passedFixtures/vue2/tsconfig.json @@ -25,6 +25,7 @@ "../vue3/#4646", "../vue3/#4649", "../vue3/#4777", + "../vue3/#4820", "../vue3/components", "../vue3/defineEmits", "../vue3/defineModel", diff --git a/test-workspace/tsc/passedFixtures/vue3.4/tsconfig.json b/test-workspace/tsc/passedFixtures/vue3.4/tsconfig.json index fb41610f8e..8a5d76fcfa 100644 --- a/test-workspace/tsc/passedFixtures/vue3.4/tsconfig.json +++ b/test-workspace/tsc/passedFixtures/vue3.4/tsconfig.json @@ -11,6 +11,7 @@ "exclude": [ "../vue3/#3820", "../vue3/#4777", + "../vue3/#4820", "../vue3/rootEl", "../vue3/templateRef", "../vue3/templateRef_native", diff --git a/test-workspace/tsc/passedFixtures/vue3/#4820/main.vue b/test-workspace/tsc/passedFixtures/vue3/#4820/main.vue new file mode 100644 index 0000000000..3a43973dc8 --- /dev/null +++ b/test-workspace/tsc/passedFixtures/vue3/#4820/main.vue @@ -0,0 +1,22 @@ + + + + +