Skip to content

Commit 9f86652

Browse files
feat: generalize back compat
1 parent c8b8194 commit 9f86652

14 files changed

Lines changed: 749 additions & 139 deletions

File tree

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

Lines changed: 25 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1261,62 +1261,51 @@ protected override ScmMethodProvider[] BuildMethods()
12611261

12621262
protected sealed override IReadOnlyList<MethodProvider> BuildMethodsForBackCompatibility(IEnumerable<MethodProvider> originalMethods)
12631263
{
1264-
List<MethodProvider> materializedMethods = [.. originalMethods];
1265-
12661264
if (LastContractView?.Methods == null || LastContractView.Methods.Count == 0)
12671265
{
1268-
return materializedMethods;
1266+
return [.. originalMethods];
12691267
}
12701268

1271-
var currentMethodSignatures = BuildCurrentMethodSignatures(materializedMethods);
1272-
1273-
ProcessBackCompatForParameterReordering(materializedMethods, currentMethodSignatures);
1274-
ProcessBackCompatForNewOptionalParameters(materializedMethods, currentMethodSignatures);
1269+
// Snapshot the current signatures before the base reorders any of them in place, so we
1270+
// can fix up convenience method bodies that call reordered protocol methods afterwards.
1271+
var originalSignatures = new Dictionary<MethodProvider, MethodSignature>(ReferenceEqualityComparer.Instance);
1272+
foreach (var method in originalMethods)
1273+
{
1274+
originalSignatures.TryAdd(method, method.Signature);
1275+
}
12751276

1276-
return materializedMethods;
1277-
}
1277+
List<MethodProvider> result = [.. base.BuildMethodsForBackCompatibility(originalMethods)];
12781278

1279-
private void ProcessBackCompatForParameterReordering(
1280-
IList<MethodProvider> materializedMethods,
1281-
Dictionary<MethodSignature, MethodProvider> currentMethodSignatures)
1282-
{
1279+
// Determine which existing methods had their parameters reordered by the base and fix up
1280+
// convenience method bodies that call the reordered protocol methods.
12831281
var updatedSignatureToOriginal = new Dictionary<MethodSignature, MethodSignature>(MethodSignature.MethodSignatureComparer);
12841282
var methodsWithReorderedParams = new List<MethodProvider>();
1285-
1286-
foreach (var previousMethod in LastContractView!.Methods)
1283+
foreach (var (method, originalSignature) in originalSignatures)
12871284
{
1288-
if (!ShouldProcessMethodForBackCompat(previousMethod.Signature, currentMethodSignatures))
1285+
if (method.Signature.Name.Equals(originalSignature.Name)
1286+
&& !MethodSignatureHelper.HaveSameParametersInSameOrder(method.Signature, originalSignature))
12891287
{
1290-
continue;
1291-
}
1292-
1293-
var methodToUpdate = FindMethodWithSameParametersButDifferentOrder(
1294-
previousMethod.Signature,
1295-
currentMethodSignatures);
1296-
1297-
if (methodToUpdate != null && TryReorderCurrentMethodParameters(
1298-
methodToUpdate,
1299-
previousMethod.Signature,
1300-
updatedSignatureToOriginal))
1301-
{
1302-
methodsWithReorderedParams.Add(methodToUpdate);
1303-
CodeModelGenerator.Instance.Emitter.Debug(
1304-
$"Reordered parameters of '{Name}.{methodToUpdate.Signature.Name}' to match last contract.",
1305-
BackCompatibilityChangeCategory.MethodParameterReordering);
1288+
updatedSignatureToOriginal.TryAdd(method.Signature, originalSignature);
1289+
methodsWithReorderedParams.Add(method);
13061290
}
13071291
}
13081292

13091293
if (methodsWithReorderedParams.Count > 0)
13101294
{
1311-
UpdateConvenienceMethodsForBackCompat(materializedMethods, methodsWithReorderedParams, updatedSignatureToOriginal);
1295+
UpdateConvenienceMethodsForBackCompat(result, methodsWithReorderedParams, updatedSignatureToOriginal);
13121296
}
1297+
1298+
// Add hidden overloads for methods that gained new optional non-body parameters.
1299+
ProcessBackCompatForNewOptionalParameters(result, BuildCurrentMethodSignatureMap(result));
1300+
1301+
return result;
13131302
}
13141303

