diff --git a/.gitignore b/.gitignore
index e2cd5ecd2684..84a4e7bf3425 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,7 +19,8 @@ obj/
*.binlog
-sdk.sln.DotSettings.user
+# User specific files
+*.user
# Debian and python stuff
*.dsc
diff --git a/CODEOWNERS b/CODEOWNERS
index 767ae68495a7..584e649fd8ba 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -53,3 +53,7 @@
/src/Tests/dotnet-watch.Tests/ @captainsafia, @pranavkm, @mkArtakMSFT
/src/Tests/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/ @captainsafia, @pranavkm, @mkArtakMSFT
/src/BuiltInTools/ @captainsafia, @pranavkm, @mkArtakMSFT
+
+# Compatibility tools owned by runtime team
+/src/Compatibility/ @Anipik, @safern, @ericstj
+/src/Tests/Microsoft.DotNet.ApiCompatibility/ @Anipik, @safern, @ericstj
diff --git a/sdk.sln b/sdk.sln
index 88ec33d89eba..d0a04273e520 100644
--- a/sdk.sln
+++ b/sdk.sln
@@ -341,6 +341,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.NativeWrap
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.NET.Sdk.Razor.SourceGenerators.Tests", "src\Tests\Microsoft.NET.Sdk.Razor.SourceGenerators.Tests\Microsoft.NET.Sdk.Razor.SourceGenerators.Tests.csproj", "{A71FC21D-D90A-49B5-9B5A-AD4776287B55}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Compatibility", "Compatibility", "{AF683E5C-421E-4DE0-ADD7-9841E5D12BFA}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.ApiCompatibility", "src\Compatibility\Microsoft.DotNet.ApiCompatibility\Microsoft.DotNet.ApiCompatibility.csproj", "{3F5A028C-C51B-434A-8C10-37680CD2635C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.DotNet.ApiCompatibility.Tests", "src\Tests\Microsoft.DotNet.ApiCompatibility.Tests\Microsoft.DotNet.ApiCompatibility.Tests.csproj", "{24F084ED-35BB-401E-89F5-63E5E22C3B3B}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Win32.Msi", "src\Microsoft.Win32.Msi\Microsoft.Win32.Msi.csproj", "{3D002392-6308-41DF-8BD5-224CCC5B049F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Win32.Msi.Tests", "src\Tests\Microsoft.Win32.Msi.Tests\Microsoft.Win32.Msi.Tests.csproj", "{80932949-B8B2-4163-B325-76F8FDBE3897}"
@@ -621,6 +626,14 @@ Global
{A71FC21D-D90A-49B5-9B5A-AD4776287B55}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A71FC21D-D90A-49B5-9B5A-AD4776287B55}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A71FC21D-D90A-49B5-9B5A-AD4776287B55}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3F5A028C-C51B-434A-8C10-37680CD2635C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3F5A028C-C51B-434A-8C10-37680CD2635C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3F5A028C-C51B-434A-8C10-37680CD2635C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3F5A028C-C51B-434A-8C10-37680CD2635C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {24F084ED-35BB-401E-89F5-63E5E22C3B3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {24F084ED-35BB-401E-89F5-63E5E22C3B3B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {24F084ED-35BB-401E-89F5-63E5E22C3B3B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {24F084ED-35BB-401E-89F5-63E5E22C3B3B}.Release|Any CPU.Build.0 = Release|Any CPU
{3D002392-6308-41DF-8BD5-224CCC5B049F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3D002392-6308-41DF-8BD5-224CCC5B049F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3D002392-6308-41DF-8BD5-224CCC5B049F}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -746,6 +759,9 @@ Global
{1BBFA19C-03F0-4D27-9D0D-0F8172642107} = {71A9F549-0EB6-41F9-BC16-4A6C5007FC91}
{E97E9E7F-11B4-42F7-8B55-D0451F5E82A0} = {8F22FBD6-BDC8-431E-8402-B7460D3A9724}
{A71FC21D-D90A-49B5-9B5A-AD4776287B55} = {580D1AE7-AA8F-4912-8B76-105594E00B3B}
+ {AF683E5C-421E-4DE0-ADD7-9841E5D12BFA} = {22AB674F-ED91-4FBC-BFEE-8A1E82F9F05E}
+ {3F5A028C-C51B-434A-8C10-37680CD2635C} = {AF683E5C-421E-4DE0-ADD7-9841E5D12BFA}
+ {24F084ED-35BB-401E-89F5-63E5E22C3B3B} = {580D1AE7-AA8F-4912-8B76-105594E00B3B}
{3D002392-6308-41DF-8BD5-224CCC5B049F} = {22AB674F-ED91-4FBC-BFEE-8A1E82F9F05E}
{80932949-B8B2-4163-B325-76F8FDBE3897} = {580D1AE7-AA8F-4912-8B76-105594E00B3B}
{EEF4C7DD-CDC9-44B6-8B4F-725647D54ED8} = {580D1AE7-AA8F-4912-8B76-105594E00B3B}
diff --git a/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/CompatDifference.cs b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/CompatDifference.cs
new file mode 100644
index 000000000000..c693dc74339f
--- /dev/null
+++ b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/CompatDifference.cs
@@ -0,0 +1,88 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.CodeAnalysis;
+using System;
+
+namespace Microsoft.DotNet.ApiCompatibility.Abstractions
+{
+ ///
+ /// Class representing a difference of compatibility, containing detailed information about it.
+ ///
+ public class CompatDifference : IDiagnostic, IEquatable
+ {
+ ///
+ /// The Diagnostic ID for this difference.
+ ///
+ public string DiagnosticId { get; }
+
+ ///
+ /// The .
+ ///
+ public DifferenceType Type { get; }
+
+ ///
+ /// A diagnostic message for the difference.
+ ///
+ public virtual string Message { get; }
+
+ ///
+ /// A unique ID in order to identify the API that the difference was raised for.
+ ///
+ public string ReferenceId { get; }
+
+ private CompatDifference() { }
+
+ ///
+ /// Instantiate a new object representing the compatibility difference.
+ ///
+ /// representing the diagnostic ID.
+ /// message describing the difference.
+ /// to describe the type of the difference.
+ /// for which the difference is associated to.
+ public CompatDifference(string diagnosticId, string message, DifferenceType type, ISymbol member)
+ : this(diagnosticId, message, type, member?.GetDocumentationCommentId())
+ {
+ }
+
+ ///
+ /// Instantiate a new object representing the compatibility difference.
+ ///
+ /// representing the diagnostic ID.
+ /// message describing the difference.
+ /// to describe the type of the difference.
+ /// containing the member ID for which the difference is associated to.
+ public CompatDifference(string diagnosticId, string message, DifferenceType type, string memberId)
+ {
+ DiagnosticId = diagnosticId ?? throw new ArgumentNullException(nameof(diagnosticId));
+ Message = message ?? throw new ArgumentNullException(nameof(message));
+ Type = type;
+ ReferenceId = memberId ?? throw new ArgumentNullException(nameof(memberId));
+ }
+
+ ///
+ /// Evaluates whether the current object is equal to another .
+ ///
+ /// to compare against.
+ /// True if equals, False if different.
+ public bool Equals(CompatDifference other) =>
+ other != null &&
+ Type == other.Type &&
+ DiagnosticId.Equals(other.DiagnosticId, StringComparison.OrdinalIgnoreCase) &&
+ ReferenceId.Equals(other.ReferenceId, StringComparison.OrdinalIgnoreCase) &&
+ Message.Equals(other.Message, StringComparison.OrdinalIgnoreCase);
+
+ ///
+ /// Gets the hashcode that reperesents this instance.
+ ///
+ /// Unique based on the properties' values of the instance.
+ public override int GetHashCode() =>
+ HashCode.Combine(ReferenceId, DiagnosticId, Message, Type);
+
+ ///
+ /// Gets a representation of the difference.
+ ///
+ /// describing the difference.
+ public override string ToString() => $"{DiagnosticId} : {Message}";
+ }
+}
diff --git a/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/DifferenceType.cs b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/DifferenceType.cs
new file mode 100644
index 000000000000..30042793526f
--- /dev/null
+++ b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/DifferenceType.cs
@@ -0,0 +1,15 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Microsoft.DotNet.ApiCompatibility.Abstractions
+{
+ ///
+ /// Enum representing the different type of differences available.
+ ///
+ public enum DifferenceType
+ {
+ Changed,
+ Added,
+ Removed
+ }
+}
diff --git a/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/Filtering/ISymbolFilter.cs b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/Filtering/ISymbolFilter.cs
new file mode 100644
index 000000000000..6b06a0d05ff9
--- /dev/null
+++ b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/Filtering/ISymbolFilter.cs
@@ -0,0 +1,20 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.CodeAnalysis;
+
+namespace Microsoft.DotNet.ApiCompatibility.Abstractions
+{
+ ///
+ /// Provides a mechanism to filter when building the .
+ ///
+ public interface ISymbolFilter
+ {
+ ///
+ /// Determines whether the should be included.
+ ///
+ /// to evaluate.
+ /// True to include the or false to filter it out.
+ bool Include(ISymbol symbol);
+ }
+}
diff --git a/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/Filtering/SymbolAccessibilityBasedFilter.cs b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/Filtering/SymbolAccessibilityBasedFilter.cs
new file mode 100644
index 000000000000..983e49593038
--- /dev/null
+++ b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/Filtering/SymbolAccessibilityBasedFilter.cs
@@ -0,0 +1,23 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.CodeAnalysis;
+
+namespace Microsoft.DotNet.ApiCompatibility.Abstractions
+{
+ internal class SymbolAccessibilityBasedFilter : ISymbolFilter
+ {
+ private readonly bool _includeInternalSymbols;
+
+ internal SymbolAccessibilityBasedFilter(bool includeInternalSymbols)
+ {
+ _includeInternalSymbols = includeInternalSymbols;
+ }
+
+ public bool Include(ISymbol symbol) =>
+ symbol.DeclaredAccessibility == Accessibility.Public ||
+ symbol.DeclaredAccessibility == Accessibility.Protected ||
+ symbol.DeclaredAccessibility == Accessibility.ProtectedOrInternal ||
+ (_includeInternalSymbols && symbol.DeclaredAccessibility != Accessibility.Private);
+ }
+}
diff --git a/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/IDiagnostic.cs b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/IDiagnostic.cs
new file mode 100644
index 000000000000..c8e3540f1c00
--- /dev/null
+++ b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/IDiagnostic.cs
@@ -0,0 +1,21 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Microsoft.DotNet.ApiCompatibility.Abstractions
+{
+ ///
+ /// Interface that describes a diagnostic.
+ ///
+ public interface IDiagnostic
+ {
+ ///
+ /// String representing the diagnostic ID.
+ ///
+ string DiagnosticId { get; }
+
+ ///
+ /// String representing the ID for the object that the diagnostic was created for.
+ ///
+ string ReferenceId { get; }
+ }
+}
diff --git a/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/IRuleRunner.cs b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/IRuleRunner.cs
new file mode 100644
index 000000000000..4203622d2829
--- /dev/null
+++ b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/IRuleRunner.cs
@@ -0,0 +1,21 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.s
+
+using System.Collections.Generic;
+
+namespace Microsoft.DotNet.ApiCompatibility.Abstractions
+{
+ ///
+ /// Interface for rule drivers to implement in order to be used returned by the
+ ///
+ public interface IRuleRunner
+ {
+ ///
+ /// Runs the registered rules on the mapper.
+ ///
+ /// The underlying type on the mapper.
+ /// The mapper to run the rules on.
+ ///
+ IEnumerable Run(ElementMapper mapper);
+ }
+}
diff --git a/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/IRuleRunnerFactory.cs b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/IRuleRunnerFactory.cs
new file mode 100644
index 000000000000..eca973e74494
--- /dev/null
+++ b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/IRuleRunnerFactory.cs
@@ -0,0 +1,13 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.s
+
+namespace Microsoft.DotNet.ApiCompatibility.Abstractions
+{
+ ///
+ /// The factory to create the driver that the differ should use based on the .
+ ///
+ public interface IRuleRunnerFactory
+ {
+ IRuleRunner GetRuleRunner();
+ }
+}
diff --git a/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/MapperVisitor.cs b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/MapperVisitor.cs
new file mode 100644
index 000000000000..f6305f08aa6e
--- /dev/null
+++ b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/MapperVisitor.cs
@@ -0,0 +1,121 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+
+namespace Microsoft.DotNet.ApiCompatibility.Abstractions
+{
+ ///
+ /// Class that implements a visitor pattern to visit the tree for a given mapper.
+ ///
+ public class MapperVisitor
+ {
+ ///
+ /// Visits the tree for the given .
+ ///
+ /// Underlying type for the objects that the mapper holds.
+ /// to visit.
+ public void Visit(ElementMapper mapper)
+ {
+ if (mapper is AssemblySetMapper assemblySetMapper)
+ {
+ Visit(assemblySetMapper);
+ }
+ else if (mapper is AssemblyMapper assemblyMapper)
+ {
+ Visit(assemblyMapper);
+ }
+ else if (mapper is NamespaceMapper nsMapper)
+ {
+ Visit(nsMapper);
+ }
+ else if (mapper is TypeMapper typeMapper)
+ {
+ Visit(typeMapper);
+ }
+ else if (mapper is MemberMapper memberMapper)
+ {
+ Visit(memberMapper);
+ }
+ }
+
+ ///
+ /// Visits the and visits each in the mapper.
+ ///
+ /// The to visit.
+ public virtual void Visit(AssemblySetMapper mapper)
+ {
+ if (mapper == null)
+ {
+ throw new ArgumentNullException(nameof(mapper));
+ }
+
+ foreach (AssemblyMapper assembly in mapper.GetAssemblies())
+ {
+ Visit(assembly);
+ }
+ }
+
+ ///
+ /// Visits the and visits each in the mapper.
+ ///
+ /// The to visit.
+ public virtual void Visit(AssemblyMapper mapper)
+ {
+ if (mapper == null)
+ {
+ throw new ArgumentNullException(nameof(mapper));
+ }
+
+ foreach (NamespaceMapper nsMapper in mapper.GetNamespaces())
+ {
+ Visit(nsMapper);
+ }
+ }
+
+ ///
+ /// Visits the and visits each in the mapper.
+ ///
+ /// The to visit.
+ public virtual void Visit(NamespaceMapper mapper)
+ {
+ if (mapper == null)
+ {
+ throw new ArgumentNullException(nameof(mapper));
+ }
+
+ foreach (TypeMapper type in mapper.GetTypes())
+ {
+ Visit(type);
+ }
+ }
+
+ ///
+ /// Visits the and visits the nested types and members in the mapper.
+ ///
+ /// The to visit.
+ public virtual void Visit(TypeMapper mapper)
+ {
+ if (mapper == null)
+ {
+ throw new ArgumentNullException(nameof(mapper));
+ }
+
+ foreach (TypeMapper type in mapper.GetNestedTypes())
+ {
+ Visit(type);
+ }
+
+ foreach (MemberMapper member in mapper.GetMembers())
+ {
+ Visit(member);
+ }
+ }
+
+ ///
+ /// Visits the .
+ ///
+ /// The to visit.
+ public virtual void Visit(MemberMapper mapper) { }
+ }
+}
diff --git a/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/Mappers/AssemblyMapper.cs b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/Mappers/AssemblyMapper.cs
new file mode 100644
index 000000000000..c547c96902a0
--- /dev/null
+++ b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/Mappers/AssemblyMapper.cs
@@ -0,0 +1,98 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.CodeAnalysis;
+using System.Collections.Generic;
+
+namespace Microsoft.DotNet.ApiCompatibility.Abstractions
+{
+ ///
+ /// Object that represents a mapping between two objects.
+ /// This also holds a list of to represent the mapping of namespaces in between
+ /// and .
+ ///
+ public class AssemblyMapper : ElementMapper
+ {
+ private Dictionary _namespaces;
+
+ ///
+ /// Instantiates an object with the provided .
+ ///
+ /// The settings used to diff the elements in the mapper.
+ public AssemblyMapper(ComparingSettings settings) : base(settings) { }
+
+ ///
+ /// Gets the mappers for the namespaces contained in and
+ ///
+ /// The list of .
+ public IEnumerable GetNamespaces()
+ {
+ if (_namespaces == null)
+ {
+ _namespaces = new Dictionary(Settings.EqualityComparer);
+ Dictionary> typeForwards;
+ if (Left != null)
+ {
+ typeForwards = ResolveTypeForwards(Left, Settings.EqualityComparer);
+ AddOrCreateMappers(Left.GlobalNamespace, 0);
+ }
+
+ if (Right != null)
+ {
+ typeForwards = ResolveTypeForwards(Right, Settings.EqualityComparer);
+ AddOrCreateMappers(Right.GlobalNamespace, 1);
+ }
+
+ void AddOrCreateMappers(INamespaceSymbol ns, int index)
+ {
+ Stack stack = new();
+ stack.Push(ns);
+ while (stack.Count > 0)
+ {
+ INamespaceSymbol symbol = stack.Pop();
+ if (typeForwards.TryGetValue(symbol, out List forwardedTypes) || symbol.GetTypeMembers().Length > 0)
+ {
+ if (!_namespaces.TryGetValue(symbol, out NamespaceMapper mapper))
+ {
+ mapper = new NamespaceMapper(Settings);
+ _namespaces.Add(symbol, mapper);
+ }
+
+ mapper.AddElement(symbol, index);
+ mapper.AddForwardedTypes(forwardedTypes ?? new List(), index);
+ }
+
+ foreach (INamespaceSymbol child in symbol.GetNamespaceMembers())
+ stack.Push(child);
+ }
+ }
+
+ static Dictionary> ResolveTypeForwards(IAssemblySymbol assembly, IEqualityComparer comparer)
+ {
+ Dictionary> typeForwards = new(comparer);
+ foreach (INamedTypeSymbol symbol in assembly.GetForwardedTypes())
+ {
+ if (symbol.TypeKind != TypeKind.Error)
+ {
+ if (!typeForwards.TryGetValue(symbol.ContainingNamespace, out List types))
+ {
+ types = new List();
+ typeForwards.Add(symbol.ContainingNamespace, types);
+ }
+
+ types.Add(symbol);
+ }
+ else
+ {
+ // TODO: Log Warning;
+ }
+ }
+
+ return typeForwards;
+ }
+ }
+
+ return _namespaces.Values;
+ }
+ }
+}
diff --git a/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/Mappers/AssemblySetMapper.cs b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/Mappers/AssemblySetMapper.cs
new file mode 100644
index 000000000000..e34a877506ab
--- /dev/null
+++ b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/Mappers/AssemblySetMapper.cs
@@ -0,0 +1,58 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.CodeAnalysis;
+using System.Collections.Generic;
+
+namespace Microsoft.DotNet.ApiCompatibility.Abstractions
+{
+ ///
+ /// Object that represents a mapping between two lists of .
+ ///
+ public class AssemblySetMapper : ElementMapper>
+ {
+ private Dictionary _assemblies;
+
+ ///
+ /// Instantiates an object with the provided .
+ ///
+ /// The settings used to diff the elements in the mapper.
+ public AssemblySetMapper(ComparingSettings settings) : base(settings) { }
+
+ ///
+ /// Gets the assembly mappers built from the provided lists of .
+ /// The list of representing the underlying assemblies.
+ public IEnumerable GetAssemblies()
+ {
+ if (_assemblies == null)
+ {
+ _assemblies = new Dictionary(Settings.EqualityComparer);
+
+ if (Left != null)
+ {
+ AddOrCreateMappers(Left, 0);
+ }
+
+ if (Right != null)
+ {
+ AddOrCreateMappers(Right, 1);
+ }
+
+ void AddOrCreateMappers(IEnumerable elements, int index)
+ {
+ foreach (IAssemblySymbol assembly in elements)
+ {
+ if (!_assemblies.TryGetValue(assembly, out AssemblyMapper mapper))
+ {
+ mapper = new AssemblyMapper(Settings);
+ _assemblies.Add(assembly, mapper);
+ }
+ mapper.AddElement(assembly, index);
+ }
+ }
+ }
+
+ return _assemblies.Values;
+ }
+ }
+}
diff --git a/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/Mappers/ElementMapper.cs b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/Mappers/ElementMapper.cs
new file mode 100644
index 000000000000..d64251109ea8
--- /dev/null
+++ b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/Mappers/ElementMapper.cs
@@ -0,0 +1,69 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.DotNet.ApiCompatibility.Abstractions
+{
+ ///
+ /// Class that represents a mapping in between two objects of type .
+ ///
+ public class ElementMapper
+ {
+ private IEnumerable _differences;
+
+ ///
+ /// Property representing the Left hand side of the mapping.
+ ///
+ public T Left { get; private set; }
+
+ ///
+ /// Property representing the Right hand side of the mapping.
+ ///
+ public T Right { get; private set; }
+
+ ///
+ /// The used to diff and .
+ ///
+ public ComparingSettings Settings { get; }
+
+ ///
+ /// Instantiates an object with the provided .
+ ///
+ /// The settings used to diff the elements in the mapper.
+ public ElementMapper(ComparingSettings settings)
+ {
+ Settings = settings;
+ }
+
+ ///
+ /// Adds an element to the mapping given the index, 0 (Left) or 1 (Right).
+ ///
+ /// The element to add to the mapping.
+ /// Index representing the side of the mapping, 0 (Left) or 1 (Right).
+ public virtual void AddElement(T element, int index)
+ {
+ if ((uint)index > 1)
+ throw new ArgumentOutOfRangeException(nameof(index), "index should either be 0 or 1");
+
+ if (index == 0)
+ {
+ Left = element;
+ }
+ else
+ {
+ Right = element;
+ }
+ }
+
+ ///
+ /// Runs the rules found by the rule driver on the element mapper and returns a list of differences.
+ ///
+ /// The list of .
+ public IEnumerable GetDifferences()
+ {
+ return _differences ??= Settings.RuleRunnerFactory.GetRuleRunner().Run(this);
+ }
+ }
+}
diff --git a/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/Mappers/MemberMapper.cs b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/Mappers/MemberMapper.cs
new file mode 100644
index 000000000000..92fc0f79fc46
--- /dev/null
+++ b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/Mappers/MemberMapper.cs
@@ -0,0 +1,19 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.CodeAnalysis;
+
+namespace Microsoft.DotNet.ApiCompatibility.Abstractions
+{
+ ///
+ /// Object that represents a mapping between two objects.
+ ///
+ public class MemberMapper : ElementMapper
+ {
+ ///
+ /// Instantiates an object with the provided .
+ ///
+ /// The settings used to diff the elements in the mapper.
+ public MemberMapper(ComparingSettings settings) : base(settings) { }
+ }
+}
diff --git a/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/Mappers/NamespaceMapper.cs b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/Mappers/NamespaceMapper.cs
new file mode 100644
index 000000000000..6ff25603e0be
--- /dev/null
+++ b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/Mappers/NamespaceMapper.cs
@@ -0,0 +1,94 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.CodeAnalysis;
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.DotNet.ApiCompatibility.Abstractions
+{
+ ///
+ /// Object that represents a mapping between two objects.
+ /// This also holds a list of to represent the mapping of types in between
+ /// and .
+ ///
+ public class NamespaceMapper : ElementMapper
+ {
+ private Dictionary _types;
+ private IEnumerable _leftForwardedTypes;
+ private IEnumerable _rightForwardedTypes;
+
+ ///
+ /// Instantiates an object with the provided .
+ ///
+ /// The settings used to diff the elements in the mapper.
+ public NamespaceMapper(ComparingSettings settings) : base(settings) { }
+
+ ///
+ /// Gets all the representing the types defined in the namespace including the typeforwards.
+ ///
+ /// The mapper representing the types in the namespace
+ public IEnumerable GetTypes()
+ {
+ if (_types == null)
+ {
+ _types = new Dictionary(Settings.EqualityComparer);
+ IEnumerable types;
+
+ if (Left != null)
+ {
+ types = Left.GetTypeMembers().AddRange(_leftForwardedTypes);
+ AddOrCreateMappers(0);
+ }
+
+ if (Right != null)
+ {
+ types = Right.GetTypeMembers().AddRange(_rightForwardedTypes);
+ AddOrCreateMappers(1);
+ }
+
+ void AddOrCreateMappers(int index)
+ {
+ if (types == null)
+ return;
+
+ foreach (var type in types)
+ {
+ if (Settings.Filter.Include(type))
+ {
+ if (!_types.TryGetValue(type, out TypeMapper mapper))
+ {
+ mapper = new TypeMapper(Settings);
+ _types.Add(type, mapper);
+ }
+
+ mapper.AddElement(type, index);
+ }
+ }
+ }
+ }
+
+ return _types.Values;
+ }
+
+ ///
+ /// Adds forwarded types to the mapper to the index specified in the mapper.
+ ///
+ /// List containing the that represents the forwarded types.
+ /// Index to add the forwarded types into, 0 or 1.
+ public void AddForwardedTypes(IEnumerable forwardedTypes, int index)
+ {
+ if ((uint)index > 1)
+ throw new ArgumentOutOfRangeException(nameof(index), $"Value must be 0 or 1");
+
+ if (index == 0)
+ {
+ _leftForwardedTypes = forwardedTypes;
+ }
+ else
+ {
+ _rightForwardedTypes = forwardedTypes;
+ }
+ }
+ }
+}
diff --git a/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/Mappers/TypeMapper.cs b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/Mappers/TypeMapper.cs
new file mode 100644
index 000000000000..634052ff029b
--- /dev/null
+++ b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/Mappers/TypeMapper.cs
@@ -0,0 +1,110 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.CodeAnalysis;
+using System.Collections.Generic;
+
+namespace Microsoft.DotNet.ApiCompatibility.Abstractions
+{
+ ///
+ /// Object that represents a mapping between two objects.
+ /// This also holds the nested types as a list of and the members defined within the type
+ /// as a list of
+ ///
+ public class TypeMapper : ElementMapper
+ {
+ private Dictionary _nestedTypes;
+ private Dictionary _members;
+
+ ///
+ /// Instantiates an object with the provided .
+ ///
+ /// The settings used to diff the elements in the mapper.
+ public TypeMapper(ComparingSettings settings) : base(settings) { }
+
+ ///
+ /// Indicates whether we have a complete mapper and if the members should be diffed.
+ ///
+ public bool ShouldDiffMembers => Left != null && Right != null;
+
+ ///
+ /// Gets the nested types within the mapped types.
+ ///
+ /// The list of representing the nested types.
+ public IEnumerable GetNestedTypes()
+ {
+ if (_nestedTypes == null)
+ {
+ _nestedTypes = new Dictionary(Settings.EqualityComparer);
+
+ if (Left != null)
+ {
+ AddOrCreateMappers(Left.GetTypeMembers(), 0);
+ }
+
+ if (Right != null)
+ {
+ AddOrCreateMappers(Right.GetTypeMembers(), 1);
+ }
+
+ void AddOrCreateMappers(IEnumerable symbols, int index)
+ {
+ foreach (var nestedType in symbols)
+ {
+ if (Settings.Filter.Include(nestedType))
+ {
+ if (!_nestedTypes.TryGetValue(nestedType, out TypeMapper mapper))
+ {
+ mapper = new TypeMapper(Settings);
+ _nestedTypes.Add(nestedType, mapper);
+ }
+ mapper.AddElement(nestedType, index);
+ }
+ }
+ }
+ }
+
+ return _nestedTypes.Values;
+ }
+
+ ///
+ /// Gets the members defined in this type.
+ ///
+ /// The list of representing the members.
+ public IEnumerable GetMembers()
+ {
+ if (_members == null)
+ {
+ _members = new Dictionary(Settings.EqualityComparer);
+
+ if (Left != null)
+ {
+ AddOrCreateMappers(Left.GetMembers(), 0);
+ }
+
+ if (Right != null)
+ {
+ AddOrCreateMappers(Right.GetMembers(), 1);
+ }
+
+ void AddOrCreateMappers(IEnumerable symbols, int index)
+ {
+ foreach (var member in symbols)
+ {
+ if (Settings.Filter.Include(member) && member is not ITypeSymbol)
+ {
+ if (!_members.TryGetValue(member, out MemberMapper mapper))
+ {
+ mapper = new MemberMapper(Settings);
+ _members.Add(member, mapper);
+ }
+ mapper.AddElement(member, index);
+ }
+ }
+ }
+ }
+
+ return _members.Values;
+ }
+ }
+}
diff --git a/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/Rule.cs b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/Rule.cs
new file mode 100644
index 000000000000..579ce07b12a4
--- /dev/null
+++ b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Abstractions/Rule.cs
@@ -0,0 +1,40 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+
+namespace Microsoft.DotNet.ApiCompatibility.Abstractions
+{
+ ///
+ /// Base class for Rules to use in order to be discovered and invoked by the
+ ///
+ public abstract class Rule
+ {
+ ///
+ /// Entrypoint to evaluate an
+ ///
+ /// The to evaluate.
+ /// The list of to add any differences to.
+ public virtual void Run(AssemblyMapper mapper, IList differences)
+ {
+ }
+
+ ///
+ /// Entrypoint to evaluate an
+ ///
+ /// The to evaluate.
+ /// The list of to add any differences to.
+ public virtual void Run(TypeMapper mapper, IList differences)
+ {
+ }
+
+ ///
+ /// Entrypoint to evaluate an
+ ///
+ /// The to evaluate.
+ /// The list of to add any differences to.
+ public virtual void Run(MemberMapper mapper, IList differences)
+ {
+ }
+ }
+}
diff --git a/src/Compatibility/Microsoft.DotNet.ApiCompatibility/ApiComparer.cs b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/ApiComparer.cs
new file mode 100644
index 000000000000..88f20828d31a
--- /dev/null
+++ b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/ApiComparer.cs
@@ -0,0 +1,86 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.CodeAnalysis;
+using Microsoft.DotNet.ApiCompatibility.Abstractions;
+using System.Collections.Generic;
+
+namespace Microsoft.DotNet.ApiCompatibility
+{
+ ///
+ /// Class that performs api comparison based on ISymbol inputs.
+ ///
+ public class ApiComparer
+ {
+ private readonly ComparingSettings _settings;
+
+ ///
+ /// Instantiate an object with the default .
+ ///
+ public ApiComparer() : this(new ComparingSettings()) { }
+
+ ///
+ /// Instantiate an object with the provided
+ ///
+ /// Settings to use for comparison.
+ public ApiComparer(ComparingSettings settings)
+ {
+ _settings = settings;
+ }
+
+ ///
+ /// Instantiate an object that includes comparing internal symbols.
+ ///
+ /// Indicates whether internal symbols should be included or not.
+ public ApiComparer(bool includeInternalSymbols)
+ {
+ _settings = new ComparingSettings(filter: new SymbolAccessibilityBasedFilter(includeInternalSymbols));
+ }
+
+ ///
+ /// Comma separated list to ignore diagnostic IDs.
+ ///
+ public string NoWarn { get; set; } = string.Empty;
+
+ ///
+ /// Array of diagnosticId and memberId to ignore those differences.
+ ///
+ public (string diagnosticId, string memberId)[] IgnoredDifferences { get; set; }
+
+ ///
+ /// Get's the differences when comparing Left vs Right based on the settings when instanciating the object.
+ /// It compares two lists of symbols.
+ ///
+ /// Left symbols to compare against.
+ /// Right symbols to compare against.
+ /// List of found differences.
+ public IEnumerable GetDifferences(IEnumerable left, IEnumerable right)
+ {
+ AssemblySetMapper mapper = new(_settings);
+ mapper.AddElement(left, 0);
+ mapper.AddElement(right, 1);
+
+ DiferenceVisitor visitor = new(noWarn: NoWarn, ignoredDifferences: IgnoredDifferences);
+ visitor.Visit(mapper);
+ return visitor.Differences;
+ }
+
+ ///
+ /// Get's the differences when comparing Left vs Right based on the settings when instanciating the object.
+ /// It compares two symbols.
+ ///
+ /// Left symbol to compare against.
+ /// Right symbol to compare against.
+ /// List of found differences.
+ public IEnumerable GetDifferences(IAssemblySymbol left, IAssemblySymbol right)
+ {
+ AssemblyMapper mapper = new(_settings);
+ mapper.AddElement(left, 0);
+ mapper.AddElement(right, 1);
+
+ DiferenceVisitor visitor = new(noWarn: NoWarn, ignoredDifferences: IgnoredDifferences);
+ visitor.Visit(mapper);
+ return visitor.Differences;
+ }
+ }
+}
diff --git a/src/Compatibility/Microsoft.DotNet.ApiCompatibility/AssemblySymbolLoader.cs b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/AssemblySymbolLoader.cs
new file mode 100644
index 000000000000..1b78c1aaab7f
--- /dev/null
+++ b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/AssemblySymbolLoader.cs
@@ -0,0 +1,377 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+
+namespace Microsoft.DotNet.ApiCompatibility
+{
+ ///
+ /// Class that loads objects from source files, binaries or directories containing binaries.
+ ///
+ public class AssemblySymbolLoader
+ {
+ private readonly HashSet _referenceSearchPaths = new();
+ private readonly List _warnings = new();
+ private readonly Dictionary _loadedAssemblies;
+ private readonly bool _resolveReferences;
+ private CSharpCompilation _cSharpCompilation;
+
+ ///
+ /// Instanciate an object with the desired setting to resolve assembly references or not.
+ ///
+ /// Indicates whether it should try to resolve assembly references when loading or not.
+ public AssemblySymbolLoader(bool resolveAssemblyReferences = false)
+ {
+ _loadedAssemblies = new Dictionary();
+ var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable);
+ _cSharpCompilation = CSharpCompilation.Create($"AssemblyLoader_{DateTime.Now:MM_dd_yy_HH_mm_ss_FFF}", options: compilationOptions);
+ _resolveReferences = resolveAssemblyReferences;
+ }
+
+ ///
+ /// Adds a set of paths to the search directories to resolve references from.
+ /// This is only used when the setting to resolve assembly references is set to true.
+ ///
+ /// Comma separated list of paths to register as search directories.
+ public void AddReferenceSearchDirectories(string paths) => AddReferenceSearchDirectories(SplitPaths(paths));
+
+ ///
+ /// Adds a set of paths to the search directories to resolve references from.
+ /// This is only used when the setting to resolve assembly references is set to true.
+ ///
+ /// The list of paths to register as search directories.
+ public void AddReferenceSearchDirectories(IEnumerable paths)
+ {
+ if (paths == null)
+ {
+ throw new ArgumentNullException(nameof(paths));
+ }
+
+ foreach (string path in paths)
+ _referenceSearchPaths.Add(path);
+ }
+
+ ///
+ /// Indicates if the used to resolve binaries has any roslyn diagnostics.
+ /// Might be useful when loading an assembly from source files.
+ ///
+ /// List of diagnostics.
+ /// True if there are any diagnostics, false otherwise.
+ public bool HasRoslynDiagnostics(out IEnumerable diagnostics)
+ {
+ diagnostics = _cSharpCompilation.GetDiagnostics();
+ return diagnostics.Any();
+ }
+
+ ///
+ /// Indicates if the loader emitted any warnings that might affect the assembly resolution.
+ ///
+ /// List of warnings.
+ /// True if there are any warnings, false otherwise.
+ public bool HasLoadWarnings(out IEnumerable warnings)
+ {
+ warnings = _warnings;
+ return _warnings.Count > 0;
+ }
+
+ private static string[] SplitPaths(string paths) =>
+ paths == null ? null : paths.Split(new char[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries);
+
+ ///
+ /// Loads a list of assemblies and gets its corresponding from the specified paths.
+ ///
+ /// Comma separated list of paths to load binaries from. Can be full paths to binaries or a directory.
+ /// The list of resolved .
+ public IEnumerable LoadAssemblies(string paths) => LoadAssemblies(SplitPaths(paths));
+
+ ///
+ /// Loads a list of assemblies and gets its corresponding from the specified paths.
+ ///
+ /// List of paths to load binaries from. Can be full paths to binaries or a directory.
+ /// The list of resolved .
+ public IEnumerable LoadAssemblies(IEnumerable paths)
+ {
+ if (paths == null)
+ {
+ throw new ArgumentNullException(nameof(paths));
+ }
+
+ IEnumerable assembliesToReturn = LoadFromPaths(paths);
+
+ List result = new();
+ foreach (MetadataReference metadataReference in assembliesToReturn)
+ {
+ ISymbol symbol = _cSharpCompilation.GetAssemblyOrModuleSymbol(metadataReference);
+ if (symbol is IAssemblySymbol assemblySymbol)
+ {
+ result.Add(assemblySymbol);
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// Loads an assembly from the provided path.
+ ///
+ /// The full path to the assembly.
+ /// representing the loaded assembly.
+ public IAssemblySymbol LoadAssembly(string path)
+ {
+ if (path == null)
+ {
+ throw new ArgumentNullException(nameof(path));
+ }
+
+ MetadataReference metadataReference = CreateOrGetMetadataReferenceFromPath(path);
+ return (IAssemblySymbol)_cSharpCompilation.GetAssemblyOrModuleSymbol(metadataReference);
+ }
+
+ ///
+ /// Loads an assembly using the provided name from a given .
+ ///
+ /// The name to use to resolve the assembly.
+ /// The stream to read the metadata from.
+ /// respresenting the given . If an
+ /// assembly with the same was already loaded, the previously loaded assembly is returned.
+ public IAssemblySymbol LoadAssembly(string name, Stream stream)
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ if (stream == null)
+ {
+ throw new ArgumentNullException(nameof(stream));
+ }
+
+ if (stream.Position >= stream.Length)
+ {
+ throw new ArgumentException("Stream position is greater than it's length, so there are no contents available to read", nameof(stream));
+ }
+
+ if (!_loadedAssemblies.TryGetValue(name, out MetadataReference metadataReference))
+ {
+ metadataReference = CreateAndAddReferenceToCompilation(name, stream);
+ }
+
+ return (IAssemblySymbol)_cSharpCompilation.GetAssemblyOrModuleSymbol(metadataReference);
+ }
+
+ ///
+ /// Loads an containing the metadata from the provided source files and given assembly name.
+ ///
+ /// The file paths to use as syntax trees to create the .
+ /// The name of the .
+ /// Paths to use as references if we want all the references to be included in the metadata.
+ /// The containing the metadata from the provided source files.
+ public IAssemblySymbol LoadAssemblyFromSourceFiles(IEnumerable filePaths, string assemblyName, IEnumerable referencePaths)
+ {
+ if (filePaths == null || filePaths.Count() == 0)
+ {
+ throw new ArgumentNullException(nameof(filePaths), $"Should not be null and contain at least one element");
+ }
+
+ if (string.IsNullOrEmpty(assemblyName))
+ {
+ throw new ArgumentNullException(nameof(assemblyName), $"Should provide a valid assembly name");
+ }
+
+ _cSharpCompilation = _cSharpCompilation.WithAssemblyName(assemblyName);
+
+ List syntaxTrees = new();
+ foreach (string filePath in filePaths)
+ {
+ if (!File.Exists(filePath))
+ {
+ throw new FileNotFoundException($"File '{filePath}' does not exist.");
+ }
+
+ syntaxTrees.Add(CSharpSyntaxTree.ParseText(File.ReadAllText(filePath)));
+ }
+
+ _cSharpCompilation = _cSharpCompilation.AddSyntaxTrees(syntaxTrees);
+
+ LoadFromPaths(referencePaths);
+ return _cSharpCompilation.Assembly;
+ }
+
+ ///
+ /// Loads a list of matching assemblies given the "from" list that we should try and load it's matching assembly from the given paths.
+ ///
+ /// List of to search for.
+ /// List of search paths.
+ /// Indicates if we should validate that the identity of the resolved assembly is the same.
+ /// Indicates if a warning should be added to the warning list when a matching assembly is not found.
+ /// The list of matching assemblies represented as .
+ public IEnumerable LoadMatchingAssemblies(IEnumerable fromAssemblies, IEnumerable searchPaths, bool validateMatchingIdentity = true, bool warnOnMissingAssemblies = true)
+ {
+ if (fromAssemblies == null)
+ {
+ throw new ArgumentNullException(nameof(fromAssemblies));
+ }
+
+ if (searchPaths == null)
+ {
+ throw new ArgumentNullException(nameof(searchPaths));
+ }
+
+ List matchingAssemblies = new();
+ foreach (IAssemblySymbol assembly in fromAssemblies)
+ {
+ bool found = false;
+ string name = $"{assembly.Name}.dll";
+ foreach (string directory in searchPaths)
+ {
+ if (!Directory.Exists(directory))
+ {
+ throw new FileNotFoundException($"Matching assembly search directory '{directory}' does not exist", nameof(searchPaths));
+ }
+
+ string possiblePath = Path.Combine(directory, name);
+ if (File.Exists(possiblePath))
+ {
+ MetadataReference reference = CreateOrGetMetadataReferenceFromPath(possiblePath);
+ ISymbol symbol = _cSharpCompilation.GetAssemblyOrModuleSymbol(reference);
+ if (symbol is IAssemblySymbol matchingAssembly)
+ {
+ if (validateMatchingIdentity && !matchingAssembly.Identity.Equals(assembly.Identity))
+ {
+ _cSharpCompilation = _cSharpCompilation.RemoveReferences(new[] { reference });
+ _loadedAssemblies.Remove(name);
+ continue;
+ }
+
+ matchingAssemblies.Add(matchingAssembly);
+ found = true;
+ break;
+ }
+ }
+ }
+
+ if (warnOnMissingAssemblies && !found)
+ {
+ string assemblyInfo = validateMatchingIdentity ? assembly.Identity.GetDisplayName() : assembly.Name;
+ _warnings.Add($"Could not find matching assembly: '{assemblyInfo}' in any of the search directories.");
+ }
+ }
+
+ return matchingAssemblies;
+ }
+
+ private IEnumerable LoadFromPaths(IEnumerable paths)
+ {
+ List result = new();
+ foreach (string path in paths)
+ {
+ string resolvedPath = Environment.ExpandEnvironmentVariables(path);
+ string directory = null;
+ if (Directory.Exists(resolvedPath))
+ {
+ directory = resolvedPath;
+ result.AddRange(LoadAssembliesFromDirectory(resolvedPath));
+ }
+ else if (File.Exists(resolvedPath))
+ {
+ directory = Path.GetDirectoryName(resolvedPath);
+ result.Add(CreateOrGetMetadataReferenceFromPath(resolvedPath));
+ }
+ else
+ {
+ _warnings.Add($"Could not find the provided path '{resolvedPath}' to load binaries from.");
+ }
+
+ if (_resolveReferences && !string.IsNullOrEmpty(directory))
+ _referenceSearchPaths.Add(directory);
+ }
+
+ return result;
+ }
+
+ private IEnumerable LoadAssembliesFromDirectory(string directory)
+ {
+ foreach (string assembly in Directory.EnumerateFiles(directory, "*.dll"))
+ {
+ yield return CreateOrGetMetadataReferenceFromPath(assembly);
+ }
+ }
+
+ private MetadataReference CreateOrGetMetadataReferenceFromPath(string path)
+ {
+ // Roslyn doesn't support having two assemblies as references with the same identity and then getting the symbol for it.
+ string name = Path.GetFileName(path);
+ if (!_loadedAssemblies.TryGetValue(name, out MetadataReference metadataReference))
+ {
+ using FileStream stream = File.OpenRead(path);
+ metadataReference = CreateAndAddReferenceToCompilation(name, stream);
+ }
+
+ return metadataReference;
+ }
+
+ private MetadataReference CreateAndAddReferenceToCompilation(string name, Stream fileStream)
+ {
+ // If we need to resolve references we can't reuse the same stream after creating the metadata
+ // reference from it as Roslyn closes it. So instead we use PEReader and get the bytes
+ // and create the metadata reference from that.
+ using PEReader reader = new(fileStream);
+
+ if (!reader.HasMetadata)
+ {
+ throw new ArgumentException($"Provided stream for assembly {name} doesn't have any metadata to read from");
+ }
+
+ PEMemoryBlock image = reader.GetEntireImage();
+ MetadataReference metadataReference = MetadataReference.CreateFromImage(image.GetContent());
+ _loadedAssemblies.Add(name, metadataReference);
+ _cSharpCompilation = _cSharpCompilation.AddReferences(new MetadataReference[] { metadataReference });
+
+ if (_resolveReferences)
+ {
+ ResolveReferences(reader);
+ }
+
+
+ return metadataReference;
+ }
+
+ private void ResolveReferences(PEReader peReader)
+ {
+ MetadataReader reader = peReader.GetMetadataReader();
+ foreach (AssemblyReferenceHandle handle in reader.AssemblyReferences)
+ {
+ AssemblyReference reference = reader.GetAssemblyReference(handle);
+ string name = $"{reader.GetString(reference.Name)}.dll";
+ bool found = _loadedAssemblies.TryGetValue(name, out MetadataReference _);
+ if (!found)
+ {
+ foreach (string directory in _referenceSearchPaths)
+ {
+ string potentialPath = Path.Combine(directory, name);
+ if (File.Exists(potentialPath))
+ {
+ // TODO: add version check and add a warning if it doesn't match?
+ using FileStream resolvedStream = File.OpenRead(potentialPath);
+ CreateAndAddReferenceToCompilation(name, resolvedStream);
+ found = true;
+ break;
+ }
+ }
+ }
+
+ if (!found)
+ {
+ _warnings.Add($"Could not resolve reference '{name}' in any of the provided search directories.");
+ }
+ }
+ }
+ }
+}
diff --git a/src/Compatibility/Microsoft.DotNet.ApiCompatibility/ComparingSettings.cs b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/ComparingSettings.cs
new file mode 100644
index 000000000000..d218ec6e4f43
--- /dev/null
+++ b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/ComparingSettings.cs
@@ -0,0 +1,44 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.CodeAnalysis;
+using Microsoft.DotNet.ApiCompatibility.Abstractions;
+using Microsoft.DotNet.ApiCompatibility.Rules;
+using System.Collections.Generic;
+
+namespace Microsoft.DotNet.ApiCompatibility
+{
+ ///
+ /// Class that contains all the settings used to filter metadata, compare symbols and run rules.
+ ///
+ public class ComparingSettings
+ {
+ ///
+ /// The factory to get the .
+ ///
+ public IRuleRunnerFactory RuleRunnerFactory { get; }
+
+ ///
+ /// The metadata filter to use when creating the .
+ ///
+ public ISymbolFilter Filter { get; }
+
+ ///
+ /// The comparer to map metadata.
+ ///
+ public IEqualityComparer EqualityComparer { get; }
+
+ ///
+ /// Instantiate an object with the desired settings.
+ ///
+ /// The factory to create a
+ /// The symbol filter.
+ /// The comparer to map metadata.
+ public ComparingSettings(IRuleRunnerFactory ruleRunnerFactory = null, ISymbolFilter filter = null, IEqualityComparer equalityComparer = null)
+ {
+ RuleRunnerFactory = ruleRunnerFactory ?? new RuleRunnerFactory();
+ Filter = filter ?? new SymbolAccessibilityBasedFilter(includeInternalSymbols: false);
+ EqualityComparer = equalityComparer ?? new DefaultSymbolsEqualityComparer();
+ }
+ }
+}
diff --git a/src/Compatibility/Microsoft.DotNet.ApiCompatibility/DefaultSymbolsEqualityComparer.cs b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/DefaultSymbolsEqualityComparer.cs
new file mode 100644
index 000000000000..3d6af65d7507
--- /dev/null
+++ b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/DefaultSymbolsEqualityComparer.cs
@@ -0,0 +1,29 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.CodeAnalysis;
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.DotNet.ApiCompatibility
+{
+ internal class DefaultSymbolsEqualityComparer : IEqualityComparer
+ {
+ public bool Equals(ISymbol x, ISymbol y) =>
+ string.Equals(GetKey(x), GetKey(y), StringComparison.OrdinalIgnoreCase);
+
+ public int GetHashCode(ISymbol obj) =>
+ GetKey(obj).GetHashCode();
+
+ private static string GetKey(ISymbol symbol) =>
+ symbol switch
+ {
+ IMethodSymbol => symbol.ToDisplayString(),
+ IFieldSymbol => symbol.ToDisplayString(),
+ IPropertySymbol => symbol.ToDisplayString(),
+ IEventSymbol => symbol.ToDisplayString(),
+ ITypeSymbol => symbol.ToDisplayString(),
+ _ => symbol.Name,
+ };
+ }
+}
diff --git a/src/Compatibility/Microsoft.DotNet.ApiCompatibility/DiagnosticBag.cs b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/DiagnosticBag.cs
new file mode 100644
index 000000000000..9450fafcff26
--- /dev/null
+++ b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/DiagnosticBag.cs
@@ -0,0 +1,78 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.DotNet.ApiCompatibility.Abstractions;
+using System.Collections.Generic;
+
+namespace Microsoft.DotNet.ApiCompatibility
+{
+ ///
+ /// This is a bag that contains a list of and filters them out based on the
+ /// noWarn and ignoredDifferences settings when they are added to the bag.
+ ///
+ /// Type to represent the diagnostics.
+ public class DiagnosticBag where T : IDiagnostic
+ {
+ private readonly Dictionary> _ignore;
+ private readonly HashSet _noWarn;
+
+ private readonly List _differences = new();
+
+ ///
+ /// Instantiate an diagnostic bag with the provided settings to ignore diagnostics.
+ ///
+ /// Comma separated list of diagnostic IDs to ignore.
+ /// An array of differences to ignore based on diagnostic ID and reference ID.
+ public DiagnosticBag(string noWarn, (string diagnosticId, string referenceId)[] ignoredDifferences)
+ {
+ _noWarn = new HashSet(noWarn?.Split(';'));
+ _ignore = new Dictionary>();
+
+ foreach ((string diagnosticId, string referenceId) in ignoredDifferences)
+ {
+ if (!_ignore.TryGetValue(diagnosticId, out HashSet members))
+ {
+ members = new HashSet();
+ _ignore.Add(diagnosticId, members);
+ }
+
+ members.Add(referenceId);
+ }
+ }
+
+ ///
+ /// Adds the differences to the diagnostic bag if they are not found in the exclusion settings.
+ ///
+ /// The differences to add.
+ public void AddRange(IEnumerable differences)
+ {
+ foreach (T difference in differences)
+ Add(difference);
+ }
+
+ ///
+ /// Adds a difference to the diagnostic bag if they are not found in the exclusion settings.
+ ///
+ /// The difference to add.
+ public void Add(T difference)
+ {
+ if (_noWarn.Contains(difference.DiagnosticId))
+ return;
+
+ if (_ignore.TryGetValue(difference.DiagnosticId, out HashSet members))
+ {
+ if (members.Contains(difference.ReferenceId))
+ {
+ return;
+ }
+ }
+
+ _differences.Add(difference);
+ }
+
+ ///
+ /// A list of differences contained in the diagnostic bag.
+ ///
+ public IEnumerable Differences => _differences;
+ }
+}
diff --git a/src/Compatibility/Microsoft.DotNet.ApiCompatibility/DiagnosticIds.cs b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/DiagnosticIds.cs
new file mode 100644
index 000000000000..9114576645f4
--- /dev/null
+++ b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/DiagnosticIds.cs
@@ -0,0 +1,14 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Microsoft.DotNet.ApiCompatibility
+{
+ ///
+ /// Class containing the strings representing the Diagnostic IDs that can be returned in the compatibility differences.
+ ///
+ public static class DiagnosticIds
+ {
+ public const string TypeMustExist = "CP0001";
+ public const string MemberMustExist = "CP0002";
+ }
+}
diff --git a/src/Compatibility/Microsoft.DotNet.ApiCompatibility/DiferenceVisitor.cs b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/DiferenceVisitor.cs
new file mode 100644
index 000000000000..c725ecd24df1
--- /dev/null
+++ b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/DiferenceVisitor.cs
@@ -0,0 +1,80 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.DotNet.ApiCompatibility.Abstractions;
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.DotNet.ApiCompatibility
+{
+ ///
+ /// The visitor that traverses the mappers' tree and gets it's differences in a .
+ ///
+ public class DiferenceVisitor : MapperVisitor
+ {
+ private readonly DiagnosticBag _differenceBag;
+
+ ///
+ /// Instantiates the visitor with the desired settings.
+ ///
+ /// A comma separated list of diagnostic IDs to ignore.
+ /// A list of tuples to ignore diagnostic IDs by symbol.
+ public DiferenceVisitor(string noWarn = null, (string diagnosticId, string symbolId)[] ignoredDifferences = null)
+ {
+ _differenceBag = new DiagnosticBag(noWarn ?? string.Empty, ignoredDifferences ?? Array.Empty<(string, string)>());
+ }
+
+ ///
+ /// Visits an and adds it's differences to the .
+ ///
+ /// The mapper to visit.
+ public override void Visit(AssemblyMapper assembly)
+ {
+ if (assembly == null)
+ {
+ throw new ArgumentNullException(nameof(assembly));
+ }
+
+ _differenceBag.AddRange(assembly.GetDifferences());
+ base.Visit(assembly);
+ }
+
+ ///
+ /// Visits an and adds it's differences to the .
+ ///
+ /// The mapper to visit.
+ public override void Visit(TypeMapper type)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException(nameof(type));
+ }
+
+ _differenceBag.AddRange(type.GetDifferences());
+
+ if (type.ShouldDiffMembers)
+ {
+ base.Visit(type);
+ }
+ }
+
+ ///
+ /// Visits an and adds it's differences to the .
+ ///
+ /// The mapper to visit.
+ public override void Visit(MemberMapper member)
+ {
+ if (member == null)
+ {
+ throw new ArgumentNullException(nameof(member));
+ }
+
+ _differenceBag.AddRange(member.GetDifferences());
+ }
+
+ ///
+ /// The differences that the has at this point.
+ ///
+ public IEnumerable Differences => _differenceBag.Differences;
+ }
+}
diff --git a/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Extensions/TypeSymbolExtensions.cs b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Extensions/TypeSymbolExtensions.cs
new file mode 100644
index 000000000000..095c0e4432a6
--- /dev/null
+++ b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Extensions/TypeSymbolExtensions.cs
@@ -0,0 +1,30 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.CodeAnalysis;
+using System.Collections.Generic;
+
+namespace Microsoft.DotNet.ApiCompatibility.Extensions
+{
+ internal static class TypeSymbolExtensions
+ {
+ internal static IEnumerable GetAllBaseTypes(this ITypeSymbol type)
+ {
+ if (type.TypeKind == TypeKind.Interface)
+ {
+ foreach (ITypeSymbol @interface in type.Interfaces)
+ {
+ yield return @interface;
+ foreach (ITypeSymbol baseInterface in @interface.GetAllBaseTypes())
+ yield return baseInterface;
+ }
+ }
+ else if (type.BaseType != null)
+ {
+ yield return type.BaseType;
+ foreach (ITypeSymbol baseType in type.BaseType.GetAllBaseTypes())
+ yield return baseType;
+ }
+ }
+ }
+}
diff --git a/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Microsoft.DotNet.ApiCompatibility.csproj b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Microsoft.DotNet.ApiCompatibility.csproj
new file mode 100644
index 000000000000..ce1ea9aa3968
--- /dev/null
+++ b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Microsoft.DotNet.ApiCompatibility.csproj
@@ -0,0 +1,18 @@
+
+
+
+ netstandard2.0
+ true
+ true
+ Open
+ 0.1.0
+
+ RS1024;$(NoWarn)
+
+
+
+
+
+
+
+
diff --git a/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Rules/MembersMustExist.cs b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Rules/MembersMustExist.cs
new file mode 100644
index 000000000000..ad310a7b9fe4
--- /dev/null
+++ b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Rules/MembersMustExist.cs
@@ -0,0 +1,103 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.CodeAnalysis;
+using Microsoft.DotNet.ApiCompatibility.Abstractions;
+using Microsoft.DotNet.ApiCompatibility.Extensions;
+using System.Collections.Generic;
+
+namespace Microsoft.DotNet.ApiCompatibility.Rules
+{
+ ///
+ /// Rule that evaluates whether a member exists on Left and Right.
+ /// If the member doesn't exist on Right but it does on Left, it adds a to the list of differences.
+ ///
+ public class MembersMustExist : Rule
+ {
+ ///
+ /// Evaluates whether a type exists on both sides of the .
+ ///
+ /// The to evaluate.
+ /// The list of to add differences to.
+ public override void Run(TypeMapper mapper, IList differences)
+ {
+ ITypeSymbol left = mapper.Left;
+ if (left != null && mapper.Right == null)
+ differences.Add(new CompatDifference(DiagnosticIds.TypeMustExist, $"Type '{left.ToDisplayString()}' exists on the left but not on the right", DifferenceType.Removed, left));
+ }
+
+ ///
+ /// Evaluates whether member (Field, Property, Method, Constructor, Event) exists on both sides of the .
+ ///
+ /// The to evaluate.
+ /// The list of to add differences to.
+ public override void Run(MemberMapper mapper, IList differences)
+ {
+ ISymbol left = mapper.Left;
+ if (left != null && mapper.Right == null)
+ {
+ // Events and properties are handled via their accessors.
+ if (left.Kind == SymbolKind.Property || left.Kind == SymbolKind.Event)
+ return;
+
+ if (left is IMethodSymbol method)
+ {
+ // Will be handled by a different rule
+ if (method.MethodKind == MethodKind.ExplicitInterfaceImplementation)
+ return;
+
+ // If method is an override or hides a base type definition removing it from right is compatible.
+ if (method.IsOverride || FindMatchingOnBaseType(method))
+ return;
+ }
+
+ differences.Add(new CompatDifference(DiagnosticIds.MemberMustExist, $"Member '{left.ToDisplayString()}' exists on the left but not on the right", DifferenceType.Removed, left));
+ }
+ }
+
+ private bool FindMatchingOnBaseType(IMethodSymbol method)
+ {
+ foreach (ITypeSymbol type in method.ContainingType.GetAllBaseTypes())
+ {
+ foreach (ISymbol symbol in type.GetMembers())
+ {
+ if (symbol is IMethodSymbol candidate && IsMatchingMethod(method, candidate))
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private bool IsMatchingMethod(IMethodSymbol method, IMethodSymbol candidate)
+ {
+ if (method.Name == candidate.Name)
+ {
+ if (ParametersMatch(method, candidate) && ReturnTypesMatch(method, candidate))
+ {
+ if (method.TypeParameters.Length == candidate.TypeParameters.Length)
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private bool ReturnTypesMatch(IMethodSymbol method, IMethodSymbol candidate) =>
+ method.ReturnType.ToDisplayString() == candidate.ReturnType.ToDisplayString();
+
+ private bool ParametersMatch(IMethodSymbol method, IMethodSymbol candidate)
+ {
+ if (method.Parameters.Length != candidate.Parameters.Length)
+ return false;
+
+ for (int i = 0; i < method.Parameters.Length; i++)
+ {
+ if (method.Parameters[i].Type.ToDisplayString() != method.Parameters[i].Type.ToDisplayString())
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Rules/RuleRunner.cs b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Rules/RuleRunner.cs
new file mode 100644
index 000000000000..c354949516c2
--- /dev/null
+++ b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Rules/RuleRunner.cs
@@ -0,0 +1,75 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.s
+
+using Microsoft.DotNet.ApiCompatibility.Abstractions;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Microsoft.DotNet.ApiCompatibility.Rules
+{
+ internal class RuleRunner : IRuleRunner
+ {
+ private readonly Rule[] _rules;
+
+ internal RuleRunner()
+ {
+ _rules = GetRules();
+ }
+
+ public IEnumerable Run(ElementMapper mapper)
+ {
+ List differences = new();
+ if (mapper is AssemblyMapper am)
+ {
+ Run(am, differences);
+ }
+ if (mapper is TypeMapper tm)
+ {
+ Run(tm, differences);
+ }
+ if (mapper is MemberMapper mm)
+ {
+ Run(mm, differences);
+ }
+
+ return differences;
+ }
+
+ private void Run(AssemblyMapper mapper, List differences)
+ {
+ foreach (Rule rule in _rules)
+ {
+ rule.Run(mapper, differences);
+ }
+ }
+
+ private void Run(TypeMapper mapper, List differences)
+ {
+ foreach (Rule rule in _rules)
+ {
+ rule.Run(mapper, differences);
+ }
+ }
+
+ private void Run(MemberMapper mapper, List differences)
+ {
+ foreach (Rule rule in _rules)
+ {
+ rule.Run(mapper, differences);
+ }
+ }
+
+ private Rule[] GetRules()
+ {
+ List rules = new();
+ foreach (Type type in GetType().Assembly.GetTypes())
+ {
+ if (!type.IsAbstract && typeof(Rule).IsAssignableFrom(type))
+ rules.Add((Rule)Activator.CreateInstance(type));
+ }
+
+ return rules.ToArray();
+ }
+ }
+}
diff --git a/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Rules/RuleRunnerFactory.cs b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Rules/RuleRunnerFactory.cs
new file mode 100644
index 000000000000..1c3b87008252
--- /dev/null
+++ b/src/Compatibility/Microsoft.DotNet.ApiCompatibility/Rules/RuleRunnerFactory.cs
@@ -0,0 +1,19 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.s
+
+using Microsoft.DotNet.ApiCompatibility.Abstractions;
+
+namespace Microsoft.DotNet.ApiCompatibility.Rules
+{
+ public class RuleRunnerFactory : IRuleRunnerFactory
+ {
+ private RuleRunner _driver;
+ public IRuleRunner GetRuleRunner()
+ {
+ if (_driver == null)
+ _driver = new RuleRunner();
+
+ return _driver;
+ }
+ }
+}
diff --git a/src/Tests/Microsoft.DotNet.ApiCompatibility.Tests/AssemblySymbolLoaderTests.cs b/src/Tests/Microsoft.DotNet.ApiCompatibility.Tests/AssemblySymbolLoaderTests.cs
new file mode 100644
index 000000000000..7e062ee58b7b
--- /dev/null
+++ b/src/Tests/Microsoft.DotNet.ApiCompatibility.Tests/AssemblySymbolLoaderTests.cs
@@ -0,0 +1,356 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using Microsoft.CodeAnalysis;
+using Microsoft.DotNet.Cli.Utils;
+using Microsoft.NET.TestFramework;
+using Microsoft.NET.TestFramework.Assertions;
+using Microsoft.NET.TestFramework.Commands;
+using Microsoft.NET.TestFramework.ProjectConstruction;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.DotNet.ApiCompatibility.Tests
+{
+ public class AssemblySymbolLoaderTests : SdkTest
+ {
+ public AssemblySymbolLoaderTests(ITestOutputHelper log) : base(log) { }
+
+ private const string SimpleAssemblySourceContents = @"
+namespace MyNamespace
+{
+ public class MyClass
+ {
+ }
+}
+";
+
+ // Since we use typeof(string).Assembly.Location to resolve references
+ // We need to target a framework compatible with what the test is being
+ // built for so that we resolve the references correctly.
+#if NETCOREAPP
+ private const string TargetFrameworks = "net5.0";
+#else
+ private const string TargetFrameworks = "net471";
+#endif
+
+ private class TestAssetInfo
+ {
+ public TestAsset TestAsset { get; set; }
+ public string OutputDirectory { get; set; }
+ }
+
+ // We use the same asset in multiple tests.
+ // Creating a TestAsset and building it for each test
+ // creates a lot of overhead, using the cache
+ // speeds up test execution ~3x in this test assembly.
+ // Tests within the same class run serially in xunit, so
+ // it is fine to reuse the same asset.
+ private class TestAssetCache
+ {
+ public static TestAssetCache Instance = new TestAssetCache();
+
+ private ConcurrentDictionary Dictionary = new();
+
+ private TestAssetInfo _asset = null;
+
+ public TestAssetInfo GetSimpleAsset(TestAssetsManager manager)
+ => _asset ?? InitAsset(manager);
+
+ private TestAssetInfo InitAsset(TestAssetsManager manager)
+ {
+ Interlocked.CompareExchange(ref _asset, GetAsset(manager), null);
+ return _asset;
+ }
+
+ private TestAssetInfo GetAsset(TestAssetsManager manager)
+ {
+ TestProject project = new("SimpleAsset")
+ {
+ TargetFrameworks = TargetFrameworks,
+ IsExe = false
+ };
+
+ project.SourceFiles.Add("MyClass.cs", SimpleAssemblySourceContents);
+
+ TestAsset testAsset = manager.CreateTestProject(project);
+ BuildTestAsset(testAsset, out string outDir)
+ .Should()
+ .Pass();
+
+ return new TestAssetInfo()
+ {
+ TestAsset = testAsset,
+ OutputDirectory = outDir,
+ };
+ }
+ }
+
+ private TestAssetInfo GetSimpleTestAsset() => TestAssetCache.Instance.GetSimpleAsset(_testAssetsManager);
+
+ [Fact]
+ public void LoadAssembly_Throws()
+ {
+ AssemblySymbolLoader loader = new();
+ Assert.Throws(() => loader.LoadAssembly(Guid.NewGuid().ToString("N").Substring(0, 8)));
+ Assert.Throws("path", () => loader.LoadAssembly(null));
+ Assert.Throws("stream", () => loader.LoadAssembly("Assembly", null));
+ Assert.Throws("name", () => loader.LoadAssembly(null, new MemoryStream()));
+ }
+
+ [Fact]
+ public void LoadAssemblies_Throws()
+ {
+ AssemblySymbolLoader loader = new();
+ Assert.Throws("paths", () => loader.LoadAssemblies((string)null));
+ Assert.Throws("paths", () => loader.LoadAssemblies((IEnumerable)null));
+ }
+
+ [Fact]
+ public void LoadAssemblyFromSourceFiles_Throws()
+ {
+ AssemblySymbolLoader loader = new();
+ IEnumerable paths = new[] { Guid.NewGuid().ToString("N") };
+ Assert.Throws(() => loader.LoadAssemblyFromSourceFiles(paths, "assembly1", Array.Empty()));
+ Assert.Throws("filePaths", () => loader.LoadAssemblyFromSourceFiles(null, "assembly1", Array.Empty()));
+ Assert.Throws("filePaths", () => loader.LoadAssemblyFromSourceFiles(Array.Empty(), "assembly1", Array.Empty()));
+ Assert.Throws("assemblyName", () => loader.LoadAssemblyFromSourceFiles(paths, null, Array.Empty()));
+ }
+
+ [Fact]
+ public void LoadMatchingAssemblies_Throws()
+ {
+ AssemblySymbolLoader loader = new();
+ IEnumerable paths = new[] { Guid.NewGuid().ToString("N") };
+ IAssemblySymbol assembly = SymbolFactory.GetAssemblyFromSyntax("namespace MyNamespace { class Foo { } }");
+
+ Assert.Throws(() => loader.LoadMatchingAssemblies(new[] { assembly }, paths));
+ Assert.Throws("fromAssemblies", () => loader.LoadMatchingAssemblies(null, paths));
+ Assert.Throws("searchPaths", () => loader.LoadMatchingAssemblies(Array.Empty(), null));
+ }
+
+ [Fact]
+ public void LoadMatchingAssembliesWarns()
+ {
+ IAssemblySymbol assembly = SymbolFactory.GetAssemblyFromSyntax("namespace MyNamespace { class Foo { } }");
+ IEnumerable paths = new[] { AppContext.BaseDirectory };
+
+ AssemblySymbolLoader loader = new();
+ IEnumerable symbols = loader.LoadMatchingAssemblies(new[] { assembly }, paths);
+ Assert.Empty(symbols);
+ Assert.True(loader.HasLoadWarnings(out IEnumerable warnings));
+
+ IEnumerable expected = new[]
+ {
+ $"Could not find matching assembly: '{assembly.Identity.GetDisplayName()}' in any of the search directories."
+ };
+
+ Assert.Equal(expected, warnings, StringComparer.Ordinal);
+ }
+
+ [Fact]
+ public void LoadMatchingAssembliesSameIdentitySucceeds()
+ {
+ string assemblyName = nameof(LoadMatchingAssembliesSameIdentitySucceeds);
+ IAssemblySymbol fromAssembly = SymbolFactory.GetAssemblyFromSyntax(SimpleAssemblySourceContents, assemblyName: assemblyName);
+
+ TestProject testProject = new(assemblyName)
+ {
+ TargetFrameworks = "net5.0",
+ IsExe = false,
+ };
+
+ testProject.SourceFiles.Add("MyClass.cs", SimpleAssemblySourceContents);
+ testProject.AdditionalProperties.Add("AssemblyVersion", "0.0.0.0");
+ TestAsset testAsset = _testAssetsManager.CreateTestProject(testProject);
+
+ BuildTestAsset(testAsset, out string outputDirectory)
+ .Should()
+ .Pass();
+
+ AssemblySymbolLoader loader = new();
+ IEnumerable matchingAssemblies = loader.LoadMatchingAssemblies(new[] { fromAssembly }, new[] { outputDirectory });
+
+ Assert.Single(matchingAssemblies);
+ Assert.False(loader.HasLoadWarnings(out var _));
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void LoadMatchingAssemblies_DifferentIdentity(bool validateIdentities)
+ {
+ var assetInfo = GetSimpleTestAsset();
+ IAssemblySymbol fromAssembly = SymbolFactory.GetAssemblyFromSyntax(SimpleAssemblySourceContents, assemblyName: assetInfo.TestAsset.TestProject.Name);
+
+ AssemblySymbolLoader loader = new();
+ IEnumerable matchingAssemblies = loader.LoadMatchingAssemblies(new[] { fromAssembly }, new[] { assetInfo.OutputDirectory }, validateMatchingIdentity: validateIdentities);
+
+ if (validateIdentities)
+ {
+ Assert.Empty(matchingAssemblies);
+ Assert.True(loader.HasLoadWarnings(out IEnumerable warnings));
+
+ IEnumerable expected = new[]
+ {
+ $"Could not find matching assembly: '{fromAssembly.Identity.GetDisplayName()}' in any of the search directories."
+ };
+
+ Assert.Equal(expected, warnings, StringComparer.Ordinal);
+ }
+ else
+ {
+ Assert.Single(matchingAssemblies);
+ Assert.False(loader.HasLoadWarnings(out var _));
+ Assert.NotEqual(fromAssembly.Identity, matchingAssemblies.FirstOrDefault().Identity);
+ }
+ }
+
+ [Fact]
+ public void LoadsSimpleAssemblyFromDirectory()
+ {
+ var assetInfo = GetSimpleTestAsset();
+ AssemblySymbolLoader loader = new();
+ IEnumerable symbols = loader.LoadAssemblies(assetInfo.OutputDirectory);
+ Assert.Single(symbols);
+
+ IEnumerable types = symbols.FirstOrDefault()
+ .GlobalNamespace
+ .GetNamespaceMembers()
+ .FirstOrDefault()
+ .GetTypeMembers();
+
+ Assert.Single(types);
+ Assert.Equal("MyNamespace.MyClass", types.FirstOrDefault().ToDisplayString());
+ }
+
+ [Fact]
+ public void LoadSimpleAssemblyFullPath()
+ {
+ var assetInfo = GetSimpleTestAsset();
+ AssemblySymbolLoader loader = new();
+ IAssemblySymbol symbol = loader.LoadAssembly(Path.Combine(assetInfo.OutputDirectory, assetInfo.TestAsset.TestProject.Name + ".dll"));
+
+ IEnumerable types = symbol.GlobalNamespace
+ .GetNamespaceMembers()
+ .FirstOrDefault()
+ .GetTypeMembers();
+
+ Assert.Single(types);
+ Assert.Equal("MyNamespace.MyClass", types.FirstOrDefault().ToDisplayString());
+ }
+
+ [Fact]
+ public void LoadsMultipleAssembliesFromDirectory()
+ {
+ TestProject first = new("LoadsMultipleAssembliesFromDirectory_First")
+ {
+ TargetFrameworks = TargetFrameworks,
+ IsExe = false
+ };
+
+ TestProject second = new("LoadsMultipleAssembliesFromDirectory_Second")
+ {
+ TargetFrameworks = TargetFrameworks,
+ IsExe = false
+ };
+
+ first.ReferencedProjects.Add(second);
+ TestAsset testAsset = _testAssetsManager.CreateTestProject(first);
+
+ BuildTestAsset(testAsset, out string outputDirectory)
+ .Should()
+ .Pass();
+
+ AssemblySymbolLoader loader = new();
+ IEnumerable symbols = loader.LoadAssemblies(outputDirectory);
+
+ Assert.Equal(2, symbols.Count());
+
+ IEnumerable expected = new[] { "LoadsMultipleAssembliesFromDirectory_First", "LoadsMultipleAssembliesFromDirectory_Second" };
+ IEnumerable actual = symbols.Select(a => a.Name).OrderBy(a => a, StringComparer.Ordinal);
+
+ Assert.Equal(expected, actual, StringComparer.Ordinal);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void LoadAssemblyResolveReferences_WarnsWhenEnabled(bool resolveReferences)
+ {
+ var assetInfo = GetSimpleTestAsset();
+ AssemblySymbolLoader loader = new(resolveAssemblyReferences: resolveReferences);
+ loader.LoadAssembly(Path.Combine(assetInfo.OutputDirectory, assetInfo.TestAsset.TestProject.Name + ".dll"));
+
+ if (resolveReferences)
+ {
+ Assert.True(loader.HasLoadWarnings(out IEnumerable warnings));
+
+ string expectedReference = "System.Runtime.dll";
+
+ if (TargetFrameworks.StartsWith("net4", StringComparison.OrdinalIgnoreCase))
+ {
+ expectedReference = "mscorlib.dll";
+ }
+
+ IEnumerable expected = new[]
+ {
+ $"Could not resolve reference '{expectedReference}' in any of the provided search directories."
+ };
+
+ Assert.Equal(expected, warnings, StringComparer.Ordinal);
+ }
+ else
+ {
+ Assert.False(loader.HasLoadWarnings(out IEnumerable warnings));
+ Assert.Empty(warnings);
+ }
+ }
+
+ [Fact]
+ public void LoadAssembliesShouldResolveReferencesNoWarnings()
+ {
+ var assetInfo = GetSimpleTestAsset();
+ AssemblySymbolLoader loader = new(resolveAssemblyReferences: true);
+ loader.AddReferenceSearchDirectories(Path.GetDirectoryName(typeof(string).Assembly.Location));
+ loader.LoadAssembly(Path.Combine(assetInfo.OutputDirectory, assetInfo.TestAsset.TestProject.Name + ".dll"));
+
+ Assert.False(loader.HasLoadWarnings(out IEnumerable warnings));
+ Assert.Empty(warnings);
+ }
+
+ [Fact]
+ public void LoadAssemblyFromStreamNoWarns()
+ {
+ var assetInfo = GetSimpleTestAsset();
+ TestProject testProject = assetInfo.TestAsset.TestProject;
+ AssemblySymbolLoader loader = new();
+ using FileStream stream = File.OpenRead(Path.Combine(assetInfo.OutputDirectory, testProject.Name + ".dll"));
+ IAssemblySymbol symbol = loader.LoadAssembly(testProject.Name, stream);
+
+ Assert.False(loader.HasLoadWarnings(out var _));
+ Assert.Equal(testProject.Name, symbol.Name, StringComparer.Ordinal);
+
+ IEnumerable types = symbol.GlobalNamespace
+ .GetNamespaceMembers()
+ .FirstOrDefault()
+ .GetTypeMembers();
+
+ Assert.Single(types);
+ Assert.Equal("MyNamespace.MyClass", types.FirstOrDefault().ToDisplayString());
+ }
+
+ private static CommandResult BuildTestAsset(TestAsset testAsset, out string outputDirectory)
+ {
+ BuildCommand buildCommand = new(testAsset);
+ outputDirectory = buildCommand.GetOutputDirectory(testAsset.TestProject.TargetFrameworks).FullName;
+ return buildCommand.Execute();
+ }
+ }
+}
diff --git a/src/Tests/Microsoft.DotNet.ApiCompatibility.Tests/Microsoft.DotNet.ApiCompatibility.Tests.csproj b/src/Tests/Microsoft.DotNet.ApiCompatibility.Tests/Microsoft.DotNet.ApiCompatibility.Tests.csproj
new file mode 100644
index 000000000000..dbf5425a530e
--- /dev/null
+++ b/src/Tests/Microsoft.DotNet.ApiCompatibility.Tests/Microsoft.DotNet.ApiCompatibility.Tests.csproj
@@ -0,0 +1,18 @@
+
+
+
+ false
+ net472;$(ToolsetTargetFramework)
+ $(ToolsetTargetFramework)
+ Exe
+
+ true
+ true
+
+
+
+
+
+
+
+
diff --git a/src/Tests/Microsoft.DotNet.ApiCompatibility.Tests/Rules/MembersMustExistTests.cs b/src/Tests/Microsoft.DotNet.ApiCompatibility.Tests/Rules/MembersMustExistTests.cs
new file mode 100644
index 000000000000..245daef6f1e9
--- /dev/null
+++ b/src/Tests/Microsoft.DotNet.ApiCompatibility.Tests/Rules/MembersMustExistTests.cs
@@ -0,0 +1,236 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.CodeAnalysis;
+using Microsoft.DotNet.ApiCompatibility.Abstractions;
+using System.Collections.Generic;
+using Xunit;
+
+namespace Microsoft.DotNet.ApiCompatibility.Tests.Rules
+{
+ public class MembersMustExistTests
+ {
+ [Fact]
+ public static void MissingMembersAreReported()
+ {
+ string leftSyntax = @"
+namespace CompatTests
+{
+ public class First
+ {
+ public string Parameterless() { }
+ public void ShouldReportMethod(string a, string b) { }
+ public string ShouldReportMissingProperty { get; }
+ public string this[int index] { get; }
+ public event EventHandler ShouldReportMissingEvent;
+ public int ReportMissingField = 0;
+ }
+
+ public delegate void EventHandler(object sender EventArgs e);
+}
+";
+
+ string rightSyntax = @"
+namespace CompatTests
+{
+ public class First
+ {
+ public string Parameterless() { }
+ }
+ public delegate void EventHandler(object sender EventArgs e);
+}
+";
+
+ IAssemblySymbol left = SymbolFactory.GetAssemblyFromSyntax(leftSyntax);
+ IAssemblySymbol right = SymbolFactory.GetAssemblyFromSyntax(rightSyntax);
+ ApiComparer differ = new();
+ IEnumerable differences = differ.GetDifferences(new[] { left }, new[] { right });
+
+ CompatDifference[] expected = new[]
+ {
+ new CompatDifference(DiagnosticIds.MemberMustExist, "Member 'CompatTests.First.ShouldReportMethod(string, string)' exists on the left but not on the right", DifferenceType.Removed, "M:CompatTests.First.ShouldReportMethod(System.String,System.String)"),
+ new CompatDifference(DiagnosticIds.MemberMustExist, "Member 'CompatTests.First.ShouldReportMissingProperty.get' exists on the left but not on the right", DifferenceType.Removed, "M:CompatTests.First.get_ShouldReportMissingProperty"),
+ new CompatDifference(DiagnosticIds.MemberMustExist, "Member 'CompatTests.First.this[int].get' exists on the left but not on the right", DifferenceType.Removed, "M:CompatTests.First.get_Item(System.Int32)"),
+ new CompatDifference(DiagnosticIds.MemberMustExist, "Member 'CompatTests.First.ShouldReportMissingEvent.add' exists on the left but not on the right", DifferenceType.Removed, "M:CompatTests.First.add_ShouldReportMissingEvent(CompatTests.EventHandler)"),
+ new CompatDifference(DiagnosticIds.MemberMustExist, "Member 'CompatTests.First.ShouldReportMissingEvent.remove' exists on the left but not on the right", DifferenceType.Removed, "M:CompatTests.First.remove_ShouldReportMissingEvent(CompatTests.EventHandler)"),
+ new CompatDifference(DiagnosticIds.MemberMustExist, "Member 'CompatTests.First.ReportMissingField' exists on the left but not on the right", DifferenceType.Removed, "F:CompatTests.First.ReportMissingField"),
+ };
+
+ Assert.Equal(expected, differences);
+ }
+
+ [Fact]
+ public static void HiddenMemberInLeftIsNotReported()
+ {
+ string leftSyntax = @"
+namespace CompatTests
+{
+ public class FirstBase
+ {
+ public void MyMethod() { }
+ public string MyMethodWithParams(string a, int b, FirstBase c) { }
+ public T MyGenericMethod(string name, T2 a, T3 b) { }
+ public virtual string MyVirtualMethod() { }
+ }
+ public class Second : FirstBase
+ {
+ public new void MyMethod() { }
+ public new string MyMethodWithParams(string a, int b, FirstBase c) { }
+ public new T MyGenericMethod(string name, T2 a, T3 b) { }
+ public override string MyVirtualMethod() { }
+ }
+}
+";
+
+ string rightSyntax = @"
+namespace CompatTests
+{
+ public class FirstBase
+ {
+ public void MyMethod() { }
+ public string MyMethodWithParams(string a, int b, FirstBase c) { }
+ public T MyGenericMethod(string name, T2 a, T3 b) { }
+ public virtual string MyVirtualMethod() { }
+ }
+ public class Second : FirstBase { }
+}
+";
+
+ IAssemblySymbol left = SymbolFactory.GetAssemblyFromSyntax(leftSyntax);
+ IAssemblySymbol right = SymbolFactory.GetAssemblyFromSyntax(rightSyntax);
+ ApiComparer differ = new();
+ IEnumerable differences = differ.GetDifferences(new[] { left }, new[] { right });
+ Assert.Empty(differences);
+ }
+
+ [Fact]
+ public static void NoDifferencesWithNoWarn()
+ {
+ string leftSyntax = @"
+namespace CompatTests
+{
+ public class First
+ {
+ public void MissingMember() { }
+ public int MissingProperty { get; }
+ public int MissingField;
+ }
+}
+";
+
+ string rightSyntax = @"
+namespace CompatTests
+{
+ public class First
+ {
+ }
+}
+";
+
+ IAssemblySymbol left = SymbolFactory.GetAssemblyFromSyntax(leftSyntax);
+ IAssemblySymbol right = SymbolFactory.GetAssemblyFromSyntax(rightSyntax);
+ ApiComparer differ = new();
+ differ.NoWarn = DiagnosticIds.MemberMustExist;
+ IEnumerable differences = differ.GetDifferences(new[] { left }, new[] { right });
+ Assert.Empty(differences);
+ }
+
+ [Fact]
+ public static void MultipleOverridesAreReported()
+ {
+ string leftSyntax = @"
+namespace CompatTests
+{
+ public class First
+ {
+ public string MultipleOverrides() { }
+ public string MultipleOverrides(string a) { }
+ public string MultipleOverrides(string a, string b) { }
+ public string MultipleOverrides(string a, int b, string c) { }
+ public string MultipleOverrides(string a, int b, int c) { }
+ }
+}
+";
+
+ string rightSyntax = @"
+namespace CompatTests
+{
+ public class First
+ {
+ public string MultipleOverrides() { }
+ public string MultipleOverrides(string a) { }
+ public string MultipleOverrides(string a, int b, int c) { }
+ }
+}
+";
+
+ IAssemblySymbol left = SymbolFactory.GetAssemblyFromSyntax(leftSyntax);
+ IAssemblySymbol right = SymbolFactory.GetAssemblyFromSyntax(rightSyntax);
+ ApiComparer differ = new();
+ IEnumerable differences = differ.GetDifferences(new[] { left }, new[] { right });
+
+ CompatDifference[] expected = new[]
+ {
+ new CompatDifference(DiagnosticIds.MemberMustExist, "Member 'CompatTests.First.MultipleOverrides(string, string)' exists on the left but not on the right", DifferenceType.Removed, "M:CompatTests.First.MultipleOverrides(System.String,System.String)"),
+ new CompatDifference(DiagnosticIds.MemberMustExist, "Member 'CompatTests.First.MultipleOverrides(string, int, string)' exists on the left but not on the right", DifferenceType.Removed, "M:CompatTests.First.MultipleOverrides(System.String,System.Int32,System.String)"),
+ };
+
+ Assert.Equal(expected, differences);
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public static void IncludeInternalsIsRespectedForMembers_IndividualAssemblies(bool includeInternals)
+ {
+ string leftSyntax = @"
+namespace CompatTests
+{
+ public class First
+ {
+ public string MultipleOverrides() { }
+ public string MultipleOverrides(string a) { }
+ public string MultipleOverrides(string a, string b) { }
+ public string MultipleOverrides(string a, int b, string c) { }
+ internal string MultipleOverrides(string a, int b, int c) { }
+ internal int InternalProperty { get; set; }
+ }
+}
+";
+
+ string rightSyntax = @"
+namespace CompatTests
+{
+ public class First
+ {
+ public string MultipleOverrides() { }
+ public string MultipleOverrides(string a) { }
+ public string MultipleOverrides(string a, string b) { }
+ public string MultipleOverrides(string a, int b, string c) { }
+ internal int InternalProperty { get; }
+ }
+}
+";
+
+ IAssemblySymbol left = SymbolFactory.GetAssemblyFromSyntax(leftSyntax);
+ IAssemblySymbol right = SymbolFactory.GetAssemblyFromSyntax(rightSyntax, assemblyName: "DifferentName");
+ ApiComparer differ = new(includeInternalSymbols: includeInternals);
+ IEnumerable differences = differ.GetDifferences(left, right);
+
+ if (includeInternals)
+ {
+ CompatDifference[] expected = new[]
+ {
+ new CompatDifference(DiagnosticIds.MemberMustExist, "Member 'CompatTests.First.MultipleOverrides(string, int, int)' exists on the left but not on the right", DifferenceType.Removed, "M:CompatTests.First.MultipleOverrides(System.String,System.Int32,System.Int32)"),
+ new CompatDifference(DiagnosticIds.MemberMustExist, "Member 'CompatTests.First.InternalProperty.set' exists on the left but not on the right", DifferenceType.Removed, "M:CompatTests.First.set_InternalProperty(System.Int32)"),
+ };
+
+ Assert.Equal(expected, differences);
+ }
+ else
+ {
+ Assert.Empty(differences);
+ }
+ }
+ }
+}
diff --git a/src/Tests/Microsoft.DotNet.ApiCompatibility.Tests/Rules/TypeMustExistTests.cs b/src/Tests/Microsoft.DotNet.ApiCompatibility.Tests/Rules/TypeMustExistTests.cs
new file mode 100644
index 000000000000..4092071fab57
--- /dev/null
+++ b/src/Tests/Microsoft.DotNet.ApiCompatibility.Tests/Rules/TypeMustExistTests.cs
@@ -0,0 +1,280 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.CodeAnalysis;
+using Microsoft.DotNet.ApiCompatibility.Abstractions;
+using System.Collections.Generic;
+using Xunit;
+
+namespace Microsoft.DotNet.ApiCompatibility.Tests
+{
+ public class TypeMustExistTests
+ {
+ [Theory]
+ [InlineData("")]
+ [InlineData("CP002")]
+ public void MissingPublicTypesInRightAreReported(string noWarn)
+ {
+ string leftSyntax = @"
+
+namespace CompatTests
+{
+ public class First { }
+ public class Second { }
+ public record MyRecord(string a, string b);
+ public struct MyStruct { }
+ public delegate void MyDelegate(object a);
+ public enum MyEnum { }
+}
+";
+
+ string rightSyntax = @"
+namespace CompatTests
+{
+ public class First { }
+}
+";
+
+ ApiComparer differ = new();
+ differ.NoWarn = noWarn;
+ bool enableNullable = false;
+ IAssemblySymbol left = SymbolFactory.GetAssemblyFromSyntax(leftSyntax, enableNullable);
+ IAssemblySymbol right = SymbolFactory.GetAssemblyFromSyntax(rightSyntax, enableNullable);
+ IEnumerable differences = differ.GetDifferences(new[] { left }, new[] { right });
+
+ CompatDifference[] expected = new[]
+ {
+ new CompatDifference(DiagnosticIds.TypeMustExist, $"Type 'CompatTests.Second' exists on the left but not on the right", DifferenceType.Removed, "T:CompatTests.Second"),
+ new CompatDifference(DiagnosticIds.TypeMustExist, $"Type 'CompatTests.MyRecord' exists on the left but not on the right", DifferenceType.Removed, "T:CompatTests.MyRecord"),
+ new CompatDifference(DiagnosticIds.TypeMustExist, $"Type 'CompatTests.MyStruct' exists on the left but not on the right", DifferenceType.Removed, "T:CompatTests.MyStruct"),
+ new CompatDifference(DiagnosticIds.TypeMustExist, $"Type 'CompatTests.MyDelegate' exists on the left but not on the right", DifferenceType.Removed, "T:CompatTests.MyDelegate"),
+ new CompatDifference(DiagnosticIds.TypeMustExist, $"Type 'CompatTests.MyEnum' exists on the left but not on the right", DifferenceType.Removed, "T:CompatTests.MyEnum"),
+ };
+
+ Assert.Equal(expected, differences);
+ }
+
+ [Fact]
+ public void MissingTypeFromTypeForwardIsReported()
+ {
+ string forwardedTypeSyntax = @"
+namespace CompatTests
+{
+ public class ForwardedTestType { }
+}
+";
+ string leftSyntax = @"
+[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(CompatTests.ForwardedTestType))]
+namespace CompatTests
+{
+ public class First { }
+}
+";
+
+ string rightSyntax = @"
+namespace CompatTests
+{
+ public class First { }
+}
+";
+ IAssemblySymbol left = SymbolFactory.GetAssemblyFromSyntaxWithReferences(leftSyntax, new[] { forwardedTypeSyntax }, includeDefaultReferences: true);
+ IAssemblySymbol right = SymbolFactory.GetAssemblyFromSyntax(rightSyntax);
+ ApiComparer differ = new();
+ IEnumerable differences = differ.GetDifferences(new[] { left }, new[] { right });
+
+ CompatDifference[] expected = new[]
+ {
+ new CompatDifference(DiagnosticIds.TypeMustExist, $"Type 'CompatTests.ForwardedTestType' exists on the left but not on the right", DifferenceType.Removed, "T:CompatTests.ForwardedTestType")
+ };
+
+ Assert.Equal(expected, differences);
+ }
+
+ [Fact]
+ public void TypeForwardExistsOnBoth()
+ {
+ string forwardedTypeSyntax = @"
+namespace CompatTests
+{
+ public class ForwardedTestType { }
+}
+";
+ string syntax = @"
+[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(CompatTests.ForwardedTestType))]
+namespace CompatTests
+{
+ public class First { }
+}
+";
+ IEnumerable references = new[] { forwardedTypeSyntax };
+ IAssemblySymbol left = SymbolFactory.GetAssemblyFromSyntaxWithReferences(syntax, references, includeDefaultReferences: true);
+ IAssemblySymbol right = SymbolFactory.GetAssemblyFromSyntaxWithReferences(syntax, references, includeDefaultReferences: true);
+ ApiComparer differ = new();
+ Assert.Empty(differ.GetDifferences(new[] { left }, new[] { right }));
+ }
+
+ [Fact]
+ public void NoDifferencesReportedWithNoWarn()
+ {
+ string leftSyntax = @"
+
+namespace CompatTests
+{
+ public class First { }
+ public class Second { }
+}
+";
+
+ string rightSyntax = @"
+namespace CompatTests
+{
+ public class First { }
+}
+";
+
+ ApiComparer differ = new();
+ differ.NoWarn = DiagnosticIds.TypeMustExist;
+ IAssemblySymbol left = SymbolFactory.GetAssemblyFromSyntax(leftSyntax);
+ IAssemblySymbol right = SymbolFactory.GetAssemblyFromSyntax(rightSyntax);
+ Assert.Empty(differ.GetDifferences(new[] { left }, new[] { right }));
+ }
+
+ [Fact]
+ public void DifferenceIsIgnoredForMember()
+ {
+ string leftSyntax = @"
+
+namespace CompatTests
+{
+ public record First(string a, string b);
+ public class Second { }
+ public class Third { }
+ public class Fourth { }
+ public enum MyEnum { }
+}
+";
+
+ string rightSyntax = @"
+namespace CompatTests
+{
+ public record First(string a, string b);
+}
+";
+
+ (string, string)[] ignoredDifferences = new[]
+ {
+ (DiagnosticIds.TypeMustExist, "T:CompatTests.Second"),
+ (DiagnosticIds.TypeMustExist, "T:CompatTests.MyEnum"),
+ };
+
+ ApiComparer differ = new();
+ differ.IgnoredDifferences = ignoredDifferences;
+
+ IAssemblySymbol left = SymbolFactory.GetAssemblyFromSyntax(leftSyntax);
+ IAssemblySymbol right = SymbolFactory.GetAssemblyFromSyntax(rightSyntax);
+ IEnumerable differences = differ.GetDifferences(new[] { left }, new[] { right });
+
+ CompatDifference[] expected = new[]
+ {
+ new CompatDifference(DiagnosticIds.TypeMustExist, $"Type 'CompatTests.Third' exists on the left but not on the right", DifferenceType.Removed, "T:CompatTests.Third"),
+ new CompatDifference(DiagnosticIds.TypeMustExist, $"Type 'CompatTests.Fourth' exists on the left but not on the right", DifferenceType.Removed, "T:CompatTests.Fourth")
+ };
+
+ Assert.Equal(expected, differences);
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void InternalTypesAreIgnoredWhenSpecified(bool includeInternalSymbols)
+ {
+ string leftSyntax = @"
+
+namespace CompatTests
+{
+ public class First { }
+ internal class InternalType { }
+}
+";
+
+ string rightSyntax = @"
+namespace CompatTests
+{
+ public class First { }
+}
+";
+
+ ApiComparer differ = new(includeInternalSymbols);
+ IAssemblySymbol left = SymbolFactory.GetAssemblyFromSyntax(leftSyntax);
+ IAssemblySymbol right = SymbolFactory.GetAssemblyFromSyntax(rightSyntax);
+ IEnumerable differences = differ.GetDifferences(new[] { left }, new[] { right });
+
+ if (!includeInternalSymbols)
+ {
+ Assert.Empty(differences);
+ }
+ else
+ {
+ CompatDifference[] expected = new[]
+ {
+ new CompatDifference(DiagnosticIds.TypeMustExist, $"Type 'CompatTests.InternalType' exists on the left but not on the right", DifferenceType.Removed, "T:CompatTests.InternalType")
+ };
+
+ Assert.Equal(expected, differences);
+ }
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public static void MissingNestedTypeIsReported(bool includeInternalSymbols)
+ {
+ string leftSyntax = @"
+
+namespace CompatTests
+{
+ public class First
+ {
+ public class FirstNested
+ {
+ public class SecondNested { }
+ }
+ internal class InternalNested
+ {
+ internal class DoubleNested { }
+ }
+ }
+}
+";
+
+ string rightSyntax = @"
+namespace CompatTests
+{
+ public class First
+ {
+ internal class InternalNested { }
+ }
+}
+";
+
+ ApiComparer differ = new(includeInternalSymbols);
+ IAssemblySymbol left = SymbolFactory.GetAssemblyFromSyntax(leftSyntax);
+ IAssemblySymbol right = SymbolFactory.GetAssemblyFromSyntax(rightSyntax);
+ IEnumerable differences = differ.GetDifferences(new[] { left }, new[] { right });
+
+ List expected = new()
+ {
+ new CompatDifference(DiagnosticIds.TypeMustExist, $"Type 'CompatTests.First.FirstNested' exists on the left but not on the right", DifferenceType.Removed, "T:CompatTests.First.FirstNested"),
+ };
+
+ if (includeInternalSymbols)
+ {
+ expected.Add(
+ new CompatDifference(DiagnosticIds.TypeMustExist, $"Type 'CompatTests.First.InternalNested.DoubleNested' exists on the left but not on the right", DifferenceType.Removed, "T:CompatTests.First.InternalNested.DoubleNested")
+ );
+ }
+
+ Assert.Equal(expected, differences);
+ }
+ }
+}
diff --git a/src/Tests/Microsoft.DotNet.ApiCompatibility.Tests/SymbolFactory.cs b/src/Tests/Microsoft.DotNet.ApiCompatibility.Tests/SymbolFactory.cs
new file mode 100644
index 000000000000..513efd53661c
--- /dev/null
+++ b/src/Tests/Microsoft.DotNet.ApiCompatibility.Tests/SymbolFactory.cs
@@ -0,0 +1,55 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+
+namespace Microsoft.DotNet.ApiCompatibility.Tests
+{
+ internal static class SymbolFactory
+ {
+ internal static IAssemblySymbol GetAssemblyFromSyntax(string syntax, bool enableNullable = false, bool includeDefaultReferences = false, [CallerMemberName] string assemblyName = "")
+ {
+ CSharpCompilation compilation = CreateCSharpCompilationFromSyntax(syntax, assemblyName, enableNullable, includeDefaultReferences);
+ return compilation.Assembly;
+ }
+
+ internal static IAssemblySymbol GetAssemblyFromSyntaxWithReferences(string syntax, IEnumerable referencesSyntax, bool enableNullable = false, bool includeDefaultReferences = false, [CallerMemberName] string assemblyName = "")
+ {
+ CSharpCompilation compilation = CreateCSharpCompilationFromSyntax(syntax, assemblyName, enableNullable, includeDefaultReferences);
+ CSharpCompilation compilationWithReferences = CreateCSharpCompilationFromSyntax(referencesSyntax, $"{assemblyName}_reference", enableNullable, includeDefaultReferences);
+
+ compilation = compilation.AddReferences(compilationWithReferences.ToMetadataReference());
+ return compilation.Assembly;
+ }
+
+ private static CSharpCompilation CreateCSharpCompilationFromSyntax(string syntax, string name, bool enableNullable, bool includeDefaultReferences)
+ {
+ CSharpCompilation compilation = CreateCSharpCompilation(name, enableNullable, includeDefaultReferences);
+ return compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(syntax));
+ }
+
+ private static CSharpCompilation CreateCSharpCompilationFromSyntax(IEnumerable syntax, string name, bool enableNullable, bool includeDefaultReferences)
+ {
+ CSharpCompilation compilation = CreateCSharpCompilation(name, enableNullable, includeDefaultReferences);
+ IEnumerable syntaxTrees = syntax.Select(s => CSharpSyntaxTree.ParseText(s));
+ return compilation.AddSyntaxTrees(syntaxTrees);
+ }
+
+ private static CSharpCompilation CreateCSharpCompilation(string name, bool enableNullable, bool includeDefaultReferences)
+ {
+ var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary,
+ nullableContextOptions: enableNullable ? NullableContextOptions.Enable : NullableContextOptions.Disable);
+
+ return CSharpCompilation.Create(name, options: compilationOptions, references: includeDefaultReferences ? DefaultReferences : null);
+ }
+
+ private static IEnumerable DefaultReferences { get; } = new[]
+ {
+ MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
+ };
+ }
+}