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..280d7cbb382 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,93 @@ protected internal sealed override IReadOnlyList BuildMethodsFor return [.. factoryMethods]; } + internal 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; + } + + if (TryGetArrayElementName(type, out var elementName)) + { + if (!IsTypeAvailable(type.Namespace, elementName, type.DeclaringType?.Name)) + { + unavailableTypes.Add(string.IsNullOrEmpty(type.Namespace) ? elementName : $"{type.Namespace}.{elementName}"); + } + 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 static bool TryGetArrayElementName(CSharpType type, [NotNullWhen(true)] out string? elementName) + { + var bracketIndex = type.Name.IndexOf('['); + if (bracketIndex <= 0) + { + elementName = null; + return false; + } + + elementName = type.Name[..bracketIndex]; + return true; + } + + private static bool IsTypeAvailable(string ns, string name, string? declaringTypeName) + { + var typeFactory = CodeModelGenerator.Instance.TypeFactory; + if (typeFactory.CSharpTypeMap.Keys.Any( + type => type.Name == name && + (string.IsNullOrEmpty(ns) || type.Namespace == ns) && + type.DeclaringType?.Name == declaringTypeName)) + { + return true; + } + + return CodeModelGenerator.Instance.SourceInputModel.FindForTypeInCustomization( + ns, + name, + declaringTypeName, + includeReferencedAssemblies: true) != null; + } + 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/src/Utilities/TypeSymbolExtensions.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/TypeSymbolExtensions.cs index 719b007bc48..9e8c4a6c493 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/TypeSymbolExtensions.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/TypeSymbolExtensions.cs @@ -196,7 +196,11 @@ private static CSharpType ConstructCSharpTypeFromSymbol( bool isNullable = fullyQualifiedName.StartsWith(NullableTypeName); bool isEnum = typeSymbol.TypeKind == TypeKind.Enum || (isNullable && typeArg?.TypeKind == TypeKind.Enum); bool isNullableUnknownType = isNullable && typeArg?.TypeKind == TypeKind.Error; - string name = isNullableUnknownType ? fullyQualifiedName : typeSymbol.Name; + string name = isNullableUnknownType + ? fullyQualifiedName + : typeSymbol is IArrayTypeSymbol + ? fullyQualifiedName[(fullyQualifiedName.LastIndexOf('.') + 1)..] + : typeSymbol.Name; // get everything before ` in case of generics string[] pieces = fullyQualifiedName.Split('`')[0].Split('.'); List arguments = []; 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..a3315b96226 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,152 @@ 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() + { + 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 async Task BackCompatibility_PreservesMethodWithAvailableArrayParameterType() + { + var tool = InputFactory.Model("Tool"); + var hostedAgentDefinition = InputFactory.Model( + "HostedAgentDefinition", + properties: + [ + InputFactory.Property("Tools", InputFactory.Array(tool)) + ]); + + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [tool, hostedAgentDefinition], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync())).Object; + + var modelFactory = _instance.OutputLibrary.TypeProviders.OfType().Single(); + var previousMethod = modelFactory.LastContractView!.Methods + .Single(method => method.Signature.Name == "HostedAgentDefinition"); + Assert.IsEmpty(ModelFactoryProvider.GetUnavailableSignatureTypes(previousMethod.Signature)); + } + + [Test] + public async Task BackCompatibility_SkipsMethodWithUnavailableArrayParameterType() + { + 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.TypeProviders.OfType().Single(); + var previousMethod = modelFactory.LastContractView!.Methods + .Single(method => method.Signature.Name == "HostedAgentDefinition"); + CollectionAssert.AreEqual( + new[] { "Sample.Models.ProjectsAgentTool" }, + ModelFactoryProvider.GetUnavailableSignatureTypes(previousMethod.Signature)); + + modelFactory.ProcessTypeForBackCompatibility(); + + var methods = modelFactory.Methods + .Where(method => method.Signature.Name == "HostedAgentDefinition") + .ToList(); + Assert.AreEqual(1, methods.Count); + + var content = new TypeProviderWriter(modelFactory).Write().Content; + 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 void ModelWithNestedDiscriminators() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_PreservesMethodWithAvailableArrayParameterType/LastContract.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_PreservesMethodWithAvailableArrayParameterType/LastContract.cs new file mode 100644 index 00000000000..2f3437900ea --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_PreservesMethodWithAvailableArrayParameterType/LastContract.cs @@ -0,0 +1,21 @@ +namespace Sample.Models +{ + public class Tool + { + } + + public class HostedAgentDefinition + { + } +} + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static Models.HostedAgentDefinition HostedAgentDefinition(Models.Tool[] tools) + { + return null; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_SkipsMethodWithUnavailableArrayParameterType/LastContract.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_SkipsMethodWithUnavailableArrayParameterType/LastContract.cs new file mode 100644 index 00000000000..3d5677df569 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_SkipsMethodWithUnavailableArrayParameterType/LastContract.cs @@ -0,0 +1,21 @@ +namespace Sample.Models +{ + public class HostedAgentDefinition + { + } + + public class ProjectsAgentTool + { + } +} + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static Models.HostedAgentDefinition HostedAgentDefinition(Models.ProjectsAgentTool[] tools) + { + return null; + } + } +} 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; + } + } +} 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; + } + } +}