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
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,14 @@
// 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.CodeAnalysis.CSharp;
using Microsoft.TypeSpec.Generator.Expressions;
using Microsoft.TypeSpec.Generator.Input;
using Microsoft.TypeSpec.Generator.Primitives;
Expand Down Expand Up @@ -262,6 +265,74 @@ 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 syntaxTrees = generatedProviders.Select(provider =>
CSharpSyntaxTree.ParseText(
new TypeProviderWriter(provider).Write().Content,
path: $"{provider.Name}.cs"))
.Append(CSharpSyntaxTree.ParseText(
"namespace Sample { public partial class SampleContext { public static SampleContext Default => null; } }",
path: "SampleContext.Default.cs"));
var references = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location))
.Select(a => MetadataReference.CreateFromFile(a.Location));
var compilation = CSharpCompilation.Create(
"DiscriminatorModels",
syntaxTrees,
references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
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",
BackCompatibilityChangeCategory.ConstructorModifierPreserved => "Constructor Modifier Preserved",
BackCompatibilityChangeCategory.EnumMemberReordering => "Enum Member Reordering",
BackCompatibilityChangeCategory.ApiVersionEnumMemberAdded => "Api Version Enum Member Added From Last Contract",
Expand Down
Loading
Loading