Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
93faf2b
Add regression tests for last-contract model bases
live1206 Sep 2, 2026
4d0a14a
Focus base regression on shipped inherited properties
live1206 Sep 2, 2026
5f63071
Preserve model base types from last contract
live1206 Sep 2, 2026
d87c7af
Use try pattern for base type resolution
live1206 Sep 2, 2026
9738574
Handle unrestorable last-contract model bases
live1206 Sep 2, 2026
29ff3b8
Validate referenced base constructor accessibility
live1206 Sep 2, 2026
2cc9dcb
Add model base back-compat hook
live1206 Sep 3, 2026
fb39c2c
Guard incompatible model base restoration
live1206 Sep 3, 2026
fa01391
Normalize model property collision names
live1206 Sep 3, 2026
2769b76
Guard symbol-backed base property collisions
live1206 Sep 3, 2026
18993c4
Share final model property naming for base guards
live1206 Sep 3, 2026
30a06fc
Resolve nested last-contract base types
live1206 Sep 3, 2026
924e18c
Continue past unusable base type candidates
live1206 Sep 3, 2026
690a2bf
Use metadata identity for base type matching
live1206 Sep 3, 2026
d6e041c
Fail model compilation regressions on warnings
live1206 Sep 3, 2026
454e549
Handle generic bases and private property collisions
live1206 Sep 3, 2026
1198942
Keep compatibility diagnostics private
live1206 Sep 4, 2026
c0d3c88
Limit base compatibility hook to derived providers
live1206 Sep 4, 2026
c5dcf27
Merge origin/main into test/11816-last-contract-base-type
live1206 Sep 9, 2026
8c08b6e
Guard restored bases against additional property collisions
live1206 Sep 9, 2026
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 @@ -46,6 +46,7 @@ public class ScmModelProvider : ModelProvider

internal const string ScmEvaluationTypeDiagnosticId = "SCME0001";
internal const string FileBinaryContentDiagnosticId = "SCME0004";
private const string IncompatibleBackcompatBaseTypeDiagnostic = "incompatible-backcompat-base-type";

internal const string ScmEvaluationTypeSuppressionJustification =
"Type is for evaluation purposes only and is subject to change or removal in future updates.";
Expand Down Expand Up @@ -73,6 +74,22 @@ public ScmModelProvider(InputModelType inputModel) : base(inputModel)
BaseJsonPatchProperty = new(GetBaseJsonPatchProperty());
}

protected override CSharpType? BuildBaseTypeForBackCompatibility(CSharpType? currentBase)
{
var previousBase = LastContractView?.BaseType;
if (_inputModel.DiscriminatorValue is not null &&
previousBase is not null &&
!IsInBaseTypeHierarchy(currentBase, previousBase))
{
CodeModelGenerator.Instance.Emitter.ReportDiagnostic(
IncompatibleBackcompatBaseTypeDiagnostic,
$"Could not preserve base type '{previousBase.FullyQualifiedName}' on model '{BuildNamespace()}.{BuildName()}' because the model participates in the current discriminator hierarchy.");
return currentBase;
}

return base.BuildBaseTypeForBackCompatibility(currentBase);
}

