Skip to content
Closed
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 @@ -7,6 +7,7 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.TypeSpec.Generator.Expressions;
using Microsoft.TypeSpec.Generator.Primitives;
Expand Down Expand Up @@ -40,7 +41,45 @@ protected override TypeSignatureModifiers BuildDeclarationModifiers()
protected override IReadOnlyList<MethodBodyStatement> BuildAttributesForWrite()
{
var visitorAttributes = base.BuildAttributesForWrite().Where(static attribute => !IsBuildableAttribute(attribute));
return [.. BuildAttributes(), .. visitorAttributes];
return MergeLastContractAttributesForWrite([.. BuildAttributes(), .. visitorAttributes]);
}

protected override bool ShouldPreserveLastContractAttributeForWrite(AttributeStatement attribute)
=> attribute.Type.Equals(typeof(ModelReaderWriterBuildableAttribute))
&& attribute.Arguments is [TypeOfExpression typeOf]
&& IsTypeAvailableForWrite(typeOf.Type);

protected override MethodBodyStatement BuildLastContractAttributeForWrite(AttributeStatement attribute)
{
if (attribute.Arguments is not [TypeOfExpression typeOf])
{
return attribute;
}

var type = typeOf.Type;
var suppressions = new List<DiagnosticSuppression>();
foreach (var componentType in EnumerateTypeComponents(type).Distinct())
{
var experimentalTypeJustification = $"{componentType} is experimental and may change in future versions.";
var obsoleteTypeJustification = $"{componentType} is obsolete and may be removed in future versions.";

if (componentType.IsFrameworkType)
{
suppressions.AddRange(GetDiagnosticSuppressions(
componentType.FrameworkType,
experimentalTypeJustification,
obsoleteTypeJustification));
}
else if (FindCurrentTypeProvider(componentType) is { } typeProvider)
{
suppressions.AddRange(GetDiagnosticSuppressions(
typeProvider,
experimentalTypeJustification,
obsoleteTypeJustification));
}
}

return ApplyDiagnosticSuppressions(attribute, suppressions);
}

protected override IReadOnlyList<MethodBodyStatement> BuildAttributes()
Expand Down Expand Up @@ -100,16 +139,17 @@ protected override IReadOnlyList<MethodBodyStatement> BuildAttributes()

private static bool IsBuildableAttribute(MethodBodyStatement statement)
{
var attribute = statement switch
return GetAttributeStatement(statement)?.Type.Equals(typeof(ModelReaderWriterBuildableAttribute)) == true;
}

private static AttributeStatement? GetAttributeStatement(MethodBodyStatement statement) =>
statement switch
{
AttributeStatement directAttribute => directAttribute,
SuppressionStatement suppression => suppression.AsStatement<AttributeStatement>(),
AttributeStatement attribute => attribute,
SuppressionStatement { Inner: { } inner } => GetAttributeStatement(inner),
_ => null
};

return attribute?.Type.Equals(typeof(ModelReaderWriterBuildableAttribute)) == true;
}

