From 6ae4a962ee2c3d52e61326fdeff0420d335e592f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:10:06 +0000 Subject: [PATCH 1/5] Initial plan From 0f3541d1a0a03b6b50b16f6b3f9ef32d6a9ffe06 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:20:17 +0000 Subject: [PATCH 2/5] Downgrade convenience method to internal when a parameter type is internal Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../Providers/ScmMethodProviderCollection.cs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) 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 f8bd0588eac..83b29533b36 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 @@ -152,7 +152,7 @@ private ScmMethodProvider BuildConvenienceMethod(MethodProvider protocolMethod, methodSignature = new MethodSignature( methodName, DocHelpers.GetFormattableDescription(ServiceMethod.Operation.Summary, ServiceMethod.Operation.Doc) ?? FormattableStringHelpers.FromString(ServiceMethod.Operation.Name), - protocolMethod.Signature.Modifiers, + GetConvenienceMethodModifiers(protocolMethod.Signature.Modifiers, signatureParameters), GetResponseType(ServiceMethod.Operation.Responses, true, isAsync, out _), null, signatureParameters); @@ -215,6 +215,29 @@ .. GetStackVariablesForReturnValueConversion(result, responseBodyType, isAsync, return convenienceMethod; } + /// + /// Determines the modifiers for a convenience method. When a convenience method would be + /// generated as public on a public type but has a parameter whose type is internal (for example, + /// a model that was customized to be internal via client.tsp or custom code), the method is + /// downgraded to internal to avoid an inconsistent accessibility compilation error. + /// + private MethodSignatureModifiers GetConvenienceMethodModifiers( + MethodSignatureModifiers modifiers, + IReadOnlyList signatureParameters) + { + // Only public methods on a public enclosing type are affected. If the enclosing type is + // already internal, the convenience method is effectively internal and needs no adjustment. + if (modifiers.HasFlag(MethodSignatureModifiers.Public) && + EnclosingType.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public) && + signatureParameters.Any(p => !p.Type.IsPublic)) + { + modifiers &= ~MethodSignatureModifiers.Public; + modifiers |= MethodSignatureModifiers.Internal; + } + + return modifiers; + } + private IEnumerable GetStackVariablesForProtocolParamConversion(IReadOnlyList convenienceMethodParameters, out Dictionary declarations) { List statements = new List(); From 235d7d7987415c1fe8a78002cf1c715fb6bcb383 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:34:00 +0000 Subject: [PATCH 3/5] Add unit tests for internal-parameter convenience method accessibility Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../ScmMethodProviderCollectionTests.cs | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmMethodProviderCollectionTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmMethodProviderCollectionTests.cs index 65e6edf5982..6c1d2595118 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmMethodProviderCollectionTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmMethodProviderCollectionTests.cs @@ -2001,5 +2001,96 @@ public void ConvenienceMethod_JsonListBody_DoesNotUseXmlFromEnumerable() Assert.IsFalse(methodBody.Contains("rootNameHint")); Assert.IsFalse(methodBody.Contains("childNameHint")); } + + [Test] + public void ConvenienceMethod_WithInternalParameterType_IsInternal() + { + // A model that is customized to be internal (for example via client.tsp) results in a + // convenience method parameter whose type is internal. The convenience method must be + // generated as internal to avoid an inconsistent accessibility compilation error. + var internalModel = InputFactory.Model( + "InternalModel", + access: "internal", + usage: InputModelTypeUsage.Input | InputModelTypeUsage.Json, + properties: [InputFactory.Property("Name", InputPrimitiveType.String)]); + + var bodyParam = InputFactory.BodyParameter("body", internalModel, isRequired: true); + var methodBodyParam = InputFactory.MethodParameter( + "body", + internalModel, + isRequired: true, + location: InputRequestLocation.Body); + + var operation = InputFactory.Operation( + "Foo", + httpMethod: "POST", + parameters: [bodyParam], + responses: [InputFactory.OperationResponse([200])]); + + var serviceMethod = InputFactory.BasicServiceMethod("Foo", operation, parameters: [methodBodyParam]); + var inputClient = InputFactory.Client("TestClient", methods: [serviceMethod]); + + MockHelpers.LoadMockGenerator(clients: () => [inputClient], inputModels: () => [internalModel]); + + var client = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(inputClient); + var methodCollection = new ScmMethodProviderCollection(serviceMethod, client!); + + var convenienceMethods = methodCollection.Where(m => + m.Signature.Parameters.All(p => p.Name != "content") && + m.Signature.Name.StartsWith("Foo")).ToList(); + Assert.AreEqual(2, convenienceMethods.Count); + + foreach (var convenienceMethod in convenienceMethods) + { + // The parameter type is internal. + Assert.IsTrue(convenienceMethod.Signature.Parameters.Any(p => !p.Type.IsPublic)); + // The convenience method should therefore be internal, not public. + Assert.IsTrue(convenienceMethod.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Internal)); + Assert.IsFalse(convenienceMethod.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)); + } + } + + [Test] + public void ConvenienceMethod_WithPublicParameterType_IsPublic() + { + // When all convenience method parameter types are public, the method should remain public. + var publicModel = InputFactory.Model( + "PublicModel", + usage: InputModelTypeUsage.Input | InputModelTypeUsage.Json, + properties: [InputFactory.Property("Name", InputPrimitiveType.String)]); + + var bodyParam = InputFactory.BodyParameter("body", publicModel, isRequired: true); + var methodBodyParam = InputFactory.MethodParameter( + "body", + publicModel, + isRequired: true, + location: InputRequestLocation.Body); + + var operation = InputFactory.Operation( + "Foo", + httpMethod: "POST", + parameters: [bodyParam], + responses: [InputFactory.OperationResponse([200])]); + + var serviceMethod = InputFactory.BasicServiceMethod("Foo", operation, parameters: [methodBodyParam]); + var inputClient = InputFactory.Client("TestClient", methods: [serviceMethod]); + + MockHelpers.LoadMockGenerator(clients: () => [inputClient], inputModels: () => [publicModel]); + + var client = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(inputClient); + var methodCollection = new ScmMethodProviderCollection(serviceMethod, client!); + + var convenienceMethods = methodCollection.Where(m => + m.Signature.Parameters.All(p => p.Name != "content") && + m.Signature.Name.StartsWith("Foo")).ToList(); + Assert.AreEqual(2, convenienceMethods.Count); + + foreach (var convenienceMethod in convenienceMethods) + { + Assert.IsTrue(convenienceMethod.Signature.Parameters.All(p => p.Type.IsPublic)); + Assert.IsTrue(convenienceMethod.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)); + Assert.IsFalse(convenienceMethod.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Internal)); + } + } } } From f0662a7a600c8b602e65ba77a0a7250697145c9b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:50:01 +0000 Subject: [PATCH 4/5] Address review: remove xml docs, broaden enclosing type check, add custom-code test Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../Providers/ScmMethodProviderCollection.cs | 12 ++--- .../ScmMethodProviderCollectionTests.cs | 52 ++++++++++++++++++- .../CustomInternalModel.cs | 9 ++++ 3 files changed, 63 insertions(+), 10 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ScmMethodProviderCollectionTests/ConvenienceMethod_WithCustomCodeInternalParameterType_IsInternal/CustomInternalModel.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 83b29533b36..1441718d0a5 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 @@ -215,20 +215,14 @@ .. GetStackVariablesForReturnValueConversion(result, responseBodyType, isAsync, return convenienceMethod; } - /// - /// Determines the modifiers for a convenience method. When a convenience method would be - /// generated as public on a public type but has a parameter whose type is internal (for example, - /// a model that was customized to be internal via client.tsp or custom code), the method is - /// downgraded to internal to avoid an inconsistent accessibility compilation error. - /// private MethodSignatureModifiers GetConvenienceMethodModifiers( MethodSignatureModifiers modifiers, IReadOnlyList signatureParameters) { - // Only public methods on a public enclosing type are affected. If the enclosing type is - // already internal, the convenience method is effectively internal and needs no adjustment. + var enclosingTypeModifiers = EnclosingType.DeclarationModifiers; if (modifiers.HasFlag(MethodSignatureModifiers.Public) && - EnclosingType.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public) && + !enclosingTypeModifiers.HasFlag(TypeSignatureModifiers.Internal) && + !enclosingTypeModifiers.HasFlag(TypeSignatureModifiers.Private) && signatureParameters.Any(p => !p.Type.IsPublic)) { modifiers &= ~MethodSignatureModifiers.Public; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmMethodProviderCollectionTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmMethodProviderCollectionTests.cs index 6c1d2595118..d54a65c643c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmMethodProviderCollectionTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmMethodProviderCollectionTests.cs @@ -2005,7 +2005,7 @@ public void ConvenienceMethod_JsonListBody_DoesNotUseXmlFromEnumerable() [Test] public void ConvenienceMethod_WithInternalParameterType_IsInternal() { - // A model that is customized to be internal (for example via client.tsp) results in a + // A model that is customized to be internal via client.tsp (an access override) results in a // convenience method parameter whose type is internal. The convenience method must be // generated as internal to avoid an inconsistent accessibility compilation error. var internalModel = InputFactory.Model( @@ -2050,6 +2050,56 @@ public void ConvenienceMethod_WithInternalParameterType_IsInternal() } } + [Test] + public async Task ConvenienceMethod_WithCustomCodeInternalParameterType_IsInternal() + { + // A model that is customized to be internal via custom code (an internal partial class) + // results in a convenience method parameter whose type is internal. The convenience method + // must be generated as internal to avoid an inconsistent accessibility compilation error. + var customInternalModel = InputFactory.Model( + "CustomInternalModel", + usage: InputModelTypeUsage.Input | InputModelTypeUsage.Json, + properties: [InputFactory.Property("Name", InputPrimitiveType.String)]); + + var bodyParam = InputFactory.BodyParameter("body", customInternalModel, isRequired: true); + var methodBodyParam = InputFactory.MethodParameter( + "body", + customInternalModel, + isRequired: true, + location: InputRequestLocation.Body); + + var operation = InputFactory.Operation( + "Foo", + httpMethod: "POST", + parameters: [bodyParam], + responses: [InputFactory.OperationResponse([200])]); + + var serviceMethod = InputFactory.BasicServiceMethod("Foo", operation, parameters: [methodBodyParam]); + var inputClient = InputFactory.Client("TestClient", methods: [serviceMethod]); + + await MockHelpers.LoadMockGeneratorAsync( + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(), + clients: () => [inputClient], + inputModels: () => [customInternalModel]); + + var client = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(inputClient); + var methodCollection = new ScmMethodProviderCollection(serviceMethod, client!); + + var convenienceMethods = methodCollection.Where(m => + m.Signature.Parameters.All(p => p.Name != "content") && + m.Signature.Name.StartsWith("Foo")).ToList(); + Assert.AreEqual(2, convenienceMethods.Count); + + foreach (var convenienceMethod in convenienceMethods) + { + // The parameter type is internal because it was customized via custom code. + Assert.IsTrue(convenienceMethod.Signature.Parameters.Any(p => !p.Type.IsPublic)); + // The convenience method should therefore be internal, not public. + Assert.IsTrue(convenienceMethod.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Internal)); + Assert.IsFalse(convenienceMethod.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)); + } + } + [Test] public void ConvenienceMethod_WithPublicParameterType_IsPublic() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ScmMethodProviderCollectionTests/ConvenienceMethod_WithCustomCodeInternalParameterType_IsInternal/CustomInternalModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ScmMethodProviderCollectionTests/ConvenienceMethod_WithCustomCodeInternalParameterType_IsInternal/CustomInternalModel.cs new file mode 100644 index 00000000000..72f19011ba7 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/TestData/ScmMethodProviderCollectionTests/ConvenienceMethod_WithCustomCodeInternalParameterType_IsInternal/CustomInternalModel.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Sample.Models +{ + internal partial class CustomInternalModel + { + } +} From e6269da63b61cfe2b7fbb4ebd74dc04d7dc32837 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:00:14 +0000 Subject: [PATCH 5/5] Validate convenience method tests against client.Methods Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../test/Providers/ScmMethodProviderCollectionTests.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmMethodProviderCollectionTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmMethodProviderCollectionTests.cs index d54a65c643c..6bd6a7743c5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmMethodProviderCollectionTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmMethodProviderCollectionTests.cs @@ -2033,9 +2033,8 @@ public void ConvenienceMethod_WithInternalParameterType_IsInternal() MockHelpers.LoadMockGenerator(clients: () => [inputClient], inputModels: () => [internalModel]); var client = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(inputClient); - var methodCollection = new ScmMethodProviderCollection(serviceMethod, client!); - var convenienceMethods = methodCollection.Where(m => + var convenienceMethods = client!.Methods.Where(m => m.Signature.Parameters.All(p => p.Name != "content") && m.Signature.Name.StartsWith("Foo")).ToList(); Assert.AreEqual(2, convenienceMethods.Count); @@ -2083,9 +2082,8 @@ await MockHelpers.LoadMockGeneratorAsync( inputModels: () => [customInternalModel]); var client = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(inputClient); - var methodCollection = new ScmMethodProviderCollection(serviceMethod, client!); - var convenienceMethods = methodCollection.Where(m => + var convenienceMethods = client!.Methods.Where(m => m.Signature.Parameters.All(p => p.Name != "content") && m.Signature.Name.StartsWith("Foo")).ToList(); Assert.AreEqual(2, convenienceMethods.Count); @@ -2128,9 +2126,8 @@ public void ConvenienceMethod_WithPublicParameterType_IsPublic() MockHelpers.LoadMockGenerator(clients: () => [inputClient], inputModels: () => [publicModel]); var client = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(inputClient); - var methodCollection = new ScmMethodProviderCollection(serviceMethod, client!); - var convenienceMethods = methodCollection.Where(m => + var convenienceMethods = client!.Methods.Where(m => m.Signature.Parameters.All(p => p.Name != "content") && m.Signature.Name.StartsWith("Foo")).ToList(); Assert.AreEqual(2, convenienceMethods.Count);