Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.TypeSpec.Generator.Input;
using Microsoft.TypeSpec.Generator.Primitives;
Expand Down Expand Up @@ -34,6 +35,9 @@ protected override string BuildRelativeFilePath()

protected override TypeSignatureModifiers BuildDeclarationModifiers() => _enumProvider.DeclarationModifiers;

protected override IReadOnlyList<MethodProvider> BuildMethodsForBackCompatibility(IEnumerable<MethodProvider> originalMethods)
=> [.. originalMethods];

protected override MethodProvider[] BuildMethods()
{
// for string-based extensible enums, we are using `ToString` as its serialization
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ public MrwSerializationTypeDefinition(InputModelType inputModel, ModelProvider m
protected override string BuildNamespace() => _model.Type.Namespace;

protected override TypeSignatureModifiers BuildDeclarationModifiers() => _model.DeclarationModifiers;

protected override IReadOnlyList<MethodProvider> BuildMethodsForBackCompatibility(IEnumerable<MethodProvider> originalMethods)
=> [.. originalMethods];

private ConstructorProvider SerializationConstructor => _serializationConstructor ??= _model.FullConstructor;
private PropertyProvider[] AdditionalProperties => _additionalProperties.Value;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ public MultipartFormDataSerializationDefinition(InputModelType inputModel, Model

protected override TypeSignatureModifiers BuildDeclarationModifiers() => _model.DeclarationModifiers;

protected override IReadOnlyList<MethodProvider> BuildMethodsForBackCompatibility(IEnumerable<MethodProvider> originalMethods)
=> [.. originalMethods];

protected override string BuildRelativeFilePath()
{
return Path.Combine("src", "Generated", "Models", $"{Name}.Serialization.Multipart.cs");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,8 @@ public enum BackCompatibilityChangeCategory

/// <summary>A back-compat change was skipped because the removal was accepted in the ApiCompat baseline.</summary>
BaselineAcceptedRemovalSkipped,

/// <summary>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.</summary>
EnumMemberAddedFromLastContract,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ public void WriteBufferedMessages()
BackCompatibilityChangeCategory.ModelFactoryMethodSkipped => "Model Factory Method Back-Compat Skipped",
BackCompatibilityChangeCategory.SvcMethodNewOptionalParameterOverloadAdded => "Method Back-Compat Overload Added For New Optional Parameter",
BackCompatibilityChangeCategory.BaselineAcceptedRemovalSkipped => "Back-Compat Skipped For ApiCompat Baseline Accepted Removal",
BackCompatibilityChangeCategory.EnumMemberAddedFromLastContract => "Enum Member Added From Last Contract",
_ => category.ToString(),
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public void Update(CSharpType? type = null, string? name = null)
}
if (name != null)
{
Declaration = new CodeWriterDeclaration(name);
Declaration.Update(name: name);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public CodeWriterDeclaration(string name)
RequestedName = name;
}

public string RequestedName { get; }
public string RequestedName { get; private set; }

internal string GetActualName(CodeWriter.CodeScope scope)
{
Expand All @@ -49,5 +49,13 @@ internal void SetActualName(string actualName, CodeWriter.CodeScope scope)
{
_actualNames[scope] = actualName;
}

internal void Update(string? name = null)
{
if (name != null)
{
RequestedName = name;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -168,15 +168,8 @@ private List<EnumTypeMember> BuildApiVersionEnumValuesForBackwardCompatibility(L
return currentApiVersions;
}

var processedNames = new HashSet<string>(lastContractFields.Select(f => f.Name), StringComparer.OrdinalIgnoreCase);
// Then, add new versions in the wire order
foreach (var currentVersion in currentApiVersions)
{
if (!processedNames.Contains(currentVersion.Name))
{
allMembers.Add(currentVersion);
}
}
// Then, add new versions in the wire order.
AppendMembersNotInLastContract(currentApiVersions, lastContractFields, allMembers);

for (int i = 0; i < allMembers.Count; i++)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.TypeSpec.Generator.EmitterRpc;
using Microsoft.TypeSpec.Generator.Expressions;
Expand Down Expand Up @@ -128,29 +129,46 @@ protected override IReadOnlyList<EnumTypeMember> BuildEnumValues()
{
if (currentLookup.TryGetValue(field.Name, out var existingMember))
{
// Preserve the last contract's explicit value for integer enums so members keep
// their exact values.
ValueExpression? initializationValue = null;
var memberValue = existingMember.Value;
if (IsIntValueType && field.InitializationValue is LiteralExpression { Literal: { } lastContractValue })
{
initializationValue = field.InitializationValue;
memberValue = lastContractValue;
}

var updatedField = new FieldProvider(
existingMember.Field.Modifiers,
existingMember.Field.Type,
existingMember.Name,
existingMember.Field.EnclosingType,
existingMember.Field.Description);
allMembers.Add(new EnumTypeMember(existingMember.Name, updatedField, existingMember.Value));
existingMember.Field.Description,
initializationValue);
allMembers.Add(new EnumTypeMember(existingMember.Name, updatedField, memberValue));
}
}

// Then, add new members that weren't in the last contract (in their original input order)
var processedNames = new HashSet<string>(lastContractFields.Select(f => f.Name), StringComparer.OrdinalIgnoreCase);
foreach (var current in currentValues)
{
if (!processedNames.Contains(current.Name))
else if (CodeModelGenerator.Instance.SourceInputModel?.ApiCompatBaseline.IsMemberSuppressed(Type.FullyQualifiedName, field.Name, 0) == true)
{
allMembers.Add(current);
CodeModelGenerator.Instance.Emitter.Debug(
$"Skipping re-add of enum member '{Name}.{field.Name}'; the removal is accepted in the ApiCompat baseline.",
BackCompatibilityChangeCategory.BaselineAcceptedRemovalSkipped);
}
else if (TryResurrectRemovedMember(field, out var resurrectedMember))
{
allMembers.Add(resurrectedMember);
CodeModelGenerator.Instance.Emitter.Debug(
$"Re-added enum member '{field.Name}' to enum '{Name}' to preserve a member from the last contract.",
BackCompatibilityChangeCategory.EnumMemberAddedFromLastContract);
}
}

// Report a summary-level change only if the relative order of shared members
// was actually altered to match the last contract.
if (!EnumMemberOrderMatches(currentValues, allMembers))
// Then, add new members that weren't in the last contract (in their original input order).
AppendMembersNotInLastContract(currentValues, lastContractFields, allMembers);

// Report a reordering only when the relative order of members present in BOTH the
// current values and the resulting set was actually altered.
if (SharedMemberOrderChanged(currentValues, allMembers))
{
CodeModelGenerator.Instance.Emitter.Debug(
$"Reordered members of enum '{Name}' to match last contract.",
Expand All @@ -160,22 +178,71 @@ protected override IReadOnlyList<EnumTypeMember> BuildEnumValues()
return allMembers;
}

private static bool EnumMemberOrderMatches(
IReadOnlyList<EnumTypeMember> left,
IReadOnlyList<EnumTypeMember> right)
private bool TryResurrectRemovedMember(FieldProvider lastContractField, [NotNullWhen(true)] out EnumTypeMember? member)
{
if (left.Count != right.Count)
member = null;
if (!IsIntValueType || lastContractField.InitializationValue is not LiteralExpression { Literal: { } literalValue })
{
return false;
}
for (int i = 0; i < left.Count; i++)

var field = new FieldProvider(
FieldModifiers.Public | FieldModifiers.Static,
EnumUnderlyingType,
lastContractField.Name,
this,
lastContractField.Description,
Literal(literalValue));
member = new EnumTypeMember(lastContractField.Name, field, literalValue);
return true;
}

// Appends the members of <paramref name="currentValues"/> that were not present in the last
// contract (i.e. newly introduced by the current spec), preserving their original input order.
// Shared by the fixed-enum and API-version back-compatibility passes.
private protected static void AppendMembersNotInLastContract(
IReadOnlyList<EnumTypeMember> currentValues,
IReadOnlyList<FieldProvider> lastContractFields,
List<EnumTypeMember> allMembers)
{
var lastContractNames = new HashSet<string>(lastContractFields.Select(f => f.Name), StringComparer.OrdinalIgnoreCase);
foreach (var current in currentValues)
{
if (!lastContractNames.Contains(current.Name))
{
allMembers.Add(current);
}
}
}

private static bool SharedMemberOrderChanged(
IReadOnlyList<EnumTypeMember> currentValues,
IReadOnlyList<EnumTypeMember> result)
{
var currentNames = new HashSet<string>(currentValues.Count, StringComparer.Ordinal);
foreach (var member in currentValues)
{
if (!string.Equals(left[i].Name, right[i].Name, StringComparison.Ordinal))
currentNames.Add(member.Name);
}

var index = 0;
foreach (var member in result)
{
if (!currentNames.Contains(member.Name))
{
continue;
}

if (index >= currentValues.Count
|| !string.Equals(member.Name, currentValues[index].Name, StringComparison.Ordinal))
{
return false;
return true;
}

index++;
}
return true;

return false;
}

protected internal override FieldProvider[] BuildFields()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.TypeSpec.Generator.Primitives;
using Microsoft.TypeSpec.Generator.Snippets;
using Microsoft.TypeSpec.Generator.Statements;
using Microsoft.TypeSpec.Generator.Utilities;
using static Microsoft.TypeSpec.Generator.Snippets.Snippet;

namespace Microsoft.TypeSpec.Generator.Providers
Expand Down Expand Up @@ -105,7 +106,7 @@ protected internal sealed override IReadOnlyList<MethodProvider> BuildMethodsFor
// change between the previous and current contract is a parameter rename. This
// avoids source-breaking changes for callers using named arguments (e.g. when a
// property is renamed via @@clientName, spec rename, or naming-rule change).
PreservePreviousParameterNames(factoryMethods);
BackCompatHelper.RestorePreviousParameterNames(this, factoryMethods);

HashSet<MethodSignature> currentMethodSignatures = new List<MethodProvider>([.. factoryMethods, .. CustomCodeView?.Methods ?? []])
.Select(m => m.Signature)
Expand All @@ -120,14 +121,8 @@ protected internal sealed override IReadOnlyList<MethodProvider> BuildMethodsFor

// If the removal of this factory method has already been accepted in the ApiCompat
// baseline, honor that decision and do not resurrect a compatibility shim for it.
if (CodeModelGenerator.Instance.SourceInputModel?.ApiCompatBaseline.IsMemberSuppressed(
Type.FullyQualifiedName,
previousMethod.Signature.Name,
previousMethod.Signature.Parameters.Count) == true)
if (BackCompatHelper.IsMethodRemovalAcceptedInBaseline(this, previousMethod.Signature))
{
CodeModelGenerator.Instance.Emitter.Info(
$"Skipping back-compat shim for '{Type.FullyQualifiedName}.{previousMethod.Signature.Name}'; removal is accepted in the ApiCompat baseline.",
BackCompatibilityChangeCategory.BaselineAcceptedRemovalSkipped);
continue;
}

Expand Down Expand Up @@ -212,85 +207,6 @@ protected internal sealed override IReadOnlyList<MethodProvider> BuildMethodsFor
return [.. factoryMethods];
}

// Preserve original parameter names from the previous contract when a current factory
// method matches a previous one by name + parameter types/order but differs only in
// parameter names. The rename is applied in-place via ParameterProvider.Update which
// also updates the cached variable/argument expressions used by the method body and
// XML docs, so the body and docs serialize with the preserved names automatically.
private void PreservePreviousParameterNames(List<MethodProvider> currentFactoryMethods)
{
if (LastContractView?.Methods == null)
{
return;
}

foreach (var previousMethod in LastContractView.Methods)
{
MethodProvider? matchingCurrent = null;
foreach (var current in currentFactoryMethods)
{
// MethodSignatureComparer matches on method name + parameter count + parameter
// types (positional); it does not consider parameter names. So a previous
// method whose only difference from a current method is parameter names will
// still match here.
if (MethodSignature.MethodSignatureComparer.Equals(current.Signature, previousMethod.Signature))
{
matchingCurrent = current;
break;
}
}

if (matchingCurrent is null)
{
continue;
}

var previousParameters = previousMethod.Signature.Parameters;
var currentParameters = matchingCurrent.Signature.Parameters;
if (previousParameters.Count != currentParameters.Count)
{
continue;
}

for (int i = 0; i < previousParameters.Count; i++)
{
var previousName = previousParameters[i].Name;
var currentParam = currentParameters[i];
if (string.IsNullOrEmpty(previousName) || currentParam.Name == previousName)
{
continue;
}

// Skip the rename when applying it would create a name collision with another
// current parameter (e.g. two same-typed parameters in swapped order between
// the previous and current contracts). A positional rename in that case would
// silently swap which parameter feeds which constructor field via name-based
// lookup in GetCtorArgs, producing semantically wrong (and source-breaking) code.
var previousNameToApply = previousName;
bool wouldCollide = false;
for (int j = 0; j < currentParameters.Count; j++)
{
if (j != i && string.Equals(currentParameters[j].Name, previousNameToApply, StringComparison.Ordinal))
{
wouldCollide = true;
break;
}
}

if (wouldCollide)
{
continue;
}

CodeModelGenerator.Instance.Emitter.Debug(
$"Preserved parameter name '{previousName}' on '{Name}.{matchingCurrent.Signature.Name}' from last contract (instead of '{currentParam.Name}').",
BackCompatibilityChangeCategory.ParameterNamePreserved);

currentParam.Update(name: previousName);
}
}
}

private bool TryBuildCompatibleMethodForPreviousContract(
MethodProvider previousMethod,
MethodSignature? currentMethodSignature,
Expand Down
Loading
Loading