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: Augment decorators might not run if alias caused the type to be resolved too early #2603

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
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@typespec/compiler",
"comment": "Fix: Issue where referencing a template in an alias might cause augment decorators to not be applied on types referenced in the aliased type.",
"type": "none"
}
],
"packageName": "@typespec/compiler"
}
98 changes: 73 additions & 25 deletions packages/compiler/src/core/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1858,14 +1858,14 @@ export function createChecker(program: Program): Checker {
function resolveIdentifierInTable(
node: IdentifierNode,
table: SymbolTable | undefined,
resolveDecorator = false
options: SymbolResolutionOptions
): Sym | undefined {
if (!table) {
return undefined;
}
table = augmentedSymbolTables.get(table) ?? table;
let sym;
if (resolveDecorator) {
if (options.resolveDecorators) {
sym = table.get("@" + node.sv);
} else {
sym = table.get(node.sv);
Expand Down Expand Up @@ -1921,7 +1921,11 @@ export function createChecker(program: Program): Checker {
}

lateBindMembers(containerType, container);
sym = resolveIdentifierInTable(id, container.exports ?? container.members);
sym = resolveIdentifierInTable(
id,
container.exports ?? container.members,
defaultSymbolResolutionOptions
);
break;
case IdentifierKind.Other:
return undefined;
Expand Down Expand Up @@ -1976,7 +1980,7 @@ export function createChecker(program: Program): Checker {
let base = resolveTypeReferenceSym(identifier.parent.base, undefined, false);
if (base) {
if (base.flags & SymbolFlags.Alias) {
base = getAliasedSymbol(base, undefined);
base = getAliasedSymbol(base, undefined, defaultSymbolResolutionOptions);
}
if (base) {
if (isTemplatedNode(base.declarations[0])) {
Expand Down Expand Up @@ -2075,7 +2079,7 @@ export function createChecker(program: Program): Checker {
function resolveIdentifierInScope(
node: IdentifierNode,
mapper: TypeMapper | undefined,
resolveDecorator = false
options: SymbolResolutionOptions
): Sym | undefined {
compilerAssert(
node.parent?.kind !== SyntaxKind.MemberExpression || node.parent.id !== node,
Expand All @@ -2094,12 +2098,12 @@ export function createChecker(program: Program): Checker {
while (scope && scope.kind !== SyntaxKind.TypeSpecScript) {
if (scope.symbol && "exports" in scope.symbol) {
const mergedSymbol = getMergedSymbol(scope.symbol);
binding = resolveIdentifierInTable(node, mergedSymbol.exports, resolveDecorator);
binding = resolveIdentifierInTable(node, mergedSymbol.exports, options);
if (binding) return binding;
}

if ("locals" in scope) {
binding = resolveIdentifierInTable(node, scope.locals, resolveDecorator);
binding = resolveIdentifierInTable(node, scope.locals, options);
if (binding) return binding;
}

Expand All @@ -2110,7 +2114,7 @@ export function createChecker(program: Program): Checker {
// check any blockless namespace decls
for (const ns of scope.inScopeNamespaces) {
const mergedSymbol = getMergedSymbol(ns.symbol);
binding = resolveIdentifierInTable(node, mergedSymbol.exports, resolveDecorator);
binding = resolveIdentifierInTable(node, mergedSymbol.exports, options);

if (binding) return binding;
}
Expand All @@ -2119,11 +2123,11 @@ export function createChecker(program: Program): Checker {
const globalBinding = resolveIdentifierInTable(
node,
globalNamespaceNode.symbol.exports,
resolveDecorator
options
);

// check using types
const usingBinding = resolveIdentifierInTable(node, scope.locals, resolveDecorator);
const usingBinding = resolveIdentifierInTable(node, scope.locals, options);

if (globalBinding && usingBinding) {
reportAmbiguousIdentifier(node, [globalBinding, usingBinding]);
Expand All @@ -2146,20 +2150,25 @@ export function createChecker(program: Program): Checker {
function resolveTypeReferenceSym(
node: TypeReferenceNode | MemberExpressionNode | IdentifierNode,
mapper: TypeMapper | undefined,
resolveDecorator = false
options?: Partial<SymbolResolutionOptions> | boolean
): Sym | undefined {
if (mapper === undefined && referenceSymCache.has(node)) {
return referenceSymCache.get(node);
}
const sym = resolveTypeReferenceSymInternal(node, mapper, resolveDecorator);

const resolvedOptions: SymbolResolutionOptions =
typeof options === "boolean"
? { ...defaultSymbolResolutionOptions, resolveDecorators: options }
: { ...defaultSymbolResolutionOptions, ...(options ?? {}) };
const sym = resolveTypeReferenceSymInternal(node, mapper, resolvedOptions);
referenceSymCache.set(node, sym);
return sym;
}

function resolveTypeReferenceSymInternal(
node: TypeReferenceNode | MemberExpressionNode | IdentifierNode,
mapper: TypeMapper | undefined,
resolveDecorator = false
options: SymbolResolutionOptions
): Sym | undefined {
if (hasParseError(node)) {
// Don't report synthetic identifiers used for parser error recovery.
Expand All @@ -2168,7 +2177,7 @@ export function createChecker(program: Program): Checker {
}

if (node.kind === SyntaxKind.TypeReference) {
return resolveTypeReferenceSym(node.target, mapper, resolveDecorator);
return resolveTypeReferenceSym(node.target, mapper, options);
}

if (node.kind === SyntaxKind.MemberExpression) {
Expand All @@ -2179,21 +2188,21 @@ export function createChecker(program: Program): Checker {

// when resolving a type reference based on an alias, unwrap the alias.
if (base.flags & SymbolFlags.Alias) {
base = getAliasedSymbol(base, mapper);
base = getAliasedSymbol(base, mapper, options);
if (!base) {
return undefined;
}
}

if (node.selector === ".") {
return resolveMemberInContainer(node, base, mapper, resolveDecorator);
return resolveMemberInContainer(node, base, mapper, options);
} else {
return resolveMetaProperty(node, base);
}
}

if (node.kind === SyntaxKind.Identifier) {
const sym = resolveIdentifierInScope(node, mapper, resolveDecorator);
const sym = resolveIdentifierInScope(node, mapper, options);
if (!sym) return undefined;

return sym.flags & SymbolFlags.Using ? sym.symbolSource : sym;
Expand All @@ -2206,10 +2215,10 @@ export function createChecker(program: Program): Checker {
node: MemberExpressionNode,
base: Sym,
mapper: TypeMapper | undefined,
resolveDecorator: boolean
options: SymbolResolutionOptions
) {
if (base.flags & SymbolFlags.Namespace) {
const symbol = resolveIdentifierInTable(node.id, base.exports, resolveDecorator);
const symbol = resolveIdentifierInTable(node.id, base.exports, options);
if (!symbol) {
reportCheckerDiagnostic(
createDiagnostic({
Expand Down Expand Up @@ -2247,7 +2256,7 @@ export function createChecker(program: Program): Checker {

return undefined;
} else if (base.flags & SymbolFlags.MemberContainer) {
if (isTemplatedNode(base.declarations[0])) {
if (options.checkTemplateTypes && isTemplatedNode(base.declarations[0])) {
const type =
base.flags & SymbolFlags.LateBound
? base.type!
Expand All @@ -2256,7 +2265,7 @@ export function createChecker(program: Program): Checker {
lateBindMembers(type, base);
}
}
const sym = resolveIdentifierInTable(node.id, base.members!, resolveDecorator);
const sym = resolveIdentifierInTable(node.id, base.members!, options);
if (!sym) {
reportCheckerDiagnostic(
createDiagnostic({
Expand Down Expand Up @@ -2287,7 +2296,10 @@ export function createChecker(program: Program): Checker {
}

function resolveMetaProperty(node: MemberExpressionNode, base: Sym) {
const resolved = resolveIdentifierInTable(node.id, base.metatypeMembers);
const resolved = resolveIdentifierInTable(node.id, base.metatypeMembers, {
resolveDecorators: false,
checkTemplateTypes: false,
});
if (!resolved) {
reportCheckerDiagnostic(
createDiagnostic({
Expand Down Expand Up @@ -2326,8 +2338,23 @@ export function createChecker(program: Program): Checker {
* (i.e. they contain symbols we don't know until we've instantiated the type and the type is an
* instantiation) we late bind the container which creates the symbol that will hold its members.
*/
function getAliasedSymbol(aliasSymbol: Sym, mapper: TypeMapper | undefined): Sym | undefined {
const aliasType = getTypeForNode(aliasSymbol.declarations[0] as AliasStatementNode, mapper);
function getAliasedSymbol(
aliasSymbol: Sym,
mapper: TypeMapper | undefined,
options: SymbolResolutionOptions
): Sym | undefined {
const node = aliasSymbol.declarations[0];
const targetNode = node.kind === SyntaxKind.AliasStatement ? node.value : node;
const sym = resolveTypeReferenceSymInternal(targetNode as any, mapper, options);
if (sym === undefined) {
return undefined;
}
const resolvedTargetNode = sym.declarations[0];
if (!options.checkTemplateTypes || !isTemplatedNode(resolvedTargetNode)) {
return sym;
}

const aliasType = getTypeForNode(node as AliasStatementNode, mapper);
if (isErrorType(aliasType)) {
return undefined;
}
Expand Down Expand Up @@ -2827,7 +2854,9 @@ export function createChecker(program: Program): Checker {
table.set("parameters", node.signature.parameters.symbol);
table.set("returnType", node.signature.returnType.symbol);
} else {
const sig = resolveTypeReferenceSym(node.signature.baseOperation, undefined);
const sig = resolveTypeReferenceSym(node.signature.baseOperation, undefined, {
checkTemplateTypes: false,
});
if (sig) {
visit(sig.declarations[0], sig);
const sigTable = getOrCreateAugmentedSymbolTable(sig.metatypeMembers!);
Expand Down Expand Up @@ -6076,3 +6105,22 @@ enum Related {
true = 1,
maybe = 2,
}

interface SymbolResolutionOptions {
/**
* Should resolving the symbol lookup for decorators as well.
* @default false
*/
resolveDecorators: boolean;

/**
* Should the symbol resolution instantiate templates and do a late bind of symbols.
* @default true
*/
checkTemplateTypes: boolean;
}

const defaultSymbolResolutionOptions: SymbolResolutionOptions = {
resolveDecorators: false,
checkTemplateTypes: true,
};
31 changes: 31 additions & 0 deletions packages/compiler/test/checker/augment-decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ describe("compiler: checker: augment decorators", () => {
},
});
});

it("can be defined at the root of document", async () => {
testHost.addTypeSpecFile(
"test.tsp",
Expand Down Expand Up @@ -119,6 +120,36 @@ describe("compiler: checker: augment decorators", () => {
const { Foo } = await testHost.compile("test.tsp");
strictEqual(Foo, blueThing);
});

// Regression for https://github.com/microsoft/typespec/issues/2600
it("alias of instantiated template doesn't interfere with augment decorators", async () => {
// Here we could have add an issue where `Foo` would have been checked before the `@@blue` augment decorator could be run
// As we resolve the member symbols and meta types early,
// alias `FactoryString` would have checked the template instance `Factory<string>`
// which would then have checked `Foo` and then `@@blue` wouldn't have been run
testHost.addTypeSpecFile(
"test.tsp",
`
import "./test.js";

@test model Foo {};

interface Factory<T> {
op Action(): Foo;
}

alias FactoryString = Factory<string>;

op test is FactoryString.Action;

@@doc(Foo, "This doc");
@@blue(Foo);
`
);

const { Foo } = await testHost.compile("test.tsp");
strictEqual(Foo, blueThing);
});
});

describe("augment types", () => {
Expand Down