protected override FieldProvider[] BuildFields()
{
if (JsonPatchField is null)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.TypeSpec.Generator.Expressions;
using Microsoft.TypeSpec.Generator.Input;
using Microsoft.TypeSpec.Generator.Primitives;
Expand Down Expand Up @@ -262,6 +264,70 @@ await MockHelpers.LoadMockGeneratorAsync(
Assert.AreEqual(Helpers.GetExpectedFromFile("Serialization"), serializationContent);
}

[Test]
public async Task BackCompat_DifferentLastContractBaseIsNotRestoredAcrossDiscriminatorHierarchy()
{
var previousBase = InputFactory.Model("previousBase", properties: []);
var derivedModel = InputFactory.Model(
"derivedModel",
discriminatedKind: "derived",
usage: InputModelTypeUsage.Json,
properties: []);
var currentBase = InputFactory.Model(
"currentBase",
usage: InputModelTypeUsage.Json,
properties:
[
InputFactory.Property("kind", InputPrimitiveType.String, isRequired: true, isDiscriminator: true)
],
discriminatedModels: new Dictionary<string, InputModelType> { ["derived"] = derivedModel });

await MockHelpers.LoadMockGeneratorAsync(
lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(),
inputModels: () => [previousBase, currentBase, derivedModel]);

var models = ScmCodeModelGenerator.Instance.OutputLibrary.TypeProviders
.OfType<ScmModel>()
.ToArray();
foreach (var model in models)
{
model.ProcessTypeForBackCompatibility();
}

var derivedProvider = models.Single(t => t.Name == "DerivedModel");
Assert.Multiple(() =>
{
Assert.AreEqual("PreviousBase", derivedProvider.LastContractView?.BaseType?.Name,
"The regression requires a different last-contract base");
Assert.AreEqual("CurrentBase", derivedProvider.BaseType?.Name,
"The current discriminator hierarchy must remain assignable");
Assert.That(models, Has.Some.Matches<ScmModel>(m => m.IsUnknownDiscriminatorModel),
"The current discriminator hierarchy should include its unknown subtype");
});

string[] supportingProviderNames = ["ModelSerializationExtensions", "ChangeTrackingDictionary", "SampleContext", "TypeFormatters", "SerializationFormat"];
var generatedProviders = ScmCodeModelGenerator.Instance.OutputLibrary.TypeProviders
.Where(provider => provider is ScmModel || supportingProviderNames.Contains(provider.Name))
.Concat(models.SelectMany(model => model.SerializationProviders))
.Distinct()
.ToArray();
var sourceFiles = generatedProviders
.Select(provider => (
Name: $"{provider.Name}.cs",
Content: new TypeProviderWriter(provider).Write().Content))
.Append((
Name: "SampleContext.Default.cs",
Content: "namespace Sample { public partial class SampleContext { public static SampleContext Default => null; } }"))
.Append((
Name: "SampleTypeSpecContext.Default.cs",
Content: "namespace SampleTypeSpec { public partial class SampleTypeSpecContext : System.ClientModel.Primitives.ModelReaderWriterContext { public static SampleTypeSpecContext Default => null; } }"));
var compilation = await Helpers.GetCompilationFromSourceFilesAsync(sourceFiles);
Assert.That(
compilation.GetDiagnostics().Where(d => d.Severity is DiagnosticSeverity.Warning or DiagnosticSeverity.Error),
Is.Empty,
"The discriminator deserializer should compile after incompatible base restoration is skipped");
}

[Test]
public async Task BackCompat_AccessibleParameterlessSerializationConstructorIsPreserved()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Sample.Models
{
public partial class PreviousBase
{
}

public partial class DerivedModel : PreviousBase
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ public enum BackCompatibilityChangeCategory
/// <summary>A property type was preserved from the last contract.</summary>
PropertyTypePreserved,

/// <summary>A model base type was preserved from the last contract.</summary>
ModelBaseTypePreserved,

/// <summary>A constructor modifier (e.g. <c>private protected</c> -&gt; <c>public</c>) was preserved from the last contract.</summary>
ConstructorModifierPreserved,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ public void WriteBufferedMessages()
BackCompatibilityChangeCategory.ParameterNamePreserved => "Parameter Name Preserved",
BackCompatibilityChangeCategory.AdditionalPropertiesShapePreserved => "AdditionalProperties Shape Preserved",
BackCompatibilityChangeCategory.PropertyTypePreserved => "Property Type Preserved",
BackCompatibilityChangeCategory.ModelBaseTypePreserved => "Model Base Type Preserved",
Comment thread
live1206 marked this conversation as resolved.
BackCompatibilityChangeCategory.ConstructorModifierPreserved => "Constructor Modifier Preserved",
BackCompatibilityChangeCategory.EnumMemberReordering => "Enum Member Reordering",
BackCompatibilityChangeCategory.ApiVersionEnumMemberAdded => "Api Version Enum Member Added From Last Contract",
Expand Down
Loading
Loading