Skip to content
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
165 changes: 99 additions & 66 deletions packages/language-core/lib/codegen/template/context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type * as CompilerDOM from '@vue/compiler-dom';
import * as CompilerDOM from '@vue/compiler-dom';
import type { Code, VueCodeInformation } from '../../types';
import { codeFeatures } from '../codeFeatures';
import { InlayHintInfo } from '../inlayHints';
Expand All @@ -8,6 +8,8 @@ import type { TemplateCodegenOptions } from './index';

export type TemplateCodegenContext = ReturnType<typeof createTemplateCodegenContext>;

const commentDirectiveRegex = /^<!--\s*@vue-(?<name>[-\w]+)\b(?<content>[\s\S]*)-->$/;

/**
* Creates and returns a Context object used for generating type-checkable TS code
* from the template section of a .vue file.
Expand Down Expand Up @@ -106,38 +108,29 @@ export type TemplateCodegenContext = ReturnType<typeof createTemplateCodegenCont
* and additionally how we use that to determine whether to propagate diagnostics back upward.
*/
export function createTemplateCodegenContext(options: Pick<TemplateCodegenOptions, 'scriptSetupBindingNames'>) {
let ignoredError = false;
let expectErrorToken: {
errors: number;
node: CompilerDOM.CommentNode;
} | undefined;
let lastGenericComment: {
content: string;
offset: number;
} | undefined;
let variableId = 0;

function resolveCodeFeatures(features: VueCodeInformation) {
if (features.verification) {
if (ignoredError) {
if (features.verification && stack.length) {
const data = stack[stack.length - 1];
if (data.ignoreError) {
// We are currently in a region of code covered by a @vue-ignore directive, so don't
// even bother performing any type-checking: set verification to false.
return {
...features,
verification: false,
};
}
if (expectErrorToken) {
if (data.expectError !== undefined) {
// We are currently in a region of code covered by a @vue-expect-error directive. We need to
// keep track of the number of errors encountered within this region so that we can know whether
// we will need to propagate an "unused ts-expect-error" diagnostic back to the original
// .vue file or not.
const token = expectErrorToken;
return {
...features,
verification: {
shouldReport: () => {
token.errors++;
data.expectError!.token++;
return false;
},
},
Expand Down Expand Up @@ -177,7 +170,23 @@ export function createTemplateCodegenContext(options: Pick<TemplateCodegenOption
offset: number;
}[]>();

const stack: {
ignoreError?: boolean;
expectError?: {
token: number;
node: CompilerDOM.CommentNode;
};
generic?: {
content: string;
offset: number;
},
}[] = [];
const commentBuffer: CompilerDOM.CommentNode[] = [];

return {
get currentInfo() {
return stack[stack.length - 1];
},
codeFeatures: new Proxy(codeFeatures, {
get(target, key: keyof typeof codeFeatures) {
const data = target[key];
Expand All @@ -189,7 +198,6 @@ export function createTemplateCodegenContext(options: Pick<TemplateCodegenOption
dynamicSlots,
dollarVars,
accessExternalVariables,
lastGenericComment,
blockConditions,
scopedClasses,
emptyClassOffsets,
Expand All @@ -203,7 +211,7 @@ export function createTemplateCodegenContext(options: Pick<TemplateCodegenOption
} | undefined,
singleRootElTypes: [] as string[],
singleRootNodes: new Set<CompilerDOM.ElementNode | null>(),
addTemplateRef: (name: string, typeExp: string, offset: number) => {
addTemplateRef(name: string, typeExp: string, offset: number) {
let refs = templateRefs.get(name);
if (!refs) {
templateRefs.set(name, refs = []);
Expand All @@ -219,26 +227,26 @@ export function createTemplateCodegenContext(options: Pick<TemplateCodegenOption
arr.add(offset);
}
},
hasLocalVariable: (name: string) => {
hasLocalVariable(name: string) {
return !!localVars.get(name);
},
addLocalVariable: (name: string) => {
addLocalVariable(name: string) {
localVars.set(name, (localVars.get(name) ?? 0) + 1);
},
removeLocalVariable: (name: string) => {
removeLocalVariable(name: string) {
localVars.set(name, localVars.get(name)! - 1);
},
getInternalVariable: () => {
getInternalVariable() {
return `__VLS_${variableId++}`;
},
getHoistVariable: (originalVar: string) => {
getHoistVariable(originalVar: string) {
let name = hoistVars.get(originalVar);
if (name === undefined) {
hoistVars.set(originalVar, name = `__VLS_${variableId++}`);
}
return name;
},
generateHoistVariables: function* () {
* generateHoistVariables() {
// trick to avoid TS 4081 (#5186)
if (hoistVars.size) {
yield `// @ts-ignore${newLine}`;
Expand All @@ -249,52 +257,12 @@ export function createTemplateCodegenContext(options: Pick<TemplateCodegenOption
yield endOfLine;
}
},
generateConditionGuards: function* () {
* generateConditionGuards() {
for (const condition of blockConditions) {
yield `if (!${condition}) return${endOfLine}`;
}
},
ignoreError: function* (): Generator<Code> {
if (!ignoredError) {
ignoredError = true;
yield `// @vue-ignore start${newLine}`;
}
},
expectError: function* (prevNode: CompilerDOM.CommentNode): Generator<Code> {
if (!expectErrorToken) {
expectErrorToken = {
errors: 0,
node: prevNode,
};
yield `// @vue-expect-error start${newLine}`;
}
},
resetDirectiveComments: function* (endStr: string): Generator<Code> {
if (expectErrorToken) {
const token = expectErrorToken;
yield* wrapWith(
expectErrorToken.node.loc.start.offset,
expectErrorToken.node.loc.end.offset,
{
verification: {
// If no errors/warnings/diagnostics were reported within the region of code covered
// by the @vue-expect-error directive, then we should allow any `unused @ts-expect-error`
// diagnostics to be reported upward.
shouldReport: () => token.errors === 0,
},
},
`// @ts-expect-error __VLS_TS_EXPECT_ERROR`
);
yield `${newLine}${endOfLine}`;
expectErrorToken = undefined;
yield `// @vue-expect-error ${endStr}${newLine}`;
}
if (ignoredError) {
ignoredError = false;
yield `// @vue-ignore ${endStr}${newLine}`;
}
},
generateAutoImportCompletion: function* (): Generator<Code> {
* generateAutoImportCompletion(): Generator<Code> {
const all = [...accessExternalVariables.entries()];
if (!all.some(([_, offsets]) => offsets.size)) {
return;
Expand Down Expand Up @@ -328,6 +296,71 @@ export function createTemplateCodegenContext(options: Pick<TemplateCodegenOption
offsets.clear();
}
yield `]${endOfLine}`;
}
},
enter(node: CompilerDOM.RootNode | CompilerDOM.TemplateChildNode | CompilerDOM.SimpleExpressionNode) {
if (node.type === CompilerDOM.NodeTypes.COMMENT) {
commentBuffer.push(node);
return false;
}

const data: typeof stack[number] = {};
const comments = [...commentBuffer];
commentBuffer.length = 0;

for (const comment of comments) {
const match = comment.loc.source.match(commentDirectiveRegex);
if (match) {
const { name, content } = match.groups!;
switch (name) {
case 'skip': {
return false;
}
case 'ignore': {
data.ignoreError = true;
break;
}
case 'expect-error': {
data.expectError = {
token: 0,
node: comment,
};
break;
}
case 'generic': {
const text = content.trim();
if (text.startsWith('{') && text.endsWith('}')) {
data.generic = {
content: text.slice(1, -1),
offset: comment.loc.start.offset + comment.loc.source.indexOf('{') + 1,
};
}
break;
}
}
}
}
stack.push(data);
return true;
},
* exit(): Generator<Code> {
const data = stack.pop()!;
commentBuffer.length = 0;
if (data.expectError !== undefined) {
yield* wrapWith(
data.expectError.node.loc.start.offset,
data.expectError.node.loc.end.offset,
{
verification: {
// If no errors/warnings/diagnostics were reported within the region of code covered
// by the @vue-expect-error directive, then we should allow any `unused @ts-expect-error`
// diagnostics to be reported upward.
shouldReport: () => data.expectError!.token === 0,
},
},
`// @ts-expect-error`
);
yield `${newLine}${endOfLine}`;
}
},
};
}
5 changes: 2 additions & 3 deletions packages/language-core/lib/codegen/template/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,8 +425,8 @@ function* generateCanonicalComponentName(tagText: string, offset: number, featur
function* generateComponentGeneric(
ctx: TemplateCodegenContext
): Generator<Code> {
if (ctx.lastGenericComment) {
const { content, offset } = ctx.lastGenericComment;
if (ctx.currentInfo.generic) {
const { content, offset } = ctx.currentInfo.generic;
yield* wrapWith(
offset,
offset + content.length,
Expand All @@ -441,7 +441,6 @@ function* generateComponentGeneric(
`>`
);
}
ctx.lastGenericComment = undefined;
}

function* generateElementReference(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,8 @@ export function* generateElementChildren(
ctx: TemplateCodegenContext,
node: CompilerDOM.ElementNode
): Generator<Code> {
yield* ctx.resetDirectiveComments('end of element children start');
let prev: CompilerDOM.TemplateChildNode | undefined;
for (const childNode of node.children) {
yield* generateTemplateChild(options, ctx, childNode, prev);
prev = childNode;
yield* generateTemplateChild(options, ctx, childNode);
}
yield* ctx.generateAutoImportCompletion();
}
2 changes: 1 addition & 1 deletion packages/language-core/lib/codegen/template/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function* generateTemplate(options: TemplateCodegenOptions): Generator<Co
}

if (options.template.ast) {
yield* generateTemplateChild(options, ctx, options.template.ast, undefined);
yield* generateTemplateChild(options, ctx, options.template.ast);
}

yield* generateStyleScopedClassReferences(ctx);
Expand Down
Loading