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 a8079006dd4..6ec325a7e1e 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,15 +111,46 @@ 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(); + List currentMethodSignatures = allFactoryMethods + .Select(m => m.Signature) + .ToList(); + + var compatiblePreviousMethods = new List(); + List previousPublicSignatures = []; + List preservedPreviousSignatures = []; 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); + + if (currentMethodSignatures.Any(current => + MethodSignature.MethodSignatureComparer.Equals(current, previousMethod.Signature))) { + preservedPreviousSignatures.Add(previousMethod.Signature); + + // The current model shape may have regenerated a previously published signature + // with different defaults. Restore its published required/optional boundary. + var matchingCurrentMethod = factoryMethods.FirstOrDefault(m => + MethodSignature.MethodSignatureComparer.Equals(m.Signature, previousMethod.Signature)); + if (matchingCurrentMethod is not null) + { + for (int i = 0; i < previousMethod.Signature.Parameters.Count; i++) + { + matchingCurrentMethod.Signature.Parameters[i].DefaultValue = + previousMethod.Signature.Parameters[i].DefaultValue; + } + } + continue; } @@ -138,22 +170,48 @@ 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; + } + + var previousOverloads = preservedPreviousSignatures + .Where(signature => signature.Name == currentMethod.Signature.Name) + .ToList(); + MethodSignatureHelper.RequireMinimumParameterPrefix( + currentMethod.Signature, + previousOverloads, + 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; - } - - currentOverloads.Add(currentMethodSignature); + foundCompatibleOverload = true; + break; } + + currentOverloads.Add(currentMethodSignature); } if (foundCompatibleOverload) @@ -161,32 +219,59 @@ 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. + var compatibilityOverloadSignatures = customFactoryMethods + .Select(method => method.Signature) + .Where(signature => + signature.Name == previousMethod.Signature.Name + && !previousPublicSignatures.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) + var coexistingCompatibilityOverloads = GetCurrentOverloadSignatures( + allFactoryMethods, + previousMethod.Signature.Name, + factoryMethodToRemove); + if (TryBuildCompatibleMethodForPreviousContract( + previousMethod, + currentOverload, + false, + coexistingCompatibilityOverloads, + 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); @@ -201,7 +286,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( @@ -215,10 +305,20 @@ protected internal sealed override IReadOnlyList BuildMethodsFor BackCompatibilityChangeCategory.ModelFactoryMethodSkipped); } } - return [.. factoryMethods]; } + private IReadOnlyList GetCurrentOverloadSignatures( + IEnumerable methods, + string methodName, + MethodProvider? methodToExclude = null) + { + return methods + .Where(m => (methodToExclude is null || !ReferenceEquals(m, methodToExclude)) && m.Signature.Name == methodName) + .Select(m => m.Signature) + .ToList(); + } + internal static IReadOnlyList GetUnavailableSignatureTypes(MethodSignature signature) { var unavailableTypes = new HashSet(StringComparer.Ordinal); @@ -310,6 +410,7 @@ private bool TryBuildCompatibleMethodForPreviousContract( MethodProvider previousMethod, MethodSignature? currentMethodSignature, bool hideMethod, + IReadOnlyList currentOverloadSignatures, [NotNullWhen(true)] out MethodProvider? builtMethod) { builtMethod = null; @@ -345,7 +446,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); @@ -356,7 +460,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..6f2e694eac1 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,95 @@ 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 type + // differs. Any call supplying that many arguments can no longer bind to the competing + // overload, so every trailing parameter keeps the optionality it had previously. + int overlappingParameterCount = Math.Min( + targetMethodSignature.Parameters.Count, + competingMethodSignature.Parameters.Count); + for (int i = 0; i < overlappingParameterCount; i++) + { + if (!targetMethodSignature.Parameters[i].Type.AreNamesEqual(competingMethodSignature.Parameters[i].Type)) + { + return Math.Max(i + 1, targetMinimumArgumentCount); + } + } + + // The shorter signature is a positional prefix of the other, so no argument count + // distinguishes them. Fall back to requiring every 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 3fd14bf42b4..c53235d8bec 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,334 @@ 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); + } + + // 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); + } + + // 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 +701,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(); @@ -670,9 +994,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 @@ -1250,6 +1576,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() { 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_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..b2b8eb3d04f --- /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 = default) + { + 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..6e688e2d9fc --- /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 = default) + { + 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_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_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..ef249a93cfc --- /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 = default) + { + 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_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_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_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..6f0e0d593dd --- /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 = default, string kind = default) + { + 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..ca3a3c10eb9 --- /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 = default, string description = default, 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..aa880309e32 --- /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 = default, string description = default, 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_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..32bf918ce17 --- /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 = default) + { + 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..5614331a7a5 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,167 @@ public void BuildBackCompatMethodSignature_HideMethodTrue_WithMultipleParameters } } + [Test] + public void BuildBackCompatMethodSignature_AllOptionalFactoryOverloadsRequireMinimumPrefix() + { + 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]); + + // 'enabled' is the first position whose type differs from the current overload + // ('bool?' versus 'kind'), so supplying three arguments already disambiguates the + // call and 'description' keeps the optionality it was published with. + Assert.IsNull(backCompatSignature.Parameters[0].DefaultValue); + Assert.IsNull(backCompatSignature.Parameters[1].DefaultValue); + Assert.IsNull(backCompatSignature.Parameters[2].DefaultValue); + Assert.IsNotNull(backCompatSignature.Parameters[3].DefaultValue); + Assert.IsTrue(backCompatSignature.Attributes.Any(a => a.Type.Equals(typeof(System.ComponentModel.EditorBrowsableAttribute)))); + } + + [Test] + public void BuildBackCompatMethodSignature_RequiredFactoryParametersPreserveTrailingDefaults() + { + 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.IsNotNull(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)