From f76c5ebb4100e42fa9f0d8b976b501c8eef4e1f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B1=B1=E5=90=B9=E8=89=B2=E5=BE=A1=E5=AE=88?= <85992002+KazariEX@users.noreply.github.com> Date: Tue, 31 Dec 2024 22:32:30 +0800 Subject: [PATCH] fix(language-core, typescript-plugin): handle self-reference component correctly (#5102) --- .../language-core/lib/codegen/globalTypes.ts | 3 +- .../lib/codegen/script/template.ts | 20 +--- .../lib/codegen/template/element.ts | 10 +- .../lib/codegen/template/index.ts | 1 + packages/language-core/lib/plugins/vue-tsx.ts | 16 +++ .../language-server/tests/completions.spec.ts | 2 +- .../lib/requests/componentInfos.ts | 102 +++++++++--------- .../tsc/passedFixtures/vue3/#5097/child.ts | 1 + .../tsc/passedFixtures/vue3/#5097/child.vue | 5 + .../tsc/passedFixtures/vue3/#5097/main.vue | 11 ++ 10 files changed, 98 insertions(+), 73 deletions(-) create mode 100644 test-workspace/tsc/passedFixtures/vue3/#5097/child.ts create mode 100644 test-workspace/tsc/passedFixtures/vue3/#5097/child.vue create mode 100644 test-workspace/tsc/passedFixtures/vue3/#5097/main.vue diff --git a/packages/language-core/lib/codegen/globalTypes.ts b/packages/language-core/lib/codegen/globalTypes.ts index ac0a213671..d0da04112d 100644 --- a/packages/language-core/lib/codegen/globalTypes.ts +++ b/packages/language-core/lib/codegen/globalTypes.ts @@ -41,10 +41,11 @@ export function generateGlobalTypes(lib: string, target: number, strictTemplates type __VLS_IsAny = 0 extends 1 & T ? true : false; type __VLS_PickNotAny = __VLS_IsAny extends true ? B : A; type __VLS_unknownDirective = (arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown) => void; - type __VLS_WithComponent = + type __VLS_WithComponent = N1 extends keyof LocalComponents ? N1 extends N0 ? Pick : { [K in N0]: LocalComponents[N1] } : N2 extends keyof LocalComponents ? N2 extends N0 ? Pick : { [K in N0]: LocalComponents[N2] } : N3 extends keyof LocalComponents ? N3 extends N0 ? Pick : { [K in N0]: LocalComponents[N3] } : + Self extends object ? { [K in N0]: Self } : N1 extends keyof __VLS_GlobalComponents ? N1 extends N0 ? Pick<__VLS_GlobalComponents, N0 extends keyof __VLS_GlobalComponents ? N0 : never> : { [K in N0]: __VLS_GlobalComponents[N1] } : N2 extends keyof __VLS_GlobalComponents ? N2 extends N0 ? Pick<__VLS_GlobalComponents, N0 extends keyof __VLS_GlobalComponents ? N0 : never> : { [K in N0]: __VLS_GlobalComponents[N2] } : N3 extends keyof __VLS_GlobalComponents ? N3 extends N0 ? Pick<__VLS_GlobalComponents, N0 extends keyof __VLS_GlobalComponents ? N0 : never> : { [K in N0]: __VLS_GlobalComponents[N3] } : diff --git a/packages/language-core/lib/codegen/script/template.ts b/packages/language-core/lib/codegen/script/template.ts index 982e37a909..433bb591e4 100644 --- a/packages/language-core/lib/codegen/script/template.ts +++ b/packages/language-core/lib/codegen/script/template.ts @@ -1,6 +1,5 @@ -import * as path from 'path-browserify'; import type { Code } from '../../types'; -import { getSlotsPropertyName, hyphenateTag } from '../../utils/shared'; +import { hyphenateTag } from '../../utils/shared'; import { TemplateCodegenContext, createTemplateCodegenContext } from '../template/context'; import { generateInterpolation } from '../template/interpolation'; import { generateStyleScopedClassReferences } from '../template/styleScopedClasses'; @@ -69,23 +68,6 @@ function* generateTemplateComponents(options: ScriptCodegenOptions): Generator { ` - + getSlotsPropertyName(options.vueCompilerOptions.target) - + `: typeof ${options.scriptSetupRanges?.defineSlots?.name ?? `__VLS_slots`} }) }` - ); - } - types.push(`typeof __VLS_ctx`); yield `type __VLS_LocalComponents =`; diff --git a/packages/language-core/lib/codegen/template/element.ts b/packages/language-core/lib/codegen/template/element.ts index da1ca30b8c..a52dd842ea 100644 --- a/packages/language-core/lib/codegen/template/element.ts +++ b/packages/language-core/lib/codegen/template/element.ts @@ -1,7 +1,7 @@ import * as CompilerDOM from '@vue/compiler-dom'; import { camelize, capitalize } from '@vue/shared'; import type { Code, VueCodeInformation } from '../../types'; -import { hyphenateTag } from '../../utils/shared'; +import { getSlotsPropertyName, hyphenateTag } from '../../utils/shared'; import { createVBindShorthandInlayHintInfo } from '../inlayHints'; import { collectVars, createTsAst, endOfLine, newLine, normalizeAttributeValue, variableNameRegex, wrapWith } from '../utils'; import { generateCamelized } from '../utils/camelized'; @@ -151,6 +151,14 @@ export function* generateComponent( } else if (!isComponentTag) { yield `const ${var_originalComponent} = ({} as __VLS_WithComponent<'${getCanonicalComponentName(node.tag)}', __VLS_LocalComponents, `; + if (options.selfComponentName && possibleOriginalNames.includes(options.selfComponentName)) { + yield `typeof __VLS_self & (new () => { ` + + getSlotsPropertyName(options.vueCompilerOptions.target) + + `: typeof ${options.slotsAssignName ?? `__VLS_slots`} }), `; + } + else { + yield `void, `; + } yield getPossibleOriginalComponentNames(node.tag, false) .map(name => `'${name}'`) .join(`, `); diff --git a/packages/language-core/lib/codegen/template/index.ts b/packages/language-core/lib/codegen/template/index.ts index 63bcdfa5d6..b19c0d461d 100644 --- a/packages/language-core/lib/codegen/template/index.ts +++ b/packages/language-core/lib/codegen/template/index.ts @@ -23,6 +23,7 @@ export interface TemplateCodegenOptions { slotsAssignName?: string; propsAssignName?: string; inheritAttrs: boolean; + selfComponentName?: string; } export function* generateTemplate(options: TemplateCodegenOptions): Generator { diff --git a/packages/language-core/lib/plugins/vue-tsx.ts b/packages/language-core/lib/plugins/vue-tsx.ts index a378b240e8..42e6015679 100644 --- a/packages/language-core/lib/plugins/vue-tsx.ts +++ b/packages/language-core/lib/plugins/vue-tsx.ts @@ -1,5 +1,7 @@ import type { Mapping } from '@volar/language-core'; +import { camelize, capitalize } from '@vue/shared'; import { computed, unstable } from 'alien-signals'; +import * as path from 'path-browserify'; import { generateScript } from '../codegen/script'; import { generateTemplate } from '../codegen/template'; import { parseScriptRanges } from '../parsers/scriptRanges'; @@ -153,6 +155,19 @@ function createTsx( const value = scriptSetupRanges.get()?.defineOptions?.inheritAttrs ?? scriptRanges.get()?.exportDefault?.inheritAttrsOption; return value !== 'false'; }); + const selfComponentName = computed(() => { + const { exportDefault } = scriptRanges.get() ?? {}; + if (_sfc.script && exportDefault?.nameOption) { + const { nameOption } = exportDefault; + return _sfc.script.content.slice(nameOption.start + 1, nameOption.end - 1); + } + const { defineOptions } = scriptSetupRanges.get() ?? {}; + if (_sfc.scriptSetup && defineOptions?.name) { + return defineOptions.name; + } + const baseName = path.basename(fileName); + return capitalize(camelize(baseName.slice(0, baseName.lastIndexOf('.')))); + }); const generatedTemplate = computed(() => { if (vueCompilerOptions.get().skipTemplateCodegen || !_sfc.template) { @@ -174,6 +189,7 @@ function createTsx( slotsAssignName: slotsAssignName.get(), propsAssignName: propsAssignName.get(), inheritAttrs: inheritAttrs.get(), + selfComponentName: selfComponentName.get() }); let current = codegen.next(); diff --git a/packages/language-server/tests/completions.spec.ts b/packages/language-server/tests/completions.spec.ts index dcc67b3a06..1e56bf6307 100644 --- a/packages/language-server/tests/completions.spec.ts +++ b/packages/language-server/tests/completions.spec.ts @@ -187,8 +187,8 @@ describe('Completions', async () => { "component", "slot", "template", - "fixture", "BaseTransition", + "Fixture", ] `); }); diff --git a/packages/typescript-plugin/lib/requests/componentInfos.ts b/packages/typescript-plugin/lib/requests/componentInfos.ts index 764f6029eb..b3920c23f2 100644 --- a/packages/typescript-plugin/lib/requests/componentInfos.ts +++ b/packages/typescript-plugin/lib/requests/componentInfos.ts @@ -1,5 +1,6 @@ import * as vue from '@vue/language-core'; import { camelize, capitalize } from '@vue/shared'; +import * as path from 'node:path'; import type * as ts from 'typescript'; import type { RequestContext } from './types'; @@ -21,28 +22,11 @@ export function getComponentProps( return []; } - const name = tag.split('.'); - - let componentSymbol = components.type.getProperty(name[0]) - ?? components.type.getProperty(camelize(name[0])) - ?? components.type.getProperty(capitalize(camelize(name[0]))); - - if (!componentSymbol) { + const componentType = getComponentType(ts, languageService, vueCode, components, fileName, tag); + if (!componentType) { return []; } - let componentType = checker.getTypeOfSymbolAtLocation(componentSymbol, components.node); - - for (let i = 1; i < name.length; i++) { - componentSymbol = componentType.getProperty(name[i]); - if (componentSymbol) { - componentType = checker.getTypeOfSymbolAtLocation(componentSymbol, components.node); - } - else { - return []; - } - } - const result = new Map(); // for (const sig of componentType.getCallSignatures()) { @@ -185,13 +149,7 @@ export function getComponentNames( return; } const vueCode = volarFile.generated.root; - - return getVariableType(ts, languageService, vueCode, '__VLS_components') - ?.type - ?.getProperties() - .map(c => c.name) - .filter(entry => !entry.includes('$') && !entry.startsWith('_')) - ?? []; + return _getComponentNames(ts, languageService, vueCode); } export function _getComponentNames( @@ -199,12 +157,15 @@ export function _getComponentNames( tsLs: ts.LanguageService, vueCode: vue.VueVirtualCode ) { - return getVariableType(ts, tsLs, vueCode, '__VLS_components') + const names = getVariableType(ts, tsLs, vueCode, '__VLS_components') ?.type ?.getProperties() .map(c => c.name) .filter(entry => !entry.includes('$') && !entry.startsWith('_')) ?? []; + + names.push(getSelfComponentName(vueCode.fileName)); + return names; } export function getElementAttrs( @@ -242,6 +203,42 @@ export function getElementAttrs( return []; } +function getComponentType( + ts: typeof import('typescript'), + languageService: ts.LanguageService, + vueCode: vue.VueVirtualCode, + components: NonNullable>, + fileName: string, + tag: string +) { + const program = languageService.getProgram()!; + const checker = program.getTypeChecker(); + const name = tag.split('.'); + + let componentSymbol = components.type.getProperty(name[0]) + ?? components.type.getProperty(camelize(name[0])) + ?? components.type.getProperty(capitalize(camelize(name[0]))); + let componentType: ts.Type | undefined; + + if (!componentSymbol) { + const name = getSelfComponentName(fileName); + if (name === capitalize(camelize(tag))) { + componentType = getVariableType(ts, languageService, vueCode, '__VLS_self')?.type; + } + } + else { + componentType = checker.getTypeOfSymbolAtLocation(componentSymbol, components.node); + for (let i = 1; i < name.length; i++) { + componentSymbol = componentType.getProperty(name[i]); + if (componentSymbol) { + componentType = checker.getTypeOfSymbolAtLocation(componentSymbol, components.node); + } + } + } + + return componentType; +} + function getVariableType( ts: typeof import('typescript'), languageService: ts.LanguageService, @@ -266,6 +263,11 @@ function getVariableType( } } +function getSelfComponentName(fileName: string) { + const baseName = path.basename(fileName); + return capitalize(camelize(baseName.slice(0, baseName.lastIndexOf('.')))); +} + function searchVariableDeclarationNode( ts: typeof import('typescript'), sourceFile: ts.SourceFile, @@ -298,7 +300,6 @@ function generateCommentMarkdown(parts: ts.SymbolDisplayPart[], jsDocTags: ts.JS return result; } - function _symbolDisplayPartsToMarkdown(parts: ts.SymbolDisplayPart[]) { return parts.map(part => { switch (part.kind) { @@ -312,7 +313,6 @@ function _symbolDisplayPartsToMarkdown(parts: ts.SymbolDisplayPart[]) { }).join(''); } - function _jsDocTagInfoToMarkdown(jsDocTags: ts.JSDocTagInfo[]) { return jsDocTags.map(tag => { const tagName = `*@${tag.name}*`; diff --git a/test-workspace/tsc/passedFixtures/vue3/#5097/child.ts b/test-workspace/tsc/passedFixtures/vue3/#5097/child.ts new file mode 100644 index 0000000000..85375a6521 --- /dev/null +++ b/test-workspace/tsc/passedFixtures/vue3/#5097/child.ts @@ -0,0 +1 @@ +export { default as Main } from './child.vue'; \ No newline at end of file diff --git a/test-workspace/tsc/passedFixtures/vue3/#5097/child.vue b/test-workspace/tsc/passedFixtures/vue3/#5097/child.vue new file mode 100644 index 0000000000..14869f761e --- /dev/null +++ b/test-workspace/tsc/passedFixtures/vue3/#5097/child.vue @@ -0,0 +1,5 @@ + diff --git a/test-workspace/tsc/passedFixtures/vue3/#5097/main.vue b/test-workspace/tsc/passedFixtures/vue3/#5097/main.vue new file mode 100644 index 0000000000..f3b923a5b4 --- /dev/null +++ b/test-workspace/tsc/passedFixtures/vue3/#5097/main.vue @@ -0,0 +1,11 @@ + + +