private HashSet<string> GetCustomizedBuildableTypes()
{
var customizedTypes = new HashSet<string>(StringComparer.Ordinal);
Expand Down Expand Up @@ -526,23 +566,30 @@ private static void AddAttributeForType(
TypeProvider typeProvider,
string experimentalTypeJustification,
string obsoleteTypeJustification)
{
AttributeStatement? experimentalOrObsoleteAttribute = typeProvider.CanonicalView.Attributes
.FirstOrDefault(a => a.Type.Equals(typeof(ExperimentalAttribute)) || a.Type.Equals(typeof(ObsoleteAttribute)));

var key = typeProvider.Type.FullyQualifiedName;
=> attributes.Add(
typeProvider.Type.FullyQualifiedName,
ApplyDiagnosticSuppressions(
attributeStatement,
GetDiagnosticSuppressions(
typeProvider,
experimentalTypeJustification,
obsoleteTypeJustification)));

if (experimentalOrObsoleteAttribute?.Type.Equals(typeof(ExperimentalAttribute)) == true)
{
attributes.Add(key, new SuppressionStatement(attributeStatement, experimentalOrObsoleteAttribute.Arguments[0], experimentalTypeJustification));
}
else if (experimentalOrObsoleteAttribute?.Type.Equals(typeof(ObsoleteAttribute)) == true)
{
attributes.Add(key, new SuppressionStatement(attributeStatement, Literal(DefaultObsoleteDiagnosticId), obsoleteTypeJustification));
}
else
private static IEnumerable<DiagnosticSuppression> GetDiagnosticSuppressions(
TypeProvider typeProvider,
string experimentalTypeJustification,
string obsoleteTypeJustification)
{
foreach (var attribute in typeProvider.CanonicalView.Attributes)
{
attributes.Add(key, attributeStatement);
if (attribute.Type.Equals(typeof(ExperimentalAttribute)) && attribute.Arguments.Count > 0)
{
yield return new DiagnosticSuppression(attribute.Arguments[0], experimentalTypeJustification);
}
else if (attribute.Type.Equals(typeof(ObsoleteAttribute)))
{
yield return new DiagnosticSuppression(Literal(DefaultObsoleteDiagnosticId), obsoleteTypeJustification);
}
}
}

Expand All @@ -552,16 +599,26 @@ private static void AddAttributeForType(
Type frameworkType,
string experimentalTypeJustification,
string obsoleteTypeJustification)
{
var key = frameworkType.FullName ?? frameworkType.Name;
=> attributes.Add(
frameworkType.FullName ?? frameworkType.Name,
ApplyDiagnosticSuppressions(
attributeStatement,
GetDiagnosticSuppressions(
frameworkType,
experimentalTypeJustification,
obsoleteTypeJustification)));

private static IEnumerable<DiagnosticSuppression> GetDiagnosticSuppressions(
Type frameworkType,
string experimentalTypeJustification,
string obsoleteTypeJustification)
{
var experimentalAttr = frameworkType.GetCustomAttributes(typeof(ExperimentalAttribute), false)
.FirstOrDefault();
if (experimentalAttr != null)
{
var diagnosticId = experimentalAttr.GetType().GetProperty("DiagnosticId")?.GetValue(experimentalAttr);
attributes.Add(key, new SuppressionStatement(attributeStatement, Literal(diagnosticId), experimentalTypeJustification));
return;
yield return new DiagnosticSuppression(Literal(diagnosticId), experimentalTypeJustification);
}

var obsoleteAttr = frameworkType.GetCustomAttributes(typeof(ObsoleteAttribute), false)
Expand All @@ -570,13 +627,128 @@ private static void AddAttributeForType(
{
var diagnosticId = obsoleteAttr.GetType().GetProperty("DiagnosticId")?.GetValue(obsoleteAttr)
?? DefaultObsoleteDiagnosticId;
attributes.Add(key, new SuppressionStatement(attributeStatement, Literal(diagnosticId), obsoleteTypeJustification));
return;
yield return new DiagnosticSuppression(Literal(diagnosticId), obsoleteTypeJustification);
}
}

attributes.Add(key, attributeStatement);
private static MethodBodyStatement ApplyDiagnosticSuppressions(
MethodBodyStatement statement,
IEnumerable<DiagnosticSuppression> suppressions)
{
var appliedDiagnosticIds = new HashSet<ValueExpression>();
foreach (var suppression in suppressions)
{
if (appliedDiagnosticIds.Add(suppression.DiagnosticId))
{
statement = new SuppressionStatement(statement, suppression.DiagnosticId, suppression.Justification);
}
}

return statement;
}

private readonly record struct DiagnosticSuppression(ValueExpression DiagnosticId, string Justification);

private static bool IsTypeAvailableForWrite(CSharpType type)
{
foreach (var componentType in EnumerateTypeComponents(type).Distinct())
{
if (componentType.IsFrameworkType)
{
if (componentType.FrameworkType.GetCustomAttribute<ObsoleteAttribute>()?.IsError == true)
{
return false;
}
continue;
}

var typeProvider = FindCurrentTypeProvider(componentType);
if (typeProvider is null ||
HasErrorObsoleteAttribute(typeProvider) ||
!IsResolvableBuildableType(componentType))
{
return false;
}
}

return true;
}

private static bool HasErrorObsoleteAttribute(TypeProvider typeProvider) =>
typeProvider.CanonicalView.Attributes.Any(attribute =>
attribute.Type.Equals(typeof(ObsoleteAttribute)) &&
attribute.Arguments.Count > 1 &&
attribute.Arguments[1] is LiteralExpression { Literal: true });

private static IEnumerable<CSharpType> EnumerateTypeComponents(CSharpType type)
{
if (type.DeclaringType is not null)
{
foreach (var declaringType in EnumerateTypeComponents(type.DeclaringType))
{
yield return declaringType;
}
}

foreach (var argument in type.Arguments)
{
foreach (var argumentType in EnumerateTypeComponents(argument))
{
yield return argumentType;
}
}

yield return type;
}

private static TypeProvider? FindCurrentTypeProvider(CSharpType type) =>
FindCurrentTypeProvider(CodeModelGenerator.Instance.OutputLibrary.TypeProviders, type)
?? CodeModelGenerator.Instance.SourceInputModel.FindForTypeInCustomization(
type.Namespace,
GetTypeMetadataName(type),
GetDeclaringTypeMetadataName(type.DeclaringType),
includeReferencedAssemblies: true);

private static TypeProvider? FindCurrentTypeProvider(IEnumerable<TypeProvider> providers, CSharpType type)
{
foreach (var provider in providers)
{
if (provider.Type.Equals(type))
{
return provider;
}

var nestedProvider = FindCurrentTypeProvider(provider.NestedTypes, type);
if (nestedProvider is not null)
{
return nestedProvider;
}

var serializationProvider = FindCurrentTypeProvider(provider.SerializationProviders, type);
if (serializationProvider is not null)
{
return serializationProvider;
}
}

return null;
}

private static string? GetDeclaringTypeMetadataName(CSharpType? declaringType)
{
if (declaringType is null)
{
return null;
}

var parentName = GetDeclaringTypeMetadataName(declaringType.DeclaringType);
var metadataName = GetTypeMetadataName(declaringType);
return parentName is null ? metadataName : $"{parentName}+{metadataName}";
}

private static string GetTypeMetadataName(CSharpType type) =>
type.Arguments.Count == 0 ? type.Name : $"{type.Name}`{type.Arguments.Count}";

private static bool IsModelReaderWriterInterfaceType(CSharpType type)
{
return type.Name.StartsWith("IPersistableModel") || type.Name.StartsWith("IJsonModel");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.TypeSpec.Generator.ClientModel.Providers;
using Microsoft.TypeSpec.Generator.Input;
using Microsoft.TypeSpec.Generator.Primitives;
Expand Down Expand Up @@ -71,6 +73,79 @@ public void ValidateModelReaderWriterBuildableAttributesAreGenerated()
Assert.AreEqual(1, buildableAttributes.Count(), "Exactly one ModelReaderWriterBuildableAttribute should be generated for TestModel");
}

[Test]
public async Task PreservesPreviousBuildableAttributesButNotUnrelatedAttributes()
{
MockHelpers.LoadMockGenerator();
var compilation = await Helpers.GetCompilationFromDirectoryAsync(parameters: "Custom");
await MockHelpers.LoadMockGeneratorAsync(
inputModels: () =>
[
InputFactory.Model("CurrentModel", properties:
[
InputFactory.Property("Name", InputPrimitiveType.String)
])
],
compilation: () => Task.FromResult(compilation),
lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

var contextDefinition = new ModelReaderWriterContextDefinition();
var file = new TypeProviderWriter(contextDefinition).Write();

Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
StringAssert.DoesNotContain("RemovedModel", file.Content);
StringAssert.DoesNotContain("ErrorObsoleteModel", file.Content);
StringAssert.DoesNotContain("ErrorObsoleteOuter", file.Content);
StringAssert.DoesNotContain("ErrorObsoleteTypeArgument", file.Content);

AssertCompilesWithoutWarnings(compilation, file.Content);
}

[Test]
public async Task PreservesDiagnosticSuppressionForDeeplyNestedBuildableType()
{
MockHelpers.LoadMockGenerator();
var compilation = await Helpers.GetCompilationFromDirectoryAsync(parameters: "Custom");
await MockHelpers.LoadMockGeneratorAsync(
compilation: () => Task.FromResult(compilation),
lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

var contextDefinition = new ModelReaderWriterContextDefinition();
var file = new TypeProviderWriter(contextDefinition).Write();

Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
AssertCompilesWithoutWarnings(compilation, file.Content);
}

[Test]
public async Task PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType()
{
MockHelpers.LoadMockGenerator();
var compilation = await Helpers.GetCompilationFromDirectoryAsync(parameters: "Custom");
await MockHelpers.LoadMockGeneratorAsync(
compilation: () => Task.FromResult(compilation),
lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

var contextDefinition = new ModelReaderWriterContextDefinition();
var file = new TypeProviderWriter(contextDefinition).Write();

Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
AssertCompilesWithoutWarnings(compilation, file.Content);
}

private static void AssertCompilesWithoutWarnings(Compilation compilation, string generatedCode)
{
var generatedTree = CSharpSyntaxTree.ParseText(generatedCode);
var diagnostics = compilation
.AddSyntaxTrees(generatedTree)
.GetDiagnostics()
.Where(diagnostic =>
diagnostic.Severity >= DiagnosticSeverity.Warning &&
diagnostic.Location.SourceTree == generatedTree)
.ToList();
Assert.That(diagnostics, Is.Empty, string.Join(Environment.NewLine, diagnostics));
}

[TestCase(true)]
[TestCase(false)]
public void ValidateModelReaderWriterBuildableAttributesAreGeneratedForNonModelsThatImplementMRW(bool implementsIPersistable)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using System.Diagnostics.CodeAnalysis;

namespace Sample.Models
{
[Experimental("ARG001")]
[Obsolete("Use another type argument instead.")]
public class TypeArgument
{
}

[Experimental("ARG001")]
public class Outer<T>
{
public class Middle
{
public class DeepModel
{
}
}
}
}
Loading
Loading