Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.TypeSpec.Generator.Expressions;
Expand Down Expand Up @@ -89,6 +90,44 @@ protected override IReadOnlyList<EnumTypeMember> BuildEnumValues()
return values;
}

protected internal override IReadOnlyList<EnumTypeMember>? BuildEnumValuesForBackCompatibility(IReadOnlyList<EnumTypeMember> currentValues)
{
var lastContractFields = LastContractView?.Fields;
if (lastContractFields == null || lastContractFields.Count == 0)
{
return null;
}

var currentLookup = currentValues.ToDictionary(v => v.Name, StringComparer.OrdinalIgnoreCase);
var allMembers = new List<EnumTypeMember>(currentValues.Count);

foreach (var field in lastContractFields)
{
if (currentLookup.TryGetValue(field.Name, out var existingMember))
{
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));
}
}

// 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))
{
allMembers.Add(current);
}
}

return allMembers;
}

protected internal override FieldProvider[] BuildFields()
=> EnumValues.Select(v => v.Field).ToArray();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -580,17 +580,33 @@ internal void ProcessTypeForBackCompatibility()
var hasMethods = LastContractView?.Methods != null && LastContractView.Methods.Count > 0;
var hasConstructors = LastContractView?.Constructors != null && LastContractView.Constructors.Count > 0;

if (!hasMethods && !hasConstructors)
IEnumerable<FieldProvider>? newFields = null;
if (this is EnumProvider)
{
return;
var hasFields = LastContractView?.Fields != null && LastContractView.Fields.Count > 0;
if (hasFields)
{
var newEnumValues = BuildEnumValuesForBackCompatibility(EnumValues);
if (newEnumValues != null)
{
_enumValues = newEnumValues;
newFields = newEnumValues.Select(v => v.Field);
}
}
}

var newMethods = hasMethods ? BuildMethodsForBackCompatibility(Methods) : null;
var newConstructors = hasConstructors ? BuildConstructorsForBackCompatibility(Constructors) : null;

Update(methods: newMethods, constructors: newConstructors);
if (newFields != null || newMethods != null || newConstructors != null)
{
Update(fields: newFields, methods: newMethods, constructors: newConstructors);
}
}

protected internal virtual IReadOnlyList<EnumTypeMember>? BuildEnumValuesForBackCompatibility(IReadOnlyList<EnumTypeMember> originalEnumValues)
=> null;

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

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

using System.Linq;
using System.Threading.Tasks;
using Microsoft.TypeSpec.Generator.Expressions;
using Microsoft.TypeSpec.Generator.Input;
using Microsoft.TypeSpec.Generator.Utilities;
Expand Down Expand Up @@ -364,6 +365,139 @@ public void InternalModelsAreNotIncludedInAdditionalRootTypes()
Assert.IsFalse(rootTypes.Contains("Sample.Models.StringEnum"));
}

