Skip to content

Commit 5b4ef0a

Browse files
committed
Add enum support. Fix minor bug with records with members.
1 parent 62078f7 commit 5b4ef0a

File tree

5 files changed

+488
-20
lines changed

5 files changed

+488
-20
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"profiles": {
3+
"Microsoft.DotNet.ApiDiff.Tool": {
4+
"commandName": "Project",
5+
"commandLineArgs": "-b \"C:/Users/calope/source/repos/tmp/Microsoft.NETCore.App.Before/ref/net10.0/\" -a \"C:/Users/calope/source/repos/tmp/Microsoft.NETCore.App.After/ref/net10.0/\" -o \"C:/Users/calope/source/repos/tmp/output/\" -tc \"perf-prev-2\" -bfn \"before\" -afn \"after\"\r\n\r\n",
6+
"workingDirectory": "C:\\Users\\calope\\source\\repos\\sdk2\\artifacts\\bin\\Microsoft.DotNet.ApiDiff.Tool\\Release\\net8.0\\"
7+
}
8+
}
9+
}

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

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
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;
57
using System.Diagnostics;
68
using System.Diagnostics.CodeAnalysis;
9+
using System.Reflection;
710
using DiffPlex.DiffBuilder;
811
using DiffPlex.DiffBuilder.Model;
912
using Microsoft.CodeAnalysis;
@@ -39,6 +42,7 @@ public class MemoryOutputDiffGenerator : IDiffGenerator
3942
private readonly SyntaxList<AttributeListSyntax> _emptyAttributeList;
4043
private readonly IEnumerable<KeyValuePair<string, ReportDiagnostic>> _diagnosticOptions;
4144
private readonly ConcurrentDictionary<string, string> _results;
45+
private readonly ChildrenNodesComparer _childrenNodesComparer;
4246

4347
/// <summary>
4448
/// Initializes a new instance of the <see cref="MemoryOutputDiffGenerator"/> class.
@@ -80,6 +84,7 @@ internal MemoryOutputDiffGenerator(
8084
_emptyAttributeList = SyntaxFactory.List<AttributeListSyntax>();
8185
_results = [];
8286
_endOfLineTrivia = Environment.NewLine == "\r\n" ? SyntaxFactory.CarriageReturnLineFeed : SyntaxFactory.LineFeed;
87+
_childrenNodesComparer = new ChildrenNodesComparer();
8388
}
8489

8590
/// <inheritdoc/>
@@ -217,7 +222,7 @@ private static string GetFinalAssemblyDiff(string assemblyName, string diffText)
217222
StringBuilder sb = new();
218223
// Traverse all the elements found on the left side. This only visits unchanged, modified and deleted APIs.
219224
// In other words, this loop excludes those that are new on the right. Those are handled later.
220-
foreach ((string beforeMemberName, MemberDeclarationSyntax beforeMemberNode) in beforeChildrenNodes.OrderBy(x => x.Key))
225+
foreach ((string beforeMemberName, MemberDeclarationSyntax beforeMemberNode) in beforeChildrenNodes.Order(_childrenNodesComparer))
221226
{
222227
if (afterChildrenNodes.TryGetValue(beforeMemberName, out MemberDeclarationSyntax? afterMemberNode) &&
223228
beforeMemberNode.Kind() == afterMemberNode.Kind())
@@ -265,7 +270,7 @@ private static string GetFinalAssemblyDiff(string assemblyName, string diffText)
265270
if (!afterChildrenNodes.IsEmpty)
266271
{
267272
// Traverse all the elements that are new on the right side which were not found on the left. They are all treated as new APIs.
268-
foreach ((string newMemberName, MemberDeclarationSyntax newMemberNode) in afterChildrenNodes.OrderBy(x => x.Key))
273+
foreach ((string newMemberName, MemberDeclarationSyntax newMemberNode) in afterChildrenNodes.Order(_childrenNodesComparer))
269274
{
270275
// Need to do a full visit of each member of namespaces and types anyway, so that leaf bodies are removed
271276
if (newMemberNode is BaseTypeDeclarationSyntax or BaseNamespaceDeclarationSyntax)
@@ -311,30 +316,37 @@ private static ConcurrentDictionary<string, MemberDeclarationSyntax> CollectChil
311316

312317
ConcurrentDictionary<string, MemberDeclarationSyntax> dictionary = [];
313318

314-
if (parentNode is RecordDeclarationSyntax record && record.Members.Any())
319+
if (parentNode is BaseNamespaceDeclarationSyntax)
315320
{
316-
foreach (MemberDeclarationSyntax memberNode in record.ChildNodes().Where(n => n is MemberDeclarationSyntax m && IsPublicOrProtectedOrDestructor(m)).Cast<MemberDeclarationSyntax>())
317-
{
318-
// Note that these could also be nested types
319-
dictionary.TryAdd(GetDocId(memberNode, model), memberNode);
320-
}
321-
}
322-
else if (parentNode is BaseNamespaceDeclarationSyntax)
323-
{
324-
foreach (BaseTypeDeclarationSyntax typeNode in parentNode.ChildNodes().Where(n => n is BaseTypeDeclarationSyntax t && IsPublicOrProtectedOrDestructor(t)).Cast<BaseTypeDeclarationSyntax>())
321+
// Find all types
322+
foreach (BaseTypeDeclarationSyntax typeNode in GetMembersOfType<BaseTypeDeclarationSyntax>(parentNode))
325323
{
326324
dictionary.TryAdd(GetDocId(typeNode, model), typeNode);
327325
}
328-
foreach (DelegateDeclarationSyntax delegateNode in parentNode.ChildNodes().Where(n => n is DelegateDeclarationSyntax d && IsPublicOrProtectedOrDestructor(d)).Cast<DelegateDeclarationSyntax>())
326+
327+
// Find all delegates
328+
foreach (DelegateDeclarationSyntax delegateNode in GetMembersOfType<DelegateDeclarationSyntax>(parentNode))
329329
{
330330
dictionary.TryAdd(GetDocId(delegateNode, model), delegateNode);
331331
}
332332
}
333333
else if (parentNode is BaseTypeDeclarationSyntax)
334334
{
335-
foreach (MemberDeclarationSyntax memberNode in parentNode.ChildNodes().Where(n => n is MemberDeclarationSyntax m && IsPublicOrProtectedOrDestructor(m)).Cast<MemberDeclarationSyntax>())
335+
// Special case for records that have members
336+
if (parentNode is RecordDeclarationSyntax record && record.Members.Any())
336337
{
337-
dictionary.TryAdd(GetDocId(memberNode, model), memberNode);
338+
foreach (MemberDeclarationSyntax memberNode in GetMembersOfType<MemberDeclarationSyntax>(parentNode))
339+
{
340+
// Note that these could also be nested types
341+
dictionary.TryAdd(GetDocId(memberNode, model), memberNode);
342+
}
343+
}
344+
else
345+
{
346+
foreach (MemberDeclarationSyntax memberNode in GetMembersOfType<MemberDeclarationSyntax>(parentNode))
347+
{
348+
dictionary.TryAdd(GetDocId(memberNode, model), memberNode);
349+
}
338350
}
339351
}
340352
else if (parentNode is CompilationUnitSyntax)
@@ -417,10 +429,22 @@ private static ConcurrentDictionary<string, MemberDeclarationSyntax> CollectChil
417429
return null;
418430
}
419431

420-
if (node is BaseNamespaceDeclarationSyntax namespaceNode)
432+
if (node is EnumMemberDeclarationSyntax enumMember)
433+
{
434+
SyntaxTriviaList commaTrivia = SyntaxFactory.TriviaList(SyntaxFactory.SyntaxTrivia(SyntaxKind.EndOfLineTrivia, ","));
435+
return enumMember
436+
.WithAttributeLists(_emptyAttributeList)
437+
.WithLeadingTrivia(node.GetLeadingTrivia())
438+
.WithTrailingTrivia(commaTrivia);
439+
}
440+
else if (node is BaseNamespaceDeclarationSyntax namespaceNode)
421441
{
422442
return namespaceNode.WithAttributeLists(_emptyAttributeList).WithLeadingTrivia(node.GetLeadingTrivia());
423443
}
444+
else if (node is MemberDeclarationSyntax memberDeclaration)
445+
{
446+
return memberDeclaration.WithAttributeLists(_emptyAttributeList).WithLeadingTrivia(node.GetLeadingTrivia());
447+
}
424448

425449
return node.WithAttributeLists(_emptyAttributeList).WithLeadingTrivia(node.GetLeadingTrivia());
426450
}
@@ -614,9 +638,17 @@ private string GetClosingBraceCode(SyntaxNode childlessNode, SyntaxTriviaList le
614638
.ToFullString();
615639
}
616640

617-
private static bool IsPublicOrProtectedOrDestructor(MemberDeclarationSyntax m) =>
641+
private static bool IsEnumMemberOrHasPublicOrProtectedModifierOrIsDestructor(MemberDeclarationSyntax m) =>
618642
// Destructors don't have visibility modifiers so they're special-cased
619-
m.Modifiers.Any(SyntaxKind.PublicKeyword) || m.Modifiers.Any(SyntaxKind.ProtectedKeyword) || m.IsKind(SyntaxKind.DestructorDeclaration);
643+
m.Modifiers.Any(SyntaxKind.PublicKeyword) || m.Modifiers.Any(SyntaxKind.ProtectedKeyword) ||
644+
// Enum member declarations don't have any modifiers
645+
m is EnumMemberDeclarationSyntax ||
646+
m.IsKind(SyntaxKind.DestructorDeclaration);
647+
648+
private static IEnumerable<T> GetMembersOfType<T>(SyntaxNode node) where T : MemberDeclarationSyntax => node
649+
.ChildNodes()
650+
.Where(n => n is T m && IsEnumMemberOrHasPublicOrProtectedModifierOrIsDestructor(m))
651+
.Cast<T>();
620652

621653
private static string GetDocId(SyntaxNode node, SemanticModel model)
622654
{
@@ -692,4 +724,22 @@ private static string GetDocId(SyntaxNode node, SemanticModel model)
692724
}
693725
return sb.Length == 0 ? null : sb.ToString();
694726
}
727+
728+
private class ChildrenNodesComparer : IComparer<KeyValuePair<string, MemberDeclarationSyntax>>
729+
{
730+
public int Compare(KeyValuePair<string, MemberDeclarationSyntax> first, KeyValuePair<string, MemberDeclarationSyntax> second)
731+
{
732+
// Enum members need to be sorted by their value, not alphabetically, so they need to be special-cased.
733+
if (first.Value is EnumMemberDeclarationSyntax beforeMember && second.Value is EnumMemberDeclarationSyntax afterMember &&
734+
beforeMember.EqualsValue is EqualsValueClauseSyntax beforeEVCS && afterMember.EqualsValue is EqualsValueClauseSyntax afterEVCS &&
735+
beforeEVCS.Value is LiteralExpressionSyntax beforeLes && afterEVCS.Value is LiteralExpressionSyntax afterLes)
736+
{
737+
return beforeLes.Token.ValueText.CompareTo(afterLes.Token.ValueText);
738+
}
739+
740+
// Everything else is shown alphabetically.
741+
return first.Key.CompareTo(second.Key);
742+
}
743+
744+
}
695745
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ public class MyClass
144144

145145
[Fact]
146146
public Task TestPrimaryConstructorChange() => RunTestAsync(
147+
// This isn't really a modification, but a deletion and an addition, and since
148+
// deletions show up before additions, that explains the order of the expected code.
147149
beforeCode: """
148150
namespace MyNamespace
149151
{
@@ -245,6 +247,8 @@ namespace MyNamespace
245247

246248
[Fact]
247249
public Task TestChangeHideImplicitDefaultConstructors() => RunTestAsync(
250+
// This isn't really a modification, but a deletion and an addition, and since
251+
// deletions show up before additions, that explains the order of the expected code.
248252
beforeCode: """
249253
namespace MyNamespace
250254
{
@@ -330,6 +334,8 @@ namespace MyNamespace
330334

331335
[Fact]
332336
public Task TestNestedTypeChangeHideImplicitDefaultConstructors() => RunTestAsync(
337+
// This isn't really a modification, but a deletion and an addition, and since
338+
// deletions show up before additions, that explains the order of the expected code.
333339
beforeCode: """
334340
namespace MyNamespace
335341
{

0 commit comments

Comments
 (0)