Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(language-core, typescript-plugin): handle self-reference component correctly #5102

Merged
merged 3 commits into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/language-core/lib/codegen/globalTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ export function generateGlobalTypes(lib: string, target: number, strictTemplates
type __VLS_IsAny<T> = 0 extends 1 & T ? true : false;
type __VLS_PickNotAny<A, B> = __VLS_IsAny<A> extends true ? B : A;
type __VLS_unknownDirective = (arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown) => void;
type __VLS_WithComponent<N0 extends string, LocalComponents, N1 extends string, N2 extends string, N3 extends string> =
type __VLS_WithComponent<N0 extends string, LocalComponents, Self, N1 extends string, N2 extends string, N3 extends string> =
N1 extends keyof LocalComponents ? N1 extends N0 ? Pick<LocalComponents, N0 extends keyof LocalComponents ? N0 : never> : { [K in N0]: LocalComponents[N1] } :
N2 extends keyof LocalComponents ? N2 extends N0 ? Pick<LocalComponents, N0 extends keyof LocalComponents ? N0 : never> : { [K in N0]: LocalComponents[N2] } :
N3 extends keyof LocalComponents ? N3 extends N0 ? Pick<LocalComponents, N0 extends keyof LocalComponents ? N0 : never> : { [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] } :
Expand Down
20 changes: 1 addition & 19 deletions packages/language-core/lib/codegen/script/template.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -69,23 +68,6 @@ function* generateTemplateComponents(options: ScriptCodegenOptions): Generator<C
types.push(`typeof __VLS_componentsOption`);
}

let nameType: Code | undefined;
if (options.sfc.script && options.scriptRanges?.exportDefault?.nameOption) {
const { nameOption } = options.scriptRanges.exportDefault;
nameType = options.sfc.script.content.slice(nameOption.start, nameOption.end);
}
else if (options.sfc.scriptSetup) {
const baseName = path.basename(options.fileName);
nameType = `'${options.scriptSetupRanges?.defineOptions?.name ?? baseName.slice(0, baseName.lastIndexOf('.'))}'`;
}
if (nameType) {
types.push(
`{ [K in ${nameType}]: typeof __VLS_self & (new () => { `
+ getSlotsPropertyName(options.vueCompilerOptions.target)
+ `: typeof ${options.scriptSetupRanges?.defineSlots?.name ?? `__VLS_slots`} }) }`
);
}

types.push(`typeof __VLS_ctx`);

yield `type __VLS_LocalComponents =`;
Expand Down
10 changes: 9 additions & 1 deletion packages/language-core/lib/codegen/template/element.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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(`, `);
Expand Down
1 change: 1 addition & 0 deletions packages/language-core/lib/codegen/template/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface TemplateCodegenOptions {
slotsAssignName?: string;
propsAssignName?: string;
inheritAttrs: boolean;
selfComponentName?: string;
}

export function* generateTemplate(options: TemplateCodegenOptions): Generator<Code, TemplateCodegenContext> {
Expand Down
16 changes: 16 additions & 0 deletions packages/language-core/lib/plugins/vue-tsx.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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) {
Expand All @@ -174,6 +189,7 @@ function createTsx(
slotsAssignName: slotsAssignName.get(),
propsAssignName: propsAssignName.get(),
inheritAttrs: inheritAttrs.get(),
selfComponentName: selfComponentName.get()
});

let current = codegen.next();
Expand Down
2 changes: 1 addition & 1 deletion packages/language-server/tests/completions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,8 @@ describe('Completions', async () => {
"component",
"slot",
"template",
"fixture",
"BaseTransition",
"Fixture",
]
`);
});
Expand Down
102 changes: 51 additions & 51 deletions packages/typescript-plugin/lib/requests/componentInfos.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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<string, {
name: string;
required?: true;
Expand Down Expand Up @@ -104,31 +88,11 @@ export function getComponentEvents(
return [];
}

const name = tag.split('.');

let componentSymbol = components.type.getProperty(name[0]);

if (!componentSymbol) {
componentSymbol = 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 Set<string>();

// for (const sig of componentType.getCallSignatures()) {
Expand Down Expand Up @@ -185,26 +149,23 @@ 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(
ts: typeof import('typescript'),
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(
Expand Down Expand Up @@ -242,6 +203,42 @@ export function getElementAttrs(
return [];
}

function getComponentType(
ts: typeof import('typescript'),
languageService: ts.LanguageService,
vueCode: vue.VueVirtualCode,
components: NonNullable<ReturnType<typeof getVariableType>>,
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,
Expand All @@ -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,
Expand Down Expand Up @@ -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) {
Expand All @@ -312,7 +313,6 @@ function _symbolDisplayPartsToMarkdown(parts: ts.SymbolDisplayPart[]) {
}).join('');
}


function _jsDocTagInfoToMarkdown(jsDocTags: ts.JSDocTagInfo[]) {
return jsDocTags.map(tag => {
const tagName = `*@${tag.name}*`;
Expand Down
1 change: 1 addition & 0 deletions test-workspace/tsc/passedFixtures/vue3/#5097/child.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Main } from './child.vue';
5 changes: 5 additions & 0 deletions test-workspace/tsc/passedFixtures/vue3/#5097/child.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script setup lang="ts">
defineProps<{
bar: string;
}>();
</script>
11 changes: 11 additions & 0 deletions test-workspace/tsc/passedFixtures/vue3/#5097/main.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script setup lang="ts">
import { Main } from './child';

defineProps<{
foo: string;
}>();
</script>

<template>
<Main bar=""></Main>
</template>
Loading