From 143e00ed2d27cb310f4228f468cea35602c4cb03 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:05:34 +0000 Subject: [PATCH 1/5] fix(http-server-csharp): sanitize @friendlyName template placeholders in model file names Co-authored-by: abatishchev <351644+abatishchev@users.noreply.github.com> --- ...csharp-fix-friendly-name-file-2026-7-29.md | 7 ++++++ .../src/components/models/model-helpers.ts | 7 +++++- .../test/generation.test.ts | 22 +++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 .chronus/changes/http-server-csharp-fix-friendly-name-file-2026-7-29.md diff --git a/.chronus/changes/http-server-csharp-fix-friendly-name-file-2026-7-29.md b/.chronus/changes/http-server-csharp-fix-friendly-name-file-2026-7-29.md new file mode 100644 index 00000000000..fe9ce2cb565 --- /dev/null +++ b/.chronus/changes/http-server-csharp-fix-friendly-name-file-2026-7-29.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http-server-csharp" +--- + +Fix generated model file name when `@friendlyName` contains unresolved template parameter placeholders such as `{name}` (e.g. `{name}TagsUpdate`). The file is now named using the PascalCase expansion of the placeholder (e.g. `NameTagsUpdate.cs`) instead of the literal `{name}TagsUpdate.cs`. diff --git a/packages/http-server-csharp/src/components/models/model-helpers.ts b/packages/http-server-csharp/src/components/models/model-helpers.ts index 0afa6cc0173..e4261f87cef 100644 --- a/packages/http-server-csharp/src/components/models/model-helpers.ts +++ b/packages/http-server-csharp/src/components/models/model-helpers.ts @@ -287,7 +287,12 @@ export function getCSharpTypeString(program: Program, type: Type): string { export function getModelEmitName(program: Program, model: Model): string { // Check for @friendlyName first const friendlyName = getFriendlyName(program, model); - if (friendlyName) return friendlyName; + if (friendlyName) { + // Replace any unresolved template parameter placeholders like {name} with PascalCase'd names. + // This happens when @friendlyName is applied with a TemplateParameter sourceObject that + // couldn't be resolved (e.g. @@friendlyName(TagsUpdate, "{name}TagsUpdate", Resource)). + return friendlyName.replace(/\{(\w+)\}/g, (_, n) => n.charAt(0).toUpperCase() + n.slice(1)); + } // Anonymous models get sequential names if (!model.name || model.name === "") { diff --git a/packages/http-server-csharp/test/generation.test.ts b/packages/http-server-csharp/test/generation.test.ts index 9f17b2c02e5..8129a4ce4b2 100644 --- a/packages/http-server-csharp/test/generation.test.ts +++ b/packages/http-server-csharp/test/generation.test.ts @@ -3448,3 +3448,25 @@ it("emits class for model extending another model with no additional properties" ], ); }); + +it("emits correct file name for @friendlyName with unresolved template placeholders", async () => { + // Regression test for https://github.com/microsoft/typespec/issues/11454 + // When @friendlyName contains {paramName} placeholders (e.g. from ARM-style templates + // that store the format string without a sourceObject for substitution), the file name + // must use the PascalCase'd placeholder, not the raw {paramName} literal. + const fs = await compileAndValidateMultiple( + tester, + ` + @friendlyName("{name}TagsUpdate") + model NameTagsUpdate { + tags?: string; + } + + @route("/name") @get op getName(): NameTagsUpdate; + `, + [["NameTagsUpdate.cs", ["public partial class NameTagsUpdate"]]], + ); + // Verify there is no file with the literal {name} in the path + const badFile = [...fs.fs.keys()].find((k) => k.includes("{name}")); + assert.strictEqual(badFile, undefined, `Found file with literal {name} in path: ${badFile}`); +}); From dfd71f3b8f3915b1ec5b5570cf339fdbcba12f8f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Jul 2026 23:15:00 +0000 Subject: [PATCH 2/5] fix(compiler): don't store unresolved @friendlyName when sourceObject is a TemplateParameter When `@friendlyName` is applied to a template model with a template parameter as the sourceObject (e.g., `@friendlyName("{name}TagsUpdate", T)` on `Model`), the decorator is called on the template declaration with `sourceObject` being a `TemplateParameter`. In that case, `replaceTemplatedStringFromProperties` leaves the `{name}` placeholders unresolved in the stored friendly name. Fix: in `$friendlyName`, return early when `sourceObject.kind === "TemplateParameter"` rather than storing an unresolved format string. The decorator is re-applied when the template is instantiated with a concrete type, which correctly resolves the placeholders. Also: - Revert the downstream workaround in `getModelEmitName` in http-server-csharp - Update the http-server-csharp test to use the actual ARM-style template pattern - Add a compiler test verifying template declarations don't get unresolved friendly names Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/compiler/src/lib/decorators.ts | 6 +++++ .../test/decorators/decorators.test.ts | 17 ++++++++++++++ .../src/components/models/model-helpers.ts | 7 +----- .../test/generation.test.ts | 23 ++++++++----------- 4 files changed, 34 insertions(+), 19 deletions(-) diff --git a/packages/compiler/src/lib/decorators.ts b/packages/compiler/src/lib/decorators.ts index 18bb45e365a..17222bea58a 100644 --- a/packages/compiler/src/lib/decorators.ts +++ b/packages/compiler/src/lib/decorators.ts @@ -1155,6 +1155,12 @@ export const $friendlyName: FriendlyNameDecorator = ( // If an object was passed in, use it to format the friendly name if (sourceObject) { + // Template parameters are not valid source objects - skip storing the friendly name + // for template declarations. The decorator will be re-applied when the template is + // instantiated with a concrete type, at which point the placeholders can be resolved. + if (sourceObject.kind === "TemplateParameter") { + return; + } friendlyName = replaceTemplatedStringFromProperties(friendlyName, sourceObject); } diff --git a/packages/compiler/test/decorators/decorators.test.ts b/packages/compiler/test/decorators/decorators.test.ts index 07ef224ea2d..dad29bcc1fc 100644 --- a/packages/compiler/test/decorators/decorators.test.ts +++ b/packages/compiler/test/decorators/decorators.test.ts @@ -369,6 +369,23 @@ describe("@friendlyName", () => { strictEqual(getFriendlyName(program, B), "BModel"); }); + it("does not store friendly name on template declaration when sourceObject is a TemplateParameter", async () => { + const { TemplatedModel, FooModel, program } = await Tester.compile(t.code` + @friendlyName("{name}Updated", T) + model ${t.model("TemplatedModel")} { } + + model ${t.model("FooModel")} extends TemplatedModel {} + `); + // Template declaration should NOT have a friendly name stored + // (the sourceObject was a TemplateParameter at decoration time) + strictEqual(getFriendlyName(program, TemplatedModel), undefined); + + // The instantiation (accessible as the base of FooModel) should have the correct friendly name + const instantiation = FooModel.baseModel; + ok(instantiation, "FooModel should have a baseModel"); + strictEqual(getFriendlyName(program, instantiation), "FooModelUpdated"); + }); + it(" @friendlyName doesn't carry over to derived models", async () => { const { A, B, program } = await Tester.compile(t.code` @friendlyName("MyNameIsA") diff --git a/packages/http-server-csharp/src/components/models/model-helpers.ts b/packages/http-server-csharp/src/components/models/model-helpers.ts index e4261f87cef..0afa6cc0173 100644 --- a/packages/http-server-csharp/src/components/models/model-helpers.ts +++ b/packages/http-server-csharp/src/components/models/model-helpers.ts @@ -287,12 +287,7 @@ export function getCSharpTypeString(program: Program, type: Type): string { export function getModelEmitName(program: Program, model: Model): string { // Check for @friendlyName first const friendlyName = getFriendlyName(program, model); - if (friendlyName) { - // Replace any unresolved template parameter placeholders like {name} with PascalCase'd names. - // This happens when @friendlyName is applied with a TemplateParameter sourceObject that - // couldn't be resolved (e.g. @@friendlyName(TagsUpdate, "{name}TagsUpdate", Resource)). - return friendlyName.replace(/\{(\w+)\}/g, (_, n) => n.charAt(0).toUpperCase() + n.slice(1)); - } + if (friendlyName) return friendlyName; // Anonymous models get sequential names if (!model.name || model.name === "") { diff --git a/packages/http-server-csharp/test/generation.test.ts b/packages/http-server-csharp/test/generation.test.ts index 8129a4ce4b2..871e7bad09b 100644 --- a/packages/http-server-csharp/test/generation.test.ts +++ b/packages/http-server-csharp/test/generation.test.ts @@ -3449,24 +3449,21 @@ it("emits class for model extending another model with no additional properties" ); }); -it("emits correct file name for @friendlyName with unresolved template placeholders", async () => { - // Regression test for https://github.com/microsoft/typespec/issues/11454 - // When @friendlyName contains {paramName} placeholders (e.g. from ARM-style templates - // that store the format string without a sourceObject for substitution), the file name - // must use the PascalCase'd placeholder, not the raw {paramName} literal. - const fs = await compileAndValidateMultiple( +it("emits correct file name for @friendlyName with template sourceObject", async () => { + // Regression test: when @friendlyName uses a template sourceObject (ARM-style), + // the concrete instantiation gets the correctly substituted friendly name as its file name. + await compileAndValidateMultiple( tester, ` - @friendlyName("{name}TagsUpdate") - model NameTagsUpdate { + @friendlyName("{name}TagsUpdate", Resource) + model TagsUpdate { tags?: string; } - @route("/name") @get op getName(): NameTagsUpdate; + model FooResource { id: string; } + + @route("/tags") @get op getTags(): TagsUpdate; `, - [["NameTagsUpdate.cs", ["public partial class NameTagsUpdate"]]], + [["FooResourceTagsUpdate.cs", ["public partial class FooResourceTagsUpdate"]]], ); - // Verify there is no file with the literal {name} in the path - const badFile = [...fs.fs.keys()].find((k) => k.includes("{name}")); - assert.strictEqual(badFile, undefined, `Found file with literal {name} in path: ${badFile}`); }); From 8f11c5269f121c4e979085b332123f69b5f003de Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 00:26:15 +0000 Subject: [PATCH 3/5] work in progress: investigating root cause Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/compiler/src/lib/decorators.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/compiler/src/lib/decorators.ts b/packages/compiler/src/lib/decorators.ts index 17222bea58a..18bb45e365a 100644 --- a/packages/compiler/src/lib/decorators.ts +++ b/packages/compiler/src/lib/decorators.ts @@ -1155,12 +1155,6 @@ export const $friendlyName: FriendlyNameDecorator = ( // If an object was passed in, use it to format the friendly name if (sourceObject) { - // Template parameters are not valid source objects - skip storing the friendly name - // for template declarations. The decorator will be re-applied when the template is - // instantiated with a concrete type, at which point the placeholders can be resolved. - if (sourceObject.kind === "TemplateParameter") { - return; - } friendlyName = replaceTemplatedStringFromProperties(friendlyName, sourceObject); } From 1256020407fdd8f6a9b7b176e7d3f5825441bbbc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 00:34:49 +0000 Subject: [PATCH 4/5] fix(compiler): guard applyDecoratorToType against TemplateParameter args Add a safety net in `applyDecoratorToType` (checker.ts) that skips running a decorator when any of its arguments is an unresolved `TemplateParameter` or `TemplateParameterAccess`. The checker already ensures decorators are not applied to template declarations via `skipDecorators: true`, but this explicit guard at the call site makes the protection robust and self-documenting. Also: - Revert the incorrect guard from `decorators.ts` (wrong layer) - Revert the `getModelEmitName` sanitization workaround (not needed) - Update test description in `decorators.test.ts` to accurately reflect the mechanism (skipDecorators, not a guard in the decorator itself) - Update the `generation.test.ts` test to use the real ARM-style pattern Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- ...compiler-fix-template-param-decorator-2026-7-30.md | 7 +++++++ ...-server-csharp-fix-friendly-name-file-2026-7-29.md | 2 +- packages/compiler/src/core/checker.ts | 11 +++++++++++ packages/compiler/test/decorators/decorators.test.ts | 8 +++++--- 4 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 .chronus/changes/compiler-fix-template-param-decorator-2026-7-30.md diff --git a/.chronus/changes/compiler-fix-template-param-decorator-2026-7-30.md b/.chronus/changes/compiler-fix-template-param-decorator-2026-7-30.md new file mode 100644 index 00000000000..2d56aaf0d92 --- /dev/null +++ b/.chronus/changes/compiler-fix-template-param-decorator-2026-7-30.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/compiler" +--- + +`applyDecoratorToType` now skips executing a decorator if any of its arguments is an unresolved `TemplateParameter` or `TemplateParameterAccess`. This is a safety-net guard — the checker already sets `skipDecorators: true` on template declarations — but makes the protection explicit and robust against future refactoring. diff --git a/.chronus/changes/http-server-csharp-fix-friendly-name-file-2026-7-29.md b/.chronus/changes/http-server-csharp-fix-friendly-name-file-2026-7-29.md index fe9ce2cb565..e1416f424f9 100644 --- a/.chronus/changes/http-server-csharp-fix-friendly-name-file-2026-7-29.md +++ b/.chronus/changes/http-server-csharp-fix-friendly-name-file-2026-7-29.md @@ -4,4 +4,4 @@ packages: - "@typespec/http-server-csharp" --- -Fix generated model file name when `@friendlyName` contains unresolved template parameter placeholders such as `{name}` (e.g. `{name}TagsUpdate`). The file is now named using the PascalCase expansion of the placeholder (e.g. `NameTagsUpdate.cs`) instead of the literal `{name}TagsUpdate.cs`. +Fix model file name for `@friendlyName` ARM-style template patterns (e.g. `@friendlyName("{name}TagsUpdate", Resource)` on `TagsUpdate`). The instantiation now correctly receives the substituted name (e.g. `FooResourceTagsUpdate.cs`) from the compiler. diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index 30f44f4c4c5..c8f6b046049 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -8739,6 +8739,17 @@ function applyDecoratorToType( // If one of the decorator argument is an error don't run it. return; } + if ( + isType(arg.value) && + (arg.value.kind === "TemplateParameter" || arg.value.kind === "TemplateParameterAccess") + ) { + // If one of the decorator arguments is an unresolved template parameter or a + // template parameter access (e.g. T.name), don't run the decorator. + // This is a safety net — decorators should not be invoked on template declarations + // (the checker sets skipDecorators: true for those), but this guard ensures + // correctness if that protection is ever bypassed. + return; + } } // Is the decorator definition deprecated? diff --git a/packages/compiler/test/decorators/decorators.test.ts b/packages/compiler/test/decorators/decorators.test.ts index dad29bcc1fc..2c5040de35b 100644 --- a/packages/compiler/test/decorators/decorators.test.ts +++ b/packages/compiler/test/decorators/decorators.test.ts @@ -369,18 +369,20 @@ describe("@friendlyName", () => { strictEqual(getFriendlyName(program, B), "BModel"); }); - it("does not store friendly name on template declaration when sourceObject is a TemplateParameter", async () => { + it("does not apply @friendlyName decorator on template declaration — only on instantiations", async () => { const { TemplatedModel, FooModel, program } = await Tester.compile(t.code` @friendlyName("{name}Updated", T) model ${t.model("TemplatedModel")} { } model ${t.model("FooModel")} extends TemplatedModel {} `); - // Template declaration should NOT have a friendly name stored - // (the sourceObject was a TemplateParameter at decoration time) + // Template declaration should NOT have a friendly name stored. + // Decorators are not applied to template declarations (skipDecorators is true in the checker), + // so the unresolved template parameter T never reaches the decorator implementation. strictEqual(getFriendlyName(program, TemplatedModel), undefined); // The instantiation (accessible as the base of FooModel) should have the correct friendly name + // because the decorator is applied with the concrete type FooModel as sourceObject. const instantiation = FooModel.baseModel; ok(instantiation, "FooModel should have a baseModel"); strictEqual(getFriendlyName(program, instantiation), "FooModelUpdated"); From 6f90881abfdda7aaa4e244a1692f69b31b10d5c0 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 30 Jul 2026 07:59:46 -0400 Subject: [PATCH 5/5] fix(compiler): enter template scope before resolving operation param defaults Operations resolved template parameter defaults before entering the template declaration scope, unlike models/interfaces. A decorated template used as a parameter default (e.g. the ARM `Properties = TagsUpdateModel` pattern) therefore ran its decorators with the still-unresolved template parameter, storing unresolved `{name}` friendly names. Move the InTemplateDeclaration flag before checkTemplateDeclaration in checkOperation and drop the applyDecoratorToType band-aid guard. Fixes https://github.com/microsoft/typespec/issues/11454 --- ...-fix-template-param-decorator-2026-7-30.md | 2 +- packages/compiler/src/core/checker.ts | 23 ++++--- .../test/decorators/decorators.test.ts | 66 ++++++++++++++----- .../test/generation.test.ts | 40 +++++++---- 4 files changed, 90 insertions(+), 41 deletions(-) diff --git a/.chronus/changes/compiler-fix-template-param-decorator-2026-7-30.md b/.chronus/changes/compiler-fix-template-param-decorator-2026-7-30.md index 2d56aaf0d92..ac0cffe1279 100644 --- a/.chronus/changes/compiler-fix-template-param-decorator-2026-7-30.md +++ b/.chronus/changes/compiler-fix-template-param-decorator-2026-7-30.md @@ -4,4 +4,4 @@ packages: - "@typespec/compiler" --- -`applyDecoratorToType` now skips executing a decorator if any of its arguments is an unresolved `TemplateParameter` or `TemplateParameterAccess`. This is a safety-net guard — the checker already sets `skipDecorators: true` on template declarations — but makes the protection explicit and robust against future refactoring. +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>(...)`, the ARM `TagsUpdateModel` 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. diff --git a/packages/compiler/src/core/checker.ts b/packages/compiler/src/core/checker.ts index c8f6b046049..19668f0005e 100644 --- a/packages/compiler/src/core/checker.ts +++ b/packages/compiler/src/core/checker.ts @@ -2629,6 +2629,16 @@ 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`), 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 @@ -2636,7 +2646,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker 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); } @@ -8739,17 +8749,6 @@ function applyDecoratorToType( // If one of the decorator argument is an error don't run it. return; } - if ( - isType(arg.value) && - (arg.value.kind === "TemplateParameter" || arg.value.kind === "TemplateParameterAccess") - ) { - // If one of the decorator arguments is an unresolved template parameter or a - // template parameter access (e.g. T.name), don't run the decorator. - // This is a safety net — decorators should not be invoked on template declarations - // (the checker sets skipDecorators: true for those), but this guard ensures - // correctness if that protection is ever bypassed. - return; - } } // Is the decorator definition deprecated? diff --git a/packages/compiler/test/decorators/decorators.test.ts b/packages/compiler/test/decorators/decorators.test.ts index 2c5040de35b..b7a72b91a68 100644 --- a/packages/compiler/test/decorators/decorators.test.ts +++ b/packages/compiler/test/decorators/decorators.test.ts @@ -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 /** */", () => { @@ -369,23 +370,58 @@ describe("@friendlyName", () => { strictEqual(getFriendlyName(program, B), "BModel"); }); - it("does not apply @friendlyName decorator on template declaration — only on instantiations", async () => { - const { TemplatedModel, FooModel, program } = await Tester.compile(t.code` - @friendlyName("{name}Updated", T) - model ${t.model("TemplatedModel")} { } + 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`, 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"; - model ${t.model("FooModel")} extends TemplatedModel {} - `); - // Template declaration should NOT have a friendly name stored. - // Decorators are not applied to template declarations (skipDecorators is true in the checker), - // so the unresolved template parameter T never reaches the decorator implementation. - strictEqual(getFriendlyName(program, TemplatedModel), undefined); + namespace Test { + extern dec track(target: unknown, arg: unknown); + } + + using Test; + + @track(Resource) + model TagsUpdateModel { + tags?: string; + } + + op armTagsPatch< + Resource extends {}, + Properties extends {} = TagsUpdateModel + >(body: Properties): void; - // The instantiation (accessible as the base of FooModel) should have the correct friendly name - // because the decorator is applied with the concrete type FooModel as sourceObject. - const instantiation = FooModel.baseModel; - ok(instantiation, "FooModel should have a baseModel"); - strictEqual(getFriendlyName(program, instantiation), "FooModelUpdated"); + model FooResource { id: string; } + + op patch is armTagsPatch; + `, + ); + 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 instantiation", + ); }); it(" @friendlyName doesn't carry over to derived models", async () => { diff --git a/packages/http-server-csharp/test/generation.test.ts b/packages/http-server-csharp/test/generation.test.ts index 871e7bad09b..6badfa95a47 100644 --- a/packages/http-server-csharp/test/generation.test.ts +++ b/packages/http-server-csharp/test/generation.test.ts @@ -3449,21 +3449,35 @@ it("emits class for model extending another model with no additional properties" ); }); -it("emits correct file name for @friendlyName with template sourceObject", async () => { - // Regression test: when @friendlyName uses a template sourceObject (ARM-style), - // the concrete instantiation gets the correctly substituted friendly name as its file name. - await compileAndValidateMultiple( - tester, - ` - @friendlyName("{name}TagsUpdate", Resource) - model TagsUpdate { - tags?: string; - } +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` 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 { + tags?: string; + } + + op armTagsPatch< + Resource extends {}, + Properties extends {} = TagsUpdateModel + >(...Properties): void; - model FooResource { id: string; } + model FooResource { id: string; } - @route("/tags") @get op getTags(): TagsUpdate; + @route("/tags") @patch op patch is armTagsPatch; + } `, - [["FooResourceTagsUpdate.cs", ["public partial class FooResourceTagsUpdate"]]], ); + 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}`); });