-
Notifications
You must be signed in to change notification settings - Fork 380
fix(http-server-csharp): prevent _2 suffix on generated class/interface names from library namespace collisions
#11496
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| changeKind: fix | ||
| packages: | ||
| - "@typespec/http-server-csharp" | ||
| --- | ||
|
|
||
| Fix generated class/interface names incorrectly receiving a `_2` suffix (e.g. `OperationsController_2`, `IOperations_2`, `ErrorResponse_2`) when a non-service library namespace (such as `Azure.ResourceManager`) defines types with the same name as types in the `@service` namespace. The emitter now restricts its collection of interfaces and models to `@service`-decorated namespaces, preventing the naming collision. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| // Regression tests for service-discovery issues. | ||
| // These tests verify that the emitter correctly restricts collection to @service-decorated | ||
| // namespaces and does not pick up types from library/helper namespaces. | ||
|
|
||
| import { TesterInstance, TestFileSystem } from "@typespec/compiler/testing"; | ||
| import assert from "assert"; | ||
| import { beforeEach, it } from "vitest"; | ||
| import { ApiTester, compileAndDiagnose } from "./test-host.js"; | ||
|
|
||
| function getOutputFiles(fs: TestFileSystem): Map<string, string> { | ||
| return fs.fs; | ||
| } | ||
|
|
||
| let tester: TesterInstance; | ||
|
|
||
| beforeEach(async () => { | ||
| tester = await ApiTester.createInstance(); | ||
| }); | ||
|
|
||
| // Regression test for https://github.com/microsoft/typespec/issues/11493 | ||
| // When a library namespace (e.g. Azure.ResourceManager) defines an interface with the | ||
| // same name as one in the user's @service namespace (e.g. "Operations"), the emitter | ||
| // must NOT collect the library interface. Previously both were collected and the second | ||
| // got a `_2` suffix, causing a class/constructor name mismatch in the generated C#. | ||
| it("does not emit _2 suffix when a non-service namespace has an interface with the same name", async () => { | ||
| const spec = ` | ||
| // Simulate a library namespace (e.g. Azure.ResourceManager) that has its own | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Simplify this test, the azure mention are irrelevant, here we just want to test that we don't include unreachable types that are not in the service namespace |
||
| // Operations interface – the emitter must ignore it. | ||
| namespace LibraryNs { | ||
| interface Operations { | ||
| @get @route("/lib-ops") listLibOps(): void; | ||
| } | ||
| } | ||
|
|
||
| @service(#{ title: "MyService" }) | ||
| namespace MyService { | ||
| interface Operations { | ||
| @get @route("/service-ops") listServiceOps(): void; | ||
| } | ||
| } | ||
| `; | ||
|
|
||
| const [result] = await compileAndDiagnose(tester, spec, { "skip-format": true }); | ||
|
|
||
| // The service interface should be generated without any _2 suffix. | ||
| const files = getOutputFiles(result.fs); | ||
| const controllerFile = [...files.entries()].find(([k]) => k.includes("OperationsController.cs")); | ||
| const interfaceFile = [...files.entries()].find(([k]) => k.includes("IOperations.cs")); | ||
|
|
||
| assert.ok(controllerFile, "OperationsController.cs should be emitted"); | ||
| assert.ok(interfaceFile, "IOperations.cs should be emitted"); | ||
|
|
||
| const [, controllerContents] = controllerFile!; | ||
| const [, interfaceContents] = interfaceFile!; | ||
|
|
||
| assert.ok( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. those expect feel very speciifc to this instance and could easily not be testing the actual problem if the dedup logic change. |
||
| !controllerContents.includes("_2"), | ||
| `OperationsController.cs must not contain '_2', but got:\n${controllerContents}`, | ||
| ); | ||
| assert.ok( | ||
| !interfaceContents.includes("_2"), | ||
| `IOperations.cs must not contain '_2', but got:\n${interfaceContents}`, | ||
| ); | ||
|
|
||
| // Sanity-check that the correct class/interface names are present. | ||
| assert.ok( | ||
| controllerContents.includes("public partial class OperationsController"), | ||
| "Controller class should be named OperationsController", | ||
| ); | ||
| assert.ok( | ||
| interfaceContents.includes("public interface IOperations"), | ||
| "Business-logic interface should be named IOperations", | ||
| ); | ||
|
|
||
| // The library namespace interface should NOT produce a controller file. | ||
| const libraryControllerFile = [...files.entries()].find(([k]) => | ||
| k.includes("LibraryNsOperationsController.cs"), | ||
| ); | ||
| assert.ok(!libraryControllerFile, "LibraryNs controller should not be emitted"); | ||
| }); | ||
|
|
||
| // Regression test for https://github.com/microsoft/typespec/issues/11493 (model variant) | ||
| // When a library namespace defines a model with the same name as a service model, the | ||
| // emitter must not emit both, which would produce a `_2` suffix on the second class. | ||
| it("does not emit _2 suffix when a non-service namespace has a model with the same name", async () => { | ||
| const spec = ` | ||
| // Library namespace model with the same name as the service model. | ||
| namespace LibraryNs { | ||
| model ErrorResponse { | ||
| code: string; | ||
| } | ||
| } | ||
|
|
||
| @service(#{ title: "MyService" }) | ||
| namespace MyService { | ||
| @error | ||
| model ErrorResponse { | ||
| @statusCode code: 400; | ||
| message: string; | ||
| } | ||
|
|
||
| interface Ops { | ||
| @get @route("/items") list(): void; | ||
| } | ||
| } | ||
| `; | ||
|
|
||
| const [result] = await compileAndDiagnose(tester, spec, { "skip-format": true }); | ||
|
|
||
| const files = getOutputFiles(result.fs); | ||
| const modelFile = [...files.entries()].find(([k]) => k.includes("ErrorResponse.cs")); | ||
|
|
||
| assert.ok(modelFile, "ErrorResponse.cs should be emitted"); | ||
|
|
||
| const [, modelContents] = modelFile!; | ||
| assert.ok( | ||
| !modelContents.includes("_2"), | ||
| `ErrorResponse.cs must not contain '_2', but got:\n${modelContents}`, | ||
| ); | ||
| assert.ok( | ||
| modelContents.includes("public partial class ErrorResponse"), | ||
| "Model class should be named ErrorResponse", | ||
| ); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lets reformulate this as we do not want to include type that are not defined in the service namespace unless they are referenced.