diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PartialMethodCustomization.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PartialMethodCustomization.cs index 549f9e6a283..1d5682d7f09 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PartialMethodCustomization.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PartialMethodCustomization.cs @@ -166,9 +166,18 @@ public static IReadOnlyList RenameAndCloneParameters( /// The customer's partial declaration signature. /// The parameters to use for the implementation. /// Must all be required (no default values) per the C# partial method rules. + /// The return type the implementation should use. Pass the + /// generator's own return type rather than relying on the customer's parsed declaration: + /// the customer's declaration may reference types generated into the same assembly, which + /// are unresolved when the declaration is read (Roslyn surfaces them as error types with no + /// namespace), producing malformed global::.TypeName output. C# requires a partial + /// method's declaration and implementation to share the same return type, so the generator's + /// resolved return type is necessarily the correct one. When null, the customer's + /// parsed return type is used as a fallback. public static MethodSignature BuildPartialSignature( MethodSignature customSignature, - IReadOnlyList implementationParameters) + IReadOnlyList implementationParameters, + CSharpType? returnType = null) { if (customSignature is null) { @@ -184,7 +193,7 @@ public static MethodSignature BuildPartialSignature( customSignature.Name, customSignature.Description, customSignature.Modifiers | MethodSignatureModifiers.Partial, - customSignature.ReturnType, + returnType ?? customSignature.ReturnType, customSignature.ReturnDescription, implementationParameters, customSignature.Attributes, diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index 7a3047886b6..5b1af563dff 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -413,12 +413,17 @@ internal MethodProvider[] FilterCustomizedMethods(IEnumerable sp private static MethodProvider CreatePartialMethodFromCustomSignature(MethodSignature customSignature, MethodProvider generatedMethod) { // Partial method implementations require all parameters to be required (no default values). + // The generator's parameters carry the metadata and the declarations referenced by the + // method body and XML docs; the custom signature only supplies the parameter names. var requiredParameters = PartialMethodCustomization.RenameAndCloneParameters( - customSignature.Parameters, + generatedMethod.Signature.Parameters, customSignature.Parameters, removeDefaults: true); - var partialSignature = PartialMethodCustomization.BuildPartialSignature(customSignature, requiredParameters); + var partialSignature = PartialMethodCustomization.BuildPartialSignature( + customSignature, + requiredParameters, + generatedMethod.Signature.ReturnType); MethodProvider partialMethod = generatedMethod.BodyExpression != null ? new MethodProvider(partialSignature, generatedMethod.BodyExpression, generatedMethod.EnclosingType, generatedMethod.XmlDocs, generatedMethod.Suppressions) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/CustomPartialMethodImplementationKeepsParameterNames/Custom.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/CustomPartialMethodImplementationKeepsParameterNames/Custom.cs new file mode 100644 index 00000000000..1d44cc7bc8a --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/CustomPartialMethodImplementationKeepsParameterNames/Custom.cs @@ -0,0 +1,8 @@ +#nullable disable + +namespace Test; + +public partial class CustomPartialMethodType +{ + static partial void MyMethod(string input); +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/CustomPartialMethodImplementationUsesGeneratorReturnType/Custom.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/CustomPartialMethodImplementationUsesGeneratorReturnType/Custom.cs new file mode 100644 index 00000000000..740e8c32c0a --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/CustomPartialMethodImplementationUsesGeneratorReturnType/Custom.cs @@ -0,0 +1,11 @@ +#nullable disable + +namespace Test; + +public partial class CustomPartialReturnType +{ + // CustomReturnModel is a type generated into the same assembly, so it is unresolved when this + // declaration is read. The generated implementation must still use the generator's resolved + // return type (with its namespace) rather than this unresolved one. + static partial CustomReturnModel MyMethod(string input); +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs index e597456fef9..9ec0b21c32d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs @@ -433,6 +433,90 @@ public async Task TestSpecViewReturnsAllMethodsEvenWhenCustomized() Assert.AreEqual("Method3", specView.Methods[2].Signature.Name); } + [Test] + public async Task CustomPartialMethodImplementationKeepsParameterNames() + { + await MockHelpers.LoadMockGeneratorAsync(compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var inputParam = new ParameterProvider("input", $"The input value.", typeof(string)); + var generatedMethod = new MethodProvider( + new MethodSignature( + "MyMethod", + $"Does something.", + MethodSignatureModifiers.Static, + null, + null, + [inputParam]), + inputParam.Invoke("ToString").Terminate(), + new TestTypeProvider()); + + var typeProvider = new TestTypeProvider(name: "CustomPartialMethodType", methods: [generatedMethod]); + + var method = typeProvider.Methods.Single(); + Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Partial)); + + using var codeWriter = new CodeWriter(); + codeWriter.WriteMethod(method); + var result = codeWriter.ToString(false); + + // The partial implementation's signature parameter must keep the same name the body uses. + // Regression guard: the parameter must not be renamed with a numeric suffix (e.g. "input0"). + Assert.IsFalse( + result.Contains("input0"), + $"Partial implementation renamed the parameter with a numeric suffix:\n{result}"); + Assert.AreEqual("input", method.Signature.Parameters.Single().Name); + } + + [Test] + public async Task CustomPartialMethodImplementationUsesGeneratorReturnType() + { + await MockHelpers.LoadMockGeneratorAsync(compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + // The generator's resolved return type carries a namespace. The custom partial + // declaration references the same type by name, but it is unresolved when read (a + // type generated into the same assembly), so its parsed CSharpType has no namespace. + var generatorReturnType = new CSharpType( + "CustomReturnModel", + "Sample.Models", + isValueType: false, + isNullable: false, + declaringType: null, + args: [], + isPublic: true, + isStruct: false); + + var inputParam = new ParameterProvider("input", $"The input value.", typeof(string)); + var generatedMethod = new MethodProvider( + new MethodSignature( + "MyMethod", + $"Does something.", + MethodSignatureModifiers.Static, + generatorReturnType, + $"The result.", + [inputParam]), + inputParam.Invoke("ToString").Terminate(), + new TestTypeProvider()); + + var typeProvider = new TestTypeProvider(name: "CustomPartialReturnType", methods: [generatedMethod]); + + var method = typeProvider.Methods.Single(); + Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Partial)); + + // The implementation must use the generator's resolved return type (with namespace), + // not the customer's unresolved parsed return type (empty namespace). + Assert.AreEqual("Sample.Models", method.Signature.ReturnType!.Namespace); + Assert.AreEqual("CustomReturnModel", method.Signature.ReturnType.Name); + + using var codeWriter = new CodeWriter(); + codeWriter.WriteMethod(method); + var result = codeWriter.ToString(false); + + // Regression guard: an empty-namespace return type renders as the malformed `global::.`. + Assert.IsFalse( + result.Contains("global::."), + $"Partial implementation wrote a return type with no namespace:\n{result}"); + } + [Test] public async Task TestSpecViewReturnsAllPropertiesEvenWhenSuppressed() {