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 92f5269ac14..c25c27689e1 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 @@ -102,6 +102,7 @@ protected internal sealed override IReadOnlyList BuildMethodsFor return [.. originalMethods]; } + IReadOnlyList customFactoryMethods = CustomCodeView?.Methods ?? []; List factoryMethods = [.. originalMethods]; // Preserve the original parameter names on current factory methods when the only @@ -110,18 +111,69 @@ protected internal sealed override IReadOnlyList BuildMethodsFor // property is renamed via @@clientName, spec rename, or naming-rule change). BackCompatHelper.RestorePreviousParameterNames(this, factoryMethods); - HashSet currentMethodSignatures = new List([.. factoryMethods, .. CustomCodeView?.Methods ?? []]) - .Select(m => m.Signature) - .ToHashSet(MethodSignature.MethodSignatureComparer); + var allFactoryMethods = factoryMethods + .Concat(customFactoryMethods) + .ToList(); + + var compatiblePreviousMethods = new List(); + List previousPublicSignatures = []; + List preservedPreviousSignatures = []; + List nonConstrainingCustomMethods = []; foreach (var previousMethod in LastContractView.Methods) { - if (!MethodSignatureHelper.IsPublicApi(previousMethod.Signature.Modifiers) || - currentMethodSignatures.Contains(previousMethod.Signature)) + if (!MethodSignatureHelper.IsPublicApi(previousMethod.Signature.Modifiers)) { continue; } + // Record every public previous signature, including the ones skipped below, because + // a current overload that still matches one of them must never be removed. + previousPublicSignatures.Add(previousMethod.Signature); + + var matchingCurrentMethod = allFactoryMethods.FirstOrDefault(m => + MethodSignature.MethodSignatureComparer.Equals(m.Signature, previousMethod.Signature)); + if (matchingCurrentMethod is not null) + { + bool isGeneratedMethod = factoryMethods.Any(method => ReferenceEquals(method, matchingCurrentMethod)); + if (isGeneratedMethod) + { + for (int i = 0; i < previousMethod.Signature.Parameters.Count; i++) + { + var currentParameter = matchingCurrentMethod.Signature.Parameters[i]; + if (previousMethod.Signature.Parameters[i].DefaultValue is null) + { + currentParameter.DefaultValue = null; + } + else + { + currentParameter.DefaultValue ??= Default; + } + } + } + + int previousMinimumArgumentCount = previousMethod.Signature.Parameters + .TakeWhile(p => p.DefaultValue is null && !p.IsParams) + .Count(); + int currentMinimumArgumentCount = matchingCurrentMethod.Signature.Parameters + .TakeWhile(p => p.DefaultValue is null && !p.IsParams) + .Count(); + if (currentMinimumArgumentCount == previousMinimumArgumentCount) + { + preservedPreviousSignatures.Add(previousMethod.Signature); + } + else if (!isGeneratedMethod && currentMinimumArgumentCount > previousMinimumArgumentCount) + { + // A custom method can take over the published CLR signature while requiring + // more arguments. The published omitted-argument calls can then only be + // served by a longer generated overload, so this custom method must not + // constrain that overload's optionality. + nonConstrainingCustomMethods.Add(matchingCurrentMethod); + } + + continue; + } + // If the removal of this factory method has already been accepted in the ApiCompat // baseline, honor that decision and do not resurrect a compatibility shim for it. if (BackCompatHelper.IsMethodRemovalAcceptedInBaseline(this, previousMethod.Signature)) @@ -138,31 +190,74 @@ protected internal sealed override IReadOnlyList BuildMethodsFor continue; } + compatiblePreviousMethods.Add(previousMethod); + preservedPreviousSignatures.Add(previousMethod.Signature); + } + + // Preserve every published signature as-is and constrain only newly generated overloads. + // Unlike a compatibility signature, a new overload has no existing callers whose minimum + // argument count must be retained. + foreach (var currentMethod in factoryMethods) + { + if (previousPublicSignatures.Any(previous => + MethodSignature.MethodSignatureComparer.Equals(previous, currentMethod.Signature))) + { + continue; + } + + // Constrain the overload against every other method that will exist on the final + // type, not just the published ones. A custom method is immutable and can occupy a + // shape that no longer matches its published optionality, so the generated overload + // is the only side that can change to keep the pair unambiguous. + // A published signature that this method already serves is excluded: no separate + // compatibility overload is emitted for it, so there is nothing to disambiguate + // from and stripping defaults would break the published omitted-argument calls. + // The exclusion is name-based and therefore applies only to published signatures; + // a custom overload is always emitted and must always constrain. + var competingSignatures = preservedPreviousSignatures + .Where(signature => !MethodSignatureHelper.HaveSameParametersInSameOrder( + currentMethod.Signature, + signature)) + .Concat(customFactoryMethods + .Where(method => !nonConstrainingCustomMethods.Any(m => ReferenceEquals(m, method))) + .Select(method => method.Signature)) + .Where(signature => + signature.Name == currentMethod.Signature.Name + && !MethodSignature.MethodSignatureComparer.Equals(signature, currentMethod.Signature)) + .ToList(); + MethodSignatureHelper.RequireMinimumParameterPrefix( + currentMethod.Signature, + competingSignatures, + preservePublishedMinimumArgumentCount: false); + } + + foreach (var previousMethod in compatiblePreviousMethods) + { List currentOverloads = []; bool foundCompatibleOverload = false; + var currentOverloadSignatures = GetCurrentOverloadSignatures( + allFactoryMethods, + previousMethod.Signature.Name); // Attempt to find an updated method in the current contract to call - foreach (var currentMethodSignature in currentMethodSignatures) + foreach (var currentMethodSignature in currentOverloadSignatures) { - if (currentMethodSignature.Name.Equals(previousMethod.Signature.Name)) + if (MethodSignatureHelper.HaveSameParametersInSameOrder(currentMethodSignature, previousMethod.Signature)) { - if (MethodSignatureHelper.HaveSameParametersInSameOrder(currentMethodSignature, previousMethod.Signature)) - { - foundCompatibleOverload = true; - break; - } - - if (HasMatchingExactParameterNames(currentMethodSignature, previousMethod.Signature)) - { - CodeModelGenerator.Instance.Emitter.Info( - $"Model factory method '{Name}.{previousMethod.Signature.Name}' keeps its exact parameter name(s) instead of the last contract's.", - BackCompatibilityChangeCategory.ModelFactoryMethodSkipped); - foundCompatibleOverload = true; - break; - } + foundCompatibleOverload = true; + break; + } - currentOverloads.Add(currentMethodSignature); + if (HasMatchingExactParameterNames(currentMethodSignature, previousMethod.Signature)) + { + CodeModelGenerator.Instance.Emitter.Info( + $"Model factory method '{Name}.{previousMethod.Signature.Name}' keeps its exact parameter name(s) instead of the last contract's.", + BackCompatibilityChangeCategory.ModelFactoryMethodSkipped); + foundCompatibleOverload = true; + break; } + + currentOverloads.Add(currentMethodSignature); } if (foundCompatibleOverload) @@ -170,32 +265,58 @@ protected internal sealed override IReadOnlyList BuildMethodsFor continue; } + // Generated overloads were constrained above, so only immutable custom overloads can + // still force a compatibility signature to change its published optionality. A custom + // overload that reproduces a preserved published signature is excluded as well: it + // already coexisted with this signature in the published contract, so the calls that + // reach both of them were valid before and must keep compiling. + var compatibilityOverloadSignatures = customFactoryMethods + .Select(method => method.Signature) + .Where(signature => + signature.Name == previousMethod.Signature.Name + && !preservedPreviousSignatures.Any(previous => + MethodSignature.MethodSignatureComparer.Equals(previous, signature))) + .ToList(); + foreach (var currentOverload in currentOverloads) { - // If the parameter ordering is the only difference, just use the previous method + // If the parameter ordering is the only difference, just use the previous method. if (MethodSignatureHelper.ContainsSameParameters(previousMethod.Signature, currentOverload) - && TryBuildCompatibleMethodForPreviousContract(previousMethod, currentOverload, false, out MethodProvider? replacedMethod)) + && !previousPublicSignatures.Any(previous => + MethodSignature.MethodSignatureComparer.Equals(previous, currentOverload))) { - factoryMethods.Add(replacedMethod); - var factoryMethodToRemove = factoryMethods .FirstOrDefault(m => MethodSignature.MethodSignatureComparer.Equals(m.Signature, currentOverload)); - if (factoryMethodToRemove != null) + if (TryBuildCompatibleMethodForPreviousContract( + previousMethod, + currentOverload, + false, + compatibilityOverloadSignatures, + out MethodProvider? replacedMethod)) { - factoryMethods.Remove(factoryMethodToRemove); - } + factoryMethods.Add(replacedMethod); - CodeModelGenerator.Instance.Emitter.Debug( - $"Replaced model factory method '{Name}.{currentOverload.Name}' with previous parameter order from last contract.", - BackCompatibilityChangeCategory.ModelFactoryMethodReplaced); + if (factoryMethodToRemove != null) + { + factoryMethods.Remove(factoryMethodToRemove); + } - foundCompatibleOverload = true; - break; + CodeModelGenerator.Instance.Emitter.Debug( + $"Replaced model factory method '{Name}.{currentOverload.Name}' with previous parameter order from last contract.", + BackCompatibilityChangeCategory.ModelFactoryMethodReplaced); + foundCompatibleOverload = true; + break; + } } - if (TryBuildCompatibleMethodForPreviousContract(previousMethod, currentOverload, true, out replacedMethod)) + if (TryBuildCompatibleMethodForPreviousContract( + previousMethod, + currentOverload, + true, + compatibilityOverloadSignatures, + out var hiddenMethod)) { - factoryMethods.Add(replacedMethod); + factoryMethods.Add(hiddenMethod); CodeModelGenerator.Instance.Emitter.Debug( $"Added back-compat overload for model factory method '{Name}.{previousMethod.Signature.Name}' delegating to '{currentOverload.Name}'.", BackCompatibilityChangeCategory.ModelFactoryMethodAdded); @@ -210,7 +331,12 @@ protected internal sealed override IReadOnlyList BuildMethodsFor } // If no compatible overload found, try to add the previous method by instantiating the model directly. - if (TryBuildCompatibleMethodForPreviousContract(previousMethod, null, true, out var builtMethod)) + if (TryBuildCompatibleMethodForPreviousContract( + previousMethod, + null, + true, + compatibilityOverloadSignatures, + out var builtMethod)) { factoryMethods.Add(builtMethod); CodeModelGenerator.Instance.Emitter.Debug( @@ -224,10 +350,19 @@ protected internal sealed override IReadOnlyList BuildMethodsFor BackCompatibilityChangeCategory.ModelFactoryMethodSkipped); } } - return [.. factoryMethods]; } + private IReadOnlyList GetCurrentOverloadSignatures( + IEnumerable methods, + string methodName) + { + return methods + .Where(m => m.Signature.Name == methodName) + .Select(m => m.Signature) + .ToList(); + } + private static bool HasMatchingExactParameterNames(MethodSignature current, MethodSignature previous) { if (!MethodSignature.MethodSignatureComparer.Equals(current, previous)) @@ -338,6 +473,7 @@ private bool TryBuildCompatibleMethodForPreviousContract( MethodProvider previousMethod, MethodSignature? currentMethodSignature, bool hideMethod, + IReadOnlyList currentOverloadSignatures, [NotNullWhen(true)] out MethodProvider? builtMethod) { builtMethod = null; @@ -373,7 +509,10 @@ private bool TryBuildCompatibleMethodForPreviousContract( { var callToOverload = Return(new InvokeMethodExpression(null, currentMethodSignature, arguments)); builtMethod = new MethodProvider( - MethodSignatureHelper.BuildBackCompatMethodSignature(previousMethod.Signature, hideMethod), + MethodSignatureHelper.BuildBackCompatMethodSignature( + previousMethod.Signature, + hideMethod, + currentMethodSignatures: currentOverloadSignatures), callToOverload, this, previousMethod.XmlDocs); @@ -384,7 +523,10 @@ private bool TryBuildCompatibleMethodForPreviousContract( MethodBodyStatements body = ConstructMethodBody(previousMethod.Signature, modelToInstantiate); builtMethod = new MethodProvider( - MethodSignatureHelper.BuildBackCompatMethodSignature(previousMethod.Signature, hideMethod), + MethodSignatureHelper.BuildBackCompatMethodSignature( + previousMethod.Signature, + hideMethod, + currentMethodSignatures: currentOverloadSignatures), body, this, previousMethod.XmlDocs); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs index 1baac80ce24..6fe30751308 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/MethodSignatureHelper.cs @@ -57,17 +57,59 @@ internal static bool HaveSameParametersInSameOrder(MethodSignature method1, Meth return true; } - internal static MethodSignature BuildBackCompatMethodSignature(MethodSignature previousMethodSignature, bool hideMethod, bool shouldNotBeAsync = false) + internal static MethodSignature BuildBackCompatMethodSignature( + MethodSignature previousMethodSignature, + bool hideMethod, + bool shouldNotBeAsync = false) { if (hideMethod) { - // make all parameter required to avoid ambiguous call sites if necessary - foreach (var param in previousMethodSignature.Parameters) - { - param.DefaultValue = null; - } + RequireMinimumParameterPrefix(previousMethodSignature); + } + + return CreateBackCompatSignature(previousMethodSignature, hideMethod, shouldNotBeAsync); + } + + internal static MethodSignature BuildBackCompatMethodSignature( + MethodSignature previousMethodSignature, + bool hideMethod, + IReadOnlyList currentMethodSignatures, + bool shouldNotBeAsync = false) + { + RequireMinimumParameterPrefix(previousMethodSignature, currentMethodSignatures); + + return CreateBackCompatSignature(previousMethodSignature, hideMethod, shouldNotBeAsync); + } + + /// + /// Removes the default values from the leading parameters of so it + /// can no longer be called with fewer arguments than the prefix that distinguishes it from + /// . When no overloads are supplied there is nothing to + /// compare against and every parameter becomes required. + /// + internal static void RequireMinimumParameterPrefix( + MethodSignature signature, + IReadOnlyList? currentMethodSignatures = null, + bool preservePublishedMinimumArgumentCount = true) + { + int requiredParameterCount = currentMethodSignatures is null + ? signature.Parameters.Count + : GetMinimumRequiredParameterCount( + signature, + currentMethodSignatures, + preservePublishedMinimumArgumentCount); + + for (int i = 0; i < requiredParameterCount; i++) + { + signature.Parameters[i].DefaultValue = null; } + } + private static MethodSignature CreateBackCompatSignature( + MethodSignature previousMethodSignature, + bool hideMethod, + bool shouldNotBeAsync) + { var modifiers = shouldNotBeAsync ? previousMethodSignature.Modifiers & ~MethodSignatureModifiers.Async : previousMethodSignature.Modifiers; @@ -85,6 +127,109 @@ internal static MethodSignature BuildBackCompatMethodSignature(MethodSignature p Attributes: attributes); } + private static int GetMinimumRequiredParameterCount( + MethodSignature targetMethodSignature, + IReadOnlyList competingMethodSignatures, + bool preservePublishedMinimumArgumentCount) + { + int requiredParameterCount = 0; + foreach (var competingMethodSignature in competingMethodSignatures) + { + if (competingMethodSignature.Name == targetMethodSignature.Name) + { + requiredParameterCount = Math.Max( + requiredParameterCount, + GetMinimumRequiredParameterCount( + targetMethodSignature, + competingMethodSignature, + preservePublishedMinimumArgumentCount)); + } + } + + return requiredParameterCount; + } + + private static int GetMinimumRequiredParameterCount( + MethodSignature targetMethodSignature, + MethodSignature competingMethodSignature, + bool preservePublishedMinimumArgumentCount) + { + if (competingMethodSignature.Parameters.Any(p => p.IsRef || p.IsOut)) + { + return 0; + } + + int targetMinimumArgumentCount = GetMinimumArgumentCount(targetMethodSignature); + int competingMinimumArgumentCount = GetMinimumArgumentCount(competingMethodSignature); + int competingMaximumArgumentCount = competingMethodSignature.Parameters.Any(p => p.IsParams) + ? int.MaxValue + : competingMethodSignature.Parameters.Count; + + // No argument count can reach both overloads, so the target needs no additional + // required parameters. + if (Math.Max(targetMinimumArgumentCount, competingMinimumArgumentCount) > + Math.Min(targetMethodSignature.Parameters.Count, competingMaximumArgumentCount)) + { + return 0; + } + + // When the target is a published signature, do not raise its minimum argument count to + // address overlap with a competitor that cannot apply to its shorter calls. + if (preservePublishedMinimumArgumentCount && + competingMinimumArgumentCount > targetMinimumArgumentCount) + { + return 0; + } + + // Require only the prefix up to and including the first position whose parameter types + // distinguish the overloads. A type-name difference alone is insufficient because a + // null literal is applicable to any reference or nullable value type. + int overlappingParameterCount = Math.Min( + targetMethodSignature.Parameters.Count, + competingMethodSignature.Parameters.Count); + for (int i = 0; i < overlappingParameterCount; i++) + { + CSharpType targetType = targetMethodSignature.Parameters[i].Type; + CSharpType competingType = competingMethodSignature.Parameters[i].Type; + bool sameType = targetType.IsNullable == competingType.IsNullable + && targetType.AreNamesEqual(competingType); + if (!sameType + && ((targetType.IsValueType && !targetType.IsNullable) + || (competingType.IsValueType && !competingType.IsNullable))) + { + return Math.Max(i + 1, targetMinimumArgumentCount); + } + } + + // The competitor is a strict positional prefix of the target. Requiring one parameter + // beyond the competitor's maximum arity makes their applicable argument counts disjoint + // while preserving every remaining trailing default on the target. + if (targetMethodSignature.Parameters.Count > competingMaximumArgumentCount) + { + return Math.Max(competingMaximumArgumentCount + 1, targetMinimumArgumentCount); + } + + // The target is a positional prefix of the competitor, so it cannot require one more + // argument than the competitor accepts. Fall back to requiring every target parameter. + return targetMethodSignature.Parameters.Count; + } + + private static int GetMinimumArgumentCount(MethodSignature methodSignature) + { + int count = 0; + foreach (var parameter in methodSignature.Parameters) + { + if (parameter.DefaultValue is not null || parameter.IsParams) + { + break; + } + + count++; + } + + return count; + } + private sealed class ParameterProviderVariableNameComparer : IEqualityComparer { public bool Equals(ParameterProvider? x, ParameterProvider? y) 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 f60d9d799fc..0eba7a1a42f 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 @@ -3,12 +3,15 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Input.Extensions; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; +using Microsoft.TypeSpec.Generator.Snippets; +using Microsoft.TypeSpec.Generator.Statements; using Microsoft.TypeSpec.Generator.Tests.Common; using NUnit.Framework; @@ -198,7 +201,7 @@ public async Task BackCompatibility_NewModelPropertyAdded() Assert.AreEqual("listProp", parameters[2].Name); foreach (var param in parameters) { - Assert.IsNull(param.DefaultValue); + Assert.IsNotNull(param.DefaultValue); } var currentParameters = currentOverloadMethod!.Signature.Parameters; @@ -209,7 +212,7 @@ public async Task BackCompatibility_NewModelPropertyAdded() Assert.AreEqual("dictProp", currentParameters[3].Name); foreach (var param in currentParameters) { - Assert.IsNotNull(param.DefaultValue); + Assert.IsNull(param.DefaultValue); } Assert.IsTrue(parameters[0].Type.AreNamesEqual(currentParameters[0].Type)); @@ -257,10 +260,6 @@ public async Task BackCompatibility_NewPropertyAddedWithDifferentParamOrder() Assert.AreEqual("modelProp", parameters[0].Name); Assert.AreEqual("stringProp", parameters[1].Name); Assert.AreEqual("listProp", parameters[2].Name); - foreach (var param in parameters) - { - Assert.IsNull(param.DefaultValue); - } // validate the previous method body uses named arguments to ensure correct mapping // even though the parameter order differs between the previous and current methods @@ -272,6 +271,583 @@ public async Task BackCompatibility_NewPropertyAddedWithDifferentParamOrder() result); } + [Test] + public async Task BackCompatibility_ReorderedFullyOptionalParametersRequireMinimumPrefix() + { + var compatibilityModel = GetCompatibilityModel(includeCount: true); + + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [compatibilityModel], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Last"))).Object; + + var modelFactory = _instance.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + var content = new TypeProviderWriter(modelFactory).Write().Content; + Assert.AreEqual(Helpers.GetExpectedFromFile(), content); + } + + [Test] + public async Task BackCompatibility_ReorderedRequiredParametersPreserveTrailingOptionalParameters() + { + var compatibilityModel = GetCompatibilityModel(includeCount: true); + + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [compatibilityModel], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Last"))).Object; + + var modelFactory = _instance.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + var content = new TypeProviderWriter(modelFactory).Write().Content; + Assert.AreEqual(Helpers.GetExpectedFromFile(), content); + } + + [Test] + public async Task BackCompatibility_ReorderedCustomOverloadRequiresMinimumPrefix() + { + var compatibilityModel = GetCompatibilityModel(includeCount: false); + + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [compatibilityModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Custom"), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Last"))).Object; + + var modelFactory = _instance.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + var content = new TypeProviderWriter(modelFactory).Write().Content; + Assert.AreEqual(Helpers.GetExpectedFromFile(), content); + } + + [Test] + public async Task BackCompatibility_CustomOverloadsPreserveTrailingOptionalParameters() + { + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [GetCompatibilityModel(includeCount: false)], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Custom"), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Last"))).Object; + + var modelFactory = _instance.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + var content = new TypeProviderWriter(modelFactory).Write().Content; + Assert.AreEqual(Helpers.GetExpectedFromFile(), content); + } + + [Test] + public async Task BackCompatibility_CustomPublishedSignatureDoesNotRequireLongerCurrentOverload() + { + InputModelType model = InputFactory.Model("CompatibilityModel", properties: + [ + InputFactory.Property("Id", InputPrimitiveType.String), + InputFactory.Property("Description", InputPrimitiveType.String), + InputFactory.Property("Text", InputPrimitiveType.String), + InputFactory.Property("IsRegex", new InputNullableType(InputPrimitiveType.Boolean)), + ]); + + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [model], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Custom"), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Last"))).Object; + + var modelFactory = _instance.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + var currentMethod = modelFactory.Methods.Single(m => + m.Signature.Name == "CompatibilityModel" && + m.Signature.Parameters.Count == 4); + Assert.That(currentMethod.Signature.Parameters, Has.All.Property("DefaultValue").Not.Null); + } + + [Test] + public async Task BackCompatibility_OptionalCustomPublishedSignatureRequiresLongerCurrentOverload() + { + InputModelType model = InputFactory.Model("CompatibilityModel", properties: + [ + InputFactory.Property("Id", InputPrimitiveType.String), + InputFactory.Property("Description", InputPrimitiveType.String), + InputFactory.Property("Text", InputPrimitiveType.String), + InputFactory.Property("IsRegex", new InputNullableType(InputPrimitiveType.Boolean)), + ]); + + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [model], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Custom"), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Last"))).Object; + + var modelFactory = _instance.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + var currentMethod = modelFactory.Methods.Single(m => + m.Signature.Name == "CompatibilityModel" && + m.Signature.Parameters.Count == 4); + Assert.That(currentMethod.Signature.Parameters, Has.All.Property("DefaultValue").Null); + } + + [Test] + public async Task BackCompatibility_ChangedCustomOptionalityConstrainsCompatibilityOverload() + { + InputModelType model = InputFactory.Model("CompatibilityModel", properties: + [ + InputFactory.Property("Id", InputPrimitiveType.String), + InputFactory.Property("Description", InputPrimitiveType.String), + InputFactory.Property("Text", InputPrimitiveType.String), + InputFactory.Property("IsRegex", InputPrimitiveType.Boolean), + ]); + + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [model], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Custom"), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Last"))).Object; + + var modelFactory = _instance.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + var compatibilityMethod = modelFactory.Methods.Single(m => + m.Signature.Name == "CompatibilityModel" && + m.Signature.Parameters.Count == 2); + Assert.IsNull(compatibilityMethod.Signature.Parameters[0].DefaultValue); + Assert.IsNotNull(compatibilityMethod.Signature.Parameters[1].DefaultValue); + + // The custom method keeps the fully optional (string, string, string) shape, so the + // generated overload must require every parameter. Leaving any trailing default would + // make CompatibilityModel("id") and CompatibilityModel("id", "description") ambiguous. + var generatedMethod = modelFactory.Methods.Single(m => + m.Signature.Name == "CompatibilityModel" && + m.Signature.Parameters.Count == 4); + Assert.IsTrue(generatedMethod.Signature.Parameters.All(p => p.DefaultValue is null)); + } + + // A custom overload that was never published still exists on the final type, so a newly + // generated overload must be constrained against it. The published (bool, string) overload + // alone would only require the first parameter, leaving CompatibilityModel("id") ambiguous + // with the custom (string, string) overload. + [Test] + public async Task BackCompatibility_CustomOverloadConstrainsNewlyGeneratedOverload() + { + InputModelType model = InputFactory.Model("CompatibilityModel", properties: + [ + InputFactory.Property("Id", InputPrimitiveType.String), + InputFactory.Property("Description", InputPrimitiveType.String), + InputFactory.Property("Text", InputPrimitiveType.String), + InputFactory.Property("IsRegex", InputPrimitiveType.Boolean), + ]); + + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [model], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Custom"), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Last"))).Object; + + var modelFactory = _instance.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + // The custom overload accepts zero to two arguments, so the generated overload needs + // three required parameters to make their applicable argument counts disjoint. The + // trailing default is still preserved because it cannot reach the custom overload. + var content = new TypeProviderWriter(modelFactory).Write().Content; + Assert.AreEqual(Helpers.GetExpectedFromFile(), content); + } + + // A custom overload with the same parameter names and order but a different parameter type is + // still emitted, so it must constrain the generated overload. Excluding it by name would leave + // CompatibilityModel("id") ambiguous between the two. + [Test] + public async Task BackCompatibility_CustomOverloadWithDifferentTypesStillConstrains() + { + InputModelType model = InputFactory.Model("CompatibilityModel", properties: + [ + InputFactory.Property("Id", InputPrimitiveType.String), + InputFactory.Property("Description", InputPrimitiveType.String), + InputFactory.Property("Text", InputPrimitiveType.String), + InputFactory.Property("IsRegex", InputPrimitiveType.Boolean), + ]); + + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [model], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Custom"), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Last"))).Object; + + var modelFactory = _instance.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + var content = new TypeProviderWriter(modelFactory).Write().Content; + Assert.AreEqual(Helpers.GetExpectedFromFile(), content); + } + + // A published overload that the reorder path did not replace still exists on the final type, + // but it already coexisted with the reordered signature in the published contract, so every + // call that reaches both of them compiled before. Constraining the reordered compatibility + // signature against it would strip defaults the published callers rely on, breaking calls + // such as CompatibilityModel() and CompatibilityModel(5). + [Test] + public async Task BackCompatibility_ReorderKeepsOptionalityAgainstCoexistingPublishedOverload() + { + InputModelType model = InputFactory.Model("CompatibilityModel", properties: + [ + InputFactory.Property("Id", InputPrimitiveType.String), + InputFactory.Property("Count", InputPrimitiveType.Int32), + ]); + + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [model], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Custom"), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Last"))).Object; + + var modelFactory = _instance.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + var content = new TypeProviderWriter(modelFactory).Write().Content; + Assert.AreEqual(Helpers.GetExpectedFromFile(), content); + } + + // A published signature whose parameter names all still match is served by the generated + // method itself, so no separate compatibility overload is emitted for it. The generated + // method must therefore keep its defaults: constraining it against a signature it already + // serves would break the published CompatibilityModel() and CompatibilityModel(id) calls. + [Test] + public async Task BackCompatibility_ServedPublishedSignatureKeepsGeneratedOptionality() + { + InputModelType model = InputFactory.Model("CompatibilityModel", properties: + [ + InputFactory.Property("Id", InputPrimitiveType.String), + InputFactory.Property("IsRegex", InputPrimitiveType.Boolean), + ]); + + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [model], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Last"))).Object; + + var modelFactory = _instance.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + var content = new TypeProviderWriter(modelFactory).Write().Content; + Assert.AreEqual(Helpers.GetExpectedFromFile(), content); + } + + // The last contract is read from metadata, which reports a parameter's default as a constant + // rather than as syntax and so cannot tell `= default` apart from `= 0F`. Only optionality is + // restored from the published signature, so the generated `default` is kept either way. + [Test] + public async Task BackCompatibility_PublishedDefaultLiteralsDoNotChangeGeneratedDefaults() + { + InputModelType model = InputFactory.Model("CompatibilityModel", properties: + [ + InputFactory.Property("Value", InputPrimitiveType.Float32, isRequired: true), + InputFactory.Property("Kind", InputPrimitiveType.String, isRequired: true), + ]); + + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [model], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Last"))).Object; + + var modelFactory = _instance.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + var content = new TypeProviderWriter(modelFactory).Write().Content; + Assert.That(content, Does.Contain("float value = default, string kind = default)")); + Assert.That(content, Does.Not.Contain("0F")); + Assert.That(content, Does.Not.Contain("\"Unknown\"")); + } + + // Mirrors the reported Azure.ResourceManager.AppService regression: both overloads already + // existed in the last contract, so each must retain its published optionality. + [Test] + public async Task BackCompatibility_ReorderedOverloadKeptWhenStillInLastContract() + { + InputModelType model = InputFactory.Model("CompatibilityModel", properties: + [ + InputFactory.Property("Id", InputPrimitiveType.String), + InputFactory.Property("Kind", InputPrimitiveType.String), + InputFactory.Property("Image", InputPrimitiveType.String), + InputFactory.Property("IsMain", new InputNullableType(InputPrimitiveType.Boolean)), + ]); + + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [model], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Last"))).Object; + + var modelFactory = _instance.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + var content = new TypeProviderWriter(modelFactory).Write().Content; + Assert.AreEqual(Helpers.GetExpectedFromFile(), content); + } + + // A previous signature that is a positional prefix of a new current overload cannot be + // disambiguated by argument count, so every parameter on the new overload must be required. + [Test] + public async Task BackCompatibility_PositionalPrefixOverloadRequiresAllParameters() + { + InputModelType model = InputFactory.Model("CompatibilityModel", properties: + [ + InputFactory.Property("Id", InputPrimitiveType.String), + InputFactory.Property("Name", InputPrimitiveType.String), + InputFactory.Property("Count", new InputNullableType(InputPrimitiveType.Int32)), + ]); + + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [model], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Last"))).Object; + + var modelFactory = _instance.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + var content = new TypeProviderWriter(modelFactory).Write().Content; + Assert.AreEqual(Helpers.GetExpectedFromFile(), content); + } + + // When the previous signature is a strict positional prefix of a longer new overload, + // requiring one parameter beyond the previous signature makes their applicable argument + // counts disjoint. Parameters after that boundary must retain their optionality. + [Test] + public async Task BackCompatibility_LongPositionalPrefixOverloadPreservesTrailingOptionality() + { + InputModelType model = InputFactory.Model("CompatibilityModel", properties: + [ + InputFactory.Property("Id", InputPrimitiveType.String), + InputFactory.Property("Name", InputPrimitiveType.String), + InputFactory.Property("Count", new InputNullableType(InputPrimitiveType.Int32)), + InputFactory.Property("Description", InputPrimitiveType.String), + ]); + + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [model], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Last"))).Object; + + var modelFactory = _instance.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + var content = new TypeProviderWriter(modelFactory).Write().Content; + Assert.AreEqual(Helpers.GetExpectedFromFile(), content); + } + + // The management generator's ModelFactoryVisitor restores last-contract methods verbatim during + // the visitor pass, which runs before back-compatibility processing. The restored overload must + // keep its published defaults while the new generated overload acquires the required prefix. + [Test] + public async Task BackCompatibility_VisitorAddedOverloadRequiresMinimumPrefix() + { + InputModelType model = InputFactory.Model("CompatibilityModel", properties: + [ + InputFactory.Property("Id", InputPrimitiveType.String), + InputFactory.Property("Name", InputPrimitiveType.String), + InputFactory.Property("Image", InputPrimitiveType.String), + InputFactory.Property("TargetPort", InputPrimitiveType.String), + InputFactory.Property("IsMain", new InputNullableType(InputPrimitiveType.Boolean)), + InputFactory.Property("Kind", InputPrimitiveType.String), + ]); + + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [model], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Last"))).Object; + + var modelFactory = _instance.OutputLibrary.ModelFactory.Value; + + var previous = modelFactory.LastContractView!.Methods[0]; + var restored = new MethodProvider( + new MethodSignature( + previous.Signature.Name, + previous.Signature.Description, + previous.Signature.Modifiers, + previous.Signature.ReturnType, + previous.Signature.ReturnDescription, + previous.Signature.Parameters, + [.. previous.Signature.Attributes, new AttributeStatement(typeof(EditorBrowsableAttribute), Snippet.FrameworkEnumValue(EditorBrowsableState.Never))]), + Snippet.Throw(Snippet.Null), + modelFactory); + modelFactory.Update(methods: [.. modelFactory.Methods, restored]); + + modelFactory.ProcessTypeForBackCompatibility(); + + var content = new TypeProviderWriter(modelFactory).Write().Content; + Assert.AreEqual(Helpers.GetExpectedFromFile(), content); + } + + // Two previous overloads compete with the same new current method. Both retain their published + // defaults while the new overload acquires the longest prefix needed to avoid both. + [Test] + public async Task BackCompatibility_MultiplePreviousOverloadsRequireIndependentPrefixes() + { + InputModelType model = InputFactory.Model("CompatibilityModel", properties: + [ + InputFactory.Property("Id", InputPrimitiveType.String), + InputFactory.Property("Name", InputPrimitiveType.String), + InputFactory.Property("Count", new InputNullableType(InputPrimitiveType.Int32)), + InputFactory.Property("Flag", new InputNullableType(InputPrimitiveType.Boolean)), + InputFactory.Property("Kind", InputPrimitiveType.String), + ]); + + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [model], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Last"))).Object; + + var modelFactory = _instance.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + var content = new TypeProviderWriter(modelFactory).Write().Content; + Assert.AreEqual(Helpers.GetExpectedFromFile(), content); + } + + // Overloads that shipped together in the published contract already coexisted there, so they + // are not new competitors for one another; only a surviving current or custom overload can + // introduce ambiguity that was not already present. Here 'id, count' is a positional prefix + // of the wider overload, so treating them as competitors would make the wider one fully + // required and break the previously valid call 'CompatibilityModel("i", 1, "e")'. + [Test] + public async Task BackCompatibility_CoexistingPreviousOverloadsKeepPublishedOptionality() + { + InputModelType model = InputFactory.Model("CompatibilityModel", properties: + [ + InputFactory.Property("Id", InputPrimitiveType.String), + InputFactory.Property("Description", InputPrimitiveType.String), + InputFactory.Property("Name", InputPrimitiveType.String), + ]); + + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [model], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Last"))).Object; + + var modelFactory = _instance.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + var content = new TypeProviderWriter(modelFactory).Write().Content; + Assert.AreEqual(Helpers.GetExpectedFromFile(), content); + } + + // Same last-contract overloads as above but declared in the opposite order. Signatures are + // mutated in place, so this pins that the result does not depend on declaration order. + [Test] + public async Task BackCompatibility_CoexistingPreviousOverloadsKeepPublishedOptionalityReversed() + { + InputModelType model = InputFactory.Model("CompatibilityModel", properties: + [ + InputFactory.Property("Id", InputPrimitiveType.String), + InputFactory.Property("Description", InputPrimitiveType.String), + InputFactory.Property("Name", InputPrimitiveType.String), + ]); + + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [model], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Last"))).Object; + + var modelFactory = _instance.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + var content = new TypeProviderWriter(modelFactory).Write().Content; + Assert.AreEqual(Helpers.GetExpectedFromFile(), content); + } + + // An all-required previous overload is only callable at exactly its own argument count, so it + // cannot be reached by shorter calls to a coexisting all-optional overload. Promoting the + // all-optional overload against it would raise its published minimum argument count and break + // previously valid low-arity calls, so the published optionality must be preserved. This + // mirrors the shipped Azure.ResourceManager.AppService 'SiteConfigProperties' shape. + [Test] + public async Task BackCompatibility_AllRequiredPreviousOverloadKeepsPublishedOptionality() + { + InputModelType model = InputFactory.Model("CompatibilityModel", properties: + [ + InputFactory.Property("Id", InputPrimitiveType.String), + InputFactory.Property("Name", InputPrimitiveType.String), + InputFactory.Property("Count", new InputNullableType(InputPrimitiveType.Int32)), + InputFactory.Property("Kind", InputPrimitiveType.String), + ]); + + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [model], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Last"))).Object; + + var modelFactory = _instance.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + var content = new TypeProviderWriter(modelFactory).Write().Content; + Assert.AreEqual(Helpers.GetExpectedFromFile(), content); + } + + // A current generated overload can have the same signature as a previously published overload + // while acquiring different defaults from the current model shape. Preserve the published + // required boundary on that overload and the published optionality on its reordered companion + // rather than swapping their callability. + [Test] + public async Task BackCompatibility_PublishedOverloadBoundariesArePreserved() + { + InputModelType model = InputFactory.Model("CompatibilityModel", properties: + [ + InputFactory.Property("Id", InputPrimitiveType.String), + InputFactory.Property("Allow", new InputNullableType(InputPrimitiveType.Boolean)), + InputFactory.Property("Kind", InputPrimitiveType.String), + ]); + + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [model], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Last"))).Object; + + var modelFactory = _instance.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + var currentOverload = modelFactory.Methods.Single(m => + m.Signature.Name == "CompatibilityModel" + && m.Signature.Parameters[1].Name == "allow"); + var compatibilityOverload = modelFactory.Methods.Single(m => + m.Signature.Name == "CompatibilityModel" + && m.Signature.Parameters[1].Name == "kind"); + + Assert.IsTrue(currentOverload.Signature.Parameters.All(p => p.DefaultValue is null)); + Assert.IsTrue(compatibilityOverload.Signature.Parameters.All(p => p.DefaultValue is not null)); + } + + // The newly generated overload did not exist in the previous contract, so it can acquire the + // required prefix needed for disambiguation. The previous overload must remain fully optional + // so calls using its unique parameter names continue to compile. + [Test] + public async Task BackCompatibility_NewOverloadIsConstrainedToPreservePublishedNamedArguments() + { + InputModelType model = InputFactory.Model("CompatibilityModel", properties: + [ + InputFactory.Property("Id", InputPrimitiveType.String), + InputFactory.Property("Properties", new InputNullableType(InputPrimitiveType.Int32)), + ]); + + _instance = (await MockHelpers.LoadMockGeneratorAsync( + inputNamespaceName: "Sample.Namespace", + inputModelTypes: [model], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(parameters: "Last"))).Object; + + var modelFactory = _instance.OutputLibrary.ModelFactory.Value; + modelFactory.ProcessTypeForBackCompatibility(); + + var currentOverload = modelFactory.Methods.Single(m => + m.Signature.Name == "CompatibilityModel" + && m.Signature.Parameters.Count == 2); + var compatibilityOverload = modelFactory.Methods.Single(m => + m.Signature.Name == "CompatibilityModel" + && m.Signature.Parameters.Count == 3); + + Assert.IsTrue(currentOverload.Signature.Parameters.All(p => p.DefaultValue is null)); + Assert.IsTrue(compatibilityOverload.Signature.Parameters.All(p => p.DefaultValue is not null)); + } + // This test validates that only the previous model factory methods are generated when only the parameter ordering is changed // in the current library version. [Test] @@ -374,10 +950,7 @@ public async Task BackCompatibility_NoCurrentOverloadFound() var parameters = backwardCompatibilityMethod!.Signature.Parameters; Assert.AreEqual(1, parameters.Count); Assert.AreEqual("stringProp", parameters[0].Name); - foreach (var param in parameters) - { - Assert.IsNull(param.DefaultValue); - } + Assert.IsNotNull(parameters[0].DefaultValue); var attributes = backwardCompatibilityMethod!.Signature.Attributes; Assert.AreEqual(1, attributes.Count); var printedAttribute = attributes[0].ToDisplayString(); @@ -712,9 +1285,11 @@ public async Task BackCompatibility_NewPropertyAddedWithRenamedParam() Assert.AreEqual("listProp", parameters[2].Name); foreach (var param in parameters) { - Assert.IsNull(param.DefaultValue); + Assert.IsNotNull(param.DefaultValue); } + Assert.IsTrue(currentParameters.All(p => p.DefaultValue is null)); + // The backcompat overload's body instantiates the model directly because the previous // parameter names (oldStringProp, oldModelProp) do not match any current property name. // For unmatched parameters the generator falls back to passing `default` to the @@ -1348,6 +1923,24 @@ public async Task BackCompatibility_BackCompatMethodCanBeMutatedByVisitor() Assert.IsNotNull(renamed, "The visitor's rename of the back-compat method was not applied."); } + private static InputModelType GetCompatibilityModel(bool includeCount) + { + List properties = + [ + InputFactory.Property("Id", InputPrimitiveType.String), + InputFactory.Property("Name", InputPrimitiveType.String), + InputFactory.Property("Kind", InputPrimitiveType.String), + InputFactory.Property("Enabled", new InputNullableType(InputPrimitiveType.Boolean)), + InputFactory.Property("Description", InputPrimitiveType.String), + ]; + if (includeCount) + { + properties.Add(InputFactory.Property("Count", new InputNullableType(InputPrimitiveType.Int32))); + } + + return InputFactory.Model("CompatibilityModel", properties: properties); + } + private static InputModelType[] GetTestModels(bool isStringPropExact = false) { InputType additionalPropertiesUnknown = InputPrimitiveType.Any; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_AbstractReturnTypeOverloadIsGenerated.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_AbstractReturnTypeOverloadIsGenerated.cs index 8c46eba00bb..be14011abf7 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_AbstractReturnTypeOverloadIsGenerated.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_AbstractReturnTypeOverloadIsGenerated.cs @@ -9,7 +9,7 @@ namespace Sample.Namespace { public static partial class SampleNamespaceModelFactory { - public static global::Sample.Models.AbstractModel AbstractModel(string kind = default, string prop1 = default, string prop2 = default) + public static global::Sample.Models.AbstractModel AbstractModel(string kind, string prop1, string prop2) { return new global::Sample.Models.UnknownAbstractModel(kind, prop1, prop2, additionalBinaryDataProperties: null); } @@ -20,7 +20,7 @@ public static partial class SampleNamespaceModelFactory } [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] - public static global::Sample.Models.AbstractModel AbstractModel(string prop1, string kind) + public static global::Sample.Models.AbstractModel AbstractModel(string prop1 = default, string kind = default) { return AbstractModel(kind: kind, prop1: prop1, prop2: default); } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_AllRequiredPreviousOverloadKeepsPublishedOptionality(Last)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_AllRequiredPreviousOverloadKeepsPublishedOptionality(Last)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..69433192e28 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_AllRequiredPreviousOverloadKeepsPublishedOptionality(Last)/SampleNamespaceModelFactory.cs @@ -0,0 +1,22 @@ +using Sample.Models; + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + // An all-required overload. It is only callable with exactly four arguments, so it can never + // be reached by a shorter call to the all-optional overload below. + public static CompatibilityModel CompatibilityModel(string id, string name, bool? flag, string kind) + { } + + // The all-optional overload. Its published minimum is zero arguments and must stay that way. + public static CompatibilityModel CompatibilityModel(string id = default, string name = default, int? count = default, string kind = default) + { } + } +} \ No newline at end of file diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_AllRequiredPreviousOverloadKeepsPublishedOptionality.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_AllRequiredPreviousOverloadKeepsPublishedOptionality.cs new file mode 100644 index 00000000000..4b3ffa11f1c --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_AllRequiredPreviousOverloadKeepsPublishedOptionality.cs @@ -0,0 +1,23 @@ +// + +#nullable disable + +using System.ComponentModel; +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id = default, string name = default, int? count = default, string kind = default) + { + return new global::Sample.Models.CompatibilityModel(id, name, count, kind, additionalBinaryDataProperties: null); + } + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id, string name, bool? flag, string kind) + { + return new global::Sample.Models.CompatibilityModel(id, name, default, kind, additionalBinaryDataProperties: null); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ChangedCustomOptionalityConstrainsCompatibilityOverload(Custom)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ChangedCustomOptionalityConstrainsCompatibilityOverload(Custom)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..0e6f6dc4353 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ChangedCustomOptionalityConstrainsCompatibilityOverload(Custom)/SampleNamespaceModelFactory.cs @@ -0,0 +1,19 @@ +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static CompatibilityModel CompatibilityModel( + string id = default, + string description = default, + string text = default) + { } + } +} + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ChangedCustomOptionalityConstrainsCompatibilityOverload(Last)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ChangedCustomOptionalityConstrainsCompatibilityOverload(Last)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..22c614eaa02 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ChangedCustomOptionalityConstrainsCompatibilityOverload(Last)/SampleNamespaceModelFactory.cs @@ -0,0 +1,24 @@ +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static CompatibilityModel CompatibilityModel( + string id, + string description, + string text) + { } + + public static CompatibilityModel CompatibilityModel( + bool isRegex = default, + string id = default) + { } + } +} + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CoexistingPreviousOverloadsKeepPublishedOptionality(Last)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CoexistingPreviousOverloadsKeepPublishedOptionality(Last)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..f50561547c2 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CoexistingPreviousOverloadsKeepPublishedOptionality(Last)/SampleNamespaceModelFactory.cs @@ -0,0 +1,21 @@ +using Sample.Models; + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + // A compatibility shim published by an earlier version. + public static CompatibilityModel CompatibilityModel(string id, int? count = default) + { } + // The published overload. 'id, count' is a positional prefix of it, so the two only + // coexisted safely because both shipped with their trailing parameters optional. + public static CompatibilityModel CompatibilityModel(string id, int? count = default, string extra = default, string other = default) + { } + } +} \ No newline at end of file diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CoexistingPreviousOverloadsKeepPublishedOptionality.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CoexistingPreviousOverloadsKeepPublishedOptionality.cs new file mode 100644 index 00000000000..dbac26c46c9 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CoexistingPreviousOverloadsKeepPublishedOptionality.cs @@ -0,0 +1,29 @@ +// + +#nullable disable + +using System.ComponentModel; +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id, string description, string name) + { + return new global::Sample.Models.CompatibilityModel(id, description, name, additionalBinaryDataProperties: null); + } + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id, int? count = default) + { + return new global::Sample.Models.CompatibilityModel(id, default, default, additionalBinaryDataProperties: null); + } + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id, int? count = default, string extra = default, string other = default) + { + return new global::Sample.Models.CompatibilityModel(id, default, default, additionalBinaryDataProperties: null); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CoexistingPreviousOverloadsKeepPublishedOptionalityReversed(Last)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CoexistingPreviousOverloadsKeepPublishedOptionalityReversed(Last)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..4db67ddeab5 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CoexistingPreviousOverloadsKeepPublishedOptionalityReversed(Last)/SampleNamespaceModelFactory.cs @@ -0,0 +1,21 @@ +using Sample.Models; + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + // The published overload. 'id, count' is a positional prefix of it, so the two only + // coexisted safely because both shipped with their trailing parameters optional. + public static CompatibilityModel CompatibilityModel(string id, int? count = default, string extra = default, string other = default) + { } + // A compatibility shim published by an earlier version. + public static CompatibilityModel CompatibilityModel(string id, int? count = default) + { } + } +} \ No newline at end of file diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CoexistingPreviousOverloadsKeepPublishedOptionalityReversed.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CoexistingPreviousOverloadsKeepPublishedOptionalityReversed.cs new file mode 100644 index 00000000000..3fe16a53ee8 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CoexistingPreviousOverloadsKeepPublishedOptionalityReversed.cs @@ -0,0 +1,29 @@ +// + +#nullable disable + +using System.ComponentModel; +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id, string description, string name) + { + return new global::Sample.Models.CompatibilityModel(id, description, name, additionalBinaryDataProperties: null); + } + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id, int? count = default, string extra = default, string other = default) + { + return new global::Sample.Models.CompatibilityModel(id, default, default, additionalBinaryDataProperties: null); + } + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id, int? count = default) + { + return new global::Sample.Models.CompatibilityModel(id, default, default, additionalBinaryDataProperties: null); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomOverloadConstrainsNewlyGeneratedOverload(Custom)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomOverloadConstrainsNewlyGeneratedOverload(Custom)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..409cf9bd60b --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomOverloadConstrainsNewlyGeneratedOverload(Custom)/SampleNamespaceModelFactory.cs @@ -0,0 +1,18 @@ +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static CompatibilityModel CompatibilityModel( + string id = default, + string description = default) + { } + } +} + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomOverloadConstrainsNewlyGeneratedOverload(Last)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomOverloadConstrainsNewlyGeneratedOverload(Last)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..406d3e21e64 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomOverloadConstrainsNewlyGeneratedOverload(Last)/SampleNamespaceModelFactory.cs @@ -0,0 +1,18 @@ +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static CompatibilityModel CompatibilityModel( + bool isRegex = default, + string id = default) + { } + } +} + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomOverloadConstrainsNewlyGeneratedOverload.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomOverloadConstrainsNewlyGeneratedOverload.cs new file mode 100644 index 00000000000..d9c8aeedee7 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomOverloadConstrainsNewlyGeneratedOverload.cs @@ -0,0 +1,23 @@ +// + +#nullable disable + +using System.ComponentModel; +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id, string description, string text, bool? isRegex = default) + { + return new global::Sample.Models.CompatibilityModel(id, description, text, isRegex, additionalBinaryDataProperties: null); + } + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public static global::Sample.Models.CompatibilityModel CompatibilityModel(bool isRegex, string id = default) + { + return CompatibilityModel(id: id, description: default, text: default, isRegex: isRegex); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomOverloadWithDifferentTypesStillConstrains(Custom)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomOverloadWithDifferentTypesStillConstrains(Custom)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..0c45a74972b --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomOverloadWithDifferentTypesStillConstrains(Custom)/SampleNamespaceModelFactory.cs @@ -0,0 +1,7 @@ +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static Sample.Models.CompatibilityModel CompatibilityModel(string id = default, string description = default, string text = default, string isRegex = default) => null; + } +} \ No newline at end of file diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomOverloadWithDifferentTypesStillConstrains(Last)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomOverloadWithDifferentTypesStillConstrains(Last)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..0c45a74972b --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomOverloadWithDifferentTypesStillConstrains(Last)/SampleNamespaceModelFactory.cs @@ -0,0 +1,7 @@ +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static Sample.Models.CompatibilityModel CompatibilityModel(string id = default, string description = default, string text = default, string isRegex = default) => null; + } +} \ No newline at end of file diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomOverloadWithDifferentTypesStillConstrains.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomOverloadWithDifferentTypesStillConstrains.cs new file mode 100644 index 00000000000..6d60ef9319c --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomOverloadWithDifferentTypesStillConstrains.cs @@ -0,0 +1,16 @@ +// + +#nullable disable + +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id, string description, string text, bool? isRegex) + { + return new global::Sample.Models.CompatibilityModel(id, description, text, isRegex, additionalBinaryDataProperties: null); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomOverloadsPreserveTrailingOptionalParameters(Custom)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomOverloadsPreserveTrailingOptionalParameters(Custom)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..19c503d2962 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomOverloadsPreserveTrailingOptionalParameters(Custom)/SampleNamespaceModelFactory.cs @@ -0,0 +1,23 @@ +using Microsoft.TypeSpec.Generator.Customizations; +using Sample.Models; + +namespace Sample.Namespace +{ + [CodeGenSuppress("CompatibilityModel", typeof(string), typeof(string), typeof(string), typeof(bool?), typeof(string))] + public static partial class SampleNamespaceModelFactory + { + public static CompatibilityModel CompatibilityModel( + string id, + string name, + bool? enabled, + string description, + string kind) + { } + } +} + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomOverloadsPreserveTrailingOptionalParameters(Last)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomOverloadsPreserveTrailingOptionalParameters(Last)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..bdfe257c627 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomOverloadsPreserveTrailingOptionalParameters(Last)/SampleNamespaceModelFactory.cs @@ -0,0 +1,20 @@ +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static CompatibilityModel CompatibilityModel( + string id, + string name, + bool? enabled = default, + string description = default) + { } + } +} + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomOverloadsPreserveTrailingOptionalParameters.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomOverloadsPreserveTrailingOptionalParameters.cs new file mode 100644 index 00000000000..70b749e4a2e --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomOverloadsPreserveTrailingOptionalParameters.cs @@ -0,0 +1,18 @@ +// + +#nullable disable + +using System.ComponentModel; +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id, string name, bool? enabled = default, string description = default) + { + return CompatibilityModel(id: id, name: name, enabled: enabled, description: description, kind: default); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomPublishedSignatureDoesNotRequireLongerCurrentOverload(Custom)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomPublishedSignatureDoesNotRequireLongerCurrentOverload(Custom)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..a6b2e7b4106 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomPublishedSignatureDoesNotRequireLongerCurrentOverload(Custom)/SampleNamespaceModelFactory.cs @@ -0,0 +1,19 @@ +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static CompatibilityModel CompatibilityModel( + string id, + string description, + string text) + { } + } +} + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomPublishedSignatureDoesNotRequireLongerCurrentOverload(Last)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomPublishedSignatureDoesNotRequireLongerCurrentOverload(Last)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..0e6f6dc4353 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_CustomPublishedSignatureDoesNotRequireLongerCurrentOverload(Last)/SampleNamespaceModelFactory.cs @@ -0,0 +1,19 @@ +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static CompatibilityModel CompatibilityModel( + string id = default, + string description = default, + string text = default) + { } + } +} + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_LongPositionalPrefixOverloadPreservesTrailingOptionality(Last)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_LongPositionalPrefixOverloadPreservesTrailingOptionality(Last)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..cfaa92c14c2 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_LongPositionalPrefixOverloadPreservesTrailingOptionality(Last)/SampleNamespaceModelFactory.cs @@ -0,0 +1,18 @@ +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static CompatibilityModel CompatibilityModel( + string id = default, + string name = default) + { } + } +} + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_LongPositionalPrefixOverloadPreservesTrailingOptionality.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_LongPositionalPrefixOverloadPreservesTrailingOptionality.cs new file mode 100644 index 00000000000..04b10419d19 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_LongPositionalPrefixOverloadPreservesTrailingOptionality.cs @@ -0,0 +1,23 @@ +// + +#nullable disable + +using System.ComponentModel; +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id, string name, int? count, string description = default) + { + return new global::Sample.Models.CompatibilityModel(id, name, count, description, additionalBinaryDataProperties: null); + } + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id = default, string name = default) + { + return CompatibilityModel(id: id, name: name, count: default, description: default); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_MultiplePreviousOverloadsRequireIndependentPrefixes(Last)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_MultiplePreviousOverloadsRequireIndependentPrefixes(Last)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..0d70a20542e --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_MultiplePreviousOverloadsRequireIndependentPrefixes(Last)/SampleNamespaceModelFactory.cs @@ -0,0 +1,31 @@ +using Sample.Models; + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + // Positional prefix of the current method (differs only past 'count'), so no argument + // count distinguishes the two and every parameter must become required. + public static CompatibilityModel CompatibilityModel( + string id = default, + string name = default, + int? count = default, + string kind = default) + { } + + // Reordered: 'kind' moved ahead of 'count', so supplying three arguments already + // disambiguates it and 'count' keeps the optionality it shipped with. + public static CompatibilityModel CompatibilityModel( + string id = default, + string name = default, + string kind = default, + int? count = default) + { } + } +} \ No newline at end of file diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_MultiplePreviousOverloadsRequireIndependentPrefixes.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_MultiplePreviousOverloadsRequireIndependentPrefixes.cs new file mode 100644 index 00000000000..7e248bb941c --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_MultiplePreviousOverloadsRequireIndependentPrefixes.cs @@ -0,0 +1,35 @@ +// + +#nullable disable + +using System.ComponentModel; +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id, string name, int? count, bool? flag, string kind) + { + return new global::Sample.Models.CompatibilityModel( + id, + name, + count, + flag, + kind, + additionalBinaryDataProperties: null); + } + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id = default, string name = default, int? count = default, string kind = default) + { + return CompatibilityModel(id: id, name: name, count: count, flag: default, kind: kind); + } + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id = default, string name = default, string kind = default, int? count = default) + { + return CompatibilityModel(id: id, name: name, count: count, flag: default, kind: kind); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_NewOverloadIsConstrainedToPreservePublishedNamedArguments(Last)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_NewOverloadIsConstrainedToPreservePublishedNamedArguments(Last)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..5bb87f8129a --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_NewOverloadIsConstrainedToPreservePublishedNamedArguments(Last)/SampleNamespaceModelFactory.cs @@ -0,0 +1,19 @@ +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static CompatibilityModel CompatibilityModel( + string id = default, + string unit = default, + int? properties = default) + { } + } +} + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_OptionalCustomPublishedSignatureRequiresLongerCurrentOverload(Custom)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_OptionalCustomPublishedSignatureRequiresLongerCurrentOverload(Custom)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..0e6f6dc4353 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_OptionalCustomPublishedSignatureRequiresLongerCurrentOverload(Custom)/SampleNamespaceModelFactory.cs @@ -0,0 +1,19 @@ +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static CompatibilityModel CompatibilityModel( + string id = default, + string description = default, + string text = default) + { } + } +} + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_OptionalCustomPublishedSignatureRequiresLongerCurrentOverload(Last)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_OptionalCustomPublishedSignatureRequiresLongerCurrentOverload(Last)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..0e6f6dc4353 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_OptionalCustomPublishedSignatureRequiresLongerCurrentOverload(Last)/SampleNamespaceModelFactory.cs @@ -0,0 +1,19 @@ +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static CompatibilityModel CompatibilityModel( + string id = default, + string description = default, + string text = default) + { } + } +} + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_PositionalPrefixOverloadRequiresAllParameters(Last)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_PositionalPrefixOverloadRequiresAllParameters(Last)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..f40d4dfc446 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_PositionalPrefixOverloadRequiresAllParameters(Last)/SampleNamespaceModelFactory.cs @@ -0,0 +1,18 @@ +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static CompatibilityModel CompatibilityModel( + string id = default, + string name = default) + { } + } +} + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} \ No newline at end of file diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_PositionalPrefixOverloadRequiresAllParameters.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_PositionalPrefixOverloadRequiresAllParameters.cs new file mode 100644 index 00000000000..8ebf6150303 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_PositionalPrefixOverloadRequiresAllParameters.cs @@ -0,0 +1,23 @@ +// + +#nullable disable + +using System.ComponentModel; +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id, string name, int? count) + { + return new global::Sample.Models.CompatibilityModel(id, name, count, additionalBinaryDataProperties: null); + } + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id = default, string name = default) + { + return CompatibilityModel(id: id, name: name, count: default); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_PublishedDefaultLiteralsDoNotChangeGeneratedDefaults(Last)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_PublishedDefaultLiteralsDoNotChangeGeneratedDefaults(Last)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..8fce32bd693 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_PublishedDefaultLiteralsDoNotChangeGeneratedDefaults(Last)/SampleNamespaceModelFactory.cs @@ -0,0 +1,18 @@ +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static CompatibilityModel CompatibilityModel( + float value = 0F, + string kind = "Unknown") + { } + } +} + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_PublishedOverloadBoundariesArePreserved(Last)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_PublishedOverloadBoundariesArePreserved(Last)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..a3e9ac74f23 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_PublishedOverloadBoundariesArePreserved(Last)/SampleNamespaceModelFactory.cs @@ -0,0 +1,25 @@ +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static CompatibilityModel CompatibilityModel( + string id, + bool? allow, + string kind) + { } + + public static CompatibilityModel CompatibilityModel( + string id = default, + string kind = default, + bool? allow = default) + { } + } +} + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderKeepsOptionalityAgainstCoexistingPublishedOverload(Custom)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderKeepsOptionalityAgainstCoexistingPublishedOverload(Custom)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..c7e9b5e9e28 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderKeepsOptionalityAgainstCoexistingPublishedOverload(Custom)/SampleNamespaceModelFactory.cs @@ -0,0 +1,17 @@ +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + // Restores the published overload that the current contract no longer generates. + public static CompatibilityModel CompatibilityModel(string id = default, string kind = default) + { } + } +} + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} \ No newline at end of file diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderKeepsOptionalityAgainstCoexistingPublishedOverload(Last)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderKeepsOptionalityAgainstCoexistingPublishedOverload(Last)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..63a65dbb20f --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderKeepsOptionalityAgainstCoexistingPublishedOverload(Last)/SampleNamespaceModelFactory.cs @@ -0,0 +1,23 @@ +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + // The current contract produces this overload with 'id' first, so the reorder path rebuilds + // it in the published order. Its optionality must survive that rebuild. + public static CompatibilityModel CompatibilityModel(int? count = default, string id = default) + { } + + // Coexisted with the overload above in the published contract and is restored below, so it + // must not be treated as a competitor that forces the reordered signature to drop defaults. + public static CompatibilityModel CompatibilityModel(string id = default, string kind = default) + { } + } +} + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} \ No newline at end of file diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderKeepsOptionalityAgainstCoexistingPublishedOverload.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderKeepsOptionalityAgainstCoexistingPublishedOverload.cs new file mode 100644 index 00000000000..52e15a37a4c --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderKeepsOptionalityAgainstCoexistingPublishedOverload.cs @@ -0,0 +1,16 @@ +// + +#nullable disable + +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static global::Sample.Models.CompatibilityModel CompatibilityModel(int? count = default, string id = default) + { + return new global::Sample.Models.CompatibilityModel(id, count, additionalBinaryDataProperties: null); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderedCustomOverloadRequiresMinimumPrefix(Custom)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderedCustomOverloadRequiresMinimumPrefix(Custom)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..8c266785202 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderedCustomOverloadRequiresMinimumPrefix(Custom)/SampleNamespaceModelFactory.cs @@ -0,0 +1,21 @@ +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static CompatibilityModel CompatibilityModel( + string id = default, + string name = default, + string kind = default, + bool? enabled = default, + string description = default) + { } + } +} + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderedCustomOverloadRequiresMinimumPrefix(Last)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderedCustomOverloadRequiresMinimumPrefix(Last)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..a56d75fb22c --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderedCustomOverloadRequiresMinimumPrefix(Last)/SampleNamespaceModelFactory.cs @@ -0,0 +1,21 @@ +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static CompatibilityModel CompatibilityModel( + string id = default, + string name = default, + bool? enabled = default, + string description = default, + string kind = default) + { } + } +} + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderedCustomOverloadRequiresMinimumPrefix.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderedCustomOverloadRequiresMinimumPrefix.cs new file mode 100644 index 00000000000..564bb978b1a --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderedCustomOverloadRequiresMinimumPrefix.cs @@ -0,0 +1,22 @@ +// + +#nullable disable + +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id, string name, bool? enabled, string description, string kind) + { + return new global::Sample.Models.CompatibilityModel( + id, + name, + kind, + enabled, + description, + additionalBinaryDataProperties: null); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderedFullyOptionalParametersRequireMinimumPrefix(Last)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderedFullyOptionalParametersRequireMinimumPrefix(Last)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..bd6cf0cd7f7 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderedFullyOptionalParametersRequireMinimumPrefix(Last)/SampleNamespaceModelFactory.cs @@ -0,0 +1,20 @@ +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static CompatibilityModel CompatibilityModel( + string id = default, + string name = default, + bool? enabled = default, + string description = default) + { } + } +} + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderedFullyOptionalParametersRequireMinimumPrefix.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderedFullyOptionalParametersRequireMinimumPrefix.cs new file mode 100644 index 00000000000..e177eb33854 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderedFullyOptionalParametersRequireMinimumPrefix.cs @@ -0,0 +1,30 @@ +// + +#nullable disable + +using System.ComponentModel; +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id, string name, string kind, bool? enabled, string description, int? count = default) + { + return new global::Sample.Models.CompatibilityModel( + id, + name, + kind, + enabled, + description, + count, + additionalBinaryDataProperties: null); + } + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id = default, string name = default, bool? enabled = default, string description = default) + { + return CompatibilityModel(id: id, name: name, kind: default, enabled: enabled, description: description, count: default); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderedOverloadKeptWhenStillInLastContract(Last)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderedOverloadKeptWhenStillInLastContract(Last)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..784c2492912 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderedOverloadKeptWhenStillInLastContract(Last)/SampleNamespaceModelFactory.cs @@ -0,0 +1,30 @@ +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + // Still matches the current contract's natural parameter order, so this overload must be + // preserved rather than replaced by the reordered overload below. + public static CompatibilityModel CompatibilityModel( + string id = default, + string kind = default, + string image = default, + bool? isMain = default) + { } + + // The previously shipped overload with 'kind' moved to the end. + public static CompatibilityModel CompatibilityModel( + string id = default, + string image = default, + bool? isMain = default, + string kind = default) + { } + } +} + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderedOverloadKeptWhenStillInLastContract.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderedOverloadKeptWhenStillInLastContract.cs new file mode 100644 index 00000000000..c636faa9f68 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderedOverloadKeptWhenStillInLastContract.cs @@ -0,0 +1,23 @@ +// + +#nullable disable + +using System.ComponentModel; +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id = default, string kind = default, string image = default, bool? isMain = default) + { + return new global::Sample.Models.CompatibilityModel(id, kind, image, isMain, additionalBinaryDataProperties: null); + } + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id = default, string image = default, bool? isMain = default, string kind = default) + { + return new global::Sample.Models.CompatibilityModel(id, kind, image, isMain, additionalBinaryDataProperties: null); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderedRequiredParametersPreserveTrailingOptionalParameters(Last)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderedRequiredParametersPreserveTrailingOptionalParameters(Last)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..bdfe257c627 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderedRequiredParametersPreserveTrailingOptionalParameters(Last)/SampleNamespaceModelFactory.cs @@ -0,0 +1,20 @@ +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static CompatibilityModel CompatibilityModel( + string id, + string name, + bool? enabled = default, + string description = default) + { } + } +} + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderedRequiredParametersPreserveTrailingOptionalParameters.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderedRequiredParametersPreserveTrailingOptionalParameters.cs new file mode 100644 index 00000000000..4b2770a96c5 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ReorderedRequiredParametersPreserveTrailingOptionalParameters.cs @@ -0,0 +1,30 @@ +// + +#nullable disable + +using System.ComponentModel; +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id, string name, string kind, bool? enabled, string description, int? count = default) + { + return new global::Sample.Models.CompatibilityModel( + id, + name, + kind, + enabled, + description, + count, + additionalBinaryDataProperties: null); + } + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id, string name, bool? enabled = default, string description = default) + { + return CompatibilityModel(id: id, name: name, kind: default, enabled: enabled, description: description, count: default); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ServedPublishedSignatureKeepsGeneratedOptionality(Last)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ServedPublishedSignatureKeepsGeneratedOptionality(Last)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..b991f714c70 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ServedPublishedSignatureKeepsGeneratedOptionality(Last)/SampleNamespaceModelFactory.cs @@ -0,0 +1,18 @@ +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static CompatibilityModel CompatibilityModel( + string id = default, + bool isRegex = default) + { } + } +} + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ServedPublishedSignatureKeepsGeneratedOptionality.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ServedPublishedSignatureKeepsGeneratedOptionality.cs new file mode 100644 index 00000000000..a9407a2155a --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_ServedPublishedSignatureKeepsGeneratedOptionality.cs @@ -0,0 +1,16 @@ +// + +#nullable disable + +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id = default, bool? isRegex = default) + { + return new global::Sample.Models.CompatibilityModel(id, isRegex, additionalBinaryDataProperties: null); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_UnknownDiscriminatorReturnTypeOverloadIsGenerated.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_UnknownDiscriminatorReturnTypeOverloadIsGenerated.cs index c9aa1d5716c..5220d6b97f2 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_UnknownDiscriminatorReturnTypeOverloadIsGenerated.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_UnknownDiscriminatorReturnTypeOverloadIsGenerated.cs @@ -20,7 +20,7 @@ public static partial class SampleNamespaceModelFactory } [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] - public static global::Sample.Models.UnknownAbstractModel UnknownAbstractModel(string prop1, string kind) + public static global::Sample.Models.UnknownAbstractModel UnknownAbstractModel(string prop1 = default, string kind = default) { return new global::Sample.Models.UnknownAbstractModel(kind, prop1, additionalBinaryDataProperties: null); } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_VisitorAddedOverloadRequiresMinimumPrefix(Last)/SampleNamespaceModelFactory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_VisitorAddedOverloadRequiresMinimumPrefix(Last)/SampleNamespaceModelFactory.cs new file mode 100644 index 00000000000..4b9a5266e90 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_VisitorAddedOverloadRequiresMinimumPrefix(Last)/SampleNamespaceModelFactory.cs @@ -0,0 +1,23 @@ +using Sample.Models; + +namespace Sample.Models +{ + public partial class CompatibilityModel + { } +} + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + // Previously shipped overload with 'kind' earlier in the parameter list, all optional. + public static CompatibilityModel CompatibilityModel( + string id = default, + string name = default, + string kind = default, + string image = default, + string targetPort = default, + bool? isMain = default) + { } + } +} \ No newline at end of file diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_VisitorAddedOverloadRequiresMinimumPrefix.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_VisitorAddedOverloadRequiresMinimumPrefix.cs new file mode 100644 index 00000000000..93244d85086 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelFactories/TestData/ModelFactoryProviderTests/BackCompatibility_VisitorAddedOverloadRequiresMinimumPrefix.cs @@ -0,0 +1,30 @@ +// + +#nullable disable + +using System.ComponentModel; +using Sample.Models; + +namespace Sample.Namespace +{ + public static partial class SampleNamespaceModelFactory + { + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id, string name, string image, string targetPort, bool? isMain, string kind) + { + return new global::Sample.Models.CompatibilityModel( + id, + name, + image, + targetPort, + isMain, + kind, + additionalBinaryDataProperties: null); + } + + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + public static global::Sample.Models.CompatibilityModel CompatibilityModel(string id = default, string name = default, string kind = default, string image = default, string targetPort = default, bool? isMain = default) + { + throw null; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Shared/MethodSignatureHelperTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Shared/MethodSignatureHelperTests.cs index 8646fac594f..b0d703ba338 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Shared/MethodSignatureHelperTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Shared/MethodSignatureHelperTests.cs @@ -3,10 +3,12 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.TypeSpec.Generator.Primitives; using Microsoft.TypeSpec.Generator.Providers; using Microsoft.TypeSpec.Generator.Statements; +using Microsoft.TypeSpec.Generator.Tests.Common; using NUnit.Framework; using static Microsoft.TypeSpec.Generator.Snippets.Snippet; @@ -349,6 +351,186 @@ public void BuildBackCompatMethodSignature_HideMethodTrue_WithMultipleParameters } } + [Test] + public void BuildBackCompatMethodSignature_NullLiteralOverlapRequiresAllParameters() + { + var previousSignature = CreateMethodSignature("CompatibilityModel", + new ParameterProvider("id", $"", typeof(string), defaultValue: Default), + new ParameterProvider("name", $"", typeof(string), defaultValue: Default), + new ParameterProvider("enabled", $"", typeof(bool?), defaultValue: Default), + new ParameterProvider("description", $"", typeof(string), defaultValue: Default)); + var currentSignature = CreateMethodSignature("CompatibilityModel", + new ParameterProvider("id", $"", typeof(string), defaultValue: Default), + new ParameterProvider("name", $"", typeof(string), defaultValue: Default), + new ParameterProvider("kind", $"", typeof(string), defaultValue: Default), + new ParameterProvider("enabled", $"", typeof(bool?), defaultValue: Default), + new ParameterProvider("description", $"", typeof(string), defaultValue: Default), + new ParameterProvider("count", $"", typeof(int?), defaultValue: Default)); + + var backCompatSignature = MethodSignatureHelper.BuildBackCompatMethodSignature( + previousSignature, + hideMethod: true, + currentMethodSignatures: [currentSignature]); + + // Both differing positions accept null, so requiring only three parameters would leave + // CompatibilityModel("id", "name", null) ambiguous. + Assert.That(backCompatSignature.Parameters, Has.All.Property("DefaultValue").Null); + Assert.IsTrue(backCompatSignature.Attributes.Any(a => a.Type.Equals(typeof(System.ComponentModel.EditorBrowsableAttribute)))); + } + + [Test] + public void BuildBackCompatMethodSignature_NullableValueTypeDifferencePreservesTrailingDefault() + { + var previousSignature = CreateMethodSignature("CompatibilityModel", + new ParameterProvider("id", $"", typeof(string), defaultValue: Default), + new ParameterProvider("count", $"", typeof(int?), defaultValue: Default), + new ParameterProvider("description", $"", typeof(string), defaultValue: Default)); + var currentSignature = CreateMethodSignature("CompatibilityModel", + new ParameterProvider("id", $"", typeof(string), defaultValue: Default), + new ParameterProvider("count", $"", typeof(int), defaultValue: Default), + new ParameterProvider("description", $"", typeof(string), defaultValue: Default), + new ParameterProvider("kind", $"", typeof(string), defaultValue: Default)); + + var backCompatSignature = MethodSignatureHelper.BuildBackCompatMethodSignature( + previousSignature, + hideMethod: true, + currentMethodSignatures: [currentSignature]); + + Assert.IsNull(backCompatSignature.Parameters[0].DefaultValue); + Assert.IsNull(backCompatSignature.Parameters[1].DefaultValue); + Assert.IsNotNull(backCompatSignature.Parameters[2].DefaultValue); + } + + [Test] + public void BuildBackCompatMethodSignature_RequiredFactoryParametersRespectNullLiteralOverlap() + { + var previousSignature = CreateMethodSignature("CompatibilityModel", + new ParameterProvider("id", $"", typeof(string)), + new ParameterProvider("name", $"", typeof(string)), + new ParameterProvider("enabled", $"", typeof(bool?), defaultValue: Default), + new ParameterProvider("description", $"", typeof(string), defaultValue: Default)); + var currentSignature = CreateMethodSignature("CompatibilityModel", + new ParameterProvider("id", $"", typeof(string), defaultValue: Default), + new ParameterProvider("name", $"", typeof(string), defaultValue: Default), + new ParameterProvider("kind", $"", typeof(string), defaultValue: Default), + new ParameterProvider("enabled", $"", typeof(bool?), defaultValue: Default), + new ParameterProvider("description", $"", typeof(string), defaultValue: Default), + new ParameterProvider("count", $"", typeof(int?), defaultValue: Default)); + + var backCompatSignature = MethodSignatureHelper.BuildBackCompatMethodSignature( + previousSignature, + hideMethod: true, + currentMethodSignatures: [currentSignature]); + + Assert.IsNull(backCompatSignature.Parameters[0].DefaultValue); + Assert.IsNull(backCompatSignature.Parameters[1].DefaultValue); + Assert.IsNull(backCompatSignature.Parameters[2].DefaultValue); + Assert.IsNull(backCompatSignature.Parameters[3].DefaultValue); + } + + // A competitor that cannot be called with as few arguments as the previous signature only + // overlaps it at higher argument counts. Promotion raises the previous signature's minimum + // callable argument count, so it would break previously valid calls that were never + // ambiguous. Keep the published optionality instead. + [Test] + public void BuildBackCompatMethodSignature_AllRequiredCompetitorPreservesPublishedOptionality() + { + var previousSignature = CreateMethodSignature("CompatibilityModel", + new ParameterProvider("id", $"", typeof(string), defaultValue: Default), + new ParameterProvider("name", $"", typeof(string), defaultValue: Default), + new ParameterProvider("count", $"", typeof(int?), defaultValue: Default), + new ParameterProvider("kind", $"", typeof(string), defaultValue: Default)); + // Every parameter is required, so this overload is only callable with exactly three + // arguments and can never be reached by a shorter call. + var currentSignature = CreateMethodSignature("CompatibilityModel", + new ParameterProvider("id", $"", typeof(string)), + new ParameterProvider("flag", $"", typeof(bool?)), + new ParameterProvider("count", $"", typeof(int?))); + + var backCompatSignature = MethodSignatureHelper.BuildBackCompatMethodSignature( + previousSignature, + hideMethod: true, + currentMethodSignatures: [currentSignature]); + + foreach (var parameter in backCompatSignature.Parameters) + { + Assert.IsNotNull(parameter.DefaultValue); + } + } + + [Test] + public void BuildBackCompatMethodSignature_WithOverloads_HideMethodFalse_RemovesDefaultsWithoutEditorBrowsable() + { + var previousSignature = CreateMethodSignature("TestMethod", + new ParameterProvider("param1", $"", typeof(string), defaultValue: Default), + new ParameterProvider("param2", $"", typeof(string), defaultValue: Default)); + var currentSignature = CreateMethodSignature("TestMethod", + new ParameterProvider("param2", $"", typeof(string), defaultValue: Default), + new ParameterProvider("param1", $"", typeof(string), defaultValue: Default)); + + var backCompatSignature = MethodSignatureHelper.BuildBackCompatMethodSignature( + previousSignature, + hideMethod: false, + currentMethodSignatures: [currentSignature]); + + foreach (var parameter in backCompatSignature.Parameters) + { + Assert.IsNull(parameter.DefaultValue); + } + Assert.IsFalse(backCompatSignature.Attributes.Any(a => a.Type.Equals(typeof(System.ComponentModel.EditorBrowsableAttribute)))); + } + + [Test] + public void BuildBackCompatMethodSignature_PreservesDefaultsForInapplicableArgumentCount() + { + var previousSignature = CreateMethodSignature("TestMethod", + new ParameterProvider("value", $"", typeof(string), defaultValue: Default)); + var currentSignature = CreateMethodSignature("TestMethod", + new ParameterProvider("first", $"", typeof(bool)), + new ParameterProvider("second", $"", typeof(int))); + + var backCompatSignature = MethodSignatureHelper.BuildBackCompatMethodSignature( + previousSignature, + hideMethod: true, + currentMethodSignatures: [currentSignature]); + + Assert.IsNotNull(backCompatSignature.Parameters[0].DefaultValue); + } + + [Test] + public void BuildBackCompatMethodSignature_PreservesDefaultsForRefOutOverload() + { + var previousSignature = CreateMethodSignature("TestMethod", + new ParameterProvider("value", $"", typeof(string), defaultValue: Default)); + var currentSignature = CreateMethodSignature("TestMethod", + new ParameterProvider("value", $"", typeof(string), isRef: true)); + + var backCompatSignature = MethodSignatureHelper.BuildBackCompatMethodSignature( + previousSignature, + hideMethod: true, + currentMethodSignatures: [currentSignature]); + + Assert.IsNotNull(backCompatSignature.Parameters[0].DefaultValue); + } + + [Test] + public void BuildBackCompatMethodSignature_PreservesDefaultsForInapplicableParamsOverload() + { + var previousSignature = CreateMethodSignature("TestMethod", + new ParameterProvider("value", $"", typeof(string), defaultValue: Default)); + var currentSignature = CreateMethodSignature("TestMethod", + new ParameterProvider("first", $"", typeof(bool)), + new ParameterProvider("second", $"", typeof(int)), + new ParameterProvider("rest", $"", typeof(int[]), isParams: true)); + + var backCompatSignature = MethodSignatureHelper.BuildBackCompatMethodSignature( + previousSignature, + hideMethod: true, + currentMethodSignatures: [currentSignature]); + + Assert.IsNotNull(backCompatSignature.Parameters[0].DefaultValue); + } + private static MethodSignature CreateMethodSignature( string name, params ParameterProvider[] parameters)