From a74eca799d3404e18246b6546000ffdce0816585 Mon Sep 17 00:00:00 2001 From: jolov Date: Wed, 29 Jul 2026 20:07:14 -0700 Subject: [PATCH 1/3] Skip backcompat factories with unavailable types Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c4da49e5-30c3-4be2-8083-fc61b690f9fa --- .../src/Providers/ModelFactoryProvider.cs | 56 +++++++++++++++++++ .../src/Utilities/DiagnosticCodes.cs | 1 + .../ModelFactoryProviderTests.cs | 34 +++++++++++ .../LastContract.cs | 20 +++++++ 4 files changed, 111 insertions(+) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_SkipsMethodWithUnavailableGenericParameterType/LastContract.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs index f4fbf891762..fad01888aa6 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelFactoryProvider.cs @@ -128,6 +128,15 @@ protected internal sealed override IReadOnlyList BuildMethodsFor continue; } + var unavailableTypes = GetUnavailableSignatureTypes(previousMethod.Signature); + if (unavailableTypes.Count > 0) + { + CodeModelGenerator.Instance.Emitter.ReportDiagnostic( + DiagnosticCodes.UnavailableBackcompatType, + $"Skipped backward compatible model factory method '{previousMethod.Signature.FullMethodName}' because its signature references unavailable type(s): {string.Join(", ", unavailableTypes)}."); + continue; + } + List currentOverloads = []; bool foundCompatibleOverload = false; @@ -209,6 +218,53 @@ protected internal sealed override IReadOnlyList BuildMethodsFor return [.. factoryMethods]; } + private static IReadOnlyList GetUnavailableSignatureTypes(MethodSignature signature) + { + var unavailableTypes = new HashSet(StringComparer.Ordinal); + if (signature.ReturnType != null) + { + CollectUnavailableTypes(signature.ReturnType, unavailableTypes); + } + + foreach (var parameter in signature.Parameters) + { + CollectUnavailableTypes(parameter.Type, unavailableTypes); + } + + return [.. unavailableTypes.OrderBy(type => type, StringComparer.Ordinal)]; + } + + private static void CollectUnavailableTypes(CSharpType type, ISet unavailableTypes) + { + foreach (var argument in type.Arguments) + { + CollectUnavailableTypes(argument, unavailableTypes); + } + + if (type.IsFrameworkType) + { + return; + } + + var typeFactory = CodeModelGenerator.Instance.TypeFactory; + if (typeFactory.CSharpTypeMap.Keys.Any(type.AreNamesEqual)) + { + return; + } + + var declaringTypeName = type.DeclaringType?.Name; + if (CodeModelGenerator.Instance.SourceInputModel.FindForTypeInCustomization( + type.Namespace, + type.Name, + declaringTypeName, + includeReferencedAssemblies: true) != null) + { + return; + } + + unavailableTypes.Add(type.FullyQualifiedName); + } + private bool TryBuildCompatibleMethodForPreviousContract( MethodProvider previousMethod, MethodSignature? currentMethodSignature, diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/DiagnosticCodes.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/DiagnosticCodes.cs index a412ee9dd65..35a67676649 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/DiagnosticCodes.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/DiagnosticCodes.cs @@ -8,5 +8,6 @@ internal static class DiagnosticCodes public const string BaselineContractMissing = "baseline-contract-missing"; public const string InvalidAccessModifier = "invalid-access-modifier"; public const string PluginBuildFailed = "plugin-build-failed"; + public const string UnavailableBackcompatType = "unavailable-backcompat-type"; } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoryProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoryProviderTests.cs index 934239ba78f..7ae6d5135c9 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoryProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoryProviderTests.cs @@ -660,6 +660,40 @@ public async Task BackCompatibility_NewPropertyAddedWithRenamedParam() StringAssert.Contains("return new global::Sample.Models.PublicModel1(default, default, listProp.ToList(), default, additionalBinaryDataProperties: null);", bodyString); } + [Test] + public async Task BackCompatibility_SkipsMethodWithUnavailableGenericParameterType() + { + var externalTool = InputFactory.Model( + "Tool", + external: new InputExternalTypeMetadata("System.Uri", null, null)); + var hostedAgentDefinition = InputFactory.Model( + "HostedAgentDefinition", + properties: + [ + InputFactory.Property("Tools", InputFactory.Array(externalTool)) + ]); + + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [externalTool, hostedAgentDefinition], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync())).Object; + + var modelFactory = _instance.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + var methods = modelFactory.Methods + .Where(method => method.Signature.Name == "HostedAgentDefinition") + .ToList(); + Assert.AreEqual(1, methods.Count); + + var toolsParameter = methods[0].Signature.Parameters.Single(); + Assert.AreEqual(typeof(IEnumerable<>), toolsParameter.Type.FrameworkType); + Assert.AreEqual(typeof(Uri), toolsParameter.Type.Arguments.Single().FrameworkType); + + var content = new TypeProviderWriter(modelFactory).Write().Content; + StringAssert.DoesNotContain("ProjectsAgentTool", content); + } + [Test] public void ModelWithNestedDiscriminators() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_SkipsMethodWithUnavailableGenericParameterType/LastContract.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_SkipsMethodWithUnavailableGenericParameterType/LastContract.cs new file mode 100644 index 00000000000..833eba236f8 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_SkipsMethodWithUnavailableGenericParameterType/LastContract.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Sample.Namespace +{ + public class HostedAgentDefinition + { + } + + public class ProjectsAgentTool + { + } + + public static partial class SampleNamespaceModelFactory + { + public static HostedAgentDefinition HostedAgentDefinition(IEnumerable tools) + { + return null; + } + } +} From 8a00b293680a0958e70eb5c9dac829f3fd9ef944 Mon Sep 17 00:00:00 2001 From: jolov Date: Wed, 29 Jul 2026 20:13:17 -0700 Subject: [PATCH 2/3] Expand unavailable backcompat type tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c4da49e5-30c3-4be2-8083-fc61b690f9fa --- .../ModelFactoryProviderTests.cs | 77 +++++++++++++++++++ .../LastContract.cs | 20 +++++ .../LastContract.cs | 18 +++++ .../LastContract.cs | 18 +++++ 4 files changed, 133 insertions(+) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_SkipsMethodWithUnavailableGenericReturnType/LastContract.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_SkipsMethodWithUnavailableParameterType/LastContract.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_SkipsMethodWithUnavailableReturnType/LastContract.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoryProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoryProviderTests.cs index 7ae6d5135c9..5ba8adc2e88 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoryProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoryProviderTests.cs @@ -660,6 +660,37 @@ public async Task BackCompatibility_NewPropertyAddedWithRenamedParam() StringAssert.Contains("return new global::Sample.Models.PublicModel1(default, default, listProp.ToList(), default, additionalBinaryDataProperties: null);", bodyString); } + [Test] + public async Task BackCompatibility_SkipsMethodWithUnavailableParameterType() + { + var externalTool = InputFactory.Model( + "Tool", + external: new InputExternalTypeMetadata("System.Uri", null, null)); + var hostedAgentDefinition = InputFactory.Model( + "HostedAgentDefinition", + properties: + [ + InputFactory.Property("Tool", externalTool) + ]); + + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [externalTool, hostedAgentDefinition], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync())).Object; + + var modelFactory = _instance.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + var methods = modelFactory.Methods + .Where(method => method.Signature.Name == "HostedAgentDefinition") + .ToList(); + Assert.AreEqual(1, methods.Count); + Assert.AreEqual(typeof(Uri), methods[0].Signature.Parameters.Single().Type.FrameworkType); + + var content = new TypeProviderWriter(modelFactory).Write().Content; + StringAssert.DoesNotContain("ProjectsAgentTool", content); + } + [Test] public async Task BackCompatibility_SkipsMethodWithUnavailableGenericParameterType() { @@ -694,6 +725,52 @@ public async Task BackCompatibility_SkipsMethodWithUnavailableGenericParameterTy StringAssert.DoesNotContain("ProjectsAgentTool", content); } + [Test] + public async Task BackCompatibility_SkipsMethodWithUnavailableReturnType() + { + var hostedAgentDefinition = InputFactory.Model("HostedAgentDefinition"); + + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [hostedAgentDefinition], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync())).Object; + + var modelFactory = _instance.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + var methods = modelFactory.Methods + .Where(method => method.Signature.Name == "HostedAgentDefinition") + .ToList(); + Assert.AreEqual(1, methods.Count); + Assert.AreEqual("HostedAgentDefinition", methods[0].Signature.ReturnType?.Name); + + var content = new TypeProviderWriter(modelFactory).Write().Content; + StringAssert.DoesNotContain("ProjectsAgentTool", content); + } + + [Test] + public async Task BackCompatibility_SkipsMethodWithUnavailableGenericReturnType() + { + var hostedAgentDefinition = InputFactory.Model("HostedAgentDefinition"); + + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [hostedAgentDefinition], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync())).Object; + + var modelFactory = _instance.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + var methods = modelFactory.Methods + .Where(method => method.Signature.Name == "HostedAgentDefinition") + .ToList(); + Assert.AreEqual(1, methods.Count); + Assert.AreEqual("HostedAgentDefinition", methods[0].Signature.ReturnType?.Name); + + var content = new TypeProviderWriter(modelFactory).Write().Content; + StringAssert.DoesNotContain("ProjectsAgentTool", content); + } + [Test] public void ModelWithNestedDiscriminators() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_SkipsMethodWithUnavailableGenericReturnType/LastContract.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_SkipsMethodWithUnavailableGenericReturnType/LastContract.cs new file mode 100644 index 00000000000..74777f40333 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_SkipsMethodWithUnavailableGenericReturnType/LastContract.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Sample.Namespace +{ + public class HostedAgentDefinition + { + } + + public class ProjectsAgentTool + { + } + + public static partial class SampleNamespaceModelFactory + { + public static IEnumerable HostedAgentDefinition() + { + return null; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_SkipsMethodWithUnavailableParameterType/LastContract.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_SkipsMethodWithUnavailableParameterType/LastContract.cs new file mode 100644 index 00000000000..61aef4c93da --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_SkipsMethodWithUnavailableParameterType/LastContract.cs @@ -0,0 +1,18 @@ +namespace Sample.Namespace +{ + public class HostedAgentDefinition + { + } + + public class ProjectsAgentTool + { + } + + public static partial class SampleNamespaceModelFactory + { + public static HostedAgentDefinition HostedAgentDefinition(ProjectsAgentTool tool) + { + return null; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_SkipsMethodWithUnavailableReturnType/LastContract.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_SkipsMethodWithUnavailableReturnType/LastContract.cs new file mode 100644 index 00000000000..c57ab42a5ff --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_SkipsMethodWithUnavailableReturnType/LastContract.cs @@ -0,0 +1,18 @@ +namespace Sample.Namespace +{ + public class HostedAgentDefinition + { + } + + public class ProjectsAgentTool + { + } + + public static partial class SampleNamespaceModelFactory + { + public static ProjectsAgentTool HostedAgentDefinition() + { + return null; + } + } +} From 54c6512c15b5427855f0821be63453bb81bcaaa7 Mon Sep 17 00:00:00 2001 From: jolov Date: Wed, 29 Jul 2026 20:16:45 -0700 Subject: [PATCH 3/3] Remove inapplicable generic return test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c4da49e5-30c3-4be2-8083-fc61b690f9fa --- .../ModelFactoryProviderTests.cs | 23 ------------------- .../LastContract.cs | 20 ---------------- 2 files changed, 43 deletions(-) delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_SkipsMethodWithUnavailableGenericReturnType/LastContract.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoryProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoryProviderTests.cs index 5ba8adc2e88..14c65df69fc 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoryProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/ModelFactoryProviderTests.cs @@ -748,29 +748,6 @@ public async Task BackCompatibility_SkipsMethodWithUnavailableReturnType() StringAssert.DoesNotContain("ProjectsAgentTool", content); } - [Test] - public async Task BackCompatibility_SkipsMethodWithUnavailableGenericReturnType() - { - var hostedAgentDefinition = InputFactory.Model("HostedAgentDefinition"); - - _instance = (await MockHelpers.LoadMockGeneratorAsync( - inputNamespaceName: "Sample.Namespace", - inputModelTypes: [hostedAgentDefinition], - lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync())).Object; - - var modelFactory = _instance.OutputLibrary.ModelFactory.Value; - modelFactory.ProcessTypeForBackCompatibility(); - - var methods = modelFactory.Methods - .Where(method => method.Signature.Name == "HostedAgentDefinition") - .ToList(); - Assert.AreEqual(1, methods.Count); - Assert.AreEqual("HostedAgentDefinition", methods[0].Signature.ReturnType?.Name); - - var content = new TypeProviderWriter(modelFactory).Write().Content; - StringAssert.DoesNotContain("ProjectsAgentTool", content); - } - [Test] public void ModelWithNestedDiscriminators() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_SkipsMethodWithUnavailableGenericReturnType/LastContract.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_SkipsMethodWithUnavailableGenericReturnType/LastContract.cs deleted file mode 100644 index 74777f40333..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_SkipsMethodWithUnavailableGenericReturnType/LastContract.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; - -namespace Sample.Namespace -{ - public class HostedAgentDefinition - { - } - - public class ProjectsAgentTool - { - } - - public static partial class SampleNamespaceModelFactory - { - public static IEnumerable HostedAgentDefinition() - { - return null; - } - } -}