Skip to content

Commit 2904e98

Browse files
Add Support for More Back Compat Model Ctor Scenarios (#11614)
This PR introduces more covered scenarios for the back compat support for model ctors. fixes: #11588
1 parent f30cd35 commit 2904e98

17 files changed

Lines changed: 633 additions & 28 deletions

File tree

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs

Lines changed: 100 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -802,13 +802,21 @@ protected internal override IReadOnlyList<ConstructorProvider> BuildConstructors
802802
return base.BuildConstructorsForBackCompatibility(originalConstructors);
803803
}
804804

805-
var constructors = new List<ConstructorProvider>(base.BuildConstructorsForBackCompatibility(originalConstructors));
806-
var restorablePropertyLookup = BuildRestorablePropertyLookup();
805+
var originalConstructorList = originalConstructors as IReadOnlyList<ConstructorProvider> ?? [.. originalConstructors];
807806
IReadOnlyList<ConstructorProvider> candidateConstructors = CustomCodeView?.Constructors is { Count: > 0 } customConstructors
808-
? [.. constructors, .. customConstructors]
809-
: constructors;
807+
? [.. originalConstructorList, .. customConstructors]
808+
: originalConstructorList;
810809

811-
foreach (var previousConstructor in previousConstructors)
810+
var restorablePreviousConstructors = previousConstructors
811+
.Where(c => !BackCompatHelper.IsConstructorRemovalAcceptedInBaseline(this, c.Signature))
812+
.ToList();
813+
814+
RestorePreviousConstructorParameterNames(originalConstructorList, candidateConstructors, restorablePreviousConstructors);
815+
816+
var constructors = new List<ConstructorProvider>(base.BuildConstructorsForBackCompatibility(originalConstructorList));
817+
var restorablePropertyLookup = BuildRestorablePropertyLookup();
818+
819+
foreach (var previousConstructor in restorablePreviousConstructors)
812820
{
813821
if (!MethodSignatureHelper.IsPublicApi(previousConstructor.Signature.Modifiers))
814822
{
@@ -817,19 +825,17 @@ protected internal override IReadOnlyList<ConstructorProvider> BuildConstructors
817825

818826
var previousParameters = previousConstructor.Signature.Parameters;
819827

820-
if (BackCompatHelper.IsConstructorRemovalAcceptedInBaseline(this, previousConstructor.Signature))
821-
{
822-
continue;
823-
}
824-
825828
// A previously published accessible parameterless constructor is dropped when the current
826829
// generation makes a property required. Restore it and drop the generated mocking constructor
827830
// so it is not a duplicate. An accessible parameterless constructor (generated or custom code)
828-
// counts as already present; an inaccessible generated mocking constructor does not.
829-
if (!Type.IsStruct && previousParameters.Count == 0)
830-
{
831-
if (!constructors.Any(c => c.Signature.Parameters.Count == 0 && MethodSignatureHelper.IsPublicApi(c.Signature.Modifiers))
832-
&& !CanonicalView.Constructors.Any(c => c.Signature.Parameters.Count == 0 && MethodSignatureHelper.IsPublicApi(c.Signature.Modifiers)))
831+
// counts as already present; an inaccessible generated mocking constructor does not. A struct
832+
// always exposes a public parameterless constructor via its serialization (mocking)
833+
// constructor, so there is nothing to restore on the model partial.
834+
if (previousParameters.Count == 0)
835+
{
836+
if (!Type.IsStruct
837+
&& !constructors.Any(c => c.Signature.Parameters.Count == 0 && MethodSignatureHelper.IsPublicApi(c.Signature.Modifiers))
838+
&& !candidateConstructors.Any(c => c.Signature.Parameters.Count == 0 && MethodSignatureHelper.IsPublicApi(c.Signature.Modifiers)))
833839
{
834840
var parameterlessConstructor = BuildBackCompatParameterlessConstructor(previousConstructor, candidateConstructors);
835841
RemoveGeneratedMockingConstructor(constructors);
@@ -843,9 +849,9 @@ protected internal override IReadOnlyList<ConstructorProvider> BuildConstructors
843849
}
844850

845851
// If a constructor with the same parameters already exists - either still generated or
846-
// supplied by custom code (which lives in the canonical view) - there is nothing to restore.
852+
// supplied by custom code - there is nothing to restore.
847853
if (constructors.Any(c => BackCompatHelper.ParametersMatch(c.Signature.Parameters, previousParameters))
848-
|| CanonicalView.Constructors.Any(c => BackCompatHelper.ParametersMatch(c.Signature.Parameters, previousParameters)))
854+
|| candidateConstructors.Any(c => BackCompatHelper.ParametersMatch(c.Signature.Parameters, previousParameters)))
849855
{
850856
continue;
851857
}
@@ -868,6 +874,70 @@ protected internal override IReadOnlyList<ConstructorProvider> BuildConstructors
868874
return constructors;
869875
}
870876

877+
private void RestorePreviousConstructorParameterNames(
878+
IReadOnlyList<ConstructorProvider> currentConstructors,
879+
IReadOnlyList<ConstructorProvider> candidateConstructors,
880+
IReadOnlyList<ConstructorProvider> previousConstructors)
881+
{
882+
const MethodSignatureModifiers privateProtected = MethodSignatureModifiers.Private | MethodSignatureModifiers.Protected;
883+
foreach (var previousConstructor in previousConstructors)
884+
{
885+
if (!MethodSignatureHelper.IsPublicApi(previousConstructor.Signature.Modifiers))
886+
{
887+
continue;
888+
}
889+
890+
var previousParameters = previousConstructor.Signature.Parameters;
891+
892+
// A generated or custom constructor that already matches the previous signature (types and
893+
// names) satisfies the contract; renaming another constructor into it would collide.
894+
if (candidateConstructors.Any(c => BackCompatHelper.ParametersMatch(c.Signature.Parameters, previousParameters)))
895+
{
896+
continue;
897+
}
898+
899+
var currentConstructor = currentConstructors.FirstOrDefault(c =>
900+
(MethodSignatureHelper.IsPublicApi(c.Signature.Modifiers)
901+
|| (c.Signature.Modifiers & privateProtected) == privateProtected)
902+
&& MethodSignatureBase.SignatureComparer.Equals(c.Signature, previousConstructor.Signature));
903+
if (currentConstructor is null)
904+
{
905+
continue;
906+
}
907+
908+
var currentParameters = currentConstructor.Signature.Parameters;
909+
910+
// A swap or rotation keeps every previous name, so realign the existing parameter objects
911+
// to the previous order - renaming positionally would mis-bind a caller's named argument to
912+
// the wrong property. Otherwise restore names positionally where the types line up.
913+
var currentByName = currentParameters.ToDictionary(p => p.Name);
914+
IReadOnlyList<ParameterProvider> restoredParameters = previousParameters.All(p => currentByName.ContainsKey(p.Name))
915+
? [.. previousParameters.Select(p => currentByName[p.Name])]
916+
: currentParameters;
917+
if (!restoredParameters.Select((p, i) => p.Type.AreNamesEqual(previousParameters[i].Type)).All(match => match))
918+
{
919+
continue;
920+
}
921+
922+
for (int i = 0; i < restoredParameters.Count; i++)
923+
{
924+
var restoredName = previousParameters[i].Name;
925+
if (string.Equals(restoredParameters[i].Name, restoredName, StringComparison.Ordinal))
926+
{
927+
continue;
928+
}
929+
930+
CodeModelGenerator.Instance.Emitter.Debug(
931+
$"Preserved parameter name '{restoredName}' at position {i} on constructor '{Name}' from last contract (instead of '{restoredParameters[i].Name}').",
932+
BackCompatibilityChangeCategory.ParameterNamePreserved);
933+
restoredParameters[i].Update(name: restoredName);
934+
}
935+
936+
currentConstructor.Signature.Update(parameters: [.. restoredParameters]);
937+
currentConstructor.Update(signature: currentConstructor.Signature);
938+
}
939+
}
940+
871941
private bool TryBuildRestoredConstructor(
872942
ConstructorProvider previousConstructor,
873943
IReadOnlyList<ConstructorProvider> currentConstructors,
@@ -878,7 +948,9 @@ private bool TryBuildRestoredConstructor(
878948
var previousParameters = previousConstructor.Signature.Parameters;
879949

880950
// Find the public constructor to chain to: its parameters must form an in-order subsequence of
881-
// the previous constructor's parameters. Prefer the closest one.
951+
// the previous constructor's parameters. Prefer the closest one. Without a chaining target the
952+
// constructor is not restored - a standalone constructor would bypass the current constructor's
953+
// initialization (e.g. the implicit base() call and inherited get-only properties).
882954
ConstructorProvider? targetConstructor = null;
883955
foreach (var candidate in currentConstructors)
884956
{
@@ -898,7 +970,7 @@ private bool TryBuildRestoredConstructor(
898970
}
899971
}
900972

901-
if (targetConstructor == null)
973+
if (targetConstructor is null)
902974
{
903975
return false;
904976
}
@@ -925,23 +997,23 @@ private bool TryBuildRestoredConstructor(
925997
continue;
926998
}
927999

928-
if (!restorablePropertyLookup.TryGetValue(previousParameter.Name, out var property)
929-
|| !property.Type.AreNamesEqual(previousParameter.Type))
1000+
// Each extra parameter (not consumed by the chain target) must map to a settable, wire-backed
1001+
// property assigned in the restored constructor's body.
1002+
var property = restorablePropertyLookup.TryGetValue(previousParameter.Name, out var chained)
1003+
&& chained.Type.AreNamesEqual(previousParameter.Type)
1004+
? chained
1005+
: null;
1006+
1007+
if (property is null || extraAssignments.Any(a => a.Property == property))
9301008
{
9311009
return false;
9321010
}
9331011

934-
var restoredParameter = PartialMethodCustomization.CloneParameterWithName(
935-
property.AsParameter,
936-
previousParameter.Name,
937-
removeDefault: true);
938-
1012+
var restoredParameter = PartialMethodCustomization.CloneParameterWithName(property.AsParameter, previousParameter.Name, removeDefault: true);
9391013
restoredParameters.Add(restoredParameter);
9401014
extraAssignments.Add((property, restoredParameter));
9411015
}
9421016

943-
// Every target parameter must be consumed and at least one extra property must be assigned,
944-
// otherwise the restored constructor would be redundant or would produce an invalid chained call.
9451017
if (targetIndex != targetParameters.Count || extraAssignments.Count == 0)
9461018
{
9471019
return false;

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1075,6 +1075,11 @@ protected internal virtual IReadOnlyList<ConstructorProvider> BuildConstructorsF
10751075
continue;
10761076
}
10771077

1078+
if (BackCompatHelper.IsConstructorRemovalAcceptedInBaseline(this, previousConstructor.Signature))
1079+
{
1080+
continue;
1081+
}
1082+
10781083
// Find a matching constructor in the current version by parameter signature
10791084
for (int i = 0; i < constructors.Count; i++)
10801085
{

0 commit comments

Comments
 (0)