1315-
private Dictionary<MethodSignature, MethodProvider> BuildCurrentMethodSignatures(IEnumerable<MethodProvider> originalMethods)
1304+
private Dictionary<MethodSignature, MethodProvider> BuildCurrentMethodSignatureMap(IEnumerable<MethodProvider> methods)
13161305
{
13171306
var allMethods = CustomCodeView?.Methods != null
1318-
? originalMethods.Concat(CustomCodeView.Methods)
1319-
: originalMethods;
1307+
? methods.Concat(CustomCodeView.Methods)
1308+
: methods;
13201309

13211310
var result = new Dictionary<MethodSignature, MethodProvider>(MethodSignature.MethodSignatureComparer);
13221311
foreach (var method in allMethods)
@@ -1326,86 +1315,6 @@ private Dictionary<MethodSignature, MethodProvider> BuildCurrentMethodSignatures
13261315
return result;
13271316
}
13281317

1329-
private static bool ShouldProcessMethodForBackCompat(
1330-
MethodSignature previousSignature,
1331-
Dictionary<MethodSignature, MethodProvider> currentMethodSignatures)
1332-
{
1333-
if (currentMethodSignatures.ContainsKey(previousSignature))
1334-
{
1335-
return false;
1336-
}
1337-
1338-
var modifiers = previousSignature.Modifiers;
1339-
return modifiers.HasFlag(MethodSignatureModifiers.Public) ||
1340-
modifiers.HasFlag(MethodSignatureModifiers.Protected);
1341-
}
1342-
1343-
private static MethodProvider? FindMethodWithSameParametersButDifferentOrder(
1344-
MethodSignature previousSignature,
1345-
Dictionary<MethodSignature, MethodProvider> currentMethodSignatures)
1346-
{
1347-
foreach (var kvp in currentMethodSignatures)
1348-
{
1349-
var currentSignature = kvp.Key;
1350-
if (currentSignature.Name.Equals(previousSignature.Name)
1351-
&& currentSignature.ReturnType?.AreNamesEqual(previousSignature.ReturnType) == true
1352-
&& MethodSignatureHelper.ContainsSameParameters(previousSignature, currentSignature))
1353-
{
1354-
return kvp.Value;
1355-
}
1356-
}
1357-
1358-
return null;
1359-
}
1360-
1361-
private bool TryReorderCurrentMethodParameters(
1362-
MethodProvider methodToUpdate,
1363-
MethodSignature previousSignature,
1364-
Dictionary<MethodSignature, MethodSignature> updatedSignatureToOriginal)
1365-
{
1366-
var currentSignature = methodToUpdate.Signature;
1367-
// Early exit: Check if parameters are already in the same order
1368-
if (MethodSignatureHelper.HaveSameParametersInSameOrder(currentSignature, previousSignature))
1369-
{
1370-
return false;
1371-
}
1372-
1373-
var parametersByName = currentSignature.Parameters.ToDictionary(p => p.Name.ToVariableName());
1374-
var reorderedParameters = new List<ParameterProvider>(currentSignature.Parameters.Count);
1375-
1376-
foreach (var previousParam in previousSignature.Parameters)
1377-
{
1378-
if (parametersByName.TryGetValue(previousParam.Name, out var matchingParam))
1379-
{
1380-
reorderedParameters.Add(matchingParam);
1381-
}
1382-
}
1383-
1384-
if (reorderedParameters.Count != currentSignature.Parameters.Count)
1385-
{
1386-
return false;
1387-
}
1388-
1389-
var updatedSignature = new MethodSignature(
1390-
currentSignature.Name,
1391-
currentSignature.Description,
1392-
currentSignature.Modifiers,
1393-
currentSignature.ReturnType,
1394-
currentSignature.ReturnDescription,
1395-
reorderedParameters,
1396-
currentSignature.Attributes,
1397-
currentSignature.GenericArguments,
1398-
currentSignature.GenericParameterConstraints,
1399-
currentSignature.ExplicitInterface,
1400-
currentSignature.NonDocumentComment);
1401-
updatedSignatureToOriginal.TryAdd(updatedSignature, currentSignature);
1402-
1403-
UpdateXmlDocProviderForParamReorder(methodToUpdate.XmlDocs, updatedSignature);
1404-
methodToUpdate.Update(signature: updatedSignature, xmlDocProvider: methodToUpdate.XmlDocs);
1405-
1406-
return true;
1407-
}
1408-
14091318
private ParameterProvider BuildClientEndpointParameter()
14101319
{
14111320
_inputEndpointParam = _inputClient.Parameters
@@ -1743,28 +1652,6 @@ private static void ReorderMethodInvocationArguments(
17431652
}
17441653
}
17451654

1746-
private static void UpdateXmlDocProviderForParamReorder(
1747-
XmlDocProvider xmlDocs,
1748-
MethodSignature updatedSignature)
1749-
{
1750-
var paramDocsByName = xmlDocs.Parameters.ToDictionary(s => s.Parameter.Name);
1751-
var reorderedParamDocs = new List<XmlDocParamStatement>(updatedSignature.Parameters.Count);
1752-
1753-
foreach (var param in updatedSignature.Parameters)
1754-
{
1755-
if (paramDocsByName.TryGetValue(param.Name, out var paramDoc))
1756-
{
1757-
reorderedParamDocs.Add(paramDoc);
1758-
}
1759-
}
1760-
1761-
if (reorderedParamDocs.Count == xmlDocs.Parameters.Count &&
1762-
!reorderedParamDocs.SequenceEqual(xmlDocs.Parameters))
1763-
{
1764-
xmlDocs.Update(parameters: reorderedParamDocs);
1765-
}
1766-
}
1767-
17681655
private void ProcessBackCompatForNewOptionalParameters(
17691656
List<MethodProvider> methods,
17701657
Dictionary<MethodSignature, MethodProvider> currentMethodSignatures)

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

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -723,8 +723,120 @@ internal void ProcessTypeForBackCompatibility()
723723
protected internal virtual IReadOnlyList<EnumTypeMember>? BuildEnumValuesForBackCompatibility(IReadOnlyList<EnumTypeMember> originalEnumValues)
724724
=> null;
725725

726+
/// <summary>
727+
/// Returns this type's methods with backward compatibility applied against
728+
/// <see cref="LastContractView"/>. The default implementation restores the previous
729+
/// parameter order on a current method when it matches a last-contract method by name and
730+
/// return type with the same parameter set but in a different order. Reordering is done in
731+
/// place, so a method's body (which references its parameters by object) remains valid.
732+
/// Override and call <c>base</c> to extend this behavior; override without calling
733+
/// <c>base</c> to replace it.
734+
/// </summary>
726735
protected internal virtual IReadOnlyList<MethodProvider> BuildMethodsForBackCompatibility(IEnumerable<MethodProvider> originalMethods)
727-
=> [.. originalMethods];
736+
{
737+
var methods = new List<MethodProvider>(originalMethods);
738+
739+
if (LastContractView?.Methods is not { Count: > 0 } previousMethods)
740+
{
741+
return methods;
742+
}
743+
744+
var currentMethodSignatures = BuildCurrentMethodSignatureMap(methods);
745+
746+
foreach (var previousMethod in previousMethods)
747+
{
748+
if (!BackCompatHelper.ShouldApplyMethodBackCompatibility(previousMethod.Signature, currentMethodSignatures))
749+
{
750+
continue;
751+
}
752+
753+
var methodToReorder = BackCompatHelper.FindMethodWithSameParametersDifferentOrder(previousMethod.Signature, currentMethodSignatures);
754+
if (methodToReorder != null && BackCompatHelper.TryRestorePreviousParameterOrder(methodToReorder, previousMethod.Signature))
755+
{
756+
CodeModelGenerator.Instance.Emitter.Debug(
757+
$"Reordered parameters of '{Name}.{methodToReorder.Signature.Name}' to match last contract.",
758+
BackCompatibilityChangeCategory.MethodParameterReordering);
759+
}
760+
}
761+
762+
RestorePreviousParameterNames(methods);
763+
764+
return methods;
765+
}
766+
767+
/// <summary>
768+
/// Restores previously-published parameter names on this type's public/protected methods so a
769+
/// generator or spec rename does not source-break callers using named arguments. The lookup is
770+
/// keyed on each parameter's original (spec) name — retained on its source
771+
/// <see cref="ParameterProvider.InputParameter"/> — so only spec-derived parameters are
772+
/// considered; hand-built parameters (no input parameter) are left unchanged. The rename is
773+
/// applied in place via <see cref="ParameterProvider.Update"/>, which also rewrites the cached
774+
/// variable/argument declarations so the method body and XML docs follow automatically.
775+
/// </summary>
776+
private void RestorePreviousParameterNames(IReadOnlyList<MethodProvider> methods)
777+
{
778+
foreach (var method in methods)
779+
{
780+
var modifiers = method.Signature.Modifiers;
781+
if (!modifiers.HasFlag(MethodSignatureModifiers.Public) && !modifiers.HasFlag(MethodSignatureModifiers.Protected))
782+
{
783+
continue;
784+
}
785+
786+
foreach (var parameter in method.Signature.Parameters)
787+
{
788+
var inputParameter = parameter.InputParameter;
789+
if (inputParameter is null)
790+
{
791+
continue;
792+
}
793+
794+
if (!string.Equals(parameter.Name, inputParameter.Name, StringComparison.Ordinal))
795+
{
796+
continue;
797+
}
798+
799+
var originalName = inputParameter.OriginalName;
800+
if (string.IsNullOrEmpty(originalName))
801+
{
802+
continue;
803+
}
804+
805+
var preservedName = BackCompatHelper.FindPreviousParameterName(LastContractView, originalName, method.Signature.Name);
806+
807+
// The lookup matches the previous parameter case-insensitively (so a casing-only
808+
// spec change still finds it), but the decision to rename is case-sensitive: a
809+
// casing-only difference must still restore the previously-published spelling.
810+
if (preservedName is null || string.Equals(preservedName, parameter.Name, StringComparison.Ordinal))
811+
{
812+
continue;
813+
}
814+
815+
CodeModelGenerator.Instance.Emitter.Debug(
816+
$"Preserved parameter name '{preservedName}' on '{Name}.{method.Signature.Name}' from last contract (instead of '{parameter.Name}').",
817+
BackCompatibilityChangeCategory.ParameterNamePreserved);
818+
parameter.Update(name: preservedName);
819+
}
820+
}
821+
}
822+
823+
/// <summary>
824+
/// Builds a lookup of the type's current method signatures (including custom code methods)
825+
/// used to match against last-contract methods.
826+
/// </summary>
827+
private Dictionary<MethodSignature, MethodProvider> BuildCurrentMethodSignatureMap(IEnumerable<MethodProvider> methods)
828+
{
829+
var allMethods = CustomCodeView?.Methods != null
830+
? methods.Concat(CustomCodeView.Methods)
831+
: methods;
832+
833+
var result = new Dictionary<MethodSignature, MethodProvider>(MethodSignature.MethodSignatureComparer);
834+
foreach (var method in allMethods)
835+
{
836+
result.TryAdd(method.Signature, method);
837+
}
838+
return result;
839+
}
728840

729841
protected internal virtual IReadOnlyList<ConstructorProvider> BuildConstructorsForBackCompatibility(IEnumerable<ConstructorProvider> originalConstructors)
730842
=> [.. originalConstructors];

0 commit comments

Comments
 (0)