From ffe7d63e8da4c46eda7680aea9ab5b1d9d83afe8 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 23 Jul 2026 06:04:18 +0000 Subject: [PATCH 1/7] fix(csharp): preserve generated attributes from previous GA Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e373c6e0-590e-41ec-9a11-eeb70cec72d4 --- .../ModelReaderWriterContextDefinition.cs | 5 +- ...ModelReaderWriterContextDefinitionTests.cs | 19 ++++++ ...ableAttributesButNotUnrelatedAttributes.cs | 15 +++++ .../SampleContext.cs | 23 ++++++++ .../src/Providers/TypeProvider.cs | 58 ++++++++++++++++++- 5 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes/SampleContext.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs index 7d5cc8976e6..980c3910f29 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs @@ -40,9 +40,12 @@ protected override TypeSignatureModifiers BuildDeclarationModifiers() protected override IReadOnlyList BuildAttributesForWrite() { var visitorAttributes = base.BuildAttributesForWrite().Where(static attribute => !IsBuildableAttribute(attribute)); - return [.. BuildAttributes(), .. visitorAttributes]; + return BuildAttributesForBackCompatibility([.. BuildAttributes(), .. visitorAttributes]); } + protected override bool ShouldPreserveAttributeForBackCompatibility(AttributeStatement attribute) + => attribute.Type.Equals(typeof(ModelReaderWriterBuildableAttribute)); + protected override IReadOnlyList BuildAttributes() { var attributes = new Dictionary(); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs index d55537cc0d0..ad35ef25250 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs @@ -71,6 +71,25 @@ public void ValidateModelReaderWriterBuildableAttributesAreGenerated() Assert.AreEqual(1, buildableAttributes.Count(), "Exactly one ModelReaderWriterBuildableAttribute should be generated for TestModel"); } + [Test] + public async Task PreservesPreviousBuildableAttributesButNotUnrelatedAttributes() + { + await MockHelpers.LoadMockGeneratorAsync( + inputModels: () => + [ + InputFactory.Model("CurrentModel", properties: + [ + InputFactory.Property("Name", InputPrimitiveType.String) + ]) + ], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var contextDefinition = new ModelReaderWriterContextDefinition(); + var file = new TypeProviderWriter(contextDefinition).Write(); + + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + [TestCase(true)] [TestCase(false)] public void ValidateModelReaderWriterBuildableAttributesAreGeneratedForNonModelsThatImplementMRW(bool implementsIPersistable) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes.cs new file mode 100644 index 00000000000..3a9fad3d3fa --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes.cs @@ -0,0 +1,15 @@ +// + +#nullable disable + +using System.ClientModel.Primitives; +using Sample.Models; + +namespace Sample +{ + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.CurrentModel))] + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.PreviousModel))] + public partial class SampleContext : global::System.ClientModel.Primitives.ModelReaderWriterContext + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes/SampleContext.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes/SampleContext.cs new file mode 100644 index 00000000000..76b8198f002 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes/SampleContext.cs @@ -0,0 +1,23 @@ +using System.ClientModel.Primitives; +using System.ComponentModel; + +namespace Sample.Models +{ + public class CurrentModel + { + } + + public class PreviousModel + { + } +} + +namespace Sample +{ + [ModelReaderWriterBuildable(typeof(Models.CurrentModel))] + [ModelReaderWriterBuildable(typeof(Models.PreviousModel))] + [EditorBrowsable(EditorBrowsableState.Never)] + public partial class SampleContext : ModelReaderWriterContext + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index 31bc0d3fd63..c10498ebc0b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -368,7 +368,63 @@ public IReadOnlyList Attributes /// Builds the attributes emitted by the writer. Providers whose generated attributes depend on final /// generation decisions can override this without replacing attributes updated by visitors. /// - protected internal virtual IReadOnlyList BuildAttributesForWrite() => GetAttributes(); + protected internal virtual IReadOnlyList BuildAttributesForWrite() + => BuildAttributesForBackCompatibility(GetAttributes()); + + /// + /// Restores generator-owned attributes from the last contract when + /// opts in to preserving them. + /// + protected internal virtual IReadOnlyList BuildAttributesForBackCompatibility( + IEnumerable originalAttributes) + { + var original = originalAttributes as IReadOnlyList ?? [.. originalAttributes]; + if (LastContractView?.Attributes is not { Count: > 0 } lastContractAttributes) + { + return original; + } + + var attributesToPreserve = lastContractAttributes + .Where(ShouldPreserveAttributeForBackCompatibility) + .ToList(); + if (attributesToPreserve.Count == 0) + { + return original; + } + + var seen = new HashSet( + original.Select(GetAttributeStatement) + .Where(static attribute => attribute is not null) + .Select(static attribute => attribute!.ToDisplayString()), + StringComparer.Ordinal); + + List? merged = null; + foreach (var attribute in attributesToPreserve) + { + if (!seen.Add(attribute.ToDisplayString())) + { + continue; + } + + merged ??= [.. original]; + merged.Add(attribute); + } + + return merged ?? original; + } + + /// + /// Determines whether a generator-owned attribute from the last contract must be preserved. + /// + protected internal virtual bool ShouldPreserveAttributeForBackCompatibility(AttributeStatement attribute) => false; + + private static AttributeStatement? GetAttributeStatement(MethodBodyStatement statement) => + statement switch + { + AttributeStatement attribute => attribute, + SuppressionStatement suppression => suppression.AsStatement(), + _ => null + }; /// /// Indicates whether this provider's attributes should contribute to reference-map analysis. From a87ae36df2b4825111518667cd02053f7144b9d3 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 23 Jul 2026 06:49:36 +0000 Subject: [PATCH 2/7] fix(csharp): preserve diagnostics on restored attributes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e373c6e0-590e-41ec-9a11-eeb70cec72d4 --- .../ModelReaderWriterContextDefinition.cs | 110 +++++++++++++++--- ...ModelReaderWriterContextDefinitionTests.cs | 15 +++ .../Models.cs | 19 +++ ...ableAttributesButNotUnrelatedAttributes.cs | 5 + .../SampleContext.cs | 5 + .../src/Providers/TypeProvider.cs | 8 +- 6 files changed, 145 insertions(+), 17 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes(Custom)/Models.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs index 980c3910f29..b79f29a41d6 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs @@ -46,6 +46,49 @@ protected override IReadOnlyList BuildAttributesForWrite() protected override bool ShouldPreserveAttributeForBackCompatibility(AttributeStatement attribute) => attribute.Type.Equals(typeof(ModelReaderWriterBuildableAttribute)); + protected override MethodBodyStatement BuildPreservedAttributeForBackCompatibility(AttributeStatement attribute) + { + if (attribute.Arguments is not [TypeOfExpression typeOf]) + { + return attribute; + } + + var type = typeOf.Type; + var experimentalTypeJustification = $"{type} is experimental and may change in future versions."; + var obsoleteTypeJustification = $"{type} is obsolete and may be removed in future versions."; + + if (type.IsFrameworkType) + { + return ApplyDiagnosticSuppression( + attribute, + type.FrameworkType, + experimentalTypeJustification, + obsoleteTypeJustification); + } + + var typeProvider = FindCurrentTypeProvider(type) + ?? CodeModelGenerator.Instance.SourceInputModel.FindForTypeInCustomization( + type.Namespace, + type.Name, + GetDeclaringTypeMetadataName(type.DeclaringType), + includeReferencedAssemblies: true) + ?? CodeModelGenerator.Instance.SourceInputModel.FindForTypeInLastContract( + type.Namespace, + type.Name, + GetDeclaringTypeMetadataName(type.DeclaringType)); + + if (typeProvider is null) + { + return attribute; + } + + return ApplyDiagnosticSuppression( + attribute, + typeProvider, + experimentalTypeJustification, + obsoleteTypeJustification); + } + protected override IReadOnlyList BuildAttributes() { var attributes = new Dictionary(); @@ -529,24 +572,34 @@ private static void AddAttributeForType( TypeProvider typeProvider, string experimentalTypeJustification, string obsoleteTypeJustification) + => attributes.Add( + typeProvider.Type.FullyQualifiedName, + ApplyDiagnosticSuppression( + attributeStatement, + typeProvider, + experimentalTypeJustification, + obsoleteTypeJustification)); + + private static MethodBodyStatement ApplyDiagnosticSuppression( + AttributeStatement attributeStatement, + 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; - 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)); + return new SuppressionStatement(attributeStatement, experimentalOrObsoleteAttribute.Arguments[0], experimentalTypeJustification); } - else + + if (experimentalOrObsoleteAttribute?.Type.Equals(typeof(ObsoleteAttribute)) == true) { - attributes.Add(key, attributeStatement); + return new SuppressionStatement(attributeStatement, Literal(DefaultObsoleteDiagnosticId), obsoleteTypeJustification); } + + return attributeStatement; } private static void AddAttributeForType( @@ -555,16 +608,26 @@ private static void AddAttributeForType( Type frameworkType, string experimentalTypeJustification, string obsoleteTypeJustification) - { - var key = frameworkType.FullName ?? frameworkType.Name; + => attributes.Add( + frameworkType.FullName ?? frameworkType.Name, + ApplyDiagnosticSuppression( + attributeStatement, + frameworkType, + experimentalTypeJustification, + obsoleteTypeJustification)); + private static MethodBodyStatement ApplyDiagnosticSuppression( + AttributeStatement attributeStatement, + 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; + return new SuppressionStatement(attributeStatement, Literal(diagnosticId), experimentalTypeJustification); } var obsoleteAttr = frameworkType.GetCustomAttributes(typeof(ObsoleteAttribute), false) @@ -573,11 +636,26 @@ private static void AddAttributeForType( { var diagnosticId = obsoleteAttr.GetType().GetProperty("DiagnosticId")?.GetValue(obsoleteAttr) ?? DefaultObsoleteDiagnosticId; - attributes.Add(key, new SuppressionStatement(attributeStatement, Literal(diagnosticId), obsoleteTypeJustification)); - return; + return new SuppressionStatement(attributeStatement, Literal(diagnosticId), obsoleteTypeJustification); + } + + return attributeStatement; + } + + private static TypeProvider? FindCurrentTypeProvider(CSharpType type) => + CodeModelGenerator.Instance.OutputLibrary.TypeProviders + .SelectMany(static provider => new[] { provider }.Concat(provider.NestedTypes)) + .FirstOrDefault(provider => provider.Type.Equals(type)); + + private static string? GetDeclaringTypeMetadataName(CSharpType? declaringType) + { + if (declaringType is null) + { + return null; } - attributes.Add(key, attributeStatement); + var parentName = GetDeclaringTypeMetadataName(declaringType.DeclaringType); + return parentName is null ? declaringType.Name : $"{parentName}+{declaringType.Name}"; } private static bool IsModelReaderWriterInterfaceType(CSharpType type) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs index ad35ef25250..fba05d3e397 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs @@ -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; @@ -74,6 +76,8 @@ public void ValidateModelReaderWriterBuildableAttributesAreGenerated() [Test] public async Task PreservesPreviousBuildableAttributesButNotUnrelatedAttributes() { + MockHelpers.LoadMockGenerator(); + var compilation = await Helpers.GetCompilationFromDirectoryAsync(parameters: "Custom"); await MockHelpers.LoadMockGeneratorAsync( inputModels: () => [ @@ -82,12 +86,23 @@ await MockHelpers.LoadMockGeneratorAsync( 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); + + var generatedTree = CSharpSyntaxTree.ParseText(file.Content); + 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)] diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes(Custom)/Models.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes(Custom)/Models.cs new file mode 100644 index 00000000000..05eb16f4bd4 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes(Custom)/Models.cs @@ -0,0 +1,19 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Sample.Models +{ + public class CurrentModel + { + } + + [Obsolete("Use CurrentModel instead.")] + public class PreviousModel + { + } + + [Experimental("TEST001")] + public class ExperimentalPreviousModel + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes.cs index 3a9fad3d3fa..daa92bcece9 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes.cs @@ -8,7 +8,12 @@ namespace Sample { [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.CurrentModel))] +#pragma warning disable CS0618 // global::Sample.Models.PreviousModel is obsolete and may be removed in future versions. [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.PreviousModel))] +#pragma warning restore CS0618 // global::Sample.Models.PreviousModel is obsolete and may be removed in future versions. +#pragma warning disable TEST001 // global::Sample.Models.ExperimentalPreviousModel is experimental and may change in future versions. + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.ExperimentalPreviousModel))] +#pragma warning restore TEST001 // global::Sample.Models.ExperimentalPreviousModel is experimental and may change in future versions. public partial class SampleContext : global::System.ClientModel.Primitives.ModelReaderWriterContext { } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes/SampleContext.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes/SampleContext.cs index 76b8198f002..aab389c2bea 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes/SampleContext.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes/SampleContext.cs @@ -10,12 +10,17 @@ public class CurrentModel public class PreviousModel { } + + public class ExperimentalPreviousModel + { + } } namespace Sample { [ModelReaderWriterBuildable(typeof(Models.CurrentModel))] [ModelReaderWriterBuildable(typeof(Models.PreviousModel))] + [ModelReaderWriterBuildable(typeof(Models.ExperimentalPreviousModel))] [EditorBrowsable(EditorBrowsableState.Never)] public partial class SampleContext : ModelReaderWriterContext { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index c10498ebc0b..56520cafc92 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -407,7 +407,7 @@ protected internal virtual IReadOnlyList BuildAttributesFor } merged ??= [.. original]; - merged.Add(attribute); + merged.Add(BuildPreservedAttributeForBackCompatibility(attribute)); } return merged ?? original; @@ -418,6 +418,12 @@ protected internal virtual IReadOnlyList BuildAttributesFor /// protected internal virtual bool ShouldPreserveAttributeForBackCompatibility(AttributeStatement attribute) => false; + /// + /// Builds the statement used to restore a generator-owned attribute from the last contract. + /// + protected internal virtual MethodBodyStatement BuildPreservedAttributeForBackCompatibility(AttributeStatement attribute) + => attribute; + private static AttributeStatement? GetAttributeStatement(MethodBodyStatement statement) => statement switch { From 89b919c5eeaf7ac902e50a1aa829ba21a39dd561 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 23 Jul 2026 07:39:26 +0000 Subject: [PATCH 3/7] fix(csharp): skip unavailable restored attribute targets Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e373c6e0-590e-41ec-9a11-eeb70cec72d4 --- .../ModelReaderWriterContextDefinition.cs | 59 +++++++++++++------ ...ModelReaderWriterContextDefinitionTests.cs | 1 + .../SampleContext.cs | 5 ++ .../src/Providers/TypeProvider.cs | 21 +++---- 4 files changed, 59 insertions(+), 27 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs index b79f29a41d6..c1eb791151a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs @@ -40,13 +40,15 @@ protected override TypeSignatureModifiers BuildDeclarationModifiers() protected override IReadOnlyList BuildAttributesForWrite() { var visitorAttributes = base.BuildAttributesForWrite().Where(static attribute => !IsBuildableAttribute(attribute)); - return BuildAttributesForBackCompatibility([.. BuildAttributes(), .. visitorAttributes]); + return MergeLastContractAttributesForWrite([.. BuildAttributes(), .. visitorAttributes]); } - protected override bool ShouldPreserveAttributeForBackCompatibility(AttributeStatement attribute) - => attribute.Type.Equals(typeof(ModelReaderWriterBuildableAttribute)); + protected override bool ShouldPreserveLastContractAttributeForWrite(AttributeStatement attribute) + => attribute.Type.Equals(typeof(ModelReaderWriterBuildableAttribute)) + && attribute.Arguments is [TypeOfExpression typeOf] + && IsTypeAvailableForWrite(typeOf.Type); - protected override MethodBodyStatement BuildPreservedAttributeForBackCompatibility(AttributeStatement attribute) + protected override MethodBodyStatement BuildLastContractAttributeForWrite(AttributeStatement attribute) { if (attribute.Arguments is not [TypeOfExpression typeOf]) { @@ -66,16 +68,7 @@ protected override MethodBodyStatement BuildPreservedAttributeForBackCompatibili obsoleteTypeJustification); } - var typeProvider = FindCurrentTypeProvider(type) - ?? CodeModelGenerator.Instance.SourceInputModel.FindForTypeInCustomization( - type.Namespace, - type.Name, - GetDeclaringTypeMetadataName(type.DeclaringType), - includeReferencedAssemblies: true) - ?? CodeModelGenerator.Instance.SourceInputModel.FindForTypeInLastContract( - type.Namespace, - type.Name, - GetDeclaringTypeMetadataName(type.DeclaringType)); + var typeProvider = FindCurrentTypeProvider(type); if (typeProvider is null) { @@ -642,10 +635,42 @@ private static MethodBodyStatement ApplyDiagnosticSuppression( return attributeStatement; } + private static bool IsTypeAvailableForWrite(CSharpType type) => + type.IsFrameworkType || + (FindCurrentTypeProvider(type) is not null && IsResolvableBuildableType(type)); + private static TypeProvider? FindCurrentTypeProvider(CSharpType type) => - CodeModelGenerator.Instance.OutputLibrary.TypeProviders - .SelectMany(static provider => new[] { provider }.Concat(provider.NestedTypes)) - .FirstOrDefault(provider => provider.Type.Equals(type)); + FindCurrentTypeProvider(CodeModelGenerator.Instance.OutputLibrary.TypeProviders, type) + ?? CodeModelGenerator.Instance.SourceInputModel.FindForTypeInCustomization( + type.Namespace, + type.Name, + GetDeclaringTypeMetadataName(type.DeclaringType), + includeReferencedAssemblies: true); + + private static TypeProvider? FindCurrentTypeProvider(IEnumerable 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) { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs index fba05d3e397..705ec3a64c0 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs @@ -93,6 +93,7 @@ await MockHelpers.LoadMockGeneratorAsync( var file = new TypeProviderWriter(contextDefinition).Write(); Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + StringAssert.DoesNotContain("RemovedModel", file.Content); var generatedTree = CSharpSyntaxTree.ParseText(file.Content); var diagnostics = compilation diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes/SampleContext.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes/SampleContext.cs index aab389c2bea..fd2fff08ae7 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes/SampleContext.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes/SampleContext.cs @@ -14,6 +14,10 @@ public class PreviousModel public class ExperimentalPreviousModel { } + + public class RemovedModel + { + } } namespace Sample @@ -21,6 +25,7 @@ namespace Sample [ModelReaderWriterBuildable(typeof(Models.CurrentModel))] [ModelReaderWriterBuildable(typeof(Models.PreviousModel))] [ModelReaderWriterBuildable(typeof(Models.ExperimentalPreviousModel))] + [ModelReaderWriterBuildable(typeof(Models.RemovedModel))] [EditorBrowsable(EditorBrowsableState.Never)] public partial class SampleContext : ModelReaderWriterContext { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index 56520cafc92..0fab86482d0 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -369,13 +369,14 @@ public IReadOnlyList Attributes /// generation decisions can override this without replacing attributes updated by visitors. /// protected internal virtual IReadOnlyList BuildAttributesForWrite() - => BuildAttributesForBackCompatibility(GetAttributes()); + => MergeLastContractAttributesForWrite(GetAttributes()); /// - /// Restores generator-owned attributes from the last contract when - /// opts in to preserving them. + /// Adds selected attributes from the last contract to the attributes emitted by the writer. + /// This write-time merge does not update or participate in visitor + /// and reference-map processing. /// - protected internal virtual IReadOnlyList BuildAttributesForBackCompatibility( + protected internal virtual IReadOnlyList MergeLastContractAttributesForWrite( IEnumerable originalAttributes) { var original = originalAttributes as IReadOnlyList ?? [.. originalAttributes]; @@ -385,7 +386,7 @@ protected internal virtual IReadOnlyList BuildAttributesFor } var attributesToPreserve = lastContractAttributes - .Where(ShouldPreserveAttributeForBackCompatibility) + .Where(ShouldPreserveLastContractAttributeForWrite) .ToList(); if (attributesToPreserve.Count == 0) { @@ -407,21 +408,21 @@ protected internal virtual IReadOnlyList BuildAttributesFor } merged ??= [.. original]; - merged.Add(BuildPreservedAttributeForBackCompatibility(attribute)); + merged.Add(BuildLastContractAttributeForWrite(attribute)); } return merged ?? original; } /// - /// Determines whether a generator-owned attribute from the last contract must be preserved. + /// Determines whether an attribute from the last contract should be emitted by the writer. /// - protected internal virtual bool ShouldPreserveAttributeForBackCompatibility(AttributeStatement attribute) => false; + protected internal virtual bool ShouldPreserveLastContractAttributeForWrite(AttributeStatement attribute) => false; /// - /// Builds the statement used to restore a generator-owned attribute from the last contract. + /// Builds the write-time statement for a selected attribute from the last contract. /// - protected internal virtual MethodBodyStatement BuildPreservedAttributeForBackCompatibility(AttributeStatement attribute) + protected internal virtual MethodBodyStatement BuildLastContractAttributeForWrite(AttributeStatement attribute) => attribute; private static AttributeStatement? GetAttributeStatement(MethodBodyStatement statement) => From 34bd5f4c926be4e29a9377c18fad7e85d510ff8f Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 23 Jul 2026 08:37:40 +0000 Subject: [PATCH 4/7] fix(csharp): handle deeply nested preserved attribute types Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e373c6e0-590e-41ec-9a11-eeb70cec72d4 --- ...ModelReaderWriterContextDefinitionTests.cs | 23 ++++++++++++++++++- .../Models.cs | 15 ++++++++++++ ...SuppressionForDeeplyNestedBuildableType.cs | 16 +++++++++++++ .../SampleContext.cs | 22 ++++++++++++++++++ .../src/Primitives/CSharpType.cs | 2 +- .../src/Utilities/TypeSymbolExtensions.cs | 2 +- .../src/Writers/CodeWriter.cs | 12 +++++++++- 7 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForDeeplyNestedBuildableType(Custom)/Models.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForDeeplyNestedBuildableType.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForDeeplyNestedBuildableType/SampleContext.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs index 705ec3a64c0..ddd27f13c93 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs @@ -95,7 +95,28 @@ await MockHelpers.LoadMockGeneratorAsync( Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); StringAssert.DoesNotContain("RemovedModel", file.Content); - var generatedTree = CSharpSyntaxTree.ParseText(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); + } + + private static void AssertCompilesWithoutWarnings(Compilation compilation, string generatedCode) + { + var generatedTree = CSharpSyntaxTree.ParseText(generatedCode); var diagnostics = compilation .AddSyntaxTrees(generatedTree) .GetDiagnostics() diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForDeeplyNestedBuildableType(Custom)/Models.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForDeeplyNestedBuildableType(Custom)/Models.cs new file mode 100644 index 00000000000..08e097a0838 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForDeeplyNestedBuildableType(Custom)/Models.cs @@ -0,0 +1,15 @@ +using System; + +namespace Sample.Models +{ + public class Outer + { + public class Middle + { + [Obsolete("Use another model instead.")] + public class DeepModel + { + } + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForDeeplyNestedBuildableType.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForDeeplyNestedBuildableType.cs new file mode 100644 index 00000000000..ab871ac209d --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForDeeplyNestedBuildableType.cs @@ -0,0 +1,16 @@ +// + +#nullable disable + +using System.ClientModel.Primitives; +using Sample.Models; + +namespace Sample +{ +#pragma warning disable CS0618 // global::Sample.Models.Outer.Middle.DeepModel is obsolete and may be removed in future versions. + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.Outer.Middle.DeepModel))] +#pragma warning restore CS0618 // global::Sample.Models.Outer.Middle.DeepModel is obsolete and may be removed in future versions. + public partial class SampleContext : global::System.ClientModel.Primitives.ModelReaderWriterContext + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForDeeplyNestedBuildableType/SampleContext.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForDeeplyNestedBuildableType/SampleContext.cs new file mode 100644 index 00000000000..76341bb9fe6 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForDeeplyNestedBuildableType/SampleContext.cs @@ -0,0 +1,22 @@ +using System.ClientModel.Primitives; + +namespace Sample.Models +{ + public class Outer + { + public class Middle + { + public class DeepModel + { + } + } + } +} + +namespace Sample +{ + [ModelReaderWriterBuildable(typeof(Models.Outer.Middle.DeepModel))] + public partial class SampleContext : ModelReaderWriterContext + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/CSharpType.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/CSharpType.cs index f2bc01a85e0..16a0b0931dc 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/CSharpType.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/CSharpType.cs @@ -179,7 +179,7 @@ internal CSharpType( /// public string FullyQualifiedName => DeclaringType is null ? $"{Namespace}.{Name}" - : $"{Namespace}.{DeclaringType.Name}.{Name}"; + : $"{DeclaringType.FullyQualifiedName}.{Name}"; public CSharpType? DeclaringType { get; private init; } public bool IsValueType { get; private init; } public bool IsEnum => _underlyingType is not null; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/TypeSymbolExtensions.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/TypeSymbolExtensions.cs index 65924a3c308..fa245942365 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/TypeSymbolExtensions.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/TypeSymbolExtensions.cs @@ -224,7 +224,7 @@ private static CSharpType ConstructCSharpTypeFromSymbol( if (typeSymbol.ContainingType != null && typeSymbol.TypeKind != TypeKind.TypeParameter) { containingType = GetCSharpType(typeSymbol.ContainingType, visited); - ns = string.Join('.', pieces.Take(pieces.Length - 2)); + ns = typeSymbol.ContainingNamespace.GetFullyQualifiedNameFromDisplayString(); } CSharpType? baseType = null; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/CodeWriter.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/CodeWriter.cs index ac5f214e785..6f8fbdc36b9 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/CodeWriter.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/CodeWriter.cs @@ -679,7 +679,7 @@ private void AppendType(CSharpType type, bool isDeclaration, bool writeTypeNameO AppendRaw("."); if (type.DeclaringType is not null) { - AppendRaw($"{type.DeclaringType.Name}."); + AppendDeclaringTypeName(type.DeclaringType); } AppendRaw(type.Name); @@ -706,6 +706,16 @@ private void AppendType(CSharpType type, bool isDeclaration, bool writeTypeNameO } } + private void AppendDeclaringTypeName(CSharpType declaringType) + { + if (declaringType.DeclaringType is not null) + { + AppendDeclaringTypeName(declaringType.DeclaringType); + } + + AppendRaw($"{declaringType.Name}."); + } + public CodeWriter WriteLine(FormattableString formattableString) { Append(formattableString); From 2c1515d3b77cc006e64547cf9e6dd5d53cb47ed1 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 23 Jul 2026 09:03:35 +0000 Subject: [PATCH 5/7] fix(csharp): handle generic nested and error obsolete targets Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e373c6e0-590e-41ec-9a11-eeb70cec72d4 --- .../ModelReaderWriterContextDefinition.cs | 27 ++++++++++++++++--- ...ModelReaderWriterContextDefinitionTests.cs | 17 ++++++++++++ .../Models.cs | 15 +++++++++++ ...orConstructedGenericNestedBuildableType.cs | 16 +++++++++++ .../SampleContext.cs | 22 +++++++++++++++ .../Models.cs | 5 ++++ .../SampleContext.cs | 5 ++++ .../src/Writers/CodeWriter.cs | 27 ++++++++++++------- 8 files changed, 120 insertions(+), 14 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType(Custom)/Models.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType/SampleContext.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs index c1eb791151a..7025c81723a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs @@ -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; @@ -635,9 +636,24 @@ private static MethodBodyStatement ApplyDiagnosticSuppression( return attributeStatement; } - private static bool IsTypeAvailableForWrite(CSharpType type) => - type.IsFrameworkType || - (FindCurrentTypeProvider(type) is not null && IsResolvableBuildableType(type)); + private static bool IsTypeAvailableForWrite(CSharpType type) + { + if (type.IsFrameworkType) + { + return type.FrameworkType.GetCustomAttribute()?.IsError != true; + } + + var typeProvider = FindCurrentTypeProvider(type); + return typeProvider is not null && + !HasErrorObsoleteAttribute(typeProvider) && + IsResolvableBuildableType(type); + } + + 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 TypeProvider? FindCurrentTypeProvider(CSharpType type) => FindCurrentTypeProvider(CodeModelGenerator.Instance.OutputLibrary.TypeProviders, type) @@ -680,7 +696,10 @@ private static bool IsTypeAvailableForWrite(CSharpType type) => } var parentName = GetDeclaringTypeMetadataName(declaringType.DeclaringType); - return parentName is null ? declaringType.Name : $"{parentName}+{declaringType.Name}"; + var metadataName = declaringType.Arguments.Count == 0 + ? declaringType.Name + : $"{declaringType.Name}`{declaringType.Arguments.Count}"; + return parentName is null ? metadataName : $"{parentName}+{metadataName}"; } private static bool IsModelReaderWriterInterfaceType(CSharpType type) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs index ddd27f13c93..0773d3de52d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs @@ -94,6 +94,7 @@ await MockHelpers.LoadMockGeneratorAsync( Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); StringAssert.DoesNotContain("RemovedModel", file.Content); + StringAssert.DoesNotContain("ErrorObsoleteModel", file.Content); AssertCompilesWithoutWarnings(compilation, file.Content); } @@ -114,6 +115,22 @@ await MockHelpers.LoadMockGeneratorAsync( 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); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType(Custom)/Models.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType(Custom)/Models.cs new file mode 100644 index 00000000000..341fa121e04 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType(Custom)/Models.cs @@ -0,0 +1,15 @@ +using System; + +namespace Sample.Models +{ + public class Outer + { + public class Middle + { + [Obsolete("Use another model instead.")] + public class DeepModel + { + } + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType.cs new file mode 100644 index 00000000000..7fb06a46cc7 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType.cs @@ -0,0 +1,16 @@ +// + +#nullable disable + +using System.ClientModel.Primitives; +using Sample.Models; + +namespace Sample +{ +#pragma warning disable CS0618 // global::Sample.Models.Outer.Middle.DeepModel is obsolete and may be removed in future versions. + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.Outer.Middle.DeepModel))] +#pragma warning restore CS0618 // global::Sample.Models.Outer.Middle.DeepModel is obsolete and may be removed in future versions. + public partial class SampleContext : global::System.ClientModel.Primitives.ModelReaderWriterContext + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType/SampleContext.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType/SampleContext.cs new file mode 100644 index 00000000000..7cb68fa3721 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType/SampleContext.cs @@ -0,0 +1,22 @@ +using System.ClientModel.Primitives; + +namespace Sample.Models +{ + public class Outer + { + public class Middle + { + public class DeepModel + { + } + } + } +} + +namespace Sample +{ + [ModelReaderWriterBuildable(typeof(Models.Outer.Middle.DeepModel))] + public partial class SampleContext : ModelReaderWriterContext + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes(Custom)/Models.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes(Custom)/Models.cs index 05eb16f4bd4..14daca4a0ff 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes(Custom)/Models.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes(Custom)/Models.cs @@ -16,4 +16,9 @@ public class PreviousModel public class ExperimentalPreviousModel { } + + [Obsolete("This model cannot be used.", true)] + public class ErrorObsoleteModel + { + } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes/SampleContext.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes/SampleContext.cs index fd2fff08ae7..3d5afedc632 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes/SampleContext.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes/SampleContext.cs @@ -18,6 +18,10 @@ public class ExperimentalPreviousModel public class RemovedModel { } + + public class ErrorObsoleteModel + { + } } namespace Sample @@ -26,6 +30,7 @@ namespace Sample [ModelReaderWriterBuildable(typeof(Models.PreviousModel))] [ModelReaderWriterBuildable(typeof(Models.ExperimentalPreviousModel))] [ModelReaderWriterBuildable(typeof(Models.RemovedModel))] + [ModelReaderWriterBuildable(typeof(Models.ErrorObsoleteModel))] [EditorBrowsable(EditorBrowsableState.Never)] public partial class SampleContext : ModelReaderWriterContext { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/CodeWriter.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/CodeWriter.cs index 6f8fbdc36b9..5aa3e55165d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/CodeWriter.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Writers/CodeWriter.cs @@ -679,12 +679,23 @@ private void AppendType(CSharpType type, bool isDeclaration, bool writeTypeNameO AppendRaw("."); if (type.DeclaringType is not null) { - AppendDeclaringTypeName(type.DeclaringType); + AppendDeclaringTypeName(type.DeclaringType, writeTypeNameOnly, genericDepth); } AppendRaw(type.Name); } + AppendTypeArguments(type, writeTypeNameOnly, genericDepth); + + // Add '?' for nullable value types, but skip if we're writing new instance UNLESS we're inside generic type arguments + if ((!_writingNewInstance || genericDepth > 0) && !isDeclaration && type is { IsNullable: true, IsValueType: true }) + { + AppendRaw("?"); + } + } + + private void AppendTypeArguments(CSharpType type, bool writeTypeNameOnly, int genericDepth) + { if (type.Arguments.Any()) { AppendRaw(_writingXmlDocumentation ? "{" : "<"); @@ -698,22 +709,18 @@ private void AppendType(CSharpType type, bool isDeclaration, bool writeTypeNameO } AppendRaw(_writingXmlDocumentation ? "}" : ">"); } - - // Add '?' for nullable value types, but skip if we're writing new instance UNLESS we're inside generic type arguments - if ((!_writingNewInstance || genericDepth > 0) && !isDeclaration && type is { IsNullable: true, IsValueType: true }) - { - AppendRaw("?"); - } } - private void AppendDeclaringTypeName(CSharpType declaringType) + private void AppendDeclaringTypeName(CSharpType declaringType, bool writeTypeNameOnly, int genericDepth) { if (declaringType.DeclaringType is not null) { - AppendDeclaringTypeName(declaringType.DeclaringType); + AppendDeclaringTypeName(declaringType.DeclaringType, writeTypeNameOnly, genericDepth); } - AppendRaw($"{declaringType.Name}."); + AppendRaw(declaringType.Name); + AppendTypeArguments(declaringType, writeTypeNameOnly, genericDepth); + AppendRaw("."); } public CodeWriter WriteLine(FormattableString formattableString) From ef1eaba3dd2e887f251d043b082944e6e9111c30 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 23 Jul 2026 11:14:21 +0000 Subject: [PATCH 6/7] fix(csharp): validate restored attribute type components Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e373c6e0-590e-41ec-9a11-eeb70cec72d4 --- .../ModelReaderWriterContextDefinition.cs | 112 ++++++++++++------ ...ModelReaderWriterContextDefinitionTests.cs | 2 + .../Models.cs | 8 +- ...orConstructedGenericNestedBuildableType.cs | 8 +- .../SampleContext.cs | 6 +- .../Models.cs | 17 +++ .../SampleContext.cs | 17 +++ .../src/Providers/TypeProvider.cs | 2 +- 8 files changed, 128 insertions(+), 44 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs index 7025c81723a..440334680ad 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs @@ -57,30 +57,31 @@ protected override MethodBodyStatement BuildLastContractAttributeForWrite(Attrib } var type = typeOf.Type; - var experimentalTypeJustification = $"{type} is experimental and may change in future versions."; - var obsoleteTypeJustification = $"{type} is obsolete and may be removed in future versions."; - - if (type.IsFrameworkType) + MethodBodyStatement statement = attribute; + foreach (var componentType in EnumerateTypeComponents(type).Distinct()) { - return ApplyDiagnosticSuppression( - attribute, - type.FrameworkType, - experimentalTypeJustification, - obsoleteTypeJustification); - } - - var typeProvider = FindCurrentTypeProvider(type); + var experimentalTypeJustification = $"{componentType} is experimental and may change in future versions."; + var obsoleteTypeJustification = $"{componentType} is obsolete and may be removed in future versions."; - if (typeProvider is null) - { - return attribute; + if (componentType.IsFrameworkType) + { + statement = ApplyDiagnosticSuppression( + statement, + componentType.FrameworkType, + experimentalTypeJustification, + obsoleteTypeJustification); + } + else if (FindCurrentTypeProvider(componentType) is { } typeProvider) + { + statement = ApplyDiagnosticSuppression( + statement, + typeProvider, + experimentalTypeJustification, + obsoleteTypeJustification); + } } - return ApplyDiagnosticSuppression( - attribute, - typeProvider, - experimentalTypeJustification, - obsoleteTypeJustification); + return statement; } protected override IReadOnlyList BuildAttributes() @@ -140,16 +141,17 @@ protected override IReadOnlyList 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 attribute => attribute, + SuppressionStatement { Inner: { } inner } => GetAttributeStatement(inner), _ => null }; - return attribute?.Type.Equals(typeof(ModelReaderWriterBuildableAttribute)) == true; - } - private HashSet GetCustomizedBuildableTypes() { var customizedTypes = new HashSet(StringComparer.Ordinal); @@ -575,7 +577,7 @@ private static void AddAttributeForType( obsoleteTypeJustification)); private static MethodBodyStatement ApplyDiagnosticSuppression( - AttributeStatement attributeStatement, + MethodBodyStatement attributeStatement, TypeProvider typeProvider, string experimentalTypeJustification, string obsoleteTypeJustification) @@ -611,7 +613,7 @@ private static void AddAttributeForType( obsoleteTypeJustification)); private static MethodBodyStatement ApplyDiagnosticSuppression( - AttributeStatement attributeStatement, + MethodBodyStatement attributeStatement, Type frameworkType, string experimentalTypeJustification, string obsoleteTypeJustification) @@ -638,15 +640,27 @@ private static MethodBodyStatement ApplyDiagnosticSuppression( private static bool IsTypeAvailableForWrite(CSharpType type) { - if (type.IsFrameworkType) + foreach (var componentType in EnumerateTypeComponents(type).Distinct()) { - return type.FrameworkType.GetCustomAttribute()?.IsError != true; + if (componentType.IsFrameworkType) + { + if (componentType.FrameworkType.GetCustomAttribute()?.IsError == true) + { + return false; + } + continue; + } + + var typeProvider = FindCurrentTypeProvider(componentType); + if (typeProvider is null || + HasErrorObsoleteAttribute(typeProvider) || + !IsResolvableBuildableType(componentType)) + { + return false; + } } - var typeProvider = FindCurrentTypeProvider(type); - return typeProvider is not null && - !HasErrorObsoleteAttribute(typeProvider) && - IsResolvableBuildableType(type); + return true; } private static bool HasErrorObsoleteAttribute(TypeProvider typeProvider) => @@ -655,11 +669,32 @@ private static bool HasErrorObsoleteAttribute(TypeProvider typeProvider) => attribute.Arguments.Count > 1 && attribute.Arguments[1] is LiteralExpression { Literal: true }); + private static IEnumerable 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, - type.Name, + GetTypeMetadataName(type), GetDeclaringTypeMetadataName(type.DeclaringType), includeReferencedAssemblies: true); @@ -696,12 +731,13 @@ private static bool HasErrorObsoleteAttribute(TypeProvider typeProvider) => } var parentName = GetDeclaringTypeMetadataName(declaringType.DeclaringType); - var metadataName = declaringType.Arguments.Count == 0 - ? declaringType.Name - : $"{declaringType.Name}`{declaringType.Arguments.Count}"; + 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"); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs index 0773d3de52d..d8dbed66997 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/ModelReaderWriterContextDefinitionTests.cs @@ -95,6 +95,8 @@ await MockHelpers.LoadMockGeneratorAsync( 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); } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType(Custom)/Models.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType(Custom)/Models.cs index 341fa121e04..cb8caf053f0 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType(Custom)/Models.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType(Custom)/Models.cs @@ -1,12 +1,18 @@ using System; +using System.Diagnostics.CodeAnalysis; namespace Sample.Models { + [Experimental("ARG001")] + public class TypeArgument + { + } + + [Obsolete("Use another outer type instead.")] public class Outer { public class Middle { - [Obsolete("Use another model instead.")] public class DeepModel { } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType.cs index 7fb06a46cc7..8fcc77bbb5d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType.cs @@ -7,9 +7,11 @@ namespace Sample { -#pragma warning disable CS0618 // global::Sample.Models.Outer.Middle.DeepModel is obsolete and may be removed in future versions. - [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.Outer.Middle.DeepModel))] -#pragma warning restore CS0618 // global::Sample.Models.Outer.Middle.DeepModel is obsolete and may be removed in future versions. +#pragma warning disable CS0618 // global::Sample.Models.Outer is obsolete and may be removed in future versions. +#pragma warning disable ARG001 // global::Sample.Models.TypeArgument is experimental and may change in future versions. + [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.Outer.Middle.DeepModel))] +#pragma warning restore ARG001 // global::Sample.Models.TypeArgument is experimental and may change in future versions. +#pragma warning restore CS0618 // global::Sample.Models.Outer is obsolete and may be removed in future versions. public partial class SampleContext : global::System.ClientModel.Primitives.ModelReaderWriterContext { } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType/SampleContext.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType/SampleContext.cs index 7cb68fa3721..97c1ebb9e81 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType/SampleContext.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType/SampleContext.cs @@ -2,6 +2,10 @@ namespace Sample.Models { + public class TypeArgument + { + } + public class Outer { public class Middle @@ -15,7 +19,7 @@ public class DeepModel namespace Sample { - [ModelReaderWriterBuildable(typeof(Models.Outer.Middle.DeepModel))] + [ModelReaderWriterBuildable(typeof(Models.Outer.Middle.DeepModel))] public partial class SampleContext : ModelReaderWriterContext { } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes(Custom)/Models.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes(Custom)/Models.cs index 14daca4a0ff..26442a7628b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes(Custom)/Models.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes(Custom)/Models.cs @@ -21,4 +21,21 @@ public class ExperimentalPreviousModel public class ErrorObsoleteModel { } + + [Obsolete("This enclosing type cannot be used.", true)] + public class ErrorObsoleteOuter + { + public class NestedModel + { + } + } + + public class GenericModel + { + } + + [Obsolete("This type argument cannot be used.", true)] + public class ErrorObsoleteTypeArgument + { + } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes/SampleContext.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes/SampleContext.cs index 3d5afedc632..4d8c86360e3 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes/SampleContext.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesPreviousBuildableAttributesButNotUnrelatedAttributes/SampleContext.cs @@ -22,6 +22,21 @@ public class RemovedModel public class ErrorObsoleteModel { } + + public class ErrorObsoleteOuter + { + public class NestedModel + { + } + } + + public class GenericModel + { + } + + public class ErrorObsoleteTypeArgument + { + } } namespace Sample @@ -31,6 +46,8 @@ namespace Sample [ModelReaderWriterBuildable(typeof(Models.ExperimentalPreviousModel))] [ModelReaderWriterBuildable(typeof(Models.RemovedModel))] [ModelReaderWriterBuildable(typeof(Models.ErrorObsoleteModel))] + [ModelReaderWriterBuildable(typeof(Models.ErrorObsoleteOuter.NestedModel))] + [ModelReaderWriterBuildable(typeof(Models.GenericModel))] [EditorBrowsable(EditorBrowsableState.Never)] public partial class SampleContext : ModelReaderWriterContext { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index 0fab86482d0..e33f2d5525b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -429,7 +429,7 @@ protected internal virtual MethodBodyStatement BuildLastContractAttributeForWrit statement switch { AttributeStatement attribute => attribute, - SuppressionStatement suppression => suppression.AsStatement(), + SuppressionStatement { Inner: { } inner } => GetAttributeStatement(inner), _ => null }; From 28da4751b924a87d66ed75e0e3f062ea552450f9 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Thu, 23 Jul 2026 11:55:32 +0000 Subject: [PATCH 7/7] fix(csharp): apply all restored attribute suppressions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e373c6e0-590e-41ec-9a11-eeb70cec72d4 --- .../ModelReaderWriterContextDefinition.cs | 81 +++++++++++-------- .../Models.cs | 3 +- ...orConstructedGenericNestedBuildableType.cs | 4 +- 3 files changed, 50 insertions(+), 38 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs index 440334680ad..065e53f4df6 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelReaderWriterContextDefinition.cs @@ -57,7 +57,7 @@ protected override MethodBodyStatement BuildLastContractAttributeForWrite(Attrib } var type = typeOf.Type; - MethodBodyStatement statement = attribute; + var suppressions = new List(); foreach (var componentType in EnumerateTypeComponents(type).Distinct()) { var experimentalTypeJustification = $"{componentType} is experimental and may change in future versions."; @@ -65,23 +65,21 @@ protected override MethodBodyStatement BuildLastContractAttributeForWrite(Attrib if (componentType.IsFrameworkType) { - statement = ApplyDiagnosticSuppression( - statement, + suppressions.AddRange(GetDiagnosticSuppressions( componentType.FrameworkType, experimentalTypeJustification, - obsoleteTypeJustification); + obsoleteTypeJustification)); } else if (FindCurrentTypeProvider(componentType) is { } typeProvider) { - statement = ApplyDiagnosticSuppression( - statement, + suppressions.AddRange(GetDiagnosticSuppressions( typeProvider, experimentalTypeJustification, - obsoleteTypeJustification); + obsoleteTypeJustification)); } } - return statement; + return ApplyDiagnosticSuppressions(attribute, suppressions); } protected override IReadOnlyList BuildAttributes() @@ -570,32 +568,29 @@ private static void AddAttributeForType( string obsoleteTypeJustification) => attributes.Add( typeProvider.Type.FullyQualifiedName, - ApplyDiagnosticSuppression( + ApplyDiagnosticSuppressions( attributeStatement, - typeProvider, - experimentalTypeJustification, - obsoleteTypeJustification)); + GetDiagnosticSuppressions( + typeProvider, + experimentalTypeJustification, + obsoleteTypeJustification))); - private static MethodBodyStatement ApplyDiagnosticSuppression( - MethodBodyStatement attributeStatement, + private static IEnumerable GetDiagnosticSuppressions( TypeProvider typeProvider, string experimentalTypeJustification, string obsoleteTypeJustification) { - AttributeStatement? experimentalOrObsoleteAttribute = typeProvider.CanonicalView.Attributes - .FirstOrDefault(a => a.Type.Equals(typeof(ExperimentalAttribute)) || a.Type.Equals(typeof(ObsoleteAttribute))); - - if (experimentalOrObsoleteAttribute?.Type.Equals(typeof(ExperimentalAttribute)) == true) - { - return new SuppressionStatement(attributeStatement, experimentalOrObsoleteAttribute.Arguments[0], experimentalTypeJustification); - } - - if (experimentalOrObsoleteAttribute?.Type.Equals(typeof(ObsoleteAttribute)) == true) + foreach (var attribute in typeProvider.CanonicalView.Attributes) { - return new SuppressionStatement(attributeStatement, Literal(DefaultObsoleteDiagnosticId), obsoleteTypeJustification); + 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); + } } - - return attributeStatement; } private static void AddAttributeForType( @@ -606,14 +601,14 @@ private static void AddAttributeForType( string obsoleteTypeJustification) => attributes.Add( frameworkType.FullName ?? frameworkType.Name, - ApplyDiagnosticSuppression( + ApplyDiagnosticSuppressions( attributeStatement, - frameworkType, - experimentalTypeJustification, - obsoleteTypeJustification)); + GetDiagnosticSuppressions( + frameworkType, + experimentalTypeJustification, + obsoleteTypeJustification))); - private static MethodBodyStatement ApplyDiagnosticSuppression( - MethodBodyStatement attributeStatement, + private static IEnumerable GetDiagnosticSuppressions( Type frameworkType, string experimentalTypeJustification, string obsoleteTypeJustification) @@ -623,7 +618,7 @@ private static MethodBodyStatement ApplyDiagnosticSuppression( if (experimentalAttr != null) { var diagnosticId = experimentalAttr.GetType().GetProperty("DiagnosticId")?.GetValue(experimentalAttr); - return new SuppressionStatement(attributeStatement, Literal(diagnosticId), experimentalTypeJustification); + yield return new DiagnosticSuppression(Literal(diagnosticId), experimentalTypeJustification); } var obsoleteAttr = frameworkType.GetCustomAttributes(typeof(ObsoleteAttribute), false) @@ -632,12 +627,28 @@ private static MethodBodyStatement ApplyDiagnosticSuppression( { var diagnosticId = obsoleteAttr.GetType().GetProperty("DiagnosticId")?.GetValue(obsoleteAttr) ?? DefaultObsoleteDiagnosticId; - return new SuppressionStatement(attributeStatement, Literal(diagnosticId), obsoleteTypeJustification); + yield return new DiagnosticSuppression(Literal(diagnosticId), obsoleteTypeJustification); + } + } + + private static MethodBodyStatement ApplyDiagnosticSuppressions( + MethodBodyStatement statement, + IEnumerable suppressions) + { + var appliedDiagnosticIds = new HashSet(); + foreach (var suppression in suppressions) + { + if (appliedDiagnosticIds.Add(suppression.DiagnosticId)) + { + statement = new SuppressionStatement(statement, suppression.DiagnosticId, suppression.Justification); + } } - return attributeStatement; + return statement; } + private readonly record struct DiagnosticSuppression(ValueExpression DiagnosticId, string Justification); + private static bool IsTypeAvailableForWrite(CSharpType type) { foreach (var componentType in EnumerateTypeComponents(type).Distinct()) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType(Custom)/Models.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType(Custom)/Models.cs index cb8caf053f0..14fb393acb1 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType(Custom)/Models.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType(Custom)/Models.cs @@ -4,11 +4,12 @@ namespace Sample.Models { [Experimental("ARG001")] + [Obsolete("Use another type argument instead.")] public class TypeArgument { } - [Obsolete("Use another outer type instead.")] + [Experimental("ARG001")] public class Outer { public class Middle diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType.cs index 8fcc77bbb5d..25b12b7ef6c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/Definitions/TestData/ModelReaderWriterContextDefinitionTests/PreservesDiagnosticSuppressionForConstructedGenericNestedBuildableType.cs @@ -7,11 +7,11 @@ namespace Sample { -#pragma warning disable CS0618 // global::Sample.Models.Outer is obsolete and may be removed in future versions. +#pragma warning disable CS0618 // global::Sample.Models.TypeArgument is obsolete and may be removed in future versions. #pragma warning disable ARG001 // global::Sample.Models.TypeArgument is experimental and may change in future versions. [global::System.ClientModel.Primitives.ModelReaderWriterBuildableAttribute(typeof(global::Sample.Models.Outer.Middle.DeepModel))] #pragma warning restore ARG001 // global::Sample.Models.TypeArgument is experimental and may change in future versions. -#pragma warning restore CS0618 // global::Sample.Models.Outer is obsolete and may be removed in future versions. +#pragma warning restore CS0618 // global::Sample.Models.TypeArgument is obsolete and may be removed in future versions. public partial class SampleContext : global::System.ClientModel.Primitives.ModelReaderWriterContext { }