Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,27 @@ private void PreservePreviousParameterNames(List<MethodProvider> currentFactoryM
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++)
Comment thread
JoshLove-msft marked this conversation as resolved.
{
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,60 @@ public async Task BackCompatibility_NewPropertyAddedWithKeywordPropertyName()
result);
}

// Regression test for a model factory parameter SWAP bug: when the previous contract
// contained the same-typed parameters in a different order from the current contract,
// a naive positional rename would swap which parameter feeds which constructor field
// via name-based lookup in GetCtorArgs, producing semantically wrong (and
// source-breaking) generated code. Verify no rename occurs in this collision case.
[Test]
public async Task BackCompatibility_SwapTypeParamsDoesNotCorrupt()
{
var swapModelList = new[]
{
InputFactory.Model(
"SwapModel",
properties:
[
InputFactory.Property("EventId", InputPrimitiveType.String),
InputFactory.Property("ItemId", InputPrimitiveType.String),
]),
};

_instance = (await MockHelpers.LoadMockGeneratorAsync(
inputNamespaceName: "Sample.Namespace",
inputModelTypes: swapModelList,
lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync())).Object;

var modelFactory = _instance!.OutputLibrary.ModelFactory.Value;
modelFactory.ProcessTypeForBackCompatibility();

var swapMethods = modelFactory.Methods.Where(m => m.Signature.Name == "SwapModel").ToList();
Assert.AreEqual(1, swapMethods.Count);

var method = swapMethods[0];
var parameters = method.Signature.Parameters;
// The current method should keep its parameters in the order/names derived from the
// current spec: eventId, itemId. A positional rename to the previous contract names
// (which were swapped: itemId, eventId) would corrupt the body, so we explicitly
// skip the rename when it would create a name collision with another parameter.
Assert.AreEqual(2, parameters.Count);
Assert.AreEqual("eventId", parameters[0].Name);
Assert.AreEqual("itemId", parameters[1].Name);

// No EditorBrowsable hidden overload — there's a single visible method.
Assert.AreEqual(0, method.Signature.Attributes.Count);

// The body must reference the parameters in their original positions so that
// eventId continues to feed the eventId constructor field and itemId continues
// to feed the itemId constructor field.
var body = method.BodyStatements;
Assert.IsNotNull(body);
var result = body!.ToDisplayString();
Assert.AreEqual(
"return new global::Sample.Models.SwapModel(eventId, itemId, additionalBinaryDataProperties: null);\n",
result);
}

[Test]
public void RequiredConstantPropertiesAreNotExposedAsParameters()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using SampleTypeSpec;
using Sample.Models;

namespace Sample.Namespace
{
public static partial class SampleNamespaceModelFactory
{
// Previous contract listed the two same-typed parameters in the OPPOSITE order
// (itemId first, then eventId) compared to the current method (eventId first,
// then itemId). A naive positional rename in PreservePreviousParameterNames would
// swap which parameter feeds which constructor field via name-based lookup in
// GetCtorArgs, producing semantically wrong (and source-breaking) code. Verify
// that no rename is performed in this case.
public static SwapModel SwapModel(
string itemId = default,
string eventId = default)
{ }
}
}

namespace Sample.Models
{
public partial class SwapModel
{ }
}
Loading