Skip to content
Closed
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,7 @@
---
changeKind: fix
packages:
- "@typespec/compiler"
---

Fix decorators running with unresolved template parameters when a decorated template is used as a template parameter default of an operation (e.g. `op foo<Resource, Properties = Decorated<Resource>>(...)`, the ARM `TagsUpdateModel<Resource>` pattern). Operations now enter the template declaration scope before resolving template parameter defaults, so decorators on those defaults are no longer executed with the still-unresolved template parameter. This matches the existing behavior for models and interfaces.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
Comment thread
abatishchev marked this conversation as resolved.
changeKind: fix
packages:
- "@typespec/http-server-csharp"
---

Fix model file name for `@friendlyName` ARM-style template patterns (e.g. `@friendlyName("{name}TagsUpdate", Resource)` on `TagsUpdate<Resource>`). The instantiation now correctly receives the substituted name (e.g. `FooResourceTagsUpdate.cs`) from the compiler.
12 changes: 11 additions & 1 deletion packages/compiler/src/core/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2629,14 +2629,24 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
node.parent,
);
}
// Enter the template declaration scope before checking the template declaration itself.
// `checkTemplateDeclaration` resolves template parameter defaults (e.g.
// `Properties extends {} = TagsUpdateModel<Resource>`), which can instantiate types and run
// their decorators. Those instantiations still reference the enclosing template parameters, so
// they must be treated as being in a template declaration (skipDecorators) to avoid running
// decorators with unresolved template parameters. This mirrors what `checkModelStatement` does.
if (ctx.mapper === undefined && node.templateParameters.length > 0) {
ctx = ctx.withFlags(CheckFlags.InTemplateDeclaration);
}

checkTemplateDeclaration(ctx, node);

// If we are instantating operation inside of interface
if (isTemplatedNode(node) && ctx.mapper !== undefined && parentInterface) {
ctx = ctx.withMapper({ ...ctx.mapper, partial: true });
}

if ((ctx.mapper === undefined || ctx.mapper.partial) && node.templateParameters.length > 0) {
if (ctx.mapper?.partial && node.templateParameters.length > 0) {
ctx = ctx.withFlags(CheckFlags.InTemplateDeclaration);
}

Expand Down
55 changes: 55 additions & 0 deletions packages/compiler/test/decorators/decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
setMediaTypeHint,
} from "../../src/lib/decorators.js";
import { expectDiagnosticEmpty, expectDiagnostics, t } from "../../src/testing/index.js";
import { createTestHost } from "../../src/testing/test-host.js";
import { Tester } from "../tester.js";

describe("dev comment /** */", () => {
Expand Down Expand Up @@ -369,6 +370,60 @@ describe("@friendlyName", () => {
strictEqual(getFriendlyName(program, B), "BModel");
});

it("does not run decorators with unresolved template parameter arguments when a decorated template is used as a template parameter default", async () => {
// Regression test for https://github.com/microsoft/typespec/issues/11454
// When an operation uses a decorated template as a template parameter default
// (e.g. `Properties extends {} = TagsUpdateModel<Resource>`, the ARM tags-update pattern),
// checking that default must happen within the template declaration scope so the
// decorators on the default type are NOT executed with the still-unresolved template parameter.
const host = await createTestHost();
const trackArgKinds: string[] = [];
host.addJsFile("track.js", {
namespace: "Test",
$track(_ctx: any, _target: any, arg: any) {
trackArgKinds.push(arg?.kind);
},
});
host.addTypeSpecFile(
"main.tsp",
`
import "./track.js";

namespace Test {
extern dec track(target: unknown, arg: unknown);
}

using Test;

@track(Resource)
model TagsUpdateModel<Resource extends {}> {
tags?: string;
}

op armTagsPatch<
Resource extends {},
Properties extends {} = TagsUpdateModel<Resource>
>(body: Properties): void;

model FooResource { id: string; }

op patch is armTagsPatch<FooResource>;
`,
);
await host.compile("main.tsp");

// The decorator must only run on the concrete instantiation (FooResource), never with the
// unresolved `Resource` template parameter of the operation.
ok(
!trackArgKinds.includes("TemplateParameter"),
`@track should never run with a TemplateParameter argument, but ran with: [${trackArgKinds.join(", ")}]`,
);
ok(
trackArgKinds.includes("Model"),
"expected @track to run on the concrete TagsUpdateModel<FooResource> instantiation",
);
});

it(" @friendlyName doesn't carry over to derived models", async () => {
const { A, B, program } = await Tester.compile(t.code`
@friendlyName("MyNameIsA")
Expand Down
33 changes: 33 additions & 0 deletions packages/http-server-csharp/test/generation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3448,3 +3448,36 @@ it("emits class for model extending another model with no additional properties"
],
);
});

it("emits correct file name for @friendlyName template used as a template parameter default (ARM pattern)", async () => {
// Regression test for https://github.com/microsoft/typespec/issues/11454
// A model decorated with @friendlyName("{name}...", Resource) is used as an operation template
// parameter default (the ARM `TagsUpdateModel<Resource>` pattern). The concrete instantiation must
// get the substituted friendly name as its file/class name, and no file with an unresolved
// `{name}` placeholder should be emitted.
const [result] = await compileAndDiagnose(
tester,
`
@service(#{title: "Test"})
namespace Test {
@friendlyName("{name}TagsUpdate", Resource)
model TagsUpdateModel<Resource extends {}> {
tags?: string;
}

op armTagsPatch<
Resource extends {},
Properties extends {} = TagsUpdateModel<Resource>
>(...Properties): void;

model FooResource { id: string; }

@route("/tags") @patch op patch is armTagsPatch<FooResource>;
}
`,
);
const files = [...result.fs.fs.keys()];
// No emitted file should contain an unresolved template placeholder.
const unresolved = files.filter((f) => f.includes("{") || f.includes("}"));
deepStrictEqual(unresolved, [], `Unexpected files with unresolved placeholders: ${unresolved}`);
});
Loading