From b20917092471707d6a56069cddbb8680217fb33c Mon Sep 17 00:00:00 2001 From: Dapeng Zhang Date: Tue, 14 Jul 2026 13:50:31 +0800 Subject: [PATCH 1/2] fix(csharp): rebind partial method customizations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a8ef33da-e40d-4f79-86d7-7d947641e6b4 --- .../Providers/ScmMethodProviderCollection.cs | 13 +++-- .../Providers/PartialMethodCustomization.cs | 51 ++++++++++++++++++- .../src/Providers/TypeProvider.cs | 5 +- .../src/Writers/CodeWriter.cs | 7 ++- .../test/OutputLibraryVisitorTests.cs | 32 ++++++++++++ .../TestName.cs | 7 +++ .../test/Writers/CodeWriterTests.cs | 17 +++++++ 7 files changed, 119 insertions(+), 13 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/TestData/OutputLibraryVisitorTests/PartialMethodCustomizationRenamesGeneratedBodyReferences/TestName.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs index 14eb510800c..b04783c66cf 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmMethodProviderCollection.cs @@ -157,6 +157,8 @@ private ScmMethodProvider BuildConvenienceMethod(MethodProvider protocolMethod, // Detect a partial method declaration in the client's custom code matching this convenience method. MethodSignature? customSignature = null; PartialMethodCustomization.TryFindCustomSignature(client, methodName, signatureParameters, out customSignature); + var generatedModifiers = GetConvenienceMethodModifiers(protocolMethod.Signature.Modifiers, signatureParameters); + var generatedReturnType = GetResponseType(ServiceMethod.Operation.Responses, true, isAsync, out _); // Parameters used to construct the method body. When customizing, we clone the generator's // parameter providers with the customer's names but keep all generator metadata so that the @@ -180,7 +182,7 @@ private ScmMethodProvider BuildConvenienceMethod(MethodProvider protocolMethod, } convenienceBodyParameters = bodyParams; - methodSignature = PartialMethodCustomization.BuildPartialSignature(customSignature, renamedSignatureParameters); + methodSignature = PartialMethodCustomization.BuildPartialSignature(customSignature, renamedSignatureParameters, generatedReturnType, generatedModifiers); } else { @@ -188,8 +190,8 @@ private ScmMethodProvider BuildConvenienceMethod(MethodProvider protocolMethod, methodSignature = new MethodSignature( methodName, DocHelpers.GetFormattableDescription(ServiceMethod.Operation.Summary, ServiceMethod.Operation.Doc) ?? FormattableStringHelpers.FromString(ServiceMethod.Operation.Name), - GetConvenienceMethodModifiers(protocolMethod.Signature.Modifiers, signatureParameters), - GetResponseType(ServiceMethod.Operation.Responses, true, isAsync, out _), + generatedModifiers, + generatedReturnType, null, signatureParameters, Attributes: BuildConvenienceMethodAttributes()); @@ -1072,6 +1074,7 @@ private ScmMethodProvider BuildProtocolMethod(MethodProvider createRequestMethod // generated body using the customized parameter references. MethodSignature? customSignature = null; PartialMethodCustomization.TryFindCustomSignature(client, methodName, parameters, out customSignature); + var generatedReturnType = GetResponseType(ServiceMethod.Operation.Responses, false, isAsync, out _); MethodSignature methodSignature; ParameterProvider[] bodyParameters; @@ -1084,7 +1087,7 @@ private ScmMethodProvider BuildProtocolMethod(MethodProvider createRequestMethod customSignature.Parameters, removeDefaults: true).ToArray(); - methodSignature = PartialMethodCustomization.BuildPartialSignature(customSignature, requiredCustomParameters); + methodSignature = PartialMethodCustomization.BuildPartialSignature(customSignature, requiredCustomParameters, generatedReturnType, methodModifiers); bodyParameters = requiredCustomParameters; // Re-resolve the request options parameter from the customized parameter list so the @@ -1097,7 +1100,7 @@ private ScmMethodProvider BuildProtocolMethod(MethodProvider createRequestMethod methodName, DocHelpers.GetFormattableDescription(ServiceMethod.Operation.Summary, ServiceMethod.Operation.Doc) ?? FormattableStringHelpers.FromString(ServiceMethod.Operation.Name), methodModifiers, - GetResponseType(ServiceMethod.Operation.Responses, false, isAsync, out _), + generatedReturnType, $"The response returned from the service.", parameters); bodyParameters = parameters; 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 1d5682d7f09..74b1a73d88f 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 @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.TypeSpec.Generator.Expressions; using Microsoft.TypeSpec.Generator.Primitives; namespace Microsoft.TypeSpec.Generator.Providers @@ -157,6 +158,51 @@ public static IReadOnlyList RenameAndCloneParameters( return renamed; } + /// + /// Renames the generator parameters in place using the customer's partial declaration + /// parameter names while preserving the existing parameter instances. + /// + /// + /// Use this when a method body has already been constructed with the generator + /// parameters. Body expressions may hold cached + /// instances from those parameters, so replacing the parameters with clones would update + /// the signature but leave the body referencing the old declarations. + /// + public static IReadOnlyList RenameParametersInPlace( + IReadOnlyList generatorParameters, + IReadOnlyList customParameters, + bool removeDefaults) + { + if (generatorParameters is null) + { + throw new ArgumentNullException(nameof(generatorParameters)); + } + + if (customParameters is null) + { + throw new ArgumentNullException(nameof(customParameters)); + } + + if (generatorParameters.Count != customParameters.Count) + { + throw new ArgumentException( + $"Parameter counts differ ({generatorParameters.Count} vs {customParameters.Count}).", + nameof(customParameters)); + } + + for (int i = 0; i < generatorParameters.Count; i++) + { + var generatedParameter = generatorParameters[i]; + generatedParameter.Update(name: customParameters[i].Name); + if (removeDefaults) + { + generatedParameter.DefaultValue = null; + } + } + + return generatorParameters; + } + /// /// Builds a for a partial method implementation using /// (modifiers, name, return type, attributes, and @@ -177,7 +223,8 @@ public static IReadOnlyList RenameAndCloneParameters( public static MethodSignature BuildPartialSignature( MethodSignature customSignature, IReadOnlyList implementationParameters, - CSharpType? returnType = null) + CSharpType? returnType = null, + MethodSignatureModifiers generatedModifiers = MethodSignatureModifiers.None) { if (customSignature is null) { @@ -192,7 +239,7 @@ public static MethodSignature BuildPartialSignature( return new MethodSignature( customSignature.Name, customSignature.Description, - customSignature.Modifiers | MethodSignatureModifiers.Partial, + customSignature.Modifiers | MethodSignatureModifiers.Partial | (generatedModifiers & MethodSignatureModifiers.Async), returnType ?? customSignature.ReturnType, customSignature.ReturnDescription, implementationParameters, 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 e850150e8ad..d9d91dbde08 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 @@ -435,7 +435,7 @@ private static MethodProvider CreatePartialMethodFromCustomSignature(MethodSigna // 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( + var requiredParameters = PartialMethodCustomization.RenameParametersInPlace( generatedMethod.Signature.Parameters, customSignature.Parameters, removeDefaults: true); @@ -443,7 +443,8 @@ private static MethodProvider CreatePartialMethodFromCustomSignature(MethodSigna var partialSignature = PartialMethodCustomization.BuildPartialSignature( customSignature, requiredParameters, - generatedMethod.Signature.ReturnType); + generatedMethod.Signature.ReturnType, + generatedMethod.Signature.Modifiers); 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/src/Writers/CodeWriter.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/CodeWriter.cs index 1e25bccefce..9b4dedb4bf7 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/CodeWriter.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/CodeWriter.cs @@ -830,8 +830,7 @@ public IDisposable WriteMethodDeclarationNoScope(MethodSignatureBase methodBase, .AppendRawIf("private ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Private)) .AppendRawIf("protected ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Protected)) .AppendRawIf("internal ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Internal)) - .AppendRawIf("static ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Static)) - .AppendRawIf("partial ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Partial)); + .AppendRawIf("static ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Static)); if (methodBase is MethodSignature method) { @@ -839,7 +838,8 @@ public IDisposable WriteMethodDeclarationNoScope(MethodSignatureBase methodBase, .AppendRawIf("abstract ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Abstract)) .AppendRawIf("override ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Override)) .AppendRawIf("new ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.New)) - .AppendRawIf("async ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Async)); + .AppendRawIf("async ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Async)) + .AppendRawIf("partial ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Partial)); var isImplicit = methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Implicit); var isImplicitOrExplicit = isImplicit || methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Explicit); @@ -854,7 +854,6 @@ public IDisposable WriteMethodDeclarationNoScope(MethodSignatureBase methodBase, AppendRaw("void "); } } - AppendRawIf("implicit ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Implicit)) .AppendRawIf("explicit ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Explicit)) .AppendRawIf("operator ", methodBase.Modifiers.HasFlag(MethodSignatureModifiers.Operator)); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/OutputLibraryVisitorTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/OutputLibraryVisitorTests.cs index 240dd77a792..c2a039cbe5f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/OutputLibraryVisitorTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/OutputLibraryVisitorTests.cs @@ -272,6 +272,38 @@ [new ParameterProvider("param1", $"", typeof(int))]), Assert.AreEqual(typeof(float), typeProvider.Methods[0].Signature.Parameters[0].Type.FrameworkType); } + [Test] + public async Task PartialMethodCustomizationRenamesGeneratedBodyReferences() + { + var typeProvider = new MutableMethodsTypeProvider(); + var parameter = new ParameterProvider("param1", $"", typeof(int), defaultValue: Snippet.Default); + var methodProvider = new MethodProvider( + new MethodSignature("TestMethod", $"", MethodSignatureModifiers.Public | MethodSignatureModifiers.Virtual, typeof(int), $"", [parameter]), + Snippet.Return(parameter), + typeProvider); + typeProvider.MethodProviders = [methodProvider]; + + var generator = await MockHelpers.LoadMockGeneratorAsync( + createOutputLibrary: () => new TestOutputLibrary(typeProvider), + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + typeProvider.Update(reset: true); + foreach (var type in generator.Object.OutputLibrary.TypeProviders) + { + type.EnsureBuilt(); + } + + typeProvider.Update(methods: typeProvider.FilterCustomizedMethods(typeProvider.Methods)); + + var partialMethod = typeProvider.Methods.Single(); + Assert.IsTrue(partialMethod.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Partial)); + Assert.IsTrue(partialMethod.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Virtual)); + Assert.AreEqual("renamed", partialMethod.Signature.Parameters[0].Name); + Assert.IsNull(partialMethod.Signature.Parameters[0].DefaultValue); + Assert.That(partialMethod.BodyStatements?.ToDisplayString(), Does.Contain("return renamed;")); + Assert.That(partialMethod.BodyStatements?.ToDisplayString(), Does.Not.Contain("param1")); + } + [Test] public async Task MatchingConstructorSignatureIsFilteredAfterVisitorMutation() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/TestData/OutputLibraryVisitorTests/PartialMethodCustomizationRenamesGeneratedBodyReferences/TestName.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/TestData/OutputLibraryVisitorTests/PartialMethodCustomizationRenamesGeneratedBodyReferences/TestName.cs new file mode 100644 index 00000000000..c5e264e19e6 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/TestData/OutputLibraryVisitorTests/PartialMethodCustomizationRenamesGeneratedBodyReferences/TestName.cs @@ -0,0 +1,7 @@ +namespace Test +{ + public partial class TestName + { + public virtual partial int TestMethod(int renamed = 0); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Writers/CodeWriterTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Writers/CodeWriterTests.cs index b2d0c268cb5..f8fc3bda842 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Writers/CodeWriterTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Writers/CodeWriterTests.cs @@ -681,6 +681,23 @@ public void CodeWriter_WriteMethodDeclaration_WithPartialModifier() Assert.AreEqual("private partial void DefineAdditionalProperties()", result); } + [Test] + public void CodeWriter_WriteMethodDeclaration_WithVirtualPartialModifier() + { + var methodSignature = new MethodSignature( + "CreateOrUpdate", + $"Creates or updates a resource.", + MethodSignatureModifiers.Public | MethodSignatureModifiers.Virtual | MethodSignatureModifiers.Partial, + typeof(int), + null, + []); + using var codeWriter = new CodeWriter(); + codeWriter.WriteMethodDeclarationNoScope(methodSignature); + + var result = codeWriter.ToString(false); + Assert.AreEqual("public virtual partial int CreateOrUpdate()", result); + } + [Test] public void CodeWriter_WriteMethod_PartialMethodWithBody() { From 46957645c802ab98eb1ca5d26878432124430c20 Mon Sep 17 00:00:00 2001 From: Dapeng Zhang Date: Tue, 14 Jul 2026 14:19:44 +0800 Subject: [PATCH 2/2] refine partial parameter rebinding Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a8ef33da-e40d-4f79-86d7-7d947641e6b4 --- .../Providers/PartialMethodCustomization.cs | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) 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 74b1a73d88f..8d022a683e4 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 @@ -173,24 +173,7 @@ public static IReadOnlyList RenameParametersInPlace( IReadOnlyList customParameters, bool removeDefaults) { - if (generatorParameters is null) - { - throw new ArgumentNullException(nameof(generatorParameters)); - } - - if (customParameters is null) - { - throw new ArgumentNullException(nameof(customParameters)); - } - - if (generatorParameters.Count != customParameters.Count) - { - throw new ArgumentException( - $"Parameter counts differ ({generatorParameters.Count} vs {customParameters.Count}).", - nameof(customParameters)); - } - - for (int i = 0; i < generatorParameters.Count; i++) + for (int i = 0; i < generatorParameters.Count && i < customParameters.Count; i++) { var generatedParameter = generatorParameters[i]; generatedParameter.Update(name: customParameters[i].Name);