// Validates that int enum member order is preserved from the last contract when values are reordered
[Test]
public async Task BackCompat_IntEnumOrderPreserved()
{
await MockHelpers.LoadMockGeneratorAsync(
createCSharpTypeCore: (inputType) => typeof(int),
lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

// Current input has values in DIFFERENT order than last contract (Default first, Recover second)
var input = InputFactory.Int32Enum("mockInputEnum", [
("Default", 0),
("Recover", 1),
]);

var enumType = EnumProvider.Create(input);
Assert.IsFalse(enumType is ApiVersionEnumProvider);

// Simulate the back-compat processing that CSharpGen performs after visitors
enumType.EnsureBuilt();
enumType.ProcessTypeForBackCompatibility();

var fields = enumType.Fields;
Assert.AreEqual(2, fields.Count);

// Order should be preserved from last contract: Recover first, Default second
Assert.AreEqual("Recover", fields[0].Name);
Assert.AreEqual("Default", fields[1].Name);

// No explicit initialization values - compiler auto-assigns based on order
Assert.IsNull(fields[0].InitializationValue);
Assert.IsNull(fields[1].InitializationValue);
}

// Validates that int enum member order is preserved and new values are appended
[Test]
public async Task BackCompat_IntEnumNewValueAppended()
{
await MockHelpers.LoadMockGeneratorAsync(
createCSharpTypeCore: (inputType) => typeof(int),
lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

// Current input has different order AND a new value
var input = InputFactory.Int32Enum("mockInputEnum", [
("Default", 0),
("Recover", 1),
("Third", 2),
]);

var enumType = EnumProvider.Create(input);
Assert.IsFalse(enumType is ApiVersionEnumProvider);

// Simulate the back-compat processing that CSharpGen performs after visitors
enumType.EnsureBuilt();
enumType.ProcessTypeForBackCompatibility();

var fields = enumType.Fields;
Assert.AreEqual(3, fields.Count);

// Order should be preserved from last contract: Recover first, Default second, new value Third appended
Assert.AreEqual("Recover", fields[0].Name);
Assert.AreEqual("Default", fields[1].Name);
Assert.AreEqual("Third", fields[2].Name);

// No explicit initialization values for reordered members - compiler auto-assigns based on order
Assert.IsNull(fields[0].InitializationValue);
Assert.IsNull(fields[1].InitializationValue);
// New value keeps its initialization value from the input
var value3 = fields[2].InitializationValue as LiteralExpression;
Assert.IsNotNull(value3);
Assert.AreEqual(2, value3?.Literal);
}

// Validates that removed enum values from last contract are not included
[Test]
public async Task BackCompat_IntEnumValueRemoved()
{
await MockHelpers.LoadMockGeneratorAsync(
createCSharpTypeCore: (inputType) => typeof(int),
lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

// Current input has values in different order and removed "Third"
var input = InputFactory.Int32Enum("mockInputEnum", [
("Default", 0),
("Recover", 1),
]);

var enumType = EnumProvider.Create(input);
Assert.IsFalse(enumType is ApiVersionEnumProvider);

// Simulate the back-compat processing that CSharpGen performs after visitors
enumType.EnsureBuilt();
enumType.ProcessTypeForBackCompatibility();

var fields = enumType.Fields;
Assert.AreEqual(2, fields.Count);

// Order should be preserved from last contract for members that still exist
Assert.AreEqual("Recover", fields[0].Name);
Assert.AreEqual("Default", fields[1].Name);

// No explicit initialization values - compiler auto-assigns based on order
Assert.IsNull(fields[0].InitializationValue);
Assert.IsNull(fields[1].InitializationValue);
}

// Validates that string enum order is also preserved from last contract
[Test]
public async Task BackCompat_StringEnumOrderPreserved()
{
await MockHelpers.LoadMockGeneratorAsync(
createCSharpTypeCore: (inputType) => typeof(string),
lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

// Current input has values in DIFFERENT order than last contract
var input = InputFactory.StringEnum("mockInputEnum", [
("Default", "default"),
("Recover", "recover"),
]);

var enumType = EnumProvider.Create(input);

// Simulate the back-compat processing that CSharpGen performs after visitors
enumType.EnsureBuilt();
enumType.ProcessTypeForBackCompatibility();

var fields = enumType.Fields;
Assert.AreEqual(2, fields.Count);

// Order should be preserved from last contract: Recover first, Default second
Assert.AreEqual("Recover", fields[0].Name);
Assert.AreEqual("Default", fields[1].Name);
}

private static void ValidateGetHashCodeMethod(EnumProvider enumType)
{
var getHashCodeMethod = enumType.Methods.Single(m => m.Signature.Name == "GetHashCode");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#nullable disable

namespace Sample.Models
{
public enum MockInputEnum
{
Recover = 0,
Default = 1,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#nullable disable

namespace Sample.Models
{
public enum MockInputEnum
{
Recover = 0,
Default = 1,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#nullable disable

namespace Sample.Models
{
public enum MockInputEnum
{
Recover = 0,
Default = 1,
Third = 2,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#nullable disable

namespace Sample.Models
{
public enum MockInputEnum
{
Recover,
Default,
}
}
Loading