diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/BackCompatibilityChangeCategory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/BackCompatibilityChangeCategory.cs index 38f74bddf37..7f3596fb040 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/BackCompatibilityChangeCategory.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/BackCompatibilityChangeCategory.cs @@ -48,5 +48,11 @@ public enum BackCompatibilityChangeCategory /// A fixed enum member was re-added to preserve a member that existed in the last contract but is no longer produced by the current spec. EnumMemberAddedFromLastContract, + + /// A back-compat model constructor was re-added to preserve a public constructor that existed in the last contract but is no longer produced by the current spec. + ConstructorAddedFromLastContract, + + /// A back-compat model constructor could not be reconstructed from the last contract and was skipped. + ConstructorAddedFromLastContractSkipped, } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs index c3fc8efec62..1bf8d934f9f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using Microsoft.TypeSpec.Generator.EmitterRpc; @@ -774,6 +775,221 @@ protected internal override ConstructorProvider[] BuildConstructors() return [.. constructors]; } + /// + /// Restores previously-published public constructors that the current generation would otherwise + /// drop. The primary scenario is a previously required property becoming optional: the corresponding + /// parameter is removed from the initialization constructor, which is a source-breaking change for + /// callers that construct the model positionally. When the previous public constructor can be safely + /// reconstructed - i.e. every one of its extra parameters still maps to a settable property whose + /// name and type are unchanged (or a property renamed via a codegen customization but keeping the + /// same type) - a back-compat overload is added that chains to the current public constructor and + /// assigns the extra properties. + /// + protected internal override IReadOnlyList BuildConstructorsForBackCompatibility(IEnumerable originalConstructors) + { + if (LastContractView?.Constructors is not { Count: > 0 } previousConstructors) + { + return base.BuildConstructorsForBackCompatibility(originalConstructors); + } + + var constructors = new List(base.BuildConstructorsForBackCompatibility(originalConstructors)); + var restorablePropertyLookup = BuildRestorablePropertyLookup(); + + foreach (var previousConstructor in previousConstructors) + { + if (!IsPublicApi(previousConstructor.Signature.Modifiers)) + { + continue; + } + + var previousParameters = previousConstructor.Signature.Parameters; + + if (previousParameters.Count == 0) + { + continue; + } + + if (BackCompatHelper.IsConstructorRemovalAcceptedInBaseline(this, previousConstructor.Signature)) + { + continue; + } + + // If a constructor with the same parameters already exists - either still generated or + // supplied by custom code (which lives in the canonical view) - there is nothing to restore. + if (constructors.Any(c => BackCompatHelper.ParametersMatch(c.Signature.Parameters, previousParameters)) + || CanonicalView.Constructors.Any(c => BackCompatHelper.ParametersMatch(c.Signature.Parameters, previousParameters))) + { + continue; + } + + if (TryBuildRestoredConstructor(previousConstructor, constructors, restorablePropertyLookup, out var restoredConstructor)) + { + constructors.Add(restoredConstructor); + CodeModelGenerator.Instance.Emitter.Info( + $"Restored constructor '{Name}({string.Join(", ", previousParameters.Select(p => p.Type.Name))})' to match last contract.", + BackCompatibilityChangeCategory.ConstructorAddedFromLastContract); + } + else + { + CodeModelGenerator.Instance.Emitter.Info( + $"Could not restore constructor '{Name}({string.Join(", ", previousParameters.Select(p => p.Type.Name))})' from the last contract; a property name or type has changed.", + BackCompatibilityChangeCategory.ConstructorAddedFromLastContractSkipped); + } + } + + return constructors; + } + + private bool TryBuildRestoredConstructor( + ConstructorProvider previousConstructor, + IReadOnlyList currentConstructors, + Dictionary restorablePropertyLookup, + [NotNullWhen(true)] out ConstructorProvider? restoredConstructor) + { + restoredConstructor = null; + var previousParameters = previousConstructor.Signature.Parameters; + + // Find the public constructor to chain to: its parameters must form an in-order subsequence of + // the previous constructor's parameters. Prefer the closest one. + ConstructorProvider? targetConstructor = null; + foreach (var candidate in currentConstructors) + { + if (!IsPublicApi(candidate.Signature.Modifiers) + || candidate.Signature.Parameters.Count >= previousParameters.Count) + { + continue; + } + + // Check whether this candidate would improve on the current target before performing the + // more expensive subsequence lookup. + if ((targetConstructor == null + || candidate.Signature.Parameters.Count > targetConstructor.Signature.Parameters.Count) + && IsParameterSubsequence(candidate.Signature.Parameters, previousParameters)) + { + targetConstructor = candidate; + } + } + + if (targetConstructor == null) + { + return false; + } + + var targetParameters = targetConstructor.Signature.Parameters; + var restoredParameters = new List(previousParameters.Count); + var initializerArguments = new List(targetParameters.Count); + var extraAssignments = new List<(PropertyProvider Property, ParameterProvider Parameter)>(); + int targetIndex = 0; + + foreach (var previousParameter in previousParameters) + { + if (targetIndex < targetParameters.Count + && targetParameters[targetIndex].Equals(previousParameter)) + { + var keptParameter = PartialMethodCustomization.CloneParameterWithName( + targetParameters[targetIndex], + previousParameter.Name, + removeDefault: false, + validation: ParameterValidationType.None); + restoredParameters.Add(keptParameter); + initializerArguments.Add(keptParameter); + targetIndex++; + continue; + } + + if (!restorablePropertyLookup.TryGetValue(previousParameter.Name, out var property) + || !property.Type.AreNamesEqual(previousParameter.Type)) + { + return false; + } + + var restoredParameter = PartialMethodCustomization.CloneParameterWithName( + property.AsParameter, + previousParameter.Name, + removeDefault: true); + + restoredParameters.Add(restoredParameter); + extraAssignments.Add((property, restoredParameter)); + } + + // Every target parameter must be consumed and at least one extra property must be assigned, + // otherwise the restored constructor would be redundant or would produce an invalid chained call. + if (targetIndex != targetParameters.Count || extraAssignments.Count == 0) + { + return false; + } + + var bodyStatements = new List(extraAssignments.Count); + foreach (var (property, parameter) in extraAssignments) + { + ValueExpression assignee = property.BackingField is null ? property : property.BackingField; + ValueExpression value = parameter; + if (CSharpType.RequiresToList(parameter.Type, property.Type)) + { + value = parameter.Type.IsNullable ? value.NullConditional().ToList() : value.ToList(); + } + + bodyStatements.Add(assignee.Assign(value).Terminate()); + } + + var signature = new ConstructorSignature( + Type, + $"Initializes a new instance of {Type:C}", + previousConstructor.Signature.Modifiers, + restoredParameters, + initializer: new ConstructorInitializer(false, initializerArguments)); + + restoredConstructor = new ConstructorProvider(signature, bodyStatements, this); + return true; + } + + private static bool IsParameterSubsequence( + IReadOnlyList subset, + IReadOnlyList full) + { + if (subset.Count > full.Count) + { + return false; + } + + int matched = 0; + foreach (var parameter in full) + { + if (matched == subset.Count) + { + break; + } + + if (subset[matched].Equals(parameter)) + { + matched++; + } + } + + return matched == subset.Count; + } + + private Dictionary BuildRestorablePropertyLookup() + { + var lookup = new Dictionary(); + foreach (var property in CanonicalView.Properties) + { + if (!IsPublicApi(property.Modifiers) || !property.Body.HasSetter || property.WireInfo == null) + { + continue; + } + + lookup[property.AsParameter.Name] = property; + + if (property.OriginalName != null) + { + lookup.TryAdd(property.OriginalName.ToVariableName(), property); + } + } + + return lookup; + } + /// /// Determines if this model should have a dual constructor pattern. /// This is needed when the model shares the same discriminator property name as its base model diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PartialMethodCustomization.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PartialMethodCustomization.cs index 9d1d98a9785..5dbcb7f0aa1 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PartialMethodCustomization.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PartialMethodCustomization.cs @@ -213,12 +213,15 @@ public static MethodSignature BuildPartialSignature( // Clones a ParameterProvider with a new name (and optionally without its default value) // while preserving all generator metadata. Returns the source unchanged when no change is // needed. - private static ParameterProvider CloneParameterWithName( + internal static ParameterProvider CloneParameterWithName( ParameterProvider source, string newName, - bool removeDefault) + bool removeDefault, + ParameterValidationType? validation = null) { - if (source.Name == newName && !(removeDefault && source.DefaultValue != null)) + if (source.Name == newName + && !(removeDefault && source.DefaultValue != null) + && (validation == null || validation == source.Validation)) { return source; } @@ -238,7 +241,7 @@ private static ParameterProvider CloneParameterWithName( initializationValue: source.InitializationValue, location: source.Location, wireInfo: source.WireInfo, - validation: source.Validation, + validation: validation ?? source.Validation, inputParameter: source.InputParameter) { SpreadSource = source.SpreadSource, diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs index f0717c007ff..4497c86e746 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs @@ -66,6 +66,35 @@ public static bool IsMethodRemovalAcceptedInBaseline(TypeProvider enclosingType, return true; } + /// + /// Returns true when the removal of a previously-published constructor — identified by the + /// enclosing type's fully-qualified name and the exact parameter types — has been accepted in the + /// ApiCompat baseline, in which case back compatibility must not restore it. Constructors are + /// recorded in the baseline as the .ctor member of their declaring type. Emits an + /// informational log entry when a suppression is honored. + /// + public static bool IsConstructorRemovalAcceptedInBaseline(TypeProvider enclosingType, ConstructorSignature previousSignature) + { + var parameterTypes = new CSharpType[previousSignature.Parameters.Count]; + for (int i = 0; i < parameterTypes.Length; i++) + { + parameterTypes[i] = previousSignature.Parameters[i].Type; + } + + if (CodeModelGenerator.Instance.SourceInputModel?.ApiCompatBaseline.IsMethodRemovalSuppressed( + enclosingType.Type.FullyQualifiedName, + ".ctor", + parameterTypes) != true) + { + return false; + } + + CodeModelGenerator.Instance.Emitter.Info( + $"Skipping back-compat for '{enclosingType.Type.FullyQualifiedName}..ctor'; removal is accepted in the ApiCompat baseline.", + BackCompatibilityChangeCategory.BaselineAcceptedRemovalSkipped); + return true; + } + /// /// Finds the current method that has the same parameter set as /// (matched by name and return type) but in a different order, or null when there is none. diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs index 88ab6c206b8..35a0832b33e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs @@ -2329,6 +2329,303 @@ await MockHelpers.LoadMockGeneratorAsync( Assert.AreEqual("baseProp", publicConstructor!.Signature.Parameters[0].Name); } + [Test] + public async Task BackCompat_RequiredToOptionalConstructorIsRestored() + { + // "resources" was required in the last contract (so the initialization constructor + // accepted it), but the current spec relaxes it to optional which would otherwise drop + // it from the constructor. The previously published constructor should be restored. + var inputModel = InputFactory.Model( + "MockInputModel", + usage: InputModelTypeUsage.Input | InputModelTypeUsage.Json, + properties: + [ + InputFactory.Property("name", InputPrimitiveType.String, isRequired: true), + InputFactory.Property("resources", InputPrimitiveType.String, isRequired: false), + ]); + + await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; + Assert.IsNotNull(modelProvider); + + // Before back-compat processing the public constructor only takes "name". + var publicCtorBefore = modelProvider!.Constructors.SingleOrDefault(c => + c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)); + Assert.IsNotNull(publicCtorBefore); + Assert.AreEqual(1, publicCtorBefore!.Signature.Parameters.Count); + + modelProvider.ProcessTypeForBackCompatibility(); + + // After back-compat processing the previously published (name, resources) constructor is restored. + var restoredCtor = modelProvider.Constructors.SingleOrDefault(c => + c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public) + && c.Signature.Parameters.Count == 2); + Assert.IsNotNull(restoredCtor, "Expected the (name, resources) constructor to be restored for back compat"); + Assert.AreEqual("name", restoredCtor!.Signature.Parameters[0].Name); + Assert.AreEqual("resources", restoredCtor.Signature.Parameters[1].Name); + Assert.IsTrue(restoredCtor.Signature.Parameters[1].Type.Equals(typeof(string))); + + // It chains to the current (name) constructor and assigns the extra property in its body. + var initializer = restoredCtor.Signature.Initializer; + Assert.IsNotNull(initializer); + Assert.IsFalse(initializer!.IsBase); + Assert.AreEqual(1, initializer.Arguments.Count); + Assert.AreEqual("name", initializer.Arguments[0].ToDisplayString()); + + var body = restoredCtor.BodyStatements!.ToDisplayString(); + Assert.AreEqual(ParameterValidationType.None, restoredCtor.Signature.Parameters[0].Validation); + Assert.IsFalse(body.Contains("Argument.AssertNotNull(name"), $"Did not expect duplicated name validation in restored constructor, was: {body}"); + Assert.IsTrue(body.Contains("Resources = resources"), $"Expected the body to assign Resources, was: {body}"); + // "resources" is now an optional property, so the restored back-compat overload does not null-check it. + Assert.AreEqual(ParameterValidationType.None, restoredCtor.Signature.Parameters[1].Validation); + Assert.IsFalse(body.Contains("Argument.AssertNotNull(resources"), $"Did not expect null validation for the now-optional resources parameter, was: {body}"); + + // Validate the full generated model, including the restored constructor, against the expected output. + var writer = new TypeProviderWriter(modelProvider); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + + [Test] + public async Task BackCompat_ConstructorNotRestoredWhenPropertyRemoved() + { + // "resources" existed in the last contract constructor but has been removed entirely from + // the current spec, so the previous constructor cannot be safely restored. + var inputModel = InputFactory.Model( + "MockInputModel", + usage: InputModelTypeUsage.Input | InputModelTypeUsage.Json, + properties: + [ + InputFactory.Property("name", InputPrimitiveType.String, isRequired: true), + ]); + + await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; + Assert.IsNotNull(modelProvider); + + modelProvider!.ProcessTypeForBackCompatibility(); + + var twoParamPublicCtor = modelProvider.Constructors.FirstOrDefault(c => + c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public) + && c.Signature.Parameters.Count == 2); + Assert.IsNull(twoParamPublicCtor, "The constructor should not be restored when a property was removed"); + + var writer = new TypeProviderWriter(modelProvider); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + + [Test] + public async Task BackCompat_ConstructorNotRestoredWhenLastContractMissing() + { + // No last contract exists for the model, so nothing should be restored. + var inputModel = InputFactory.Model( + "MockInputModel", + usage: InputModelTypeUsage.Input | InputModelTypeUsage.Json, + properties: + [ + InputFactory.Property("name", InputPrimitiveType.String, isRequired: true), + InputFactory.Property("resources", InputPrimitiveType.String, isRequired: false), + ]); + + await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; + Assert.IsNotNull(modelProvider); + Assert.IsNull(modelProvider!.LastContractView); + + modelProvider.ProcessTypeForBackCompatibility(); + + var twoParamPublicCtor = modelProvider.Constructors.FirstOrDefault(c => + c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public) + && c.Signature.Parameters.Count == 2); + Assert.IsNull(twoParamPublicCtor); + + var writer = new TypeProviderWriter(modelProvider); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + + [Test] + public async Task BackCompat_OptionalValueTypeConstructorParameterIsRestored() + { + // "count" was a required value type in the last contract, so the initialization constructor + // accepted it. Relaxing it to optional drops it; the previously published constructor is + // restored, and because it is a value type no null validation is emitted. + var inputModel = InputFactory.Model( + "MockInputModel", + usage: InputModelTypeUsage.Input | InputModelTypeUsage.Json, + properties: + [ + InputFactory.Property("name", InputPrimitiveType.String, isRequired: true), + InputFactory.Property("count", InputPrimitiveType.Int32, isRequired: false), + ]); + + await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; + Assert.IsNotNull(modelProvider); + + modelProvider!.ProcessTypeForBackCompatibility(); + + var restoredCtor = modelProvider.Constructors.SingleOrDefault(c => + c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public) + && c.Signature.Parameters.Count == 2); + Assert.IsNotNull(restoredCtor, "Expected the (name, count) constructor to be restored for back compat"); + Assert.AreEqual("count", restoredCtor!.Signature.Parameters[1].Name); + // The restored parameter keeps the previously published non-nullable value type. + Assert.IsTrue(restoredCtor.Signature.Parameters[1].Type.Equals(typeof(int))); + Assert.AreEqual(ParameterValidationType.None, restoredCtor.Signature.Parameters[1].Validation); + + var body = restoredCtor.BodyStatements!.ToDisplayString(); + Assert.IsTrue(body.Contains("Count = count"), $"Expected the body to assign Count, was: {body}"); + // Value types never emit a null check. + Assert.IsFalse(body.Contains("AssertNotNull(count"), $"Did not expect null validation for a value type, was: {body}"); + + var writer = new TypeProviderWriter(modelProvider); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + + [Test] + public async Task BackCompat_RenamedPropertyConstructorIsRestored() + { + // The spec property "resources" is renamed to "ResourceList" via a [CodeGenMember] + // customization. The previously published constructor's "resources" parameter must still + // be matched to the renamed property (via its OriginalName) so the constructor is restored. + var inputModel = InputFactory.Model( + "MockInputModel", + usage: InputModelTypeUsage.Input | InputModelTypeUsage.Json, + properties: + [ + InputFactory.Property("name", InputPrimitiveType.String, isRequired: true), + InputFactory.Property("resources", InputPrimitiveType.String, isRequired: false), + ]); + + await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync( + method: "BackCompat_RenamedPropertyConstructorIsRestored_LastContract")); + + var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; + Assert.IsNotNull(modelProvider); + + modelProvider!.ProcessTypeForBackCompatibility(); + + var restoredCtor = modelProvider.Constructors.SingleOrDefault(c => + c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public) + && c.Signature.Parameters.Count == 2); + Assert.IsNotNull(restoredCtor, "Expected the (name, resources) constructor to be restored for back compat"); + // The restored parameter keeps the previously published (pre-rename) name. + Assert.AreEqual("resources", restoredCtor!.Signature.Parameters[1].Name); + + // The body assigns the current, renamed property. + var body = restoredCtor.BodyStatements!.ToDisplayString(); + Assert.IsTrue(body.Contains("ResourceList = resources"), $"Expected the body to assign the renamed property, was: {body}"); + + var writer = new TypeProviderWriter(modelProvider); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + + [TestCase(".txt")] + [TestCase(".xml")] + public async Task BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline(string baselineExtension) + { + // "resources" was required in the last contract and is now optional, which would normally + // cause the previous (name, resources) constructor to be restored. However its removal is + // accepted in the ApiCompat baseline (tested in both the txt and xml formats), so the + // constructor must not be resurrected. + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: baselineExtension); + + var inputModel = InputFactory.Model( + "MockInputModel", + usage: InputModelTypeUsage.Input | InputModelTypeUsage.Json, + properties: + [ + InputFactory.Property("name", InputPrimitiveType.String, isRequired: true), + InputFactory.Property("resources", InputPrimitiveType.String, isRequired: false), + ]); + + await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync( + method: "BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline_LastContract"), + apiCompatBaseline: baseline); + + var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; + Assert.IsNotNull(modelProvider); + + modelProvider!.ProcessTypeForBackCompatibility(); + + // The (name, resources) constructor removal is accepted in the baseline, so it is not restored. + var restoredCtor = modelProvider.Constructors.FirstOrDefault(c => + c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public) + && c.Signature.Parameters.Count == 2); + Assert.IsNull(restoredCtor, "The constructor should not be restored when its removal is accepted in the baseline"); + + var writer = new TypeProviderWriter(modelProvider); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + + [Test] + public async Task BackCompat_ConstructorNotRestoredWhenReplacedByCustomCode() + { + // "resources" was required in the last contract and is now optional, which would normally + // cause the previous (name, resources) constructor to be restored. Here the user has replaced + // that constructor with their own custom implementation, so the generator must not add a + // colliding back-compat overload. + var inputModel = InputFactory.Model( + "MockInputModel", + usage: InputModelTypeUsage.Input | InputModelTypeUsage.Json, + properties: + [ + InputFactory.Property("name", InputPrimitiveType.String, isRequired: true), + InputFactory.Property("resources", InputPrimitiveType.String, isRequired: false), + ]); + + await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync( + method: "BackCompat_ConstructorNotRestoredWhenReplacedByCustomCode"), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync( + method: "BackCompat_ConstructorNotRestoredWhenReplacedByCustomCode_LastContract")); + + var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; + Assert.IsNotNull(modelProvider); + + // The custom (name, resources) constructor lives in the canonical view. + var customCtor = modelProvider!.CanonicalView.Constructors.SingleOrDefault(c => + c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public) + && c.Signature.Parameters.Count == 2); + Assert.IsNotNull(customCtor, "Expected the custom (name, resources) constructor to be present"); + + modelProvider.ProcessTypeForBackCompatibility(); + + // Because the custom code already provides the (name, resources) constructor, the generator + // must not restore a colliding overload of its own. + var restoredCtor = modelProvider.Constructors.FirstOrDefault(c => + c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public) + && c.Signature.Parameters.Count == 2); + Assert.IsNull(restoredCtor, "The constructor should not be restored when it is replaced by custom code"); + + var writer = new TypeProviderWriter(modelProvider); + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + [Test] public async Task TestBuildProperties_WithObjectAdditionalPropertiesBackwardCompatibility() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenLastContractMissing.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenLastContractMissing.cs new file mode 100644 index 00000000000..61958c1b8b5 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenLastContractMissing.cs @@ -0,0 +1,33 @@ +// + +#nullable disable + +using System; +using System.Collections.Generic; +using Sample; + +namespace Sample.Models +{ + public partial class MockInputModel + { + private protected readonly global::System.Collections.Generic.IDictionary _additionalBinaryDataProperties; + + public MockInputModel(string name) + { + global::Sample.Argument.AssertNotNull(name, nameof(name)); + + Name = name; + } + + internal MockInputModel(string name, string resources, global::System.Collections.Generic.IDictionary additionalBinaryDataProperties) + { + Name = name; + Resources = resources; + _additionalBinaryDataProperties = additionalBinaryDataProperties; + } + + public string Name { get; } + + public string Resources { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenLastContractMissing/UnrelatedModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenLastContractMissing/UnrelatedModel.cs new file mode 100644 index 00000000000..78a0f6a28d2 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenLastContractMissing/UnrelatedModel.cs @@ -0,0 +1,9 @@ +namespace Sample.Models +{ + // Note: this last-contract model has a different name than the spec model + // ("MockInputModel"), so no last contract view is found for the model. + public partial class UnrelatedModel + { + public int? Count { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenPropertyRemoved.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenPropertyRemoved.cs new file mode 100644 index 00000000000..ac6d11111d5 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenPropertyRemoved.cs @@ -0,0 +1,30 @@ +// + +#nullable disable + +using System; +using System.Collections.Generic; +using Sample; + +namespace Sample.Models +{ + public partial class MockInputModel + { + private protected readonly global::System.Collections.Generic.IDictionary _additionalBinaryDataProperties; + + public MockInputModel(string name) + { + global::Sample.Argument.AssertNotNull(name, nameof(name)); + + Name = name; + } + + internal MockInputModel(string name, global::System.Collections.Generic.IDictionary additionalBinaryDataProperties) + { + Name = name; + _additionalBinaryDataProperties = additionalBinaryDataProperties; + } + + public string Name { get; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenPropertyRemoved/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenPropertyRemoved/MockInputModel.cs new file mode 100644 index 00000000000..776ffa9d8d7 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenPropertyRemoved/MockInputModel.cs @@ -0,0 +1,18 @@ +namespace Sample.Models +{ + public partial class MockInputModel + { + // In the last contract "resources" existed and was part of the constructor, but + // the current spec removes the property entirely. Because there is no matching + // property to assign, the previous constructor cannot be safely restored. + public MockInputModel(string name, string resources) + { + Name = name; + Resources = resources; + } + + public string Name { get; } + + public string Resources { get; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline.cs new file mode 100644 index 00000000000..61958c1b8b5 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline.cs @@ -0,0 +1,33 @@ +// + +#nullable disable + +using System; +using System.Collections.Generic; +using Sample; + +namespace Sample.Models +{ + public partial class MockInputModel + { + private protected readonly global::System.Collections.Generic.IDictionary _additionalBinaryDataProperties; + + public MockInputModel(string name) + { + global::Sample.Argument.AssertNotNull(name, nameof(name)); + + Name = name; + } + + internal MockInputModel(string name, string resources, global::System.Collections.Generic.IDictionary additionalBinaryDataProperties) + { + Name = name; + Resources = resources; + _additionalBinaryDataProperties = additionalBinaryDataProperties; + } + + public string Name { get; } + + public string Resources { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline.txt b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline.txt new file mode 100644 index 00000000000..2c4258cf7d3 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline.txt @@ -0,0 +1 @@ +MembersMustExist : Member 'public Sample.Models.MockInputModel..ctor(System.String, System.String)' does not exist in the implementation but it does exist in the contract. diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline.xml b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline.xml new file mode 100644 index 00000000000..36000f351a8 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline.xml @@ -0,0 +1,7 @@ + + + + CP0002 + M:Sample.Models.MockInputModel.#ctor(System.String,System.String) + + diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline_LastContract/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline_LastContract/MockInputModel.cs new file mode 100644 index 00000000000..c6e9450a34e --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline_LastContract/MockInputModel.cs @@ -0,0 +1,18 @@ +namespace Sample.Models +{ + public partial class MockInputModel + { + // In the last contract "resources" was required so the initialization constructor + // accepted it. The current spec relaxes it to optional, which would normally cause the + // previous constructor to be restored - but here its removal is accepted in the baseline. + public MockInputModel(string name, string resources) + { + Name = name; + Resources = resources; + } + + public string Name { get; } + + public string Resources { get; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenReplacedByCustomCode.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenReplacedByCustomCode.cs new file mode 100644 index 00000000000..61958c1b8b5 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenReplacedByCustomCode.cs @@ -0,0 +1,33 @@ +// + +#nullable disable + +using System; +using System.Collections.Generic; +using Sample; + +namespace Sample.Models +{ + public partial class MockInputModel + { + private protected readonly global::System.Collections.Generic.IDictionary _additionalBinaryDataProperties; + + public MockInputModel(string name) + { + global::Sample.Argument.AssertNotNull(name, nameof(name)); + + Name = name; + } + + internal MockInputModel(string name, string resources, global::System.Collections.Generic.IDictionary additionalBinaryDataProperties) + { + Name = name; + Resources = resources; + _additionalBinaryDataProperties = additionalBinaryDataProperties; + } + + public string Name { get; } + + public string Resources { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenReplacedByCustomCode/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenReplacedByCustomCode/MockInputModel.cs new file mode 100644 index 00000000000..4dc7415bd15 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenReplacedByCustomCode/MockInputModel.cs @@ -0,0 +1,18 @@ +#nullable disable + +using Sample; +using SampleTypeSpec; + +namespace Sample.Models +{ + public partial class MockInputModel + { + // The user supplies their own (name, resources) constructor, replacing the one the + // generator would otherwise restore for back compat. Restoration must be skipped so the + // generated overload does not collide with this custom code. + public MockInputModel(string name, string resources) : this(name) + { + Resources = resources; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenReplacedByCustomCode_LastContract/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenReplacedByCustomCode_LastContract/MockInputModel.cs new file mode 100644 index 00000000000..d088ba570fa --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenReplacedByCustomCode_LastContract/MockInputModel.cs @@ -0,0 +1,16 @@ +namespace Sample.Models +{ + public partial class MockInputModel + { + // The previously published constructor accepted the required "resources" parameter. + public MockInputModel(string name, string resources) + { + Name = name; + Resources = resources; + } + + public string Name { get; } + + public string Resources { get; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_OptionalValueTypeConstructorParameterIsRestored.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_OptionalValueTypeConstructorParameterIsRestored.cs new file mode 100644 index 00000000000..c6e78ea90c8 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_OptionalValueTypeConstructorParameterIsRestored.cs @@ -0,0 +1,38 @@ +// + +#nullable disable + +using System; +using System.Collections.Generic; +using Sample; + +namespace Sample.Models +{ + public partial class MockInputModel + { + private protected readonly global::System.Collections.Generic.IDictionary _additionalBinaryDataProperties; + + public MockInputModel(string name) + { + global::Sample.Argument.AssertNotNull(name, nameof(name)); + + Name = name; + } + + internal MockInputModel(string name, int count, global::System.Collections.Generic.IDictionary additionalBinaryDataProperties) + { + Name = name; + Count = count; + _additionalBinaryDataProperties = additionalBinaryDataProperties; + } + + public MockInputModel(string name, int count) : this(name) + { + Count = count; + } + + public string Name { get; } + + public int Count { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_OptionalValueTypeConstructorParameterIsRestored/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_OptionalValueTypeConstructorParameterIsRestored/MockInputModel.cs new file mode 100644 index 00000000000..adad3e228ab --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_OptionalValueTypeConstructorParameterIsRestored/MockInputModel.cs @@ -0,0 +1,18 @@ +namespace Sample.Models +{ + public partial class MockInputModel + { + // In the last contract "count" was required so the initialization constructor + // accepted it. The current spec relaxes it to optional, which drops it from the + // constructor unless it is restored for back compat. + public MockInputModel(string name, int count) + { + Name = name; + Count = count; + } + + public string Name { get; } + + public int Count { get; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RenamedPropertyConstructorIsRestored.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RenamedPropertyConstructorIsRestored.cs new file mode 100644 index 00000000000..beb896a14e3 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RenamedPropertyConstructorIsRestored.cs @@ -0,0 +1,36 @@ +// + +#nullable disable + +using System; +using System.Collections.Generic; +using Sample; + +namespace Sample.Models +{ + public partial class MockInputModel + { + private protected readonly global::System.Collections.Generic.IDictionary _additionalBinaryDataProperties; + + public MockInputModel(string name) + { + global::Sample.Argument.AssertNotNull(name, nameof(name)); + + Name = name; + } + + internal MockInputModel(string name, string resourceList, global::System.Collections.Generic.IDictionary additionalBinaryDataProperties) + { + Name = name; + ResourceList = resourceList; + _additionalBinaryDataProperties = additionalBinaryDataProperties; + } + + public MockInputModel(string name, string resources) : this(name) + { + ResourceList = resources; + } + + public string Name { get; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RenamedPropertyConstructorIsRestored/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RenamedPropertyConstructorIsRestored/MockInputModel.cs new file mode 100644 index 00000000000..e9ed6d5d4a3 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RenamedPropertyConstructorIsRestored/MockInputModel.cs @@ -0,0 +1,14 @@ +#nullable disable + +using Sample; +using SampleTypeSpec; +using Microsoft.TypeSpec.Generator.Customizations; + +namespace Sample.Models +{ + public partial class MockInputModel + { + [CodeGenMember("Resources")] + public string ResourceList { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RenamedPropertyConstructorIsRestored_LastContract/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RenamedPropertyConstructorIsRestored_LastContract/MockInputModel.cs new file mode 100644 index 00000000000..83aedb284da --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RenamedPropertyConstructorIsRestored_LastContract/MockInputModel.cs @@ -0,0 +1,16 @@ +namespace Sample.Models +{ + public partial class MockInputModel + { + // The previously published constructor used the pre-rename parameter name "resources". + public MockInputModel(string name, string resources) + { + Name = name; + Resources = resources; + } + + public string Name { get; } + + public string Resources { get; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RequiredToOptionalConstructorIsRestored.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RequiredToOptionalConstructorIsRestored.cs new file mode 100644 index 00000000000..605759a88bd --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RequiredToOptionalConstructorIsRestored.cs @@ -0,0 +1,38 @@ +// + +#nullable disable + +using System; +using System.Collections.Generic; +using Sample; + +namespace Sample.Models +{ + public partial class MockInputModel + { + private protected readonly global::System.Collections.Generic.IDictionary _additionalBinaryDataProperties; + + public MockInputModel(string name) + { + global::Sample.Argument.AssertNotNull(name, nameof(name)); + + Name = name; + } + + internal MockInputModel(string name, string resources, global::System.Collections.Generic.IDictionary additionalBinaryDataProperties) + { + Name = name; + Resources = resources; + _additionalBinaryDataProperties = additionalBinaryDataProperties; + } + + public MockInputModel(string name, string resources) : this(name) + { + Resources = resources; + } + + public string Name { get; } + + public string Resources { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RequiredToOptionalConstructorIsRestored/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RequiredToOptionalConstructorIsRestored/MockInputModel.cs new file mode 100644 index 00000000000..bc469c58d83 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RequiredToOptionalConstructorIsRestored/MockInputModel.cs @@ -0,0 +1,17 @@ +namespace Sample.Models +{ + public partial class MockInputModel + { + // In the last contract, both properties were required so the initialization + // constructor accepted both of them. + public MockInputModel(string name, string resources) + { + Name = name; + Resources = resources; + } + + public string Name { get; } + + public string Resources { get; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/ApiCompatBaselineTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/ApiCompatBaselineTests.cs index 0d0cfddde73..592b12204e2 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/ApiCompatBaselineTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/ApiCompatBaselineTests.cs @@ -365,6 +365,53 @@ public void IsMethodRemovalSuppressedMatchesDictionaryWithModelValue() Assert.IsFalse(baseline.IsMethodRemovalSuppressed("Ns.Types", "WithDictionary", [dictionaryOfInt])); } + [Test] + public void IsMethodRemovalSuppressedMatchesConstructor() + { + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "Baseline"); + + // Ns.Ctors..ctor(System.String, System.Int32) is accepted in the baseline. Constructors are + // recorded under the ".ctor" member name with their exact parameter types. + Assert.IsTrue(baseline.IsMethodRemovalSuppressed( + "Ns.Ctors", + ".ctor", + [new CSharpType(typeof(string)), new CSharpType(typeof(int))])); + + // The same types in a different order are a different constructor signature. + Assert.IsFalse(baseline.IsMethodRemovalSuppressed( + "Ns.Ctors", + ".ctor", + [new CSharpType(typeof(int)), new CSharpType(typeof(string))])); + + // A different arity must not match. + Assert.IsFalse(baseline.IsMethodRemovalSuppressed("Ns.Ctors", ".ctor", [new CSharpType(typeof(string))])); + + // A different parameter type in one slot must not match. + Assert.IsFalse(baseline.IsMethodRemovalSuppressed( + "Ns.Ctors", + ".ctor", + [new CSharpType(typeof(string)), new CSharpType(typeof(bool))])); + + // A different declaring type must not match. + Assert.IsFalse(baseline.IsMethodRemovalSuppressed( + "Ns.Other", + ".ctor", + [new CSharpType(typeof(string)), new CSharpType(typeof(int))])); + } + + [Test] + public void IsMethodRemovalSuppressedMatchesParameterlessConstructor() + { + var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "Baseline"); + + // Ns.Ctors..ctor() has no parameters; the canonical signature is empty on both sides. + Assert.IsTrue(baseline.IsMethodRemovalSuppressed("Ns.Ctors", ".ctor", [])); + + // Ns.Foo only has a constructor overload that takes arguments in the baseline, so querying + // its parameterless constructor must not match that overload. + Assert.IsFalse(baseline.IsMethodRemovalSuppressed("Ns.Foo", ".ctor", [])); + } + [Test] public void ReferencesSuppressedTypeMatchesDirectType() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/Baseline.txt b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/Baseline.txt index 725b47e3795..9769f92a6df 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/Baseline.txt +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/Baseline.txt @@ -3,6 +3,8 @@ TypesMustExist : Type 'Azure.AI.Projects.Agents.ProjectsAgentProtocol' does not MembersMustExist : Member 'public Azure.AI.Projects.Agents.ProtocolVersionRecord Azure.AI.Projects.Agents.ProjectsAgentsModelFactory.ProtocolVersionRecord(Azure.AI.Projects.Agents.ProjectsAgentProtocol, System.String)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public System.Void Ns.Foo.Reset()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Ns.Foo..ctor(Ns.Kind, System.String)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Ns.Ctors..ctor(System.String, System.Int32)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Ns.Ctors..ctor()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Ns.Kind Ns.Foo.Kind.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public System.Void Ns.Foo.Kind.set(Ns.Kind)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public System.Void Ns.Foo.Configure(System.Collections.Generic.IDictionary, System.String)' does not exist in the implementation but it does exist in the contract. diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/Baseline.xml b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/Baseline.xml index 6b8132093b2..b683202d4c6 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/Baseline.xml +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/Baseline.xml @@ -20,6 +20,14 @@ CP0002 M:Ns.Foo.#ctor(Ns.Kind,System.String) + + CP0002 + M:Ns.Ctors.#ctor(System.String,System.Int32) + + + CP0002 + M:Ns.Ctors.#ctor + CP0002 M:Ns.Foo.get_Kind