Skip to content

Commit eb98ae9

Browse files
committed
Fix bug preventing attributes with arguments from getting either suppressed or all kept (without a docId collision).
1 parent 5b4ef0a commit eb98ae9

File tree

2 files changed

+98
-8
lines changed

2 files changed

+98
-8
lines changed

src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/MemoryOutputDiffGenerator.cs

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Collections.Concurrent;
5-
using System.Collections.Generic;
6-
using System.Collections.Specialized;
75
using System.Diagnostics;
86
using System.Diagnostics.CodeAnalysis;
9-
using System.Reflection;
107
using DiffPlex.DiffBuilder;
118
using DiffPlex.DiffBuilder.Model;
129
using Microsoft.CodeAnalysis;
@@ -305,7 +302,7 @@ private static string GetFinalAssemblyDiff(string assemblyName, string diffText)
305302
return sb.ToString();
306303
}
307304

308-
private static ConcurrentDictionary<string, MemberDeclarationSyntax> CollectChildrenNodes(SyntaxNode? parentNode, SemanticModel? model)
305+
private ConcurrentDictionary<string, MemberDeclarationSyntax> CollectChildrenNodes(SyntaxNode? parentNode, SemanticModel? model)
309306
{
310307
if (parentNode == null)
311308
{
@@ -521,7 +518,7 @@ private Dictionary<string, AttributeSyntax> CollectAttributeNodes(MemberDeclarat
521518
continue;
522519
}
523520
var realAttributeNode = attributeNode.WithArgumentList(attributeNode.ArgumentList);
524-
if (!dictionary.TryAdd(realAttributeNode.ToFullString(), realAttributeNode))
521+
if (!dictionary.TryAdd(GetAttributeDocId(realAttributeNode, model), realAttributeNode))
525522
{
526523
_log.LogWarning(string.Format(Resources.AttributeAlreadyExists, realAttributeNode.ToFullString(), GetDocId(memberNode, model)));
527524
}
@@ -650,7 +647,33 @@ private static IEnumerable<T> GetMembersOfType<T>(SyntaxNode node) where T : Mem
650647
.Where(n => n is T m && IsEnumMemberOrHasPublicOrProtectedModifierOrIsDestructor(m))
651648
.Cast<T>();
652649

653-
private static string GetDocId(SyntaxNode node, SemanticModel model)
650+
private string GetAttributeDocId(AttributeSyntax attribute, SemanticModel model)
651+
{
652+
string? attributeDocId = null;
653+
if (attribute.ArgumentList is AttributeArgumentListSyntax list && list.Arguments.Any())
654+
{
655+
// Workaround to ensure that repeated attributes with different arguments are all included
656+
attributeDocId = attribute.ToFullString();
657+
}
658+
else if (model.GetSymbolInfo(attribute).Symbol is IMethodSymbol constructor)
659+
{
660+
try
661+
{
662+
attributeDocId = constructor.ContainingType.GetDocumentationCommentId();
663+
}
664+
catch (Exception e)
665+
{
666+
_log.LogWarning(e.Message);
667+
}
668+
}
669+
// Workaround because some rare cases of attributes in WinForms like System.Windows.Markup.ContentPropertyAttribute
670+
// cannot be found in the current model.
671+
attributeDocId ??= attribute.ToFullString();
672+
673+
return attributeDocId;
674+
}
675+
676+
private string GetDocId(SyntaxNode node, SemanticModel model)
654677
{
655678
ISymbol? symbol = node switch
656679
{
@@ -660,7 +683,7 @@ private static string GetDocId(SyntaxNode node, SemanticModel model)
660683
EventFieldDeclarationSyntax eventFieldDeclaration => model.GetDeclaredSymbol(eventFieldDeclaration.Declaration.Variables.First()),
661684
PropertyDeclarationSyntax propertyDeclaration => model.GetDeclaredSymbol(propertyDeclaration),
662685
_ => model.GetDeclaredSymbol(node)
663-
};
686+
};
664687

665688
if (symbol?.GetDocumentationCommentId() is string docId)
666689
{

test/Microsoft.DotNet.ApiDiff.Tests/Diff.Attribute.Tests.cs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -879,7 +879,74 @@ public class MyClass
879879
attributesToExclude: ["T:MyNamespace.MyAttributeAttribute"]); // Overrides the default list
880880

881881
[Fact]
882-
public Task TestSuppressTypeAndAttributeUsage() => RunTestAsync(
882+
public Task SuppressChangedAttribute() => RunTestAsync(
883+
// Includes dummy property addition so that something else shows up in the diff, but not the attribute change.
884+
beforeCode: """
885+
namespace MyNamespace
886+
{
887+
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Original text")]
888+
public class MyClass
889+
{
890+
}
891+
}
892+
""",
893+
afterCode: """
894+
using System;
895+
namespace MyNamespace
896+
{
897+
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Changed text")]
898+
public class MyClass
899+
{
900+
public int X { get; }
901+
}
902+
}
903+
""",
904+
expectedCode: """
905+
namespace MyNamespace
906+
{
907+
public class MyClass
908+
{
909+
+ public int X { get; }
910+
}
911+
}
912+
""",
913+
attributesToExclude: ["T:System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute"]); // Overrides the default list
914+
915+
[Fact]
916+
public Task AttributesRepeatedWithDifferentArguments() => RunTestAsync(
917+
beforeCode: """
918+
namespace MyNamespace
919+
{
920+
public class MyClass
921+
{
922+
}
923+
}
924+
""",
925+
afterCode: """
926+
using System;
927+
namespace MyNamespace
928+
{
929+
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")]
930+
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("linux")]
931+
public class MyClass
932+
{
933+
}
934+
}
935+
""",
936+
expectedCode: """
937+
namespace MyNamespace
938+
{
939+
+ [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")]
940+
+ [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("linux")]
941+
public class MyClass
942+
{
943+
}
944+
}
945+
""",
946+
attributesToExclude: null); // null forces using the default list
947+
948+
[Fact]
949+
public Task SuppressTypeAndAttributeUsage() => RunTestAsync(
883950
beforeCode: """
884951
namespace MyNamespace
885952
{

0 commit comments

Comments
 (0)