diff --git a/CODEOWNERS b/CODEOWNERS index b9bfa732520f..8b8ed15f06e6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -84,10 +84,15 @@ # Compatibility tools owned by runtime team /src/Compatibility/ @dotnet/area-infrastructure-libraries + +# Area-ApiCompat /test/Microsoft.DotNet.ApiCompatibility*/ @dotnet/area-infrastructure-libraries /test/Microsoft.DotNet.ApiCompat*/ @dotnet/area-infrastructure-libraries /test/Microsoft.DotNet.PackageValidation*/ @dotnet/area-infrastructure-libraries +# Area-ApiDiff +/test/Microsoft.DotNet.ApiDiff.Tests/ @dotnet/area-infrastructure-libraries + # Area-GenAPI /src/Compatibility/GenAPI/ @dotnet/area-infrastructure-libraries /src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/ @dotnet/area-infrastructure-libraries diff --git a/Directory.Packages.props b/Directory.Packages.props index 8b7dd4136423..ab0b8d7a7afa 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -101,6 +101,7 @@ + diff --git a/eng/Signing.props b/eng/Signing.props index ee5649c45ddf..484697efecbc 100644 --- a/eng/Signing.props +++ b/eng/Signing.props @@ -22,7 +22,7 @@ - + @@ -71,6 +71,17 @@ + + + + + + + + + + + diff --git a/eng/Versions.props b/eng/Versions.props index 07d2c69f2dfb..a09f426bf54c 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -64,6 +64,7 @@ 1.2.0-beta.435 4.0.5 2.0.0-beta5.25227.101 + 2.0.0-beta5.25227.101 1.1.2-beta1.22216.1 10.3.0 3.2.2146 diff --git a/sdk.sln b/sdk.sln index 153855a4c2f3..78ffd859f272 100644 --- a/sdk.sln +++ b/sdk.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.1.31903.286 MinimumVisualStudioVersion = 10.0.40219.1 @@ -537,6 +537,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VS.Redist.Common.Net.Core.S EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VS.Redist.Common.NetCore.Templates", "src\Layout\VS.Redist.Common.NetCore.Templates\VS.Redist.Common.NetCore.Templates.proj", "{6A7E16BE-FF2F-7927-AAF7-366CC0C42AE4}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ApiDiff", "ApiDiff", "{C66B5859-B05E-5DF4-58E9-78CA919DB89A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ApiDiff", "ApiDiff", "{AFA55F45-CFCB-9821-A210-2D3496088416}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.DotNet.ApiDiff.Tool", "src\Compatibility\ApiDiff\Microsoft.DotNet.ApiDiff.Tool\Microsoft.DotNet.ApiDiff.Tool.csproj", "{3F0093BF-A64D-4EE8-8A2A-22800BB25CFF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.DotNet.ApiDiff.Tests", "test\Microsoft.DotNet.ApiDiff.Tests\Microsoft.DotNet.ApiDiff.Tests.csproj", "{A57B724D-D12B-483E-82F2-2183DEA2DFA7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.DotNet.ApiDiff", "src\Compatibility\ApiDiff\Microsoft.DotNet.ApiDiff\Microsoft.DotNet.ApiDiff.csproj", "{4F23A9C8-945A-A4F4-51E9-FCA215943C0D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1023,7 +1033,18 @@ Global {6A7E16BE-FF2F-7927-AAF7-366CC0C42AE4}.Debug|Any CPU.Build.0 = Debug|Any CPU {6A7E16BE-FF2F-7927-AAF7-366CC0C42AE4}.Release|Any CPU.ActiveCfg = Release|Any CPU {6A7E16BE-FF2F-7927-AAF7-366CC0C42AE4}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection + {3F0093BF-A64D-4EE8-8A2A-22800BB25CFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F0093BF-A64D-4EE8-8A2A-22800BB25CFF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3F0093BF-A64D-4EE8-8A2A-22800BB25CFF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F0093BF-A64D-4EE8-8A2A-22800BB25CFF}.Release|Any CPU.Build.0 = Release|Any CPU + {A57B724D-D12B-483E-82F2-2183DEA2DFA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A57B724D-D12B-483E-82F2-2183DEA2DFA7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A57B724D-D12B-483E-82F2-2183DEA2DFA7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A57B724D-D12B-483E-82F2-2183DEA2DFA7}.Release|Any CPU.Build.0 = Release|Any CPU + {4F23A9C8-945A-A4F4-51E9-FCA215943C0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F23A9C8-945A-A4F4-51E9-FCA215943C0D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F23A9C8-945A-A4F4-51E9-FCA215943C0D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F23A9C8-945A-A4F4-51E9-FCA215943C0D}.Release|Any CPU.Build.0 = Release|Any CPU GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection @@ -1211,6 +1232,11 @@ Global {93A4A3DA-9D0F-FAEA-5AD7-A34650D326D0} = {71C279BD-E850-4A8D-9775-11CA26B8E5BA} {C5C4F9CA-25B8-2B20-826D-EA93F667C316} = {71C279BD-E850-4A8D-9775-11CA26B8E5BA} {6A7E16BE-FF2F-7927-AAF7-366CC0C42AE4} = {71C279BD-E850-4A8D-9775-11CA26B8E5BA} + {C66B5859-B05E-5DF4-58E9-78CA919DB89A} = {44E564E1-AE0D-4313-A4E9-CBF2109397E3} + {AFA55F45-CFCB-9821-A210-2D3496088416} = {3AD322BF-405B-4A53-9858-51CF66E8509F} + {3F0093BF-A64D-4EE8-8A2A-22800BB25CFF} = {C66B5859-B05E-5DF4-58E9-78CA919DB89A} + {A57B724D-D12B-483E-82F2-2183DEA2DFA7} = {AFA55F45-CFCB-9821-A210-2D3496088416} + {4F23A9C8-945A-A4F4-51E9-FCA215943C0D} = {C66B5859-B05E-5DF4-58E9-78CA919DB89A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FB8F26CE-4DE6-433F-B32A-79183020BBD6} diff --git a/src/Compatibility/ApiDiff/Directory.Build.props b/src/Compatibility/ApiDiff/Directory.Build.props new file mode 100644 index 000000000000..0e5e89296832 --- /dev/null +++ b/src/Compatibility/ApiDiff/Directory.Build.props @@ -0,0 +1,9 @@ + + + + + + true + + + diff --git a/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff.Tool/AttributesToExclude.txt b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff.Tool/AttributesToExclude.txt new file mode 100644 index 000000000000..4b26bf1f6a67 --- /dev/null +++ b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff.Tool/AttributesToExclude.txt @@ -0,0 +1,4 @@ +T:System.AttributeUsageAttribute +T:System.ComponentModel.EditorBrowsableAttribute +T:System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute +T:System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute \ No newline at end of file diff --git a/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff.Tool/Microsoft.DotNet.ApiDiff.Tool.csproj b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff.Tool/Microsoft.DotNet.ApiDiff.Tool.csproj new file mode 100644 index 000000000000..21c512fd7fcf --- /dev/null +++ b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff.Tool/Microsoft.DotNet.ApiDiff.Tool.csproj @@ -0,0 +1,31 @@ + + + + $(NetToolMinimum) + Exe + true + false + true + apidiff + Tool to emit markdown diffs between sets of assemblies. + false + true + + + + + PreserveNewest + + + + + + + + + + + + + + diff --git a/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff.Tool/Program.cs b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff.Tool/Program.cs new file mode 100644 index 000000000000..a8955d1f2f96 --- /dev/null +++ b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff.Tool/Program.cs @@ -0,0 +1,213 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CommandLine; +using System.ComponentModel; +using System.Diagnostics; +using Microsoft.DotNet.ApiSymbolExtensions.Logging; + +namespace Microsoft.DotNet.ApiDiff.Tool; + +/// +/// Entrypoint for the genapidiff tool, which generates a markdown diff of two +/// different versions of the same assembly, using the specified command line options. +/// +public static class Program +{ + private static readonly string AttributesToExcludeDefaultFileName = "AttributesToExclude.txt"; + + public static async Task Main(string[] args) + { + RootCommand rootCommand = new("genapidiff"); + + Option optionBeforeAssembliesFolderPath = new(name: "", aliases: ["--before", "-b"]) + { + Description = "The path to the folder containing the old (before) assemblies to be included in the diff.", + Arity = ArgumentArity.ExactlyOne, + Required = true + }; + + Option optionBeforeRefAssembliesFolderPath = new(name: "", aliases: ["--refbefore", "-rb"]) + { + Description = "The path to the folder containing the references required by old (before) assemblies, not to be included in the diff.", + Arity = ArgumentArity.ExactlyOne, + Required = false + }; + + Option optionAfterAssembliesFolderPath = new(name: "", aliases: ["--after", "-a"]) + { + Description = "The path to the folder containing the new (after) assemblies to be included in the diff.", + Arity = ArgumentArity.ExactlyOne, + Required = true + }; + + Option optionAfterRefAssembliesFolderPath = new(name: "", aliases: ["--refafter", "-ra"]) + { + Description = "The path to the folder containing references required by the new (after) reference assemblies, not to be included in the diff.", + Arity = ArgumentArity.ExactlyOne, + Required = false + }; + + Option optionOutputFolderPath = new(name: "", aliases: ["--output", "-o"]) + { + Description = "The path to the output folder.", + Arity = ArgumentArity.ExactlyOne, + Required = true + }; + + Option optionBeforeFriendlyName = new(name: "", aliases: ["--beforeFriendlyName", "-bfn"]) + { + Description = "The friendly name to describe the 'before' assembly.", + Arity = ArgumentArity.ExactlyOne, + Required = true + }; + + Option optionAfterFriendlyName = new(name: "", aliases: ["--afterFriendlyName", "-afn"]) + { + Description = "The friendly name to describe the 'after' assembly.", + Arity = ArgumentArity.ExactlyOne, + Required = true + }; + + Option optionTableOfContentsTitle = new(name: "", aliases: ["--tableOfContentsTitle", "-tc"]) + { + Description = $"The optional title of the markdown table of contents file that is placed in the output folder.", + Arity = ArgumentArity.ZeroOrMore, + Required = false, + DefaultValueFactory = _ => "api_diff" + }; + + Option optionFilesWithAssembliesToExclude = new(name: "", aliases: ["--assembliesToExclude", "-eas"]) + { + Description = "An optional array of filepaths, each containing a list of assemblies that should be excluded from the diff. Each file should contain one assembly name per line, with no extensions.", + Arity = ArgumentArity.ZeroOrMore, + Required = false, + DefaultValueFactory = _ => null + }; + + Option optionFilesWithAttributesToExclude = new(name: "", aliases: ["--attributesToExclude", "-eattrs"]) + { + Description = $"An optional array of filepaths, each containing a list of attributes to exclude from the diff. Each file should contain one API full name per line. You can either modify the default file '{AttributesToExcludeDefaultFileName}' to add your own attributes, or include additional files using this command line option.", + Arity = ArgumentArity.ZeroOrMore, + Required = false, + DefaultValueFactory = _ => [new FileInfo(AttributesToExcludeDefaultFileName)] + }; + + Option optionFilesWithApisToExclude = new(name: "", aliases: ["--apisToExclude", "-eapis"]) + { + Description = "An optional array of filepaths, each containing a list of APIs to exclude from the diff. Each file should contain one API full name per line.", + Arity = ArgumentArity.ZeroOrMore, + Required = false, + DefaultValueFactory = _ => null + }; + + Option optionAddPartialModifier = new(name: "", aliases: ["--addPartialModifier", "-apm"]) + { + Description = "Add the 'partial' modifier to types.", + DefaultValueFactory = _ => false + }; + + Option optionAttachDebugger = new(name: "", aliases: ["--attachDebugger", "-d"]) + { + Description = "Stops the tool at startup, prints the process ID and waits for a debugger to attach.", + DefaultValueFactory = _ => false + }; + + // Custom ordering for the help menu. + rootCommand.Options.Add(optionBeforeAssembliesFolderPath); + rootCommand.Options.Add(optionBeforeRefAssembliesFolderPath); + rootCommand.Options.Add(optionAfterAssembliesFolderPath); + rootCommand.Options.Add(optionAfterRefAssembliesFolderPath); + rootCommand.Options.Add(optionOutputFolderPath); + rootCommand.Options.Add(optionBeforeFriendlyName); + rootCommand.Options.Add(optionAfterFriendlyName); + rootCommand.Options.Add(optionTableOfContentsTitle); + rootCommand.Options.Add(optionFilesWithAssembliesToExclude); + rootCommand.Options.Add(optionFilesWithAttributesToExclude); + rootCommand.Options.Add(optionFilesWithApisToExclude); + rootCommand.Options.Add(optionAddPartialModifier); + rootCommand.Options.Add(optionAttachDebugger); + + rootCommand.SetAction(async (ParseResult result) => + { + DiffConfiguration c = new( + BeforeAssembliesFolderPath: result.GetValue(optionBeforeAssembliesFolderPath) ?? throw new NullReferenceException("Null before assemblies directory"), + BeforeAssemblyReferencesFolderPath: result.GetValue(optionBeforeRefAssembliesFolderPath), + AfterAssembliesFolderPath: result.GetValue(optionAfterAssembliesFolderPath) ?? throw new NullReferenceException("Null after assemblies directory"), + AfterAssemblyReferencesFolderPath: result.GetValue(optionAfterRefAssembliesFolderPath), + OutputFolderPath: result.GetValue(optionOutputFolderPath) ?? throw new NullReferenceException("Null output directory"), + BeforeFriendlyName: result.GetValue(optionBeforeFriendlyName) ?? throw new NullReferenceException("Null before friendly name"), + AfterFriendlyName: result.GetValue(optionAfterFriendlyName) ?? throw new NullReferenceException("Null after friendly name"), + TableOfContentsTitle: result.GetValue(optionTableOfContentsTitle) ?? throw new NullReferenceException("Null table of contents title"), + FilesWithAssembliesToExclude: result.GetValue(optionFilesWithAssembliesToExclude), + FilesWithAttributesToExclude: result.GetValue(optionFilesWithAttributesToExclude), + FilesWithApisToExclude: result.GetValue(optionFilesWithApisToExclude), + AddPartialModifier: result.GetValue(optionAddPartialModifier), + AttachDebugger: result.GetValue(optionAttachDebugger) + ); + await HandleCommandAsync(c).ConfigureAwait(false); + }); + await rootCommand.Parse(args).InvokeAsync(); + } + + private static Task HandleCommandAsync(DiffConfiguration diffConfig) + { + var log = new ConsoleLog(MessageImportance.Normal); + + string assembliesToExclude = string.Join(", ", diffConfig.FilesWithAssembliesToExclude?.Select(a => a.FullName) ?? []); + string attributesToExclude = string.Join(", ", diffConfig.FilesWithAttributesToExclude?.Select(a => a.FullName) ?? []); + string apisToExclude = string.Join(", ", diffConfig.FilesWithApisToExclude?.Select(a => a.FullName) ?? []); + + // Custom ordering to match help menu. + log.LogMessage("Selected options:"); + log.LogMessage($" - 'Before' source assemblies: {diffConfig.BeforeAssembliesFolderPath}"); + log.LogMessage($" - 'After' source assemblies: {diffConfig.AfterAssembliesFolderPath}"); + log.LogMessage($" - 'Before' reference assemblies: {diffConfig.BeforeAssemblyReferencesFolderPath}"); + log.LogMessage($" - 'After' reference assemblies: {diffConfig.AfterAssemblyReferencesFolderPath}"); + log.LogMessage($" - Output: {diffConfig.OutputFolderPath}"); + log.LogMessage($" - Files with assemblies to exclude: {assembliesToExclude}"); + log.LogMessage($" - Files with attributes to exclude: {attributesToExclude}"); + log.LogMessage($" - Files with APIs to exclude: {apisToExclude}"); + log.LogMessage($" - 'Before' friendly name: {diffConfig.BeforeFriendlyName}"); + log.LogMessage($" - 'After' friendly name: {diffConfig.AfterFriendlyName}"); + log.LogMessage($" - Table of contents title: {diffConfig.TableOfContentsTitle}"); + log.LogMessage($" - Add partial modifier to types: {diffConfig.AddPartialModifier}"); + log.LogMessage($" - Attach debugger: {diffConfig.AttachDebugger}"); + log.LogMessage(""); + + if (diffConfig.AttachDebugger) + { + WaitForDebugger(); + } + + IDiffGenerator diffGenerator = DiffGeneratorFactory.Create(log, + diffConfig.BeforeAssembliesFolderPath, + diffConfig.BeforeAssemblyReferencesFolderPath, + diffConfig.AfterAssembliesFolderPath, + diffConfig.AfterAssemblyReferencesFolderPath, + diffConfig.OutputFolderPath, + diffConfig.BeforeFriendlyName, + diffConfig.AfterFriendlyName, + diffConfig.TableOfContentsTitle, + diffConfig.FilesWithAssembliesToExclude, + diffConfig.FilesWithAttributesToExclude, + diffConfig.FilesWithApisToExclude, + diffConfig.AddPartialModifier, + writeToDisk: true, + diagnosticOptions: null // TODO: If needed, add CLI option to pass specific diagnostic options + ); + + return diffGenerator.RunAsync(); + } + + private static void WaitForDebugger() + { + while (!Debugger.IsAttached) + { + Console.WriteLine($"Attach to process {Environment.ProcessId}..."); + Thread.Sleep(1000); + } + Console.WriteLine("Debugger attached!"); + Debugger.Break(); + } +} diff --git a/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/DiffConfiguration.cs b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/DiffConfiguration.cs new file mode 100644 index 000000000000..030d7dd38713 --- /dev/null +++ b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/DiffConfiguration.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.ApiDiff; + +/// +/// Defines the necessary configuration options for API diff. +/// +public record DiffConfiguration( + string AfterAssembliesFolderPath, + string? AfterAssemblyReferencesFolderPath, + string BeforeAssembliesFolderPath, + string? BeforeAssemblyReferencesFolderPath, + string OutputFolderPath, + string BeforeFriendlyName, + string AfterFriendlyName, + string TableOfContentsTitle, + FileInfo[]? FilesWithAssembliesToExclude, + FileInfo[]? FilesWithAttributesToExclude, + FileInfo[]? FilesWithApisToExclude, + bool AddPartialModifier, + bool AttachDebugger +); diff --git a/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/DiffGeneratorFactory.cs b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/DiffGeneratorFactory.cs new file mode 100644 index 000000000000..bc30f66251da --- /dev/null +++ b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/DiffGeneratorFactory.cs @@ -0,0 +1,109 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.DotNet.ApiSymbolExtensions; +using Microsoft.DotNet.ApiSymbolExtensions.Logging; + +namespace Microsoft.DotNet.ApiDiff; + +public static class DiffGeneratorFactory +{ + /// + /// The default diagnostic options to use when generating the diff. + /// + public static readonly IEnumerable> DefaultDiagnosticOptions = [ + new ("CS8019", ReportDiagnostic.Suppress), // CS8019: Unnecessary using directive. + new ("CS8597", ReportDiagnostic.Suppress), // CS8597: Thrown value may be null. + new ("CS0067", ReportDiagnostic.Suppress), // CS0067: The API is never used. + new ("CS9113", ReportDiagnostic.Suppress), // CS9113: Parameter is unread. + new ("CS0501", ReportDiagnostic.Suppress), // CS0501: Method must declare a body because it is not marked abstract. + ]; + + /// + /// Creates a new instance of that writes the diff to disk. + /// + /// The logger to use for logging messages. + /// The folder path containing the assemblies before the change. + /// The folder path containing the assembly references before the change. + /// The folder path containing the assemblies after the change. + /// The folder path containing the assembly references after the change. + /// The folder path where the output will be written. + /// The friendly name for the assemblies before the change. + /// The friendly name for the assemblies after the change. + /// The title for the table of contents in the generated diff. + /// An optional array of filepaths each containing a list of assemblies to avoid showing in the diff. If , no assemblies are excluded. + /// An optional array of filepaths each containing a list of attributes to avoid showing in the diff. + /// An optional array of filepaths each containing a list of APIs to avoid showing in the diff. + /// Indicates whether to add the partial modifier to types. + /// If , when calling , the generated markdown files get written to disk, and no item is added to the dictionary. If , when calling , the generated markdown files get added to the dictionary (with the file path as the dictionary key) and none of them is written to disk. This is meant for testing purposes. + /// An optional list of diagnostic options to use when generating the diff. + /// A new instance of that writes the diff to disk. + /// + public static IDiffGenerator Create(ILog log, + string beforeAssembliesFolderPath, + string? beforeAssemblyReferencesFolderPath, + string afterAssembliesFolderPath, + string? afterAssemblyReferencesFolderPath, + string outputFolderPath, + string beforeFriendlyName, + string afterFriendlyName, + string tableOfContentsTitle, + FileInfo[]? filesWithAssembliesToExclude, + FileInfo[]? filesWithAttributesToExclude, + FileInfo[]? filesWithApisToExclude, + bool addPartialModifier, + bool writeToDisk, + IEnumerable>? diagnosticOptions = null) + { + return new FileOutputDiffGenerator(log, + beforeAssembliesFolderPath, + beforeAssemblyReferencesFolderPath, + afterAssembliesFolderPath, + afterAssemblyReferencesFolderPath, + outputFolderPath, + beforeFriendlyName, + afterFriendlyName, + tableOfContentsTitle, + filesWithAssembliesToExclude, + filesWithAttributesToExclude, + filesWithApisToExclude, + addPartialModifier, + writeToDisk, + diagnosticOptions); + } + + /// + /// Creates a new instance of that writes the diff to memory. + /// + /// The logger to use for logging messages. + /// The loader to use for loading the assemblies before the change. + /// The loader to use for loading the assemblies after the change. + /// The dictionary containing the assembly symbols before the change. + /// The dictionary containing the assembly symbols after the change. + /// An optional list of attributes to avoid showing in the diff. + /// An optional list of APIs to avoid showing in the diff. + /// Indicates whether to add the partial modifier to types. + /// An optional list of diagnostic options to use when generating the diff. + /// A new instance of that writes the diff to memory. + public static IDiffGenerator Create(ILog log, + IAssemblySymbolLoader beforeLoader, + IAssemblySymbolLoader afterLoader, + Dictionary beforeAssemblySymbols, + Dictionary afterAssemblySymbols, + string[]? attributesToExclude, + string[]? apisToExclude, + bool addPartialModifier, + IEnumerable>? diagnosticOptions = null) + { + return new MemoryOutputDiffGenerator(log, + beforeLoader, + afterLoader, + beforeAssemblySymbols, + afterAssemblySymbols, + attributesToExclude, + apisToExclude, + addPartialModifier, + diagnosticOptions); + } +} diff --git a/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/FileOutputDiffGenerator.cs b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/FileOutputDiffGenerator.cs new file mode 100644 index 000000000000..b86a24d77cd7 --- /dev/null +++ b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/FileOutputDiffGenerator.cs @@ -0,0 +1,192 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the + +using Microsoft.CodeAnalysis; +using Microsoft.DotNet.ApiSymbolExtensions.Logging; +using Microsoft.DotNet.ApiSymbolExtensions; +using System.Diagnostics; + +namespace Microsoft.DotNet.ApiDiff; + +/// +/// Generates a markdown diff of two different versions of the same assembly. +/// +internal sealed class FileOutputDiffGenerator : IDiffGenerator +{ + private readonly ILog _log; + private readonly string[] _beforeAssembliesFolderPaths; + private readonly string[] _beforeAssemblyReferencesFolderPaths; + private readonly string[] _afterAssembliesFolderPaths; + private readonly string[] _afterAssemblyReferencesFolderPaths; + private readonly string _outputFolderPath; + private readonly string _beforeFriendlyName; + private readonly string _afterFriendlyName; + private readonly string _tableOfContentsTitle; + private readonly string[] _assembliesToExclude; + private readonly string[] _attributesToExclude; + private readonly string[] _apisToExclude; + private readonly bool _addPartialModifier; + private readonly bool _writeToDisk; + private readonly IEnumerable>? _diagnosticOptions; + private readonly Dictionary _results; + + /// + /// Initializes a new instance of the class. + /// + /// The logger to use for logging messages. + /// The folder path containing the assemblies before the changes. + /// The folder path containing the assembly references before the changes. + /// The folder path containing the assemblies after the changes. + /// The folder path containing the assembly references after the changes. + /// The folder path where the output files will be written. + /// The friendly name for the before version of the assemblies. + /// The friendly name for the after version of the assemblies. + /// The title for the table of contents. + /// An optional array of filepaths each containing a list of assemblies to avoid showing in the diff. + /// An optional array of filepaths each containing a list of attributes to avoid showing in the diff. + /// An optional array of filepaths each containing a list of APIs to avoid showing in the diff. + /// A value indicating whether to add the partial modifier to types. + /// If , when calling , the generated markdown files get written to disk, and no item is added to the dictionary. If , when calling , the generated markdown files get added to the dictionary (with the file path as the dictionary key) and none of them is written to disk. This is meant for testing purposes. + /// An optional set of diagnostic options. + internal FileOutputDiffGenerator(ILog log, + string beforeAssembliesFolderPath, + string? beforeAssemblyReferencesFolderPath, + string afterAssembliesFolderPath, + string? afterAssemblyReferencesFolderPath, + string outputFolderPath, + string beforeFriendlyName, + string afterFriendlyName, + string tableOfContentsTitle, + FileInfo[]? filesWithAssembliesToExclude, + FileInfo[]? filesWithAttributesToExclude, + FileInfo[]? filesWithApisToExclude, + bool addPartialModifier, + bool writeToDisk, + IEnumerable>? diagnosticOptions = null) + + { + _log = log; + _beforeAssembliesFolderPaths = [beforeAssembliesFolderPath]; + _beforeAssemblyReferencesFolderPaths = beforeAssemblyReferencesFolderPath != null ? [beforeAssemblyReferencesFolderPath] : []; + _afterAssembliesFolderPaths = [afterAssembliesFolderPath]; + _afterAssemblyReferencesFolderPaths = afterAssemblyReferencesFolderPath != null ? [afterAssemblyReferencesFolderPath] : []; + _outputFolderPath = outputFolderPath; + _beforeFriendlyName = beforeFriendlyName; + _afterFriendlyName = afterFriendlyName; + _tableOfContentsTitle = tableOfContentsTitle; + _assembliesToExclude = CollectListsFromFiles(filesWithAssembliesToExclude); + _attributesToExclude = filesWithAttributesToExclude != null ? CollectListsFromFiles(filesWithAttributesToExclude) : []; + _apisToExclude = CollectListsFromFiles(filesWithApisToExclude); + _addPartialModifier = addPartialModifier; + _writeToDisk = writeToDisk; + _diagnosticOptions = diagnosticOptions ?? DiffGeneratorFactory.DefaultDiagnosticOptions; + _results = []; + } + + /// + public IReadOnlyDictionary Results => _results.AsReadOnly(); + + /// + public async Task RunAsync() + { + Debug.Assert(_beforeAssembliesFolderPaths.Length == 1); + Debug.Assert(_afterAssembliesFolderPaths.Length == 1); + + (IAssemblySymbolLoader beforeLoader, Dictionary beforeAssemblySymbols) = + AssemblySymbolLoader.CreateFromFiles( + _log, + assembliesPaths: _beforeAssembliesFolderPaths, + assemblyReferencesPaths: _beforeAssemblyReferencesFolderPaths, + assembliesToExclude: _assembliesToExclude, + diagnosticOptions: _diagnosticOptions); + + (IAssemblySymbolLoader afterLoader, Dictionary afterAssemblySymbols) = + AssemblySymbolLoader.CreateFromFiles( + _log, + assembliesPaths: _afterAssembliesFolderPaths, + assemblyReferencesPaths: _afterAssemblyReferencesFolderPaths, + assembliesToExclude: _assembliesToExclude, + diagnosticOptions: _diagnosticOptions); + + MemoryOutputDiffGenerator generator = new(_log, + beforeLoader, + afterLoader, + beforeAssemblySymbols, + afterAssemblySymbols, + _attributesToExclude, + _apisToExclude, + _addPartialModifier, + _diagnosticOptions); + + await generator.RunAsync().ConfigureAwait(false); + + // If true, output is disk. Otherwise, it's the Results dictionary. + if (_writeToDisk) + { + Directory.CreateDirectory(_outputFolderPath); + } + + StringBuilder tableOfContents = new(); + tableOfContents.AppendLine($"# API difference between {_beforeFriendlyName} and {_afterFriendlyName}"); + tableOfContents.AppendLine(); + tableOfContents.AppendLine("API listing follows standard diff formatting."); + tableOfContents.AppendLine("Lines preceded by a '+' are additions and a '-' indicates removal."); + tableOfContents.AppendLine(); + + foreach ((string assemblyName, string text) in generator.Results.OrderBy(r => r.Key)) + { + string fileName = $"{_tableOfContentsTitle}_{assemblyName}.md"; + tableOfContents.AppendLine($"* [{assemblyName}]({fileName})"); + + string filePath = Path.Combine(_outputFolderPath, fileName); + if (_writeToDisk) + { + await File.WriteAllTextAsync(filePath, text).ConfigureAwait(false); + } + else + { + _results.Add(filePath, text); + } + + _log.LogMessage($"Wrote '{filePath}'."); + } + + tableOfContents.AppendLine(); + + string tableOfContentsFilePath = Path.Combine(_outputFolderPath, $"{_tableOfContentsTitle}.md"); + + if (_writeToDisk) + { + await File.WriteAllTextAsync(tableOfContentsFilePath, tableOfContents.ToString()).ConfigureAwait(false); + } + else + { + _results.Add(tableOfContentsFilePath, tableOfContents.ToString()); + } + + _log.LogMessage($"Wrote table of contents to '{tableOfContentsFilePath}'."); + } + + private static string[] CollectListsFromFiles(FileInfo[]? filesWithLists) + { + List list = []; + + if (filesWithLists != null) + { + foreach (FileInfo file in filesWithLists) + { + // This will throw if file does not exist. + foreach (string line in File.ReadLines(file.FullName)) + { + if (!list.Contains(line)) + { + // Prevent duplicates. + list.Add(line); + } + } + } + } + + return [.. list.Order()]; + } +} diff --git a/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/IDiffGenerator.cs b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/IDiffGenerator.cs new file mode 100644 index 000000000000..32c7f2ceb478 --- /dev/null +++ b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/IDiffGenerator.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.ApiDiff; + +public interface IDiffGenerator +{ + /// + /// Gets the results of the diff. The key is the assembly name and the value is the diff. This dictionary might get populated after calling , depending on the use case. + /// + IReadOnlyDictionary Results { get; } + + /// + /// Asynchronously runs the diff generator and may populate the dictionary depending on the use case. + /// + Task RunAsync(); +} diff --git a/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/MemoryOutputDiffGenerator.cs b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/MemoryOutputDiffGenerator.cs new file mode 100644 index 000000000000..a5cb01b16ef6 --- /dev/null +++ b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/MemoryOutputDiffGenerator.cs @@ -0,0 +1,776 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using DiffPlex.DiffBuilder; +using DiffPlex.DiffBuilder.Model; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.DotNet.ApiDiff.SyntaxRewriter; +using Microsoft.DotNet.ApiSymbolExtensions; +using Microsoft.DotNet.ApiSymbolExtensions.Filtering; +using Microsoft.DotNet.ApiSymbolExtensions.Logging; +using Microsoft.DotNet.GenAPI; +using Microsoft.DotNet.GenAPI.SyntaxRewriter; + +namespace Microsoft.DotNet.ApiDiff; + +/// +/// Generates a markdown diff of two different versions of the same assembly. +/// +public class MemoryOutputDiffGenerator : IDiffGenerator +{ + private readonly ILog _log; + private readonly IAssemblySymbolLoader _beforeLoader; + private readonly IAssemblySymbolLoader _afterLoader; + private readonly ConcurrentDictionary _beforeAssemblySymbols; + private readonly ConcurrentDictionary _afterAssemblySymbols; + private readonly bool _addPartialModifier; + private readonly ISymbolFilter _attributeSymbolFilter; + private readonly ISymbolFilter _symbolFilter; + private readonly SyntaxTriviaList _twoSpacesTrivia; + private readonly SyntaxToken _missingCloseBrace; + private readonly SyntaxTrivia _endOfLineTrivia; + private readonly SyntaxList _emptyAttributeList; + private readonly IEnumerable> _diagnosticOptions; + private readonly ConcurrentDictionary _results; + private readonly ChildrenNodesComparer _childrenNodesComparer; + + /// + /// Initializes a new instance of the class. + /// + /// The logger to use for logging messages. + /// The loader for the "before" assembly symbols. + /// The loader for the "after" assembly symbols. + /// The dictionary of "before" assembly symbols. + /// The dictionary of "after" assembly symbols. + /// An optional list of attributes to avoid showing in the diff. + /// An optional list of APIs to avoid showing in the diff. + /// A boolean indicating whether to add the partial modifier to types. + /// An optional dictionary of diagnostic options. + internal MemoryOutputDiffGenerator( + ILog log, + IAssemblySymbolLoader beforeLoader, + IAssemblySymbolLoader afterLoader, + Dictionary beforeAssemblySymbols, + Dictionary afterAssemblySymbols, + string[]? attributesToExclude, + string[]? apisToExclude, + bool addPartialModifier, + IEnumerable>? diagnosticOptions = null) + { + _log = log; + _beforeLoader = beforeLoader; + _afterLoader = afterLoader; + _beforeAssemblySymbols = new ConcurrentDictionary(beforeAssemblySymbols); + _afterAssemblySymbols = new ConcurrentDictionary(afterAssemblySymbols); + _addPartialModifier = addPartialModifier; + _diagnosticOptions = diagnosticOptions ?? DiffGeneratorFactory.DefaultDiagnosticOptions; + _attributeSymbolFilter = SymbolFilterFactory.GetFilterFromList(attributesToExclude ?? [], includeExplicitInterfaceImplementationSymbols: true); + _symbolFilter = SymbolFilterFactory.GetFilterFromList(apisToExclude ?? [], includeExplicitInterfaceImplementationSymbols: true); + _twoSpacesTrivia = SyntaxFactory.TriviaList(SyntaxFactory.Space, SyntaxFactory.Space); + _missingCloseBrace = SyntaxFactory.MissingToken(SyntaxKind.CloseBraceToken); + _emptyAttributeList = SyntaxFactory.List(); + _results = []; + _endOfLineTrivia = Environment.NewLine == "\r\n" ? SyntaxFactory.CarriageReturnLineFeed : SyntaxFactory.LineFeed; + _childrenNodesComparer = new ChildrenNodesComparer(); + } + + /// + public IReadOnlyDictionary Results => _results.AsReadOnly(); + + /// + public async Task RunAsync() + { + Stopwatch swRun = Stopwatch.StartNew(); + + foreach ((string beforeAssemblyName, IAssemblySymbol beforeAssemblySymbol) in _beforeAssemblySymbols) + { + // Needs to block so the _afterAssemblySymbols dictionary gets updated. + await ProcessBeforeAndAfterAssemblyAsync(beforeAssemblyName, beforeAssemblySymbol).ConfigureAwait(false); + } + + // Needs to happen after processing the before and after assemblies and filtering out the existing ones. + foreach ((string afterAssemblyName, IAssemblySymbol afterAssemblySymbol) in _afterAssemblySymbols) + { + await ProcessNewAssemblyAsync(afterAssemblyName, afterAssemblySymbol).ConfigureAwait(false); + } + + _log.LogMessage($"FINAL TOTAL TIME: {swRun.Elapsed.TotalMilliseconds / 1000.0 / 60.0:F2} mins."); + swRun.Stop(); + } + + private async Task ProcessBeforeAndAfterAssemblyAsync(string beforeAssemblyName, IAssemblySymbol beforeAssemblySymbol) + { + StringBuilder sb = new(); + + _log.LogMessage($"Visiting assembly {beforeAssemblySymbol.Name} (before vs after)..."); + + Stopwatch swAssembly = Stopwatch.StartNew(); + (SyntaxNode beforeAssemblyNode, SemanticModel beforeModel) = await GetAssemblyRootNodeAndModelAsync(_beforeLoader, beforeAssemblySymbol).ConfigureAwait(false); + string finalMessage = $"Finished visiting {beforeAssemblySymbol.Name} (before) in {swAssembly.Elapsed.TotalMilliseconds / 1000.0:F2}s{Environment.NewLine}"; + + // See if an assembly with the same name can be found in the diff folder. + if (_afterAssemblySymbols.TryGetValue(beforeAssemblyName, out IAssemblySymbol? afterAssemblySymbol)) + { + string afterAssemblyString = await ProcessAfterAssemblyAsync(afterAssemblySymbol, beforeAssemblyNode, beforeModel).ConfigureAwait(false); + sb.Append(afterAssemblyString); + + // Remove the found ones. The remaining ones will be processed at the end because they're new. + _afterAssemblySymbols.Remove(beforeAssemblyName, out _); + } + else + { + // The assembly was removed in the diff. + //sb.Append(GenerateDeletedDiff(beforeAssemblyNode)); + sb.Append(VisitChildren(beforeAssemblyNode, afterParentNode: null, beforeModel, afterModel: null, ChangeType.Deleted, wereParentAttributesChanged: false)); + } + + if (sb.Length > 0) + { + _results.TryAdd(beforeAssemblyName, GetFinalAssemblyDiff(beforeAssemblyName, sb.ToString())); + } + } + + private async Task ProcessNewAssemblyAsync(string afterAssemblyName, IAssemblySymbol afterAssemblySymbol) + { + string afterAssemblyString = await ProcessAfterAssemblyAsync(afterAssemblySymbol).ConfigureAwait(false); + if (afterAssemblyString.Length > 0) + { + _results.TryAdd(afterAssemblyName, GetFinalAssemblyDiff(afterAssemblyName, afterAssemblyString)); + } + } + + private async Task ProcessAfterAssemblyAsync(IAssemblySymbol afterAssemblySymbol, SyntaxNode? beforeParentNode = null, SemanticModel? beforeModel = null) + { + StringBuilder sb = new(); + var swNew = Stopwatch.StartNew(); + (SyntaxNode afterAssemblyNode, SemanticModel afterModel) = await GetAssemblyRootNodeAndModelAsync(_afterLoader, afterAssemblySymbol).ConfigureAwait(false); + string assemblyDescriptor = beforeParentNode == null ? "new" : "existing"; + string finalMessage = $"Finished visiting *{assemblyDescriptor}* assembly {afterAssemblySymbol.Name} in {swNew.Elapsed.TotalMilliseconds / 1000.0:F2}s{Environment.NewLine}"; + + swNew = Stopwatch.StartNew(); + ChangeType parentChangeType = beforeParentNode == null ? ChangeType.Inserted : ChangeType.Unchanged; + // We don't care about changed assembly attributes (for now). + sb.Append(VisitChildren(beforeParentNode, afterAssemblyNode, beforeModel, afterModel, parentChangeType, wereParentAttributesChanged: false)); + finalMessage += $"Finished generating diff for *{assemblyDescriptor}* assembly {afterAssemblySymbol.Name} in {swNew.Elapsed.TotalMilliseconds / 1000.0:F2}s{Environment.NewLine}"; + + _log.LogMessage(finalMessage); + swNew.Stop(); + + return sb.ToString(); + } + + private async Task<(SyntaxNode rootNode, SemanticModel model)> GetAssemblyRootNodeAndModelAsync(IAssemblySymbolLoader loader, IAssemblySymbol assemblySymbol) + { + CSharpAssemblyDocumentGeneratorOptions options = new(loader, _symbolFilter, _attributeSymbolFilter) + { + HideImplicitDefaultConstructors = false, + ShouldFormat = true, + ShouldReduce = false, + MetadataReferences = loader.MetadataReferences, + DiagnosticOptions = _diagnosticOptions, + SyntaxRewriters = [ + // IMPORTANT: The order of these elements matters! + new TypeDeclarationCSharpSyntaxRewriter(_addPartialModifier), // This must be visited BEFORE GlobalPrefixRemover as it depends on the 'global::' prefix to be found + GlobalPrefixRemover.Singleton, // And then call this ASAP afterwards so there are fewer identifiers to visit + PrimitiveSimplificationRewriter.Singleton, + RemoveBodyCSharpSyntaxRewriter.Singleton, + SingleLineStatementCSharpSyntaxRewriter.Singleton, + ], + AdditionalAnnotations = [Formatter.Annotation] // Formatter is needed to fix some spacing + }; + + CSharpAssemblyDocumentGenerator docGenerator = new(_log, options); + + Document document = await docGenerator.GetDocumentForAssemblyAsync(assemblySymbol).ConfigureAwait(false); // Super hot and resource-intensive path + SyntaxNode root = await document.GetSyntaxRootAsync().ConfigureAwait(false) ?? throw new InvalidOperationException($"Root node could not be found for document of assembly '{assemblySymbol.Name}'."); + SemanticModel model = await document.GetSemanticModelAsync().ConfigureAwait(false) ?? throw new InvalidOperationException($"Semantic model could not be found for document of assembly '{assemblySymbol.Name}'."); + + return (root, model); + } + + private static string GetFinalAssemblyDiff(string assemblyName, string diffText) + { + StringBuilder sbAssembly = new(); + sbAssembly.AppendLine($"# {assemblyName}"); + sbAssembly.AppendLine(); + sbAssembly.AppendLine("```diff"); + sbAssembly.Append(diffText.TrimEnd()); // Leading text stays, it's valid indentation. + sbAssembly.AppendLine(); + sbAssembly.AppendLine("```"); + return sbAssembly.ToString(); + } + + private string? VisitChildren(SyntaxNode? beforeParentNode, SyntaxNode? afterParentNode, SemanticModel? beforeModel, SemanticModel? afterModel, ChangeType parentChangeType, bool wereParentAttributesChanged) + { + Debug.Assert(beforeParentNode != null || afterParentNode != null); + + ConcurrentDictionary beforeChildrenNodes = CollectChildrenNodes(beforeParentNode, beforeModel); + ConcurrentDictionary afterChildrenNodes = CollectChildrenNodes(afterParentNode, afterModel); + + StringBuilder sb = new(); + // Traverse all the elements found on the left side. This only visits unchanged, modified and deleted APIs. + // In other words, this loop excludes those that are new on the right. Those are handled later. + foreach ((string beforeMemberName, MemberDeclarationSyntax beforeMemberNode) in beforeChildrenNodes.Order(_childrenNodesComparer)) + { + if (afterChildrenNodes.TryGetValue(beforeMemberName, out MemberDeclarationSyntax? afterMemberNode) && + beforeMemberNode.Kind() == afterMemberNode.Kind()) + { + if (beforeMemberNode is BaseTypeDeclarationSyntax && afterMemberNode is BaseTypeDeclarationSyntax || + beforeMemberNode is BaseNamespaceDeclarationSyntax && afterMemberNode is BaseNamespaceDeclarationSyntax) + { + // At this point, the current API (which is a type or a namespace) is considered unmodified in its signature. + // Before visiting its children, which might have changed, we need to check if the current API has attributes that changed, and append the diff if needed. + // The children visitation should take care of wrapping the children with the current 'unmodified' parent API. + string? attributes = VisitAttributes(beforeMemberNode, afterMemberNode, beforeModel, afterModel); + if (attributes != null) + { + sb.Append(attributes); + } + sb.Append(VisitChildren(beforeMemberNode, afterMemberNode, beforeModel, afterModel, ChangeType.Unchanged, wereParentAttributesChanged: attributes != null)); + } + else + { + // This returns the current member (as changed) topped with its added/deleted/changed attributes. + sb.Append(VisitLeafNode(beforeMemberNode, afterMemberNode, beforeModel, afterModel, ChangeType.Modified)); + } + // Remove the found ones. The remaining ones will be processed at the end because they're new. + afterChildrenNodes.Remove(beforeMemberName, out _); + } + else + { + // This returns the current member (as deleted) topped with its attributes as deleted. + if (beforeMemberNode is BaseTypeDeclarationSyntax or BaseNamespaceDeclarationSyntax) + { + string? attributes = VisitAttributes(beforeMemberNode, afterMemberNode, beforeModel, afterModel); + if (attributes != null) + { + sb.Append(attributes); + } + sb.Append(VisitChildren(beforeMemberNode, afterParentNode: null, beforeModel, afterModel: null, ChangeType.Deleted, wereParentAttributesChanged: true)); + } + else + { + sb.Append(VisitLeafNode(beforeMemberNode, afterNode: null, beforeModel, afterModel, ChangeType.Deleted)); + } + } + } + + if (!afterChildrenNodes.IsEmpty) + { + // Traverse all the elements that are new on the right side which were not found on the left. They are all treated as new APIs. + foreach ((string newMemberName, MemberDeclarationSyntax newMemberNode) in afterChildrenNodes.Order(_childrenNodesComparer)) + { + // Need to do a full visit of each member of namespaces and types anyway, so that leaf bodies are removed + if (newMemberNode is BaseTypeDeclarationSyntax or BaseNamespaceDeclarationSyntax) + { + string? attributes = VisitAttributes(beforeNode: null, newMemberNode, beforeModel, afterModel); + if (attributes != null) + { + sb.Append(attributes); + } + sb.Append(VisitChildren(beforeParentNode: null, newMemberNode, beforeModel, afterModel, ChangeType.Inserted, wereParentAttributesChanged: attributes != null)); + } + else + { + // This returns the current member (as changed) topped with its added/deleted/changed attributes. + sb.Append(VisitLeafNode(beforeNode: null, newMemberNode, beforeModel, afterModel, ChangeType.Inserted)); + } + } + } + + if (sb.Length > 0 || wereParentAttributesChanged || parentChangeType != ChangeType.Unchanged) + { + if (afterParentNode is not null and not CompilationUnitSyntax) + { + return GetCodeWrappedByParent(sb.ToString(), afterParentNode, parentChangeType); + } + else if (beforeParentNode is not null and not CompilationUnitSyntax) + { + return GetCodeWrappedByParent(sb.ToString(), beforeParentNode, parentChangeType); + } + } + + return sb.ToString(); + } + + private ConcurrentDictionary CollectChildrenNodes(SyntaxNode? parentNode, SemanticModel? model) + { + if (parentNode == null) + { + return []; + } + + Debug.Assert(model != null); + + ConcurrentDictionary dictionary = []; + + if (parentNode is BaseNamespaceDeclarationSyntax) + { + // Find all types + foreach (BaseTypeDeclarationSyntax typeNode in GetMembersOfType(parentNode)) + { + dictionary.TryAdd(GetDocId(typeNode, model), typeNode); + } + + // Find all delegates + foreach (DelegateDeclarationSyntax delegateNode in GetMembersOfType(parentNode)) + { + dictionary.TryAdd(GetDocId(delegateNode, model), delegateNode); + } + } + else if (parentNode is BaseTypeDeclarationSyntax) + { + // Special case for records that have members + if (parentNode is RecordDeclarationSyntax record && record.Members.Any()) + { + foreach (MemberDeclarationSyntax memberNode in GetMembersOfType(parentNode)) + { + // Note that these could also be nested types + dictionary.TryAdd(GetDocId(memberNode, model), memberNode); + } + } + else + { + foreach (MemberDeclarationSyntax memberNode in GetMembersOfType(parentNode)) + { + dictionary.TryAdd(GetDocId(memberNode, model), memberNode); + } + } + } + else if (parentNode is CompilationUnitSyntax) + { + foreach (BaseNamespaceDeclarationSyntax namespaceNode in parentNode.DescendantNodes().OfType()) + { + dictionary.TryAdd(GetDocId(namespaceNode, model), namespaceNode); + } + } + else + { + throw new InvalidOperationException($"Unexpected node type '{parentNode.Kind()}'"); + } + + return dictionary; + } + + // Returns the specified leaf node as changed (type, member or namespace) topped with its added/deleted/changed attributes and the correct leading trivia. + private string? VisitLeafNode(MemberDeclarationSyntax? beforeNode, MemberDeclarationSyntax? afterNode, SemanticModel? beforeModel, SemanticModel? afterModel, ChangeType changeType) + { + Debug.Assert(beforeNode != null || afterModel != null); + Debug.Assert(beforeNode == null || (beforeNode != null && beforeModel != null)); + Debug.Assert(afterNode == null || (afterNode != null && afterModel != null)); + + StringBuilder sb = new(); + + // If the leaf node was added or deleted, the visited attributes will also show as added or deleted. + // If the attributes were changed, even if the leaf node wasn't, they should also show up as added+deleted. + string? attributes = VisitAttributes(beforeNode, afterNode, beforeModel, afterModel); + bool wereAttributesModified = attributes != null; + if (wereAttributesModified) + { + sb.Append(attributes); + } + + // The string builder contains the attributes. Now append them on top of the member without their default attributes. + MemberDeclarationSyntax? deconstructedBeforeNode = GetNodeWithoutAttributes(beforeNode); + MemberDeclarationSyntax? deconstructedAfterNode = GetNodeWithoutAttributes(afterNode); + + Debug.Assert(deconstructedBeforeNode != null || deconstructedAfterNode != null); + + switch (changeType) + { + case ChangeType.Deleted: + Debug.Assert(deconstructedBeforeNode != null); + sb.Append(GenerateDeletedDiff(deconstructedBeforeNode)); + break; + case ChangeType.Inserted: + Debug.Assert(deconstructedAfterNode != null); + sb.Append(GenerateAddedDiff(deconstructedAfterNode)); + break; + case ChangeType.Modified: + Debug.Assert(beforeNode != null && afterNode != null); + string? changed = GenerateChangedDiff(deconstructedBeforeNode!, deconstructedAfterNode!); + // At this stage, the attributes that decorated this API have already been analyzed and appended. + // If the API itself also changed, then we can directly append this API's diff under the attributes (if any). + if (changed != null) + { + sb.Append(changed); + } + // Otherwise, we still need to show the API as unmodified, but only if the attributes also changed. + else if (attributes != null) + { + Debug.Assert(deconstructedAfterNode != null); + sb.Append(GenerateUnchangedDiff(deconstructedAfterNode.WithoutTrailingTrivia())); + } + break; + default: + throw new NotSupportedException($"Unexpected change type '{changeType}'."); + } + + return sb.Length > 0 ? sb.ToString() : null; + } + + [return: NotNullIfNotNull(nameof(node))] + private MemberDeclarationSyntax? GetNodeWithoutAttributes(MemberDeclarationSyntax? node) + { + if (node == null) + { + return null; + } + + if (node is EnumMemberDeclarationSyntax enumMember) + { + SyntaxTriviaList commaTrivia = SyntaxFactory.TriviaList(SyntaxFactory.SyntaxTrivia(SyntaxKind.EndOfLineTrivia, ",")); + return enumMember + .WithAttributeLists(_emptyAttributeList) + .WithLeadingTrivia(node.GetLeadingTrivia()) + .WithTrailingTrivia(commaTrivia); + } + else if (node is BaseNamespaceDeclarationSyntax namespaceNode) + { + return namespaceNode.WithAttributeLists(_emptyAttributeList).WithLeadingTrivia(node.GetLeadingTrivia()); + } + else if (node is MemberDeclarationSyntax memberDeclaration) + { + return memberDeclaration.WithAttributeLists(_emptyAttributeList).WithLeadingTrivia(node.GetLeadingTrivia()); + } + + return node.WithAttributeLists(_emptyAttributeList).WithLeadingTrivia(node.GetLeadingTrivia()); + } + + // Returns a non-null string if any attribute was changed (added, deleted or modified). Returns null if all attributes were the same before and after. + private string? VisitAttributes(MemberDeclarationSyntax? beforeNode, MemberDeclarationSyntax? afterNode, SemanticModel? beforeModel, SemanticModel? afterModel) + { + Dictionary? beforeAttributeNodes = CollectAttributeNodes(beforeNode, beforeModel); + Dictionary? afterAttributeNodes = CollectAttributeNodes(afterNode, afterModel); + + StringBuilder sb = new(); + if (beforeAttributeNodes != null) + { + foreach ((string attributeName, AttributeSyntax beforeAttributeNode) in beforeAttributeNodes.OrderBy(x => x.Key)) + { + if (afterAttributeNodes != null && + afterAttributeNodes.TryGetValue(attributeName, out AttributeSyntax? afterAttributeNode)) + { + AttributeListSyntax beforeAttributeList = GetAttributeAsAttributeList(beforeAttributeNode) + .WithLeadingTrivia(beforeNode!.GetLeadingTrivia()); + AttributeListSyntax afterAttributeList = GetAttributeAsAttributeList(afterAttributeNode) + .WithLeadingTrivia(afterNode!.GetLeadingTrivia()); + + // We found the same after attribute. Retrieve the comparison string. + sb.Append(GenerateChangedDiff(beforeAttributeList, afterAttributeList)); + // Remove the found ones. The remaining ones will be processed at the end because they're new. + afterAttributeNodes.Remove(attributeName); + } + else + { + // Retrieve the attribute string as removed. + AttributeListSyntax beforeAttributeList = GetAttributeAsAttributeList(beforeAttributeNode) + .WithLeadingTrivia(beforeNode!.GetLeadingTrivia()); + sb.Append(GenerateDeletedDiff(beforeAttributeList)); + } + } + } + // At this point, we may have found changed or deleted attributes (or none). + // Check now if there were any added attributes. + if (afterAttributeNodes != null) + { + foreach ((_, AttributeSyntax newAttributeNode) in afterAttributeNodes) + { + // Retrieve the attribute string as added. + AttributeListSyntax newAttributeList = GetAttributeAsAttributeList(newAttributeNode) + .WithLeadingTrivia(afterNode!.GetLeadingTrivia()); + sb.Append(GenerateAddedDiff(newAttributeList)); + } + } + + return sb.Length > 0 ? sb.ToString() : null; + } + + // For types, members and namespaces. + private Dictionary CollectAttributeNodes(MemberDeclarationSyntax? memberNode, SemanticModel? model) + { + if (memberNode == null) + { + return []; + } + + Debug.Assert(model != null); + + Dictionary dictionary = []; + + foreach (AttributeListSyntax attributeListNode in memberNode.AttributeLists) + { + foreach (AttributeSyntax attributeNode in attributeListNode.Attributes) + { + IMethodSymbol? constructor = model.GetSymbolInfo(attributeNode).Symbol as IMethodSymbol; + if (constructor is null || !_attributeSymbolFilter.Include(constructor.ContainingType)) + { + // The attributes decorating an API are actually calls to their + // constructor method, but the attribute filter needs type docIDs. + continue; + } + var realAttributeNode = attributeNode.WithArgumentList(attributeNode.ArgumentList); + + if (!dictionary.TryAdd(GetAttributeDocId(constructor, attributeNode, model), realAttributeNode)) + { + _log.LogWarning($"Attribute already exists in dictionary. Attribute: '{realAttributeNode.ToFullString()}', Member: '{GetDocId(memberNode, model)}'"); + } + } + } + + return dictionary; + } + + private static AttributeListSyntax GetAttributeAsAttributeList(AttributeSyntax attributeNode) => + SyntaxFactory.AttributeList(SyntaxFactory.SeparatedList([attributeNode.WithArgumentList(attributeNode.ArgumentList)])); + + private string GetCodeWrappedByParent(string diffedChildrenCode, SyntaxNode parentNode, ChangeType parentChangeType) + { + if (parentNode is CompilationUnitSyntax) + { + // Nothing to wrap, parent is the actual assembly + return diffedChildrenCode; + } + + MemberDeclarationSyntax? memberParentNode = parentNode as MemberDeclarationSyntax; + Debug.Assert(memberParentNode != null); + + StringBuilder sb = new(); + + MemberDeclarationSyntax attributelessParentNode = GetNodeWithoutAttributes(memberParentNode); + SyntaxNode childlessParentNode = GetChildlessNode(attributelessParentNode); + + SyntaxTriviaList parentLeadingTrivia = parentChangeType == ChangeType.Unchanged ? _twoSpacesTrivia.AddRange(parentNode.GetLeadingTrivia()) : parentNode.GetLeadingTrivia(); + + string openingBraceCode = GetDeclarationAndOpeningBraceCode(childlessParentNode, parentLeadingTrivia); + string? diffedOpeningBraceCode = GetDiffedCode(openingBraceCode); + + string closingBraceCode = GetClosingBraceCode(childlessParentNode, parentLeadingTrivia); + string? diffedClosingBraceCode = GetDiffedCode(closingBraceCode); + + sb.Append(diffedOpeningBraceCode); + sb.Append(diffedChildrenCode); + sb.Append(diffedClosingBraceCode); + + return sb.ToString(); + + string? GetDiffedCode(string codeToDiff) + { + return parentChangeType switch + { + ChangeType.Inserted => GenerateAddedDiff(codeToDiff), + ChangeType.Deleted => GenerateDeletedDiff(codeToDiff), + ChangeType.Unchanged => codeToDiff, + _ => throw new InvalidOperationException($"Unexpected change type '{parentChangeType}'."), + }; + } + } + + private static SyntaxNode GetChildlessNode(MemberDeclarationSyntax node) + { + SyntaxNode childlessNode = node.RemoveNodes(node.ChildNodes().Where( + c => c is MemberDeclarationSyntax or BaseTypeDeclarationSyntax or DelegateDeclarationSyntax), SyntaxRemoveOptions.KeepNoTrivia)!; + + return childlessNode.WithLeadingTrivia(node.GetLeadingTrivia()); + } + + private string GetDeclarationAndOpeningBraceCode(SyntaxNode childlessNode, SyntaxTriviaList leadingTrivia) + { + SyntaxToken openBrace = SyntaxFactory.Token(SyntaxKind.OpenBraceToken) + .WithTrailingTrivia(_endOfLineTrivia); + + SyntaxNode unclosedNode; + + if (childlessNode is RecordDeclarationSyntax recordDecl) + { + if (recordDecl.OpenBraceToken.IsKind(SyntaxKind.None)) + { + unclosedNode = recordDecl; + } + else + { + // Add the missing newline after the declaration + SyntaxTriviaList endLineAndLeadingTrivia = SyntaxFactory.TriviaList(_endOfLineTrivia).AddRange(leadingTrivia); + openBrace = openBrace.WithLeadingTrivia(endLineAndLeadingTrivia); + unclosedNode = recordDecl.WithOpenBraceToken(openBrace).WithCloseBraceToken(_missingCloseBrace); + } + } + else + { + openBrace = openBrace.WithLeadingTrivia(leadingTrivia); + unclosedNode = childlessNode switch + { + BaseTypeDeclarationSyntax typeDecl => typeDecl.WithOpenBraceToken(openBrace) + .WithCloseBraceToken(_missingCloseBrace), + + NamespaceDeclarationSyntax nsDecl => nsDecl.WithOpenBraceToken(openBrace) + .WithCloseBraceToken(_missingCloseBrace), + + _ => childlessNode + }; + } + + return unclosedNode.WithLeadingTrivia(leadingTrivia).ToFullString(); + } + + private string GetClosingBraceCode(SyntaxNode childlessNode, SyntaxTriviaList leadingTrivia) + { + SyntaxToken closeBrace = childlessNode switch + { + RecordDeclarationSyntax recordDecl => recordDecl.CloseBraceToken.IsMissing ? _missingCloseBrace : recordDecl.CloseBraceToken, + BaseTypeDeclarationSyntax typeDecl => typeDecl.CloseBraceToken, + NamespaceDeclarationSyntax nsDecl => nsDecl.CloseBraceToken, + _ => throw new InvalidOperationException($"Unexpected node type '{childlessNode.Kind()}'") + }; + + return closeBrace.WithTrailingTrivia(_endOfLineTrivia) + .WithLeadingTrivia(leadingTrivia) + .ToFullString(); + } + + private static bool IsEnumMemberOrHasPublicOrProtectedModifierOrIsDestructor(MemberDeclarationSyntax m) => + // Destructors don't have visibility modifiers so they're special-cased + m.Modifiers.Any(SyntaxKind.PublicKeyword) || m.Modifiers.Any(SyntaxKind.ProtectedKeyword) || + // Enum member declarations don't have any modifiers + m is EnumMemberDeclarationSyntax || + m.IsKind(SyntaxKind.DestructorDeclaration); + + private static IEnumerable GetMembersOfType(SyntaxNode node) where T : MemberDeclarationSyntax => node + .ChildNodes() + .Where(n => n is T m && + // Interface members have no visibility modifiers + (node.IsKind(SyntaxKind.InterfaceDeclaration) || + // For the rest of the types, analyze each member directly + IsEnumMemberOrHasPublicOrProtectedModifierOrIsDestructor(m))) + .Cast(); + + private string GetAttributeDocId(IMethodSymbol attributeConstructorSymbol, AttributeSyntax attribute, SemanticModel model) + { + Debug.Assert(_attributeSymbolFilter.Include(attributeConstructorSymbol.ContainingType)); + + string? attributeDocId = null; + + if (attribute.ArgumentList is AttributeArgumentListSyntax list && list.Arguments.Any()) + { + // If the constructor has arguments, it might be added multiple times on top of an API, + // so we need to get the unique full string which includes the actual value of the arguments. + attributeDocId = attribute.ToFullString(); + } + else + { + try + { + attributeDocId = attributeConstructorSymbol.ContainingType.GetDocumentationCommentId(); + } + catch (Exception e) + { + _log.LogWarning($"Could not retrieve docId of the attribute constructor of {attribute.ToFullString()}: {e.Message} "); + } + } + + // Temporary workaround because some rare cases of attributes in WinForms cannot be found in the current model. + attributeDocId ??= attribute.ToFullString(); + + return attributeDocId; + } + + private string GetDocId(SyntaxNode node, SemanticModel model) + { + ISymbol? symbol = node switch + { + DestructorDeclarationSyntax destructorDeclaration => model.GetDeclaredSymbol(destructorDeclaration), + FieldDeclarationSyntax fieldDeclaration => model.GetDeclaredSymbol(fieldDeclaration.Declaration.Variables.First()), + EventDeclarationSyntax eventDeclaration => model.GetDeclaredSymbol(eventDeclaration), + EventFieldDeclarationSyntax eventFieldDeclaration => model.GetDeclaredSymbol(eventFieldDeclaration.Declaration.Variables.First()), + PropertyDeclarationSyntax propertyDeclaration => model.GetDeclaredSymbol(propertyDeclaration), + _ => model.GetDeclaredSymbol(node) + }; + + if (symbol?.GetDocumentationCommentId() is string docId) + { + if (node is RecordDeclarationSyntax record && record.ParameterList != null && record.ParameterList.Parameters.Any()) + { + // Special case for when a record has a parameter list, we need to be able to differentiate the signature's parameters too, + // but the regular DocId does not differentiate that. + return $"{docId}({record.ParameterList.Parameters.ToFullString()})"; + } + return docId; + } + + throw new NullReferenceException($"Could not get the DocID for node: {node}"); + } + + private static string? GenerateAddedDiff(SyntaxNode afterNode) => GenerateAddedDiff(afterNode.ToFullString()); + + private static string? GenerateDeletedDiff(SyntaxNode beforeNode) => GenerateDeletedDiff(beforeNode.ToFullString()); + + private static string? GenerateChangedDiff(SyntaxNode beforeNode, SyntaxNode afterNode) => + GenerateDiff(InlineDiffBuilder.Diff(oldText: beforeNode.ToFullString(), newText: afterNode.ToFullString())); + + private static string? GenerateAddedDiff(string afterNodeText) => + GenerateDiff(InlineDiffBuilder.Diff(oldText: string.Empty, newText: afterNodeText)); + + private static string? GenerateDeletedDiff(string beforeNodeText) => + GenerateDiff(InlineDiffBuilder.Diff(oldText:beforeNodeText, newText: string.Empty)); + + private static string? GenerateUnchangedDiff(SyntaxNode unchangedNode) + { + StringBuilder sb = new(); + string unchangedText = unchangedNode.ToFullString(); + foreach (var line in InlineDiffBuilder.Diff(oldText: unchangedText, newText: unchangedText).Lines) + { + sb.AppendLine($" {line.Text}"); + } + return sb.ToString(); + } + + private static string? GenerateDiff(DiffPaneModel diff) + { + StringBuilder sb = new(); + + foreach (var line in diff.Lines) + { + if (string.IsNullOrWhiteSpace(line.Text)) + { + continue; + } + switch (line.Type) + { + case ChangeType.Inserted: + sb.AppendLine($"+ {line.Text}"); + break; + case ChangeType.Deleted: + sb.AppendLine($"- {line.Text}"); + break; + default: + break; + } + } + return sb.Length == 0 ? null : sb.ToString(); + } + + private class ChildrenNodesComparer : IComparer> + { + public int Compare(KeyValuePair first, KeyValuePair second) + { + // Enum members need to be sorted by their value, not alphabetically, so they need to be special-cased. + if (first.Value is EnumMemberDeclarationSyntax beforeMember && second.Value is EnumMemberDeclarationSyntax afterMember && + beforeMember.EqualsValue is EqualsValueClauseSyntax beforeEVCS && afterMember.EqualsValue is EqualsValueClauseSyntax afterEVCS && + beforeEVCS.Value is LiteralExpressionSyntax beforeLes && afterEVCS.Value is LiteralExpressionSyntax afterLes) + { + return beforeLes.Token.ValueText.CompareTo(afterLes.Token.ValueText); + } + + // Everything else is shown alphabetically. + return first.Key.CompareTo(second.Key); + } + + } +} diff --git a/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/Microsoft.DotNet.ApiDiff.csproj b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/Microsoft.DotNet.ApiDiff.csproj new file mode 100644 index 000000000000..7dc8391ede44 --- /dev/null +++ b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/Microsoft.DotNet.ApiDiff.csproj @@ -0,0 +1,19 @@ + + + + $(NetToolMinimum) + false + true + + + + + + + + + + + + + diff --git a/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/SyntaxRewriter/GlobalPrefixRemover.cs b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/SyntaxRewriter/GlobalPrefixRemover.cs new file mode 100644 index 000000000000..74b1fdd5d3b1 --- /dev/null +++ b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/SyntaxRewriter/GlobalPrefixRemover.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.DotNet.ApiDiff.SyntaxRewriter; + +internal class GlobalPrefixRemover : CSharpSyntaxRewriter +{ + public static readonly GlobalPrefixRemover Singleton = new(); + + private const string GlobalPrefix = "global"; + + public override SyntaxNode? VisitQualifiedName(QualifiedNameSyntax node) + { + if (node.Left is AliasQualifiedNameSyntax alias && + alias.Alias.Identifier.Text == GlobalPrefix) + { + node = SyntaxFactory.QualifiedName(alias.Name, node.Right).WithTriviaFrom(node); + } + return base.VisitQualifiedName(node); + } +} diff --git a/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/SyntaxRewriter/PrimitiveSimplificationRewriter.cs b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/SyntaxRewriter/PrimitiveSimplificationRewriter.cs new file mode 100644 index 000000000000..8a48e3d6f990 --- /dev/null +++ b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/SyntaxRewriter/PrimitiveSimplificationRewriter.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.DotNet.ApiDiff.SyntaxRewriter; + +internal class PrimitiveSimplificationRewriter : CSharpSyntaxRewriter +{ + public static readonly PrimitiveSimplificationRewriter Singleton = new(); + + private static readonly Dictionary s_primitives = new() { + { "Boolean", SyntaxKind.BoolKeyword }, + { "Byte", SyntaxKind.ByteKeyword }, + { "Char", SyntaxKind.CharKeyword }, + { "Decimal", SyntaxKind.DecimalKeyword }, + { "Double", SyntaxKind.DoubleKeyword }, + { "Int16", SyntaxKind.ShortKeyword }, + { "Int32", SyntaxKind.IntKeyword }, + { "Int64", SyntaxKind.LongKeyword }, + { "Object", SyntaxKind.ObjectKeyword }, + { "SByte", SyntaxKind.SByteKeyword }, + { "Single", SyntaxKind.FloatKeyword }, + { "String", SyntaxKind.StringKeyword }, + { "UInt16", SyntaxKind.UShortKeyword }, + { "UInt32", SyntaxKind.UIntKeyword }, + { "UInt64", SyntaxKind.ULongKeyword }, + { "System.Boolean", SyntaxKind.BoolKeyword }, + { "System.Byte", SyntaxKind.ByteKeyword }, + { "System.Char", SyntaxKind.CharKeyword }, + { "System.Decimal", SyntaxKind.DecimalKeyword }, + { "System.Double", SyntaxKind.DoubleKeyword }, + { "System.Int16", SyntaxKind.ShortKeyword }, + { "System.Int32", SyntaxKind.IntKeyword }, + { "System.Int64", SyntaxKind.LongKeyword }, + { "System.Object", SyntaxKind.ObjectKeyword }, + { "System.SByte", SyntaxKind.SByteKeyword }, + { "System.Single", SyntaxKind.FloatKeyword }, + { "System.String", SyntaxKind.StringKeyword }, + { "System.UInt16", SyntaxKind.UShortKeyword }, + { "System.UInt32", SyntaxKind.UIntKeyword }, + { "System.UInt64", SyntaxKind.ULongKeyword }, + }; + + public override SyntaxNode? VisitQualifiedName(QualifiedNameSyntax node) + { + if (s_primitives.TryGetValue(node.Right.Identifier.Text, out SyntaxKind keyword)) + { + return SyntaxFactory.PredefinedType(SyntaxFactory.Token(keyword)).WithTriviaFrom(node); + } + + return base.VisitQualifiedName(node); + } +} diff --git a/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/SyntaxRewriter/RemoveBodySyntaxRewriter.cs b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/SyntaxRewriter/RemoveBodySyntaxRewriter.cs new file mode 100644 index 000000000000..62fb53acbd54 --- /dev/null +++ b/src/Compatibility/ApiDiff/Microsoft.DotNet.ApiDiff/SyntaxRewriter/RemoveBodySyntaxRewriter.cs @@ -0,0 +1,161 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.DotNet.ApiDiff.SyntaxRewriter; + +/// +/// Replaces the bodies of nodes that have one (methods, properties, events, etc.) with a semicolon. +/// +internal class RemoveBodyCSharpSyntaxRewriter : CSharpSyntaxRewriter +{ + public static readonly RemoveBodyCSharpSyntaxRewriter Singleton = new(); + + private readonly SyntaxToken _semiColonToken = SyntaxFactory.Token(SyntaxKind.SemicolonToken); + private readonly SyntaxToken _noneToken = SyntaxFactory.Token(SyntaxKind.None); + private readonly SyntaxTriviaList _endLineTrivia = SyntaxFactory.TriviaList((Environment.NewLine == "\r\n") ? SyntaxFactory.CarriageReturnLineFeed : SyntaxFactory.LineFeed); + + public override SyntaxNode? VisitConstructorDeclaration(ConstructorDeclarationSyntax node) + { + var result = node + .WithBody(null) // remove the default empty body wrapped by brackets + .WithoutLeadingTrivia() + .WithoutTrailingTrivia() // Remove the single space that follows this new declaration + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)) + .WithLeadingTrivia(node.GetLeadingTrivia()) + .WithTrailingTrivia(node.GetTrailingTrivia()); + return base.VisitConstructorDeclaration(result); + } + + // These bad boys look like: 'public static explicit operator int(MyClass value)' + public override SyntaxNode? VisitConversionOperatorDeclaration(ConversionOperatorDeclarationSyntax node) + { + var result = node + .WithBody(null) // remove the default empty body wrapped by brackets + .WithoutLeadingTrivia() + .WithoutTrailingTrivia() // Remove the single space that follows this new declaration + .WithSemicolonToken(_semiColonToken) + .WithLeadingTrivia(node.GetLeadingTrivia()) + .WithTrailingTrivia(node.GetTrailingTrivia()); + return base.VisitConversionOperatorDeclaration(result); + } + + public override SyntaxNode? VisitDestructorDeclaration(DestructorDeclarationSyntax node) + { + var result = node + .WithBody(null) // remove the default empty body wrapped by brackets + .WithoutLeadingTrivia() + .WithoutTrailingTrivia() // Remove the single space that follows this new declaration + .WithSemicolonToken(_semiColonToken) + .WithLeadingTrivia(node.GetLeadingTrivia()) + .WithTrailingTrivia(node.GetTrailingTrivia()); + return base.VisitDestructorDeclaration(result); + } + + public override SyntaxNode? VisitEventDeclaration(EventDeclarationSyntax node) + { + var result = node + .WithIdentifier(node.Identifier.WithoutTrivia()) + .WithAccessorList(GetEmptiedAccessors(node.AccessorList)); + return base.VisitEventDeclaration(result); + } + + public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node) + { + var result = node + .WithBody(null) // remove the default empty body wrapped by brackets + .WithoutLeadingTrivia() + .WithoutTrailingTrivia() // Remove the single space that follows this new declaration + .WithSemicolonToken(_semiColonToken) + .WithLeadingTrivia(node.GetLeadingTrivia()) + .WithTrailingTrivia(node.GetTrailingTrivia()); + return base.VisitMethodDeclaration(result); + } + + public override SyntaxNode? VisitOperatorDeclaration(OperatorDeclarationSyntax node) + { + if (node.OperatorToken.IsKind(SyntaxKind.GreaterThanToken)) + { + // Takes care of the missing space before the greater than + node = node.WithOperatorToken(SyntaxFactory.Token(SyntaxFactory.TriviaList(SyntaxFactory.Space), node.OperatorToken.Kind(), SyntaxTriviaList.Empty)); + } + var result = node + .WithBody(null) // remove the default empty body wrapped by brackets + .WithoutLeadingTrivia() + .WithoutTrailingTrivia() // Remove the single space that follows this new declaration + .WithSemicolonToken(_semiColonToken) + .WithLeadingTrivia(node.GetLeadingTrivia()) + .WithTrailingTrivia(node.GetTrailingTrivia()); + return base.VisitOperatorDeclaration(result); + } + + public override SyntaxNode? VisitPropertyDeclaration(PropertyDeclarationSyntax node) + { + var result = node.WithAccessorList(GetEmptiedAccessors(node.AccessorList)); + return base.VisitPropertyDeclaration(result); + } + + public override SyntaxNode? VisitRecordDeclaration(RecordDeclarationSyntax node) + { + if (node.Members.Any()) + { + // Fix the spaces and lack of newline + var replacedOpenBrace = SyntaxFactory.Token(_endLineTrivia.AddRange(node.GetLeadingTrivia()), SyntaxKind.OpenBraceToken, node.OpenBraceToken.TrailingTrivia); + node = node.WithOpenBraceToken(replacedOpenBrace); + } + else + { + // Replace braces with semicolon + node = node.WithOpenBraceToken(_noneToken) + .WithCloseBraceToken(_noneToken) + .WithSemicolonToken(_semiColonToken); + } + + if (node.ParameterList != null && node.ParameterList.Parameters.Any()) + { + // Fix the extra space + node = node.WithParameterList(node.ParameterList.WithoutTrailingTrivia()); + } + else + { + // Remove the parentheses if there are no arguments + node = node.WithParameterList(null); + } + + return base.VisitRecordDeclaration(node); + } + + private AccessorListSyntax? GetEmptiedAccessors(AccessorListSyntax? accessorList) + { + if (accessorList == null) + { + return null; + } + + List newAccessors = new(); + + + for (int i = 0; i < accessorList.Accessors.Count; i++) + { + AccessorDeclarationSyntax accessorDeclaration = accessorList.Accessors[i] + .WithBody(null) // remove the default empty body wrapped by brackets + .WithoutTrivia() // Important + .WithLeadingTrivia(SyntaxFactory.Space) // Add a space before the accessor + .WithSemicolonToken(_semiColonToken); // Append a semicolon at the end + + if (i == accessorList.Accessors.Count - 1) // Second to last + { + // Add a space after the semicolon only on the last accessor + accessorDeclaration = accessorDeclaration.WithTrailingTrivia(SyntaxFactory.Space); + } + + newAccessors.Add(accessorDeclaration); + } + + return SyntaxFactory.AccessorList(SyntaxFactory.List(newAccessors)).WithLeadingTrivia(SyntaxFactory.Space); + } + +} diff --git a/src/Compatibility/ApiDiff/apidiff.slnf b/src/Compatibility/ApiDiff/apidiff.slnf new file mode 100644 index 000000000000..44b080591de5 --- /dev/null +++ b/src/Compatibility/ApiDiff/apidiff.slnf @@ -0,0 +1,11 @@ +{ + "solution": { + "path": "..\\..\\..\\sdk.sln", + "projects": [ + "src\\Compatibility\\ApiDiff\\Microsoft.DotNet.ApiDiff\\Microsoft.DotNet.ApiDiff.csproj", + "src\\Compatibility\\ApiDiff\\Microsoft.DotNet.ApiDiff.Tool\\Microsoft.DotNet.ApiDiff.Tool.csproj", + "src\\Compatibility\\Microsoft.DotNet.ApiSymbolExtensions\\Microsoft.DotNet.ApiSymbolExtensions.csproj", + "test\\Microsoft.DotNet.ApiDiff.Tests\\Microsoft.DotNet.ApiDiff.Tests.csproj" + ] + } +} diff --git a/src/Compatibility/Directory.Packages.props b/src/Compatibility/Directory.Packages.props index 9898fda73ffd..be120d4cea62 100644 --- a/src/Compatibility/Directory.Packages.props +++ b/src/Compatibility/Directory.Packages.props @@ -8,8 +8,4 @@ - - - - diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpAssemblyDocumentGenerator.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpAssemblyDocumentGenerator.cs index 7df552851bd3..f22d09f2b7f7 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpAssemblyDocumentGenerator.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpAssemblyDocumentGenerator.cs @@ -4,7 +4,9 @@ using System.Collections.Immutable; using System.Reflection; using System.Runtime.CompilerServices; +using System.Xml.Linq; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeStyle; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Formatting; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -13,9 +15,7 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Simplification; using Microsoft.DotNet.ApiSymbolExtensions; -using Microsoft.DotNet.ApiSymbolExtensions.Filtering; using Microsoft.DotNet.ApiSymbolExtensions.Logging; -using Microsoft.DotNet.GenAPI.SyntaxRewriter; namespace Microsoft.DotNet.GenAPI; @@ -25,58 +25,28 @@ namespace Microsoft.DotNet.GenAPI; public sealed class CSharpAssemblyDocumentGenerator { private readonly ILog _log; - private readonly IAssemblySymbolLoader _loader; - private readonly ISymbolFilter _symbolFilter; - private readonly ISymbolFilter _attributeDataSymbolFilter; - private readonly string? _exceptionMessage; - private readonly bool _includeAssemblyAttributes; + private readonly CSharpAssemblyDocumentGeneratorOptions _options; private readonly AdhocWorkspace _adhocWorkspace; private readonly SyntaxGenerator _syntaxGenerator; - private readonly IEnumerable? _metadataReferences; - private readonly bool _addPartialModifier; - private readonly bool _hideImplicitDefaultConstructors; private readonly CSharpCompilationOptions _compilationOptions; /// /// Initializes a new instance of the class. /// /// The logger to use. - /// The assembly symbol loader to use. - /// The symbol filter to use. - /// The attribute data symbol filter to use. - /// The optional exception message to use. - /// Whether to include assembly attributes or not. - /// The metadata references to use. The default value is . - /// The optional diagnostic options to use. The default value is . - /// Whether to add the partial modifier or not. The default value is . - /// Whether to hide implicit default constructors or not. The default value is . - public CSharpAssemblyDocumentGenerator(ILog log, - IAssemblySymbolLoader loader, - ISymbolFilter symbolFilter, - ISymbolFilter attributeDataSymbolFilter, - string? exceptionMessage, - bool includeAssemblyAttributes, - IEnumerable? metadataReferences = null, - IEnumerable>? diagnosticOptions = null, - bool addPartialModifier = true, - bool hideImplicitDefaultConstructors = true) + /// The options to configure the generator. + public CSharpAssemblyDocumentGenerator(ILog log, CSharpAssemblyDocumentGeneratorOptions options) { _log = log; - _loader = loader; - _symbolFilter = symbolFilter; - _attributeDataSymbolFilter = attributeDataSymbolFilter; - _exceptionMessage = exceptionMessage; - _includeAssemblyAttributes = includeAssemblyAttributes; + _options = options; + _adhocWorkspace = new AdhocWorkspace(); _syntaxGenerator = SyntaxGenerator.GetGenerator(_adhocWorkspace, LanguageNames.CSharp); - _metadataReferences = metadataReferences; - _addPartialModifier = addPartialModifier; - _hideImplicitDefaultConstructors = hideImplicitDefaultConstructors; _compilationOptions = new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable, - specificDiagnosticOptions: diagnosticOptions); + specificDiagnosticOptions: _options.DiagnosticOptions); } /// @@ -84,16 +54,16 @@ public CSharpAssemblyDocumentGenerator(ILog log, /// /// The assembly symbol that represents the loaded assembly. /// The source code document instance of the specified assembly symbol. - public Document GetDocumentForAssembly(IAssemblySymbol assemblySymbol) + public async Task GetDocumentForAssemblyAsync(IAssemblySymbol assemblySymbol) { Project project = _adhocWorkspace.AddProject(ProjectInfo.Create( ProjectId.CreateNewId(), VersionStamp.Create(), assemblySymbol.Name, assemblySymbol.Name, LanguageNames.CSharp, compilationOptions: _compilationOptions)); - project = project.AddMetadataReferences(_metadataReferences ?? _loader.MetadataReferences); + project = project.AddMetadataReferences(_options.MetadataReferences ?? _options.Loader.MetadataReferences); - IEnumerable namespaceSymbols = EnumerateNamespaces(assemblySymbol).Where(_symbolFilter.Include); - List namespaceSyntaxNodes = []; + IEnumerable namespaceSymbols = EnumerateNamespaces(assemblySymbol).Where(_options.SymbolFilter.Include); + List namespaceSyntaxNodes = []; foreach (INamespaceSymbol namespaceSymbol in namespaceSymbols.Order()) { SyntaxNode? syntaxNode = Visit(namespaceSymbol); @@ -104,38 +74,48 @@ public Document GetDocumentForAssembly(IAssemblySymbol assemblySymbol) } } - SyntaxNode compilationUnit = _syntaxGenerator.CompilationUnit(namespaceSyntaxNodes) - .WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation) - .Rewrite(new TypeDeclarationCSharpSyntaxRewriter(_addPartialModifier)) - .Rewrite(new BodyBlockCSharpSyntaxRewriter(_exceptionMessage)); + SyntaxNode compilationUnit = _syntaxGenerator.CompilationUnit(namespaceSyntaxNodes); - if (_includeAssemblyAttributes) + if (_options.AdditionalAnnotations.Any()) + { + compilationUnit = compilationUnit.WithAdditionalAnnotations(_options.AdditionalAnnotations); + } + + if (_options.IncludeAssemblyAttributes) { compilationUnit = GenerateAssemblyAttributes(assemblySymbol, compilationUnit); } + // This depends on finding attribute by their fully qualified names, so do not rewrite the syntax tree yet. compilationUnit = GenerateForwardedTypeAssemblyAttributes(assemblySymbol, compilationUnit); compilationUnit = compilationUnit.NormalizeWhitespace(eol: Environment.NewLine); + // Rewrite after performing all the necessary compilationUnit alterations, + // but right before generating the final document. + foreach (CSharpSyntaxRewriter rewriter in _options.SyntaxRewriters) + { + compilationUnit = compilationUnit.Rewrite(rewriter); + } + Document document = project.AddDocument(assemblySymbol.Name, compilationUnit); - document = Simplifier.ReduceAsync(document).Result; - document = Formatter.FormatAsync(document, DefineFormattingOptions()).Result; + + if (_options.ShouldReduce) + { + document = await Simplifier.ReduceAsync(document).ConfigureAwait(false); + } + if (_options.ShouldFormat) + { + document = await Formatter.FormatAsync(document, DefineFormattingOptions()).ConfigureAwait(false); + } return document; } - /// - /// Returns the formatted root syntax node for the specified document. - /// - /// A source code document instance. - /// The root syntax node of the specified document. - public SyntaxNode GetFormattedRootNodeForDocument(Document document) => document.GetSyntaxRootAsync().Result!.Rewrite(new SingleLineStatementCSharpSyntaxRewriter()); - private SyntaxNode? Visit(INamespaceSymbol namespaceSymbol) { SyntaxNode namespaceNode = _syntaxGenerator.NamespaceDeclaration(namespaceSymbol.ToDisplayString()); - IEnumerable typeMembers = namespaceSymbol.GetTypeMembers().Where(_symbolFilter.Include); + IEnumerable typeMembers = namespaceSymbol.GetTypeMembers().Where(_options.SymbolFilter.Include); if (!typeMembers.Any()) { return null; @@ -144,8 +124,8 @@ public Document GetDocumentForAssembly(IAssemblySymbol assemblySymbol) foreach (INamedTypeSymbol typeMember in typeMembers.Order()) { SyntaxNode typeDeclaration = _syntaxGenerator - .DeclarationExt(typeMember, _symbolFilter) - .AddMemberAttributes(_syntaxGenerator, typeMember, _attributeDataSymbolFilter); + .DeclarationExt(typeMember, _options.SymbolFilter) + .AddMemberAttributes(_syntaxGenerator, typeMember, _options.AttributeSymbolFilter); typeDeclaration = Visit(typeDeclaration, typeMember); @@ -180,7 +160,7 @@ private bool HidesBaseMember(ISymbol member) // If they're methods, compare their names and signatures. return baseType.GetMembers(member.Name) - .Any(baseMember => _symbolFilter.Include(baseMember) && + .Any(baseMember => _options.SymbolFilter.Include(baseMember) && (baseMember.Kind != SymbolKind.Method || method.SignatureEquals((IMethodSymbol)baseMember))); } @@ -189,7 +169,7 @@ private bool HidesBaseMember(ISymbol member) // If they're indexers, compare their signatures. return baseType.GetMembers(member.Name) .Any(baseMember => baseMember is IPropertySymbol baseProperty && - _symbolFilter.Include(baseMember) && + _options.SymbolFilter.Include(baseMember) && (prop.GetMethod.SignatureEquals(baseProperty.GetMethod) || prop.SetMethod.SignatureEquals(baseProperty.SetMethod))); } @@ -197,21 +177,21 @@ private bool HidesBaseMember(ISymbol member) { // For all other kinds of members, compare their names. return baseType.GetMembers(member.Name) - .Any(_symbolFilter.Include); + .Any(_options.SymbolFilter.Include); } } private SyntaxNode Visit(SyntaxNode namedTypeNode, INamedTypeSymbol namedType) { - IEnumerable members = namedType.GetMembers().Where(_symbolFilter.Include); + IEnumerable members = namedType.GetMembers().Where(_options.SymbolFilter.Include); // If it's a value type if (namedType.TypeKind == TypeKind.Struct) { - namedTypeNode = _syntaxGenerator.AddMembers(namedTypeNode, namedType.SynthesizeDummyFields(_symbolFilter, _attributeDataSymbolFilter)); + namedTypeNode = _syntaxGenerator.AddMembers(namedTypeNode, namedType.SynthesizeDummyFields(_options.SymbolFilter, _options.AttributeSymbolFilter)); } - namedTypeNode = _syntaxGenerator.AddMembers(namedTypeNode, namedType.TryGetInternalDefaultConstructor(_symbolFilter)); + namedTypeNode = _syntaxGenerator.AddMembers(namedTypeNode, namedType.TryGetInternalDefaultConstructor(_options.SymbolFilter)); foreach (ISymbol member in members.Order()) { @@ -219,15 +199,15 @@ private SyntaxNode Visit(SyntaxNode namedTypeNode, INamedTypeSymbol namedType) { // If the method is ExplicitInterfaceImplementation and is derived from an interface that was filtered out, we must filter it out as well. if (method.MethodKind == MethodKind.ExplicitInterfaceImplementation && - method.ExplicitInterfaceImplementations.Any(m => !_symbolFilter.Include(m.ContainingSymbol) || + method.ExplicitInterfaceImplementations.Any(m => !_options.SymbolFilter.Include(m.ContainingSymbol) || // if explicit interface implementation method has inaccessible type argument - m.ContainingType.HasInaccessibleTypeArgument(_symbolFilter))) + m.ContainingType.HasInaccessibleTypeArgument(_options.SymbolFilter))) { continue; } // Filter out default constructors since these will be added automatically - if (_hideImplicitDefaultConstructors && method.IsImplicitDefaultConstructor(_symbolFilter)) + if (_options.HideImplicitDefaultConstructors && method.IsImplicitDefaultConstructor(_options.SymbolFilter)) { continue; } @@ -235,14 +215,14 @@ private SyntaxNode Visit(SyntaxNode namedTypeNode, INamedTypeSymbol namedType) // If the property is derived from an interface that was filtered out, we must not filter it out either. if (member is IPropertySymbol property && !property.ExplicitInterfaceImplementations.IsEmpty && - property.ExplicitInterfaceImplementations.Any(m => !_symbolFilter.Include(m.ContainingSymbol))) + property.ExplicitInterfaceImplementations.Any(m => !_options.SymbolFilter.Include(m.ContainingSymbol))) { continue; } SyntaxNode memberDeclaration = _syntaxGenerator - .DeclarationExt(member, _symbolFilter) - .AddMemberAttributes(_syntaxGenerator, member, _attributeDataSymbolFilter); + .DeclarationExt(member, _options.SymbolFilter) + .AddMemberAttributes(_syntaxGenerator, member, _options.AttributeSymbolFilter); if (member is INamedTypeSymbol nestedTypeSymbol) { @@ -275,14 +255,13 @@ private SyntaxNode Visit(SyntaxNode namedTypeNode, INamedTypeSymbol namedType) private SyntaxNode GenerateAssemblyAttributes(IAssemblySymbol assembly, SyntaxNode compilationUnit) { // When assembly references aren't available, assembly attributes with foreign types won't be resolved. - ImmutableArray attributes = assembly.GetAttributes().ExcludeNonVisibleOutsideOfAssembly(_attributeDataSymbolFilter); + ImmutableArray attributes = assembly.GetAttributes().ExcludeNonVisibleOutsideOfAssembly(_options.AttributeSymbolFilter); // Emit assembly attributes from the IAssemblySymbol - List attributeSyntaxNodes = attributes + List attributeSyntaxNodes = [.. attributes .Where(attribute => !attribute.IsReserved()) .Select(attribute => _syntaxGenerator.Attribute(attribute) - .WithTrailingTrivia(SyntaxFactory.LineFeed)) - .ToList(); + .WithTrailingTrivia(SyntaxFactory.LineFeed))]; // [assembly: System.Reflection.AssemblyVersion("x.x.x.x")] if (attributes.All(attribute => attribute.AttributeClass?.ToDisplayString() != typeof(AssemblyVersionAttribute).FullName)) @@ -312,7 +291,7 @@ private SyntaxNode GenerateAssemblyAttributes(IAssemblySymbol assembly, SyntaxNo private SyntaxNode GenerateForwardedTypeAssemblyAttributes(IAssemblySymbol assembly, SyntaxNode compilationUnit) { - foreach (INamedTypeSymbol symbol in assembly.GetForwardedTypes().Where(_symbolFilter.Include)) + foreach (INamedTypeSymbol symbol in assembly.GetForwardedTypes().Where(_options.SymbolFilter.Include)) { if (symbol.TypeKind != TypeKind.Error) { diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpAssemblyDocumentGeneratorOptions.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpAssemblyDocumentGeneratorOptions.cs new file mode 100644 index 000000000000..1ad7af0208da --- /dev/null +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpAssemblyDocumentGeneratorOptions.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Simplification; +using Microsoft.DotNet.ApiSymbolExtensions; +using Microsoft.DotNet.ApiSymbolExtensions.Filtering; + +namespace Microsoft.DotNet.GenAPI; + +/// +/// Options for generating C# assembly documents with , allowing customization of various aspects of the generation process. +/// +public sealed class CSharpAssemblyDocumentGeneratorOptions +{ + public CSharpAssemblyDocumentGeneratorOptions(IAssemblySymbolLoader loader, ISymbolFilter symbolFilter, ISymbolFilter attributeSymbolFilter) + { + Loader = loader; + SymbolFilter = symbolFilter; + AttributeSymbolFilter = attributeSymbolFilter; + } + + public IAssemblySymbolLoader Loader { get; set; } + public ISymbolFilter SymbolFilter { get; set; } + public ISymbolFilter AttributeSymbolFilter { get; set; } + public bool HideImplicitDefaultConstructors { get; set; } + public bool IncludeAssemblyAttributes { get; set; } + public bool ShouldFormat { get; set; } + public bool ShouldReduce { get; set; } + public IEnumerable>? DiagnosticOptions { get; set; } + public IEnumerable? MetadataReferences { get; set; } + public List SyntaxRewriters { get; set; } = []; + public List AdditionalAnnotations { get; set; } = [Formatter.Annotation, Simplifier.Annotation]; +} diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs index ee5fe5b29b86..d72ae859b89e 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/CSharpFileBuilder.cs @@ -8,6 +8,7 @@ using Microsoft.DotNet.ApiSymbolExtensions; using Microsoft.DotNet.ApiSymbolExtensions.Filtering; using Microsoft.DotNet.ApiSymbolExtensions.Logging; +using Microsoft.DotNet.GenAPI.SyntaxRewriter; namespace Microsoft.DotNet.GenAPI { @@ -45,15 +46,31 @@ public CSharpFileBuilder(ILog log, { _textWriter = textWriter; _header = header; - _docGenerator = new CSharpAssemblyDocumentGenerator(log, loader, symbolFilter, attributeDataSymbolFilter, exceptionMessage, includeAssemblyAttributes, metadataReferences, addPartialModifier: addPartialModifier); + + CSharpAssemblyDocumentGeneratorOptions options = new(loader, symbolFilter, attributeDataSymbolFilter) + { + HideImplicitDefaultConstructors = true, + ShouldFormat = true, + ShouldReduce = true, + IncludeAssemblyAttributes = includeAssemblyAttributes, + MetadataReferences = metadataReferences, + SyntaxRewriters = [ + new TypeDeclarationCSharpSyntaxRewriter(addPartialModifier), + new BodyBlockCSharpSyntaxRewriter(exceptionMessage), + SingleLineStatementCSharpSyntaxRewriter.Singleton + ] + }; + + _docGenerator = new CSharpAssemblyDocumentGenerator(log, options); } /// public void WriteAssembly(IAssemblySymbol assemblySymbol) { _textWriter.Write(GetFormattedHeader(_header)); - Document document = _docGenerator.GetDocumentForAssembly(assemblySymbol); - _docGenerator.GetFormattedRootNodeForDocument(document).WriteTo(_textWriter); + Document document = _docGenerator.GetDocumentForAssemblyAsync(assemblySymbol).Result; + SyntaxNode root = document.GetSyntaxRootAsync().Result ?? throw new InvalidOperationException(Resources.SyntaxNodeNotFound); + root.WriteTo(_textWriter); } private static string GetFormattedHeader(string? customHeader) diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs index d11a9631be15..25ef58d6481b 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/GenAPIApp.cs @@ -35,6 +35,7 @@ public static void Run(ILog log, log, assembliesPaths, assemblyReferencesPaths, + assembliesToExclude: [], respectInternals: respectInternals); Run(log, diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/Resources.resx b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/Resources.resx index e1f8c81a3f6d..d57c7e2a29b9 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/Resources.resx +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/Resources.resx @@ -123,4 +123,7 @@ Could not resolve type '{0}' in containing assembly '{1}' via type forward. Make sure that the assembly is provided as a reference and contains the type. - \ No newline at end of file + + Syntax node not found. + + diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxGeneratorExtensions.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxGeneratorExtensions.cs index 6618d47a20b1..d4938f265c35 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxGeneratorExtensions.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxGeneratorExtensions.cs @@ -105,7 +105,7 @@ public static SyntaxNode DeclarationExt(this SyntaxGenerator syntaxGenerator, IS catch (ArgumentException ex) { // re-throw the ArgumentException with the symbol that caused it. - throw new ArgumentException(ex.Message, symbol.ToDisplayString()); + throw new ArgumentException(ex.Message, symbol.ToDisplayString(), innerException: ex); } } diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxRewriter/SingleLineStatementCSharpSyntaxRewriter.cs b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxRewriter/SingleLineStatementCSharpSyntaxRewriter.cs index a002d48663b1..66990ea85300 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxRewriter/SingleLineStatementCSharpSyntaxRewriter.cs +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/SyntaxRewriter/SingleLineStatementCSharpSyntaxRewriter.cs @@ -17,6 +17,13 @@ namespace Microsoft.DotNet.GenAPI.SyntaxRewriter /// public class SingleLineStatementCSharpSyntaxRewriter : CSharpSyntaxRewriter { + // Use the singleton instead. + private SingleLineStatementCSharpSyntaxRewriter() + { + } + + public static readonly SingleLineStatementCSharpSyntaxRewriter Singleton = new(); + /// public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node) => VisitBaseMethodDeclarationSyntax(node); diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.cs.xlf b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.cs.xlf index cf42d11103aa..c0cbbea14d49 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.cs.xlf +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.cs.xlf @@ -12,6 +12,11 @@ Nepodařilo se přeložit typ {0} v sestavení {1}, které ho obsahuje, pomocí předávání typů. Ujistěte se, že sestavení je uvedeno jako odkaz a obsahuje typ. + + Syntax node not found. + Syntax node not found. + + \ No newline at end of file diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.de.xlf b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.de.xlf index 224eae4a66d4..9d3707eac030 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.de.xlf +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.de.xlf @@ -12,6 +12,11 @@ Der Typ „{0}“ in der enthaltenden Assembly „{1}“ konnte nicht über die Typweiterleitung aufgelöst werden. Stellen Sie sicher, dass die Assembly als Verweis bereitgestellt wird und den Typ enthält. + + Syntax node not found. + Syntax node not found. + + \ No newline at end of file diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.es.xlf b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.es.xlf index 763a07ee3515..79e6dc2896b2 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.es.xlf +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.es.xlf @@ -12,6 +12,11 @@ No se pudo resolver el tipo "{0}" en el ensamblado contenedor "{1}" mediante el reenvío de tipos. Asegúrese de que el ensamblado se proporciona como referencia y contiene el tipo. + + Syntax node not found. + Syntax node not found. + + \ No newline at end of file diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.fr.xlf b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.fr.xlf index 7db3ab1cf763..572d8af3dc44 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.fr.xlf +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.fr.xlf @@ -12,6 +12,11 @@ Impossible de résoudre le type '{0}' dans l’assembly conteneur '{1}' via le type Suivant. Assurez-vous que l’assembly est fourni en tant que référence et qu’il contient le type. + + Syntax node not found. + Syntax node not found. + + \ No newline at end of file diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.it.xlf b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.it.xlf index 4273ea24f205..846334601022 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.it.xlf +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.it.xlf @@ -12,6 +12,11 @@ Non è stato possibile risolvere il tipo '{0}' nell'assembly contenitore '{1}' tramite l'inoltro del tipo. Assicurarsi che l'assembly sia fornito come riferimento e contenga il tipo. + + Syntax node not found. + Syntax node not found. + + \ No newline at end of file diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.ja.xlf b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.ja.xlf index 55c2a3bce074..30db29ab6fc6 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.ja.xlf +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.ja.xlf @@ -12,6 +12,11 @@ アセンブリ '{1}' の型 '{0}' を型順方向で解決できませんでした。アセンブリが参照として指定され、型が含まれていることを確認してください。 + + Syntax node not found. + Syntax node not found. + + \ No newline at end of file diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.ko.xlf b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.ko.xlf index e86af11e0c38..7b6694ade7b6 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.ko.xlf +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.ko.xlf @@ -12,6 +12,11 @@ 형식 전달을 통해 '{1}' 어셈블리 형식을 포함하는 '{0}' 형식을 해결할 수 없습니다. 어셈블리가 참조로 제공되고 해당 형식을 포함하는지 확인하세요. + + Syntax node not found. + Syntax node not found. + + \ No newline at end of file diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.pl.xlf b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.pl.xlf index 4d6f28a3dd0d..60572f6ef1f3 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.pl.xlf +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.pl.xlf @@ -12,6 +12,11 @@ Nie można rozpoznać typu „{0}” w zawierającym go zestawie „{1}” przez przekazywanie typu. Upewnij się, że zestaw został podany jako odwołanie i zawiera typ. + + Syntax node not found. + Syntax node not found. + + \ No newline at end of file diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.pt-BR.xlf b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.pt-BR.xlf index 019f1d01bea8..2db1b9eea630 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.pt-BR.xlf +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.pt-BR.xlf @@ -12,6 +12,11 @@ Não foi possível resolver o tipo ''{0}'' no assembly que contém ''{1}'' por meio do tipo de encaminhamento. Certifique-se de que o assembly foi fornecido como uma referência e contém o tipo. + + Syntax node not found. + Syntax node not found. + + \ No newline at end of file diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.ru.xlf b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.ru.xlf index 4fb904f6fe01..79eaad017a18 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.ru.xlf +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.ru.xlf @@ -12,6 +12,11 @@ Не удалось разрешить тип "{0}" в содержащей сборке "{1}" путем перенаправления типа. Убедитесь, что сборка указана в качестве ссылки и содержит тип. + + Syntax node not found. + Syntax node not found. + + \ No newline at end of file diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.tr.xlf b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.tr.xlf index bd760ac7d8ce..ee6340a825d8 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.tr.xlf +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.tr.xlf @@ -12,6 +12,11 @@ '{1}' içeren derlemede tür yönlendirme yoluyla '{0}' türü çözülemedi. Derlemenin referans olarak verildiğinden ve türü içerdiğinden emin olun. + + Syntax node not found. + Syntax node not found. + + \ No newline at end of file diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.zh-Hans.xlf b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.zh-Hans.xlf index 80fbb68bd195..be5738573522 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.zh-Hans.xlf +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.zh-Hans.xlf @@ -12,6 +12,11 @@ 无法通过类型转发解析包含程序集 '{1}' 中的类型 '{0}'。请确保将程序集作为引用提供并包含类型。 + + Syntax node not found. + Syntax node not found. + + \ No newline at end of file diff --git a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.zh-Hant.xlf b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.zh-Hant.xlf index a889f1347e5c..8950b83662b6 100644 --- a/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.zh-Hant.xlf +++ b/src/Compatibility/GenAPI/Microsoft.DotNet.GenAPI/xlf/Resources.zh-Hant.xlf @@ -12,6 +12,11 @@ 無法透過類型轉送來解析包含組件 '{1}' 中的類型 '{0}'。請確定已提供組件作為參考,且包含類型。 + + Syntax node not found. + Syntax node not found. + + \ No newline at end of file diff --git a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/AssemblySymbolLoader.cs b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/AssemblySymbolLoader.cs index caf4300aac15..b908f6525f73 100644 --- a/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/AssemblySymbolLoader.cs +++ b/src/Compatibility/Microsoft.DotNet.ApiSymbolExtensions/AssemblySymbolLoader.cs @@ -57,10 +57,17 @@ public class AssemblySymbolLoader : IAssemblySymbolLoader /// The logger instance to use for message logging. /// A collection of paths where the assembly DLLs should be searched. /// An optional collection of paths where the assembly references should be searched. + /// A collection of assembly names that should not be skipped by the symbol loader. Set to empty to allow all assemblies. /// An optional list of diagnostic options to use when compiling the loaded assemblies. /// Whether to include internal symbols or not. /// A tuple containing an assembly symbol loader and its corresponding dictionary of assembly symbols. - public static (AssemblySymbolLoader, Dictionary) CreateFromFiles(ILog log, string[] assembliesPaths, string[]? assemblyReferencesPaths, IEnumerable>? diagnosticOptions = null, bool respectInternals = false) + public static (AssemblySymbolLoader, Dictionary) CreateFromFiles( + ILog log, + string[] assembliesPaths, + string[]? assemblyReferencesPaths, + string[] assembliesToExclude, + IEnumerable>? diagnosticOptions = null, + bool respectInternals = false) { if (assembliesPaths.Length == 0) { @@ -68,7 +75,7 @@ public static (AssemblySymbolLoader, Dictionary) Create new Dictionary()); } - bool atLeastOneReferencePath = assemblyReferencesPaths?.Count() > 0; + bool atLeastOneReferencePath = assemblyReferencesPaths?.Length > 0; AssemblySymbolLoader loader = new(log, diagnosticOptions, resolveAssemblyReferences: atLeastOneReferencePath, includeInternalSymbols: respectInternals); if (atLeastOneReferencePath) { @@ -79,6 +86,7 @@ public static (AssemblySymbolLoader, Dictionary) Create // Reference assemblies of the passed in assemblies that themselves are passed in, will be skipped to be resolved, // as they are resolved as part of the loop below. ImmutableHashSet fileNames = assembliesPaths.Select(path => Path.GetFileName(path)).ToImmutableHashSet(); + List assembliesToReturn = loader.LoadFromPaths(assembliesPaths, fileNames); // Create IAssemblySymbols out of the MetadataReferences. @@ -86,7 +94,8 @@ public static (AssemblySymbolLoader, Dictionary) Create Dictionary dictionary = []; foreach (MetadataReference metadataReference in assembliesToReturn) { - if (loader._cSharpCompilation.GetAssemblyOrModuleSymbol(metadataReference) is IAssemblySymbol assemblySymbol) + if (loader._cSharpCompilation.GetAssemblyOrModuleSymbol(metadataReference) is IAssemblySymbol assemblySymbol && + !assembliesToExclude.Contains(assemblySymbol.Name)) { dictionary.Add(assemblySymbol.Name, assemblySymbol); } diff --git a/src/Compatibility/compatibility.slnf b/src/Compatibility/compatibility.slnf index ab10435bc0e9..3dde06cd30ce 100644 --- a/src/Compatibility/compatibility.slnf +++ b/src/Compatibility/compatibility.slnf @@ -9,6 +9,8 @@ "src\\Compatibility\\ApiCompat\\Microsoft.DotNet.ApiCompat.Tool\\Microsoft.DotNet.ApiCompat.Tool.csproj", "src\\Compatibility\\ApiCompat\\Microsoft.DotNet.ApiCompatibility\\Microsoft.DotNet.ApiCompatibility.csproj", "src\\Compatibility\\ApiCompat\\Microsoft.DotNet.PackageValidation\\Microsoft.DotNet.PackageValidation.csproj", + "src\\Compatibility\\ApiDiff\\Microsoft.DotNet.ApiDiff.Tool\\Microsoft.DotNet.ApiDiff.Tool.csproj", + "src\\Compatibility\\ApiDiff\\Microsoft.DotNet.ApiDiff\\Microsoft.DotNet.ApiDiff.csproj", "src\\Compatibility\\GenAPI\\Microsoft.DotNet.GenAPI.Task\\Microsoft.DotNet.GenAPI.Task.csproj", "src\\Compatibility\\GenAPI\\Microsoft.DotNet.GenAPI.Tool\\Microsoft.DotNet.GenAPI.Tool.csproj", "src\\Compatibility\\GenAPI\\Microsoft.DotNet.GenAPI\\Microsoft.DotNet.GenAPI.csproj", @@ -16,10 +18,11 @@ "test\\Microsoft.DotNet.ApiCompat.IntegrationTests\\Microsoft.DotNet.ApiCompat.IntegrationTests.csproj", "test\\Microsoft.DotNet.ApiCompat.Tests\\Microsoft.DotNet.ApiCompat.Tests.csproj", "test\\Microsoft.DotNet.ApiCompatibility.Tests\\Microsoft.DotNet.ApiCompatibility.Tests.csproj", + "test\\Microsoft.DotNet.ApiDiff.Tests\\Microsoft.DotNet.ApiDiff.Tests.csproj", "test\\Microsoft.DotNet.ApiSymbolExtensions.Tests\\Microsoft.DotNet.ApiSymbolExtensions.Tests.csproj", "test\\Microsoft.DotNet.GenAPI.Tests\\Microsoft.DotNet.GenAPI.Tests.csproj", "test\\Microsoft.DotNet.PackageValidation.Tests\\Microsoft.DotNet.PackageValidation.Tests.csproj", "test\\Microsoft.NET.TestFramework\\Microsoft.NET.TestFramework.csproj" ] } -} \ No newline at end of file +} diff --git a/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Assembly.Tests.cs b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Assembly.Tests.cs new file mode 100644 index 000000000000..26d8de40313c --- /dev/null +++ b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Assembly.Tests.cs @@ -0,0 +1,109 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.ApiDiff.Tests; + +public class DiffAssemblyTests : DiffBaseTests +{ + [Fact] + public Task AssemblyAdd() => RunTestAsync( + before: [], + after: [("MyAddedAssembly.dll", """ + namespace MyNamespace + { + public struct MyStruct + { + } + } + """)], + expected: new Dictionary() { + { "MyAddedAssembly", """ + + namespace MyNamespace + + { + + public struct MyStruct + + { + + } + + } + """ } + }); + + [Fact] + public Task AssemblyChange() => RunTestAsync( + before: [("MyBeforeAssembly.dll", """ + namespace MyNamespace + { + public struct MyStruct + { + } + } + """)], + after: [("MyAfterAssembly.dll", """ + namespace MyNamespace + { + public struct MyStruct + { + } + } + """)], + expected: new Dictionary() { + { "MyBeforeAssembly", """ + - namespace MyNamespace + - { + - public struct MyStruct + - { + - } + - } + """ }, + { "MyAfterAssembly", """ + + namespace MyNamespace + + { + + public struct MyStruct + + { + + } + + } + """ } + }); + + [Fact] + public Task AssemblyDelete() => RunTestAsync( + before: [("MyRemovedAssembly.dll", """ + namespace MyNamespace + { + public struct MyStruct + { + } + } + """)], + after: [], + expected: new Dictionary() { + { "MyRemovedAssembly", """ + - namespace MyNamespace + - { + - public struct MyStruct + - { + - } + - } + """ } + }); + + [Fact] + public Task AssemblyUnchanged() => RunTestAsync( + before: [("MyAssembly.dll", """ + namespace MyNamespace + { + public struct MyStruct + { + } + } + """)], + after: [("MyAssembly.dll", """ + namespace MyNamespace + { + public struct MyStruct + { + } + } + """)], + expected: []); // No changes + +} diff --git a/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Attribute.Tests.cs b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Attribute.Tests.cs new file mode 100644 index 000000000000..a63f89e546f9 --- /dev/null +++ b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Attribute.Tests.cs @@ -0,0 +1,1066 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.ApiDiff.Tests; + +public class DiffAttributeTests : DiffBaseTests +{ + #region Type attributes + + [Fact] + public Task TypeAttributeAdd() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttributeAttribute : System.Attribute + { + public MyAttributeAttribute() { } + } + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttributeAttribute : System.Attribute + { + public MyAttributeAttribute() { } + } + [MyAttribute] + public class MyClass + { + } + } + """, + expectedCode: """ + namespace MyNamespace + { + + [MyNamespace.MyAttributeAttribute] + public class MyClass + { + } + } + """, + attributesToExclude: []); + + [Fact] + public Task TypeAttributeDeleteAndAdd() => + // Added APIs always show up at the end. + RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttribute1Attribute : System.Attribute + { + public MyAttribute1Attribute() { } + } + [MyAttribute1] + public class MyClass + { + public MyClass() { } + } + } + """, + afterCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttribute2Attribute : System.Attribute + { + public MyAttribute2Attribute() { } + } + [MyAttribute2] + public class MyClass + { + public MyClass() { } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + - [System.AttributeUsageAttribute(System.AttributeTargets.All)] + - public class MyAttribute1Attribute : System.Attribute + - { + - public MyAttribute1Attribute(); + - } + - [MyNamespace.MyAttribute1Attribute] + + [MyNamespace.MyAttribute2Attribute] + public class MyClass + { + } + + [System.AttributeUsageAttribute(System.AttributeTargets.All)] + + public class MyAttribute2Attribute : System.Attribute + + { + + public MyAttribute2Attribute(); + + } + } + """, + attributesToExclude: []); + + [Fact] + public Task TypeAttributeSwitch() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttribute1Attribute : System.Attribute + { + public MyAttribute1Attribute() { } + } + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttribute2Attribute : System.Attribute + { + public MyAttribute2Attribute() { } + } + [MyAttribute1] + public class MyClass + { + public MyClass() { } + } + } + """, + afterCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttribute1Attribute : System.Attribute + { + public MyAttribute1Attribute() { } + } + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttribute2Attribute : System.Attribute + { + public MyAttribute2Attribute() { } + } + [MyAttribute2] + public class MyClass + { + public MyClass() { } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + - [MyNamespace.MyAttribute1Attribute] + + [MyNamespace.MyAttribute2Attribute] + public class MyClass + { + } + } + """, + attributesToExclude: []); + + [Fact] + public Task TypeChangeAndAttributeAdd() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttributeAttribute : System.Attribute + { + public MyAttributeAttribute() { } + } + public class MyClass1 + { + public MyClass1() { } + } + } + """, + afterCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttributeAttribute : System.Attribute + { + public MyAttributeAttribute() { } + } + [MyAttribute] + public class MyClass2 + { + public MyClass2() { } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + - public class MyClass1 + - { + - public MyClass1(); + - } + + [MyNamespace.MyAttributeAttribute] + + public class MyClass2 + + { + + public MyClass2(); + + } + } + """, + attributesToExclude: []); + + [Fact] + public Task TypeChangeButAttributeStays() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttributeAttribute : System.Attribute + { + public MyAttributeAttribute() { } + } + [MyAttribute] + public class MyClass1 + { + public MyClass1() { } + } + } + """, + afterCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttributeAttribute : System.Attribute + { + public MyAttributeAttribute() { } + } + [MyAttribute] + public class MyClass2 + { + public MyClass2() { } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + - [MyNamespace.MyAttributeAttribute] + - public class MyClass1 + - { + - public MyClass1(); + - } + + [MyNamespace.MyAttributeAttribute] + + public class MyClass2 + + { + + public MyClass2(); + + } + } + """, + attributesToExclude: []); + + #endregion + + #region Member attributes + + [Fact] + public Task MemberAttributeAdd() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttributeAttribute : System.Attribute + { + public MyAttributeAttribute() { } + } + public class MyClass + { + public void MyMethod() { } + } + } + """, + afterCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttributeAttribute : System.Attribute + { + public MyAttributeAttribute() { } + } + public class MyClass + { + [MyAttribute] + public void MyMethod() { } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + [MyNamespace.MyAttributeAttribute] + public void MyMethod(); + } + } + """, + attributesToExclude: []); + + [Fact] + public Task MemberAttributeDeleteAndAdd() => + // Added APIs always show up at the end. + RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttribute1Attribute : System.Attribute + { + public MyAttribute1Attribute() { } + } + public class MyClass + { + public MyClass() { } + [MyAttribute1] + public void MyMethod() { } + } + } + """, + afterCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttribute2Attribute : System.Attribute + { + public MyAttribute2Attribute() { } + } + public class MyClass + { + public MyClass() { } + [MyAttribute2] + public void MyMethod() { } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + - [System.AttributeUsageAttribute(System.AttributeTargets.All)] + - public class MyAttribute1Attribute : System.Attribute + - { + - public MyAttribute1Attribute(); + - } + public class MyClass + { + - [MyNamespace.MyAttribute1Attribute] + + [MyNamespace.MyAttribute2Attribute] + public void MyMethod(); + } + + [System.AttributeUsageAttribute(System.AttributeTargets.All)] + + public class MyAttribute2Attribute : System.Attribute + + { + + public MyAttribute2Attribute(); + + } + } + """, + attributesToExclude: []); + + [Fact] + public Task MemberAttributeSwitch() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttribute1Attribute : System.Attribute + { + public MyAttribute1Attribute() { } + } + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttribute2Attribute : System.Attribute + { + public MyAttribute2Attribute() { } + } + public class MyClass + { + public MyClass() { } + [MyAttribute1] + public void MyMethod() { } + } + } + """, + afterCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttribute1Attribute : System.Attribute + { + public MyAttribute1Attribute() { } + } + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttribute2Attribute : System.Attribute + { + public MyAttribute2Attribute() { } + } + public class MyClass + { + public MyClass() { } + [MyAttribute2] + public void MyMethod() { } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - [MyNamespace.MyAttribute1Attribute] + + [MyNamespace.MyAttribute2Attribute] + public void MyMethod(); + } + } + """, + attributesToExclude: []); + + [Fact] + public Task MemberChangeAndAttributeAdd() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttributeAttribute : System.Attribute + { + public MyAttributeAttribute() { } + } + public class MyClass + { + public MyClass() { } + public void MyMethod1() { } + } + } + """, + afterCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttributeAttribute : System.Attribute + { + public MyAttributeAttribute() { } + } + public class MyClass + { + public MyClass() { } + [MyAttribute] + public void MyMethod2() { } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - public void MyMethod1(); + + [MyNamespace.MyAttributeAttribute] + + public void MyMethod2(); + } + } + """, + attributesToExclude: []); + + [Fact] + public Task MemberChangeButAttributeStays() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttributeAttribute : System.Attribute + { + public MyAttributeAttribute() { } + } + public class MyClass + { + public MyClass() { } + [MyAttribute] + public void MyMethod1() { } + } + } + """, + afterCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttributeAttribute : System.Attribute + { + public MyAttributeAttribute() { } + } + public class MyClass + { + public MyClass() { } + [MyAttribute] + public void MyMethod2() { } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - [MyNamespace.MyAttributeAttribute] + - public void MyMethod1(); + + [MyNamespace.MyAttributeAttribute] + + public void MyMethod2(); + } + } + """, + attributesToExclude: []); + + #endregion + + #region Parameter attributes + + //[Fact] + [Fact(Skip = "Parameter attributes are not showing up in the syntax tree.")] + public Task ParameterAttributeAdd() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.Parameter)] + public class MyAttributeAttribute : System.Attribute + { + public MyAttributeAttribute() { } + } + public class MyClass + { + public MyClass(int x) { } + public void MyMethod(int y) { } + } + } + """, + afterCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.Parameter)] + public class MyAttributeAttribute : System.Attribute + { + public MyAttributeAttribute() { } + } + public class MyClass + { + public MyClass([MyAttribute] int x) { } + public void MyMethod([MyAttribute] int y) { } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - public MyClass(int x); + + public MyClass([MyNamespace.MyAttributeAttribute] int x); + - public void MyMethod(int y); + + public void MyMethod([MyNamespace.MyAttributeAttribute] int y); + } + } + """, + attributesToExclude: []); + + #endregion + + #region Attribute list expansion + + [Fact] + public Task TypeAttributeListExpansion() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttribute1Attribute : System.Attribute + { + public MyAttribute1Attribute() { } + } + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttribute2Attribute : System.Attribute + { + public MyAttribute2Attribute() { } + } + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttribute1Attribute : System.Attribute + { + public MyAttribute1Attribute() { } + } + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttribute2Attribute : System.Attribute + { + public MyAttribute2Attribute() { } + } + [MyAttribute1, MyAttribute2] + public class MyClass + { + } + } + """, + expectedCode: """ + namespace MyNamespace + { + + [MyNamespace.MyAttribute1Attribute] + + [MyNamespace.MyAttribute2Attribute] + public class MyClass + { + } + } + """, + attributesToExclude: []); + + [Fact] + public Task MethodAttributeListExpansion() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttribute1Attribute : System.Attribute + { + public MyAttribute1Attribute() { } + } + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttribute2Attribute : System.Attribute + { + public MyAttribute2Attribute() { } + } + public class MyClass + { + public MyClass(int x) { } + } + } + """, + afterCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttribute1Attribute : System.Attribute + { + public MyAttribute1Attribute() { } + } + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttribute2Attribute : System.Attribute + { + public MyAttribute2Attribute() { } + } + public class MyClass + { + [MyAttribute1, MyAttribute2] + public MyClass(int x) { } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + [MyNamespace.MyAttribute1Attribute] + + [MyNamespace.MyAttribute2Attribute] + public MyClass(int x); + } + } + """, + attributesToExclude: []); + + //[Fact] + [Fact(Skip = "Parameter attributes are not showing up in the syntax tree.")] + public Task ParameterAttributeListNoExpansion() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.Parameter)] + public class MyAttribute1Attribute : System.Attribute + { + public MyAttribute1Attribute() { } + } + [System.AttributeUsage(System.AttributeTargets.Parameter)] + public class MyAttribute2Attribute : System.Attribute + { + public MyAttribute2Attribute() { } + } + public class MyClass + { + public MyClass(int x) { } + public void MyMethod(int y) { } + } + } + """, + afterCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.Parameter)] + public class MyAttribute1Attribute : System.Attribute + { + public MyAttribute1Attribute() { } + } + [System.AttributeUsage(System.AttributeTargets.Parameter)] + public class MyAttribute2Attribute : System.Attribute + { + public MyAttribute2Attribute() { } + } + public class MyClass + { + public MyClass([MyAttribute1, MyAttribute2] int x) { } + public void MyMethod([MyAttribute1, MyAttribute2] int y) { } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - public MyClass(int x); + + public MyClass([MyNamespace.MyAttribute1Attribute, MyNamespace.MyAttribute2Attribute] int x); + - public void MyMethod(int y); + + public void MyMethod([MyNamespace.MyAttribute1Attribute, MyNamespace.MyAttribute2Attribute] int y); + } + } + """, + attributesToExclude: []); + + #endregion + + #region Attribute exclusion + + [Fact] + public Task SuppressAllDefaultAttributesUsedByTool() + { + // The attributes that should get hidden in this test must all be part of + // the AttributesToExclude.txt file that the ApiDiff tool uses by default. + + FileInfo file = new FileInfo("AttributesToExclude.txt"); + if (!file.Exists) + { + throw new FileNotFoundException($"{file.FullName} file not found."); + } + string[] attributesToExclude = File.ReadAllLines(file.FullName); + + return RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + using System; + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttributeAttribute : System.Attribute + { + public MyAttributeAttribute() { } + } + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Text")] + [MyAttribute] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Text")] + public class MyClass + { + } + } + """, + expectedCode: """ + namespace MyNamespace + { + + [MyNamespace.MyAttributeAttribute] + public class MyClass + { + } + + public class MyAttributeAttribute : System.Attribute + + { + + public MyAttributeAttribute(); + + } + } + """, + attributesToExclude: attributesToExclude); + } + + [Fact] + public Task SuppressNone() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + using System; + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttributeAttribute : System.Attribute + { + public MyAttributeAttribute() { } + } + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Text")] + [MyAttribute] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Text")] + public class MyClass + { + } + } + """, + expectedCode: """ + namespace MyNamespace + { + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + + [System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("Text")] + + [MyNamespace.MyAttributeAttribute] + + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("Text")] + public class MyClass + { + } + + [System.AttributeUsageAttribute(System.AttributeTargets.All)] + + public class MyAttributeAttribute : System.Attribute + + { + + public MyAttributeAttribute(); + + } + } + """, + attributesToExclude: []); + + [Fact] + public Task SuppressOnlyCustom() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + using System; + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttributeAttribute : System.Attribute + { + public MyAttributeAttribute() { } + } + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Text")] + [MyAttribute] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Text")] + public class MyClass + { + } + } + """, + expectedCode: """ + namespace MyNamespace + { + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + + [System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("Text")] + + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("Text")] + public class MyClass + { + } + + [System.AttributeUsageAttribute(System.AttributeTargets.All)] + + public class MyAttributeAttribute : System.Attribute + + { + + public MyAttributeAttribute(); + + } + } + """, + attributesToExclude: ["T:MyNamespace.MyAttributeAttribute"]); // Overrides the default list + + [Fact] + public Task SuppressChangedAttribute() => RunTestAsync( + // Includes dummy property addition so that something else shows up in the diff, but not the attribute change. + beforeCode: """ + namespace MyNamespace + { + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Original text")] + public class MyClass + { + } + } + """, + afterCode: """ + using System; + namespace MyNamespace + { + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Changed text")] + public class MyClass + { + public int X { get; } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public int X { get; } + } + } + """, + attributesToExclude: ["T:System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute"]); // Overrides the default list + + [Fact] + public Task SuppressAttributesRepeatedWithDifferentArguments() => RunTestAsync( + // Include dummy property + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + using System; + namespace MyNamespace + { + [System.Runtime.Versioning.UnsupportedOSPlatform("tvos")] + [System.Runtime.Versioning.UnsupportedOSPlatform("linux")] + public class MyClass + { + public int X { get; } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public int X { get; } + } + } + """, + attributesToExclude: ["T:System.Runtime.Versioning.UnsupportedOSPlatformAttribute"]); // Overrides the default list + + [Fact] + public Task SuppressTypeAndAttributeUsage() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttributeAttribute : System.Attribute + { + } + [MyAttribute] + public class MyClass + { + } + } + """, + expectedCode: "", + attributesToExclude: ["T:MyNamespace.MyAttributeAttribute"], // Exclude the attribute decorating other APIs + apisToExclude: ["T:MyNamespace.MyAttributeAttribute"]); // Excludes the type definition of the attribute itself + + #endregion + + #region Attributes with arguments + + [Fact] + public Task AttributeWithArguments() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public MyClass() { } + } + } + """, + afterCode: """ + namespace MyNamespace + { + [System.AttributeUsage(System.AttributeTargets.All)] + public class MyAttributeAttribute : System.Attribute + { + public string First { get; } + public string Second { get; } + public MyAttributeAttribute(string first, string second) + { + First = first; + Second = second; + } + } + [MyAttribute(first: "First", second: "Second")] + public class MyClass + { + public MyClass() { } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + + [MyNamespace.MyAttributeAttribute("First", "Second")] + public class MyClass + { + } + + [System.AttributeUsageAttribute(System.AttributeTargets.All)] + + public class MyAttributeAttribute : System.Attribute + + { + + public MyAttributeAttribute(string first, string second); + + public string First { get; } + + public string Second { get; } + + } + } + """, + attributesToExclude: []); // Make sure to show AttributeUsage, which by default is suppressed + + [Fact] + public Task AttributesRepeatedWithDifferentArguments() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + using System; + namespace MyNamespace + { + [System.Runtime.Versioning.UnsupportedOSPlatform("tvos")] + [System.Runtime.Versioning.UnsupportedOSPlatform("linux")] + public class MyClass + { + } + } + """, + expectedCode: """ + namespace MyNamespace + { + + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")] + + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("linux")] + public class MyClass + { + } + } + """, + attributesToExclude: null); // null forces using the default list + + #endregion +} diff --git a/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Base.Tests.cs b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Base.Tests.cs new file mode 100644 index 000000000000..d4d9f893b80b --- /dev/null +++ b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Base.Tests.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis; +using Microsoft.DotNet.ApiSymbolExtensions; +using Microsoft.DotNet.ApiSymbolExtensions.Logging; +using Microsoft.DotNet.GenAPI.Tests; +using Moq; +using VerifyTests; + +namespace Microsoft.DotNet.ApiDiff.Tests; + +public abstract class DiffBaseTests +{ + private readonly Mock _log = new(); + protected const string AssemblyName = "MyAssembly"; + private readonly string[] _separator = [Environment.NewLine]; + + protected Task RunTestAsync( + string beforeCode, + string afterCode, + string expectedCode, + string[]? attributesToExclude = null, + string[]? apisToExclude = null, + bool addPartialModifier = false) + => RunTestAsync( + before: [($"{AssemblyName}.dll", beforeCode)], + after: [($"{AssemblyName}.dll", afterCode)], + expected: new() { { AssemblyName, expectedCode } }, + attributesToExclude, + apisToExclude, + addPartialModifier); + + protected async Task RunTestAsync( + (string, string)[] before, + (string, string)[] after, + Dictionary expected, + string[]? attributesToExclude = null, + string[]? apisToExclude = null, + bool addPartialModifier = false) + { + // CreateFromTexts will assert on any loader diagnostics via SyntaxFactory. + + (IAssemblySymbolLoader beforeLoader, Dictionary beforeAssemblySymbols) + = TestAssemblyLoaderFactory.CreateFromTexts(_log.Object, assemblyTexts: before, diagnosticOptions: DiffGeneratorFactory.DefaultDiagnosticOptions); + + (IAssemblySymbolLoader afterLoader, Dictionary afterAssemblySymbols) + = TestAssemblyLoaderFactory.CreateFromTexts(_log.Object, assemblyTexts: after, diagnosticOptions: DiffGeneratorFactory.DefaultDiagnosticOptions); + + using MemoryStream outputStream = new(); + + IDiffGenerator generator = DiffGeneratorFactory.Create( + _log.Object, + beforeLoader, + afterLoader, + beforeAssemblySymbols, + afterAssemblySymbols, + attributesToExclude, + apisToExclude, + addPartialModifier, + DiffGeneratorFactory.DefaultDiagnosticOptions); + + await generator.RunAsync(); + + foreach ((string expectedAssemblyName, string expectedCode) in expected) + { + if (string.IsNullOrEmpty(expectedCode)) + { + Assert.False(generator.Results.TryGetValue(expectedAssemblyName, out string? _), $"Assembly should've been absent among the results: {expectedAssemblyName}"); + } + else + { + Assert.True(generator.Results.TryGetValue(expectedAssemblyName, out string? actualCode), $"Assembly should've been present among the results: {expectedAssemblyName}"); + string fullExpectedCode = GetExpected(expectedCode, expectedAssemblyName); + if (!fullExpectedCode.Equals(actualCode)) + { + Assert.Fail($"Expected:\n[{ReplacedNewLines(fullExpectedCode)}]\nActual:\n[{ReplacedNewLines(actualCode)}]"); + } + } + } + } + + private static string ReplacedNewLines(string orig) => orig.Replace("\n", "\\n\n").Replace("\r", "\\r"); + + private static string GetExpected(string expectedCode, string expectedAssemblyName) => + $""" + # {Path.GetFileNameWithoutExtension(expectedAssemblyName)} + + ```diff + {expectedCode} + ``` + + """; +} diff --git a/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Class.Tests.cs b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Class.Tests.cs new file mode 100644 index 000000000000..a8c692dc898a --- /dev/null +++ b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Class.Tests.cs @@ -0,0 +1,143 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.ApiDiff.Tests; + +public class DiffClassTests : DiffBaseTests +{ + #region Classes + + [Fact] + public Task ClassAdd() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public struct MyStruct + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public struct MyStruct + { + } + public class MyAddedClass + { + } + } + """, + expectedCode: """ + namespace MyNamespace + { + + public class MyAddedClass + + { + + public MyAddedClass(); + + } + } + """); + + [Fact] + public Task ClassAddWithDefaultConstructor() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public struct MyStruct + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public struct MyStruct + { + } + public class MyAddedClass + { + public MyAddedClass() { } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + + public class MyAddedClass + + { + + public MyAddedClass(); + + } + } + """); + + [Fact] + public Task ClassChange() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public struct MyStruct + { + } + public class MyBeforeClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public struct MyStruct + { + } + public class MyAfterClass + { + } + } + """, + expectedCode: """ + namespace MyNamespace + { + - public class MyBeforeClass + - { + - public MyBeforeClass(); + - } + + public class MyAfterClass + + { + + public MyAfterClass(); + + } + } + """); + + [Fact] + public Task ClassDelete() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public struct MyStruct + { + } + public class MyAddedClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public struct MyStruct + { + } + } + """, + expectedCode: """ + namespace MyNamespace + { + - public class MyAddedClass + - { + - public MyAddedClass(); + - } + } + """); + + #endregion +} diff --git a/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Constructor.Tests.cs b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Constructor.Tests.cs new file mode 100644 index 000000000000..622b5cb038ec --- /dev/null +++ b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Constructor.Tests.cs @@ -0,0 +1,286 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.DotNet.ApiDiff.Tests; + +public class DiffConstructorTests : DiffBaseTests +{ + #region Constructors + + [Fact] + public Task ConstructorAdd() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public MyClass() { } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public MyClass() { } + public MyClass(int x) + { + } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public MyClass(int x); + } + } + """); + + [Fact] + public Task ConstructorChange() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public MyClass(bool x) + { + } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public MyClass(int x) + { + } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - public MyClass(bool x); + + public MyClass(int x); + } + } + """); + + [Fact] + public Task ConstructorDelete() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public MyClass() { } + public MyClass(int x) + { + } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public MyClass() { } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - public MyClass(int x); + } + } + """); + + #endregion + + #region Primary constructors + + + [Fact] + public Task PrimaryConstructorAdd() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public MyClass() { } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass(string x) + { + public MyClass() : this("") { } + public string X { get; } = x; + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public MyClass(string x); + + public string X { get; } + } + } + """); + + [Fact] + public Task PrimaryConstructorChange() => RunTestAsync( + // This isn't really a modification, but a deletion and an addition, and since + // deletions show up before additions, that explains the order of the expected code. + beforeCode: """ + namespace MyNamespace + { + public class MyClass(double x) + { + public double X { get; } = x; + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass(string x) + { + public string X { get; } = x; + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - public MyClass(double x); + - public double X { get; } + + public string X { get; } + + public MyClass(string x); + } + } + """); + + [Fact] + public Task PrimaryConstructorDelete() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass(string x) + { + public MyClass() : this("") { } + public string X { get; } = x; + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public MyClass() { } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - public MyClass(string x); + - public string X { get; } + } + } + """); + + #endregion + + #region Visibility + + [Fact] + public Task DefaultConstructorMakePrivate() + { + string beforeCode = """ + namespace MyNamespace + { + public class MyClass + { + public MyClass() + { + } + } + } + """; + string afterCode = """ + namespace MyNamespace + { + public class MyClass + { + private MyClass() + { + } + } + } + """; + return RunTestAsync( + before: [($"{AssemblyName}.dll", beforeCode)], + after: [($"{AssemblyName}.dll", afterCode)], + expected: []); // No results expected as the API is not getting removed + } + + [Fact] + public Task DefaultConstructorMakePublic() + { + string beforeCode = """ + namespace MyNamespace + { + public class MyClass + { + private MyClass() + { + } + } + } + """; + string afterCode = """ + namespace MyNamespace + { + public class MyClass + { + public MyClass() + { + } + } + } + """; + return RunTestAsync( + before: [($"{AssemblyName}.dll", beforeCode)], + after: [($"{AssemblyName}.dll", afterCode)], + expected: []); // No results expected as the API is not getting added + } + + #endregion +} diff --git a/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Delegate.Tests.cs b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Delegate.Tests.cs new file mode 100644 index 000000000000..73c015658662 --- /dev/null +++ b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Delegate.Tests.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.ApiDiff.Tests; + +public class DiffDelegateTests : DiffBaseTests +{ + #region Delegates + + [Fact] + public Task DelegateAdd() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public struct MyStruct + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public struct MyStruct + { + } + public delegate void MyDelegate(); + } + """, + expectedCode: """ + namespace MyNamespace + { + + public delegate void MyDelegate(); + } + """); + + [Fact] + public Task DelegateChange() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public delegate void MyBeforeDelegate(); + } + """, + afterCode: """ + namespace MyNamespace + { + public delegate void MyAfterDelegate(); + } + """, + expectedCode: """ + namespace MyNamespace + { + - public delegate void MyBeforeDelegate(); + + public delegate void MyAfterDelegate(); + } + """); + + [Fact] + public Task DelegateDelete() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public struct MyStruct + { + } + public delegate void MyDelegate(); + } + """, + afterCode: """ + namespace MyNamespace + { + public struct MyStruct + { + } + } + """, + expectedCode: """ + namespace MyNamespace + { + - public delegate void MyDelegate(); + } + """); + + #endregion +} diff --git a/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Destructor.Tests.cs b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Destructor.Tests.cs new file mode 100644 index 000000000000..8c9cbe5fd656 --- /dev/null +++ b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Destructor.Tests.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.ApiDiff.Tests; + +public class DiffDestructorTests : DiffBaseTests +{ + [Fact] + public Task DestructorAdd() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + ~MyClass() + { + } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + ~MyClass(); + } + } + """); + + [Fact] + public Task DestructorDelete() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + ~MyClass() + { + } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - ~MyClass(); + } + } + """); + +} diff --git a/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Disk.Tests.cs b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Disk.Tests.cs new file mode 100644 index 000000000000..cb699c336267 --- /dev/null +++ b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Disk.Tests.cs @@ -0,0 +1,561 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Emit; +using Microsoft.DotNet.ApiSymbolExtensions.Logging; +using Microsoft.DotNet.ApiSymbolExtensions.Tests; +using Moq; + +namespace Microsoft.DotNet.ApiDiff.Tests; + +public class DiffDiskTests +{ + private const string DefaultBeforeFriendlyName = "Before"; + private const string DefaultAfterFriendlyName = "After"; + private const string DefaultTableOfContentsTitle = "MyTitle"; + private const string DefaultAssemblyName = "MyAssembly"; + + private const string ExpectedTableOfContents = $""" + # API difference between {DefaultBeforeFriendlyName} and {DefaultAfterFriendlyName} + + API listing follows standard diff formatting. + Lines preceded by a '+' are additions and a '-' indicates removal. + + * [{DefaultAssemblyName}]({DefaultTableOfContentsTitle}_{DefaultAssemblyName}.md) + + + """; + + private const string ExpectedEmptyTableOfContents = $""" + # API difference between {DefaultBeforeFriendlyName} and {DefaultAfterFriendlyName} + + API listing follows standard diff formatting. + Lines preceded by a '+' are additions and a '-' indicates removal. + + + + """; + + private const string BeforeCode = """ + namespace MyNamespace + { + public class MyClass + { + public MyClass() { } + } + } + """; + + private const string AfterCode = """ + namespace MyNamespace + { + public class MyClass + { + public MyClass() { } + public void MyMethod() { } + } + } + """; + + private const string DefaultExpectedMarkdown = $@"# {DefaultAssemblyName} + +```diff + namespace MyNamespace + {{ + public class MyClass + {{ ++ public void MyMethod(); + }} + }} +``` +"; + + private readonly Dictionary DefaultBeforeAssemblyAndCodeFiles = new() { { DefaultAssemblyName, [BeforeCode] } }; + private readonly Dictionary DefaultAfterAssemblyAndCodeFiles = new() { { DefaultAssemblyName, [AfterCode] } }; + private readonly Dictionary DefaultExpectedAssemblyMarkdowns = new() { { DefaultAssemblyName, DefaultExpectedMarkdown } }; + + /// + /// This test reads two DLLs, where the only difference between the types in the assembly is one method that is added on the new type. + /// The output goes to disk. + /// + [Fact] + public async Task DiskRead_DiskWrite() + { + using TempDirectory inputFolderPath = new(); + using TempDirectory outputFolderPath = new(); + + IDiffGenerator generator = TestDiskShared( + inputFolderPath.DirPath, + DefaultBeforeFriendlyName, + DefaultAfterFriendlyName, + DefaultTableOfContentsTitle, + DefaultBeforeAssemblyAndCodeFiles, + DefaultAfterAssemblyAndCodeFiles, + outputFolderPath.DirPath, + writeToDisk: true); + + await generator.RunAsync(); + + VerifyDiskWrite(outputFolderPath.DirPath, DefaultTableOfContentsTitle, ExpectedTableOfContents, DefaultExpectedAssemblyMarkdowns); + } + + // Each assembly should have its own file. Confirm that namespaces spread throughout multiple assemblies does not accidentally overwrite any existing files. + [Fact] + public async Task DiskRead_DiskWrite_AssembliesWithRepeatedNamespaces() + { + string beforeAssembly1Code1 = """ + namespace MyNamespace + { + public class MyClass1 + { + public MyClass1() { } + } + } + """; + string beforeAssembly1Code2 = """ + namespace MyNamespace + { + public class MyClass2 + { + public MyClass2() { } + } + } + """; + string beforeAssembly2Code1 = """ + namespace MyNamespace + { + public class MyClass3 + { + public MyClass3() { } + } + } + """; + string beforeAssembly2Code2 = """ + namespace MyNamespace + { + public class MyClass4 + { + public MyClass4() { } + } + } + """; + + string afterAssembly1Code1 = """ + namespace MyNamespace + { + public class MyClass1 + { + public MyClass1() { } + public void MyMethod() { } + } + } + """; + string afterAssembly1Code2 = """ + namespace MyNamespace + { + public class MyClass2 + { + public MyClass2() { } + public void MyMethod() { } + } + } + """; + string afterAssembly2Code1 = """ + namespace MyNamespace + { + public class MyClass3 + { + public MyClass3() { } + public void MyMethod() { } + } + } + """; + string afterAssembly2Code2 = """ + namespace MyNamespace + { + public class MyClass4 + { + public MyClass4() { } + public void MyMethod() { } + } + } + """; + + Dictionary beforeAssemblyAndCodeFiles = new() + { + { "Assembly1", [beforeAssembly1Code1, beforeAssembly1Code2] }, + { "Assembly2", [beforeAssembly2Code1, beforeAssembly2Code2] } + }; + + Dictionary afterAssemblyAndCodeFiles = new() + { + { "Assembly1", [afterAssembly1Code1, afterAssembly1Code2] }, + { "Assembly2", [afterAssembly2Code1, afterAssembly2Code2] } + }; + + Dictionary expectedAssemblyMarkdowns = new() + { + { "Assembly1", $@"# Assembly1 + +```diff + namespace MyNamespace + {{ + public class MyClass1 + {{ ++ public void MyMethod(); + }} + public class MyClass2 + {{ ++ public void MyMethod(); + }} + }} +``` +" }, + { "Assembly2", $@"# Assembly2 + +```diff + namespace MyNamespace + {{ + public class MyClass3 + {{ ++ public void MyMethod(); + }} + public class MyClass4 + {{ ++ public void MyMethod(); + }} + }} +``` +" } + + }; + + string expectedTableOfContents = $@"# API difference between {DefaultBeforeFriendlyName} and {DefaultAfterFriendlyName} + +API listing follows standard diff formatting. +Lines preceded by a '+' are additions and a '-' indicates removal. + +* [Assembly1]({DefaultTableOfContentsTitle}_Assembly1.md) +* [Assembly2]({DefaultTableOfContentsTitle}_Assembly2.md) + +"; + + using TempDirectory inputFolderPath = new(); + using TempDirectory outputFolderPath = new(); + + IDiffGenerator generator = TestDiskShared( + inputFolderPath.DirPath, + DefaultBeforeFriendlyName, + DefaultAfterFriendlyName, + DefaultTableOfContentsTitle, + beforeAssemblyAndCodeFiles, + afterAssemblyAndCodeFiles, + outputFolderPath.DirPath, + writeToDisk: true); + + await generator.RunAsync(); + + VerifyDiskWrite(outputFolderPath.DirPath, DefaultTableOfContentsTitle, expectedTableOfContents, expectedAssemblyMarkdowns); + } + + /// + /// This test reads two DLLs, where the assembly is explicitly excluded. + /// The output is disk is simply the table of contents markdown file with no assembly list. No other files. + /// + [Fact] + public async Task DiskRead_DiskWrite_ExcludeAssembly() + { + using TempDirectory root = new(); + + DirectoryInfo inputFolderPath = new(Path.Join(root.DirPath, "inputFolder")); + DirectoryInfo outputFolderPath = new(Path.Join(root.DirPath, "outputFolder")); + + inputFolderPath.Create(); + outputFolderPath.Create(); + + IDiffGenerator generator = TestDiskShared( + inputFolderPath.FullName, + DefaultBeforeFriendlyName, + DefaultAfterFriendlyName, + DefaultTableOfContentsTitle, + DefaultBeforeAssemblyAndCodeFiles, + DefaultAfterAssemblyAndCodeFiles, + outputFolderPath.FullName, + writeToDisk: true, + filesWithAssembliesToExclude: await GetFileWithListsAsync(root, [DefaultAssemblyName])); + + await generator.RunAsync(); + + VerifyDiskWrite(outputFolderPath.FullName, DefaultTableOfContentsTitle, ExpectedEmptyTableOfContents, []); + } + + /// + /// Many namespaces belonging to a single assembly should go into the same output markdown file for that assembly. + /// + [Fact] + public async Task DiskRead_DiskWrite_MultiNamespaces() + { + using TempDirectory inputFolderPath = new(); + using TempDirectory outputFolderPath = new(); + + string beforeCode1 = """ + namespace MyNamespace + { + public class MyClass + { + public MyClass() { } + } + } + """; + + string beforeCode2 = """ + namespace MyNamespace.MySubNamespace + { + public class MySubClass + { + public MySubClass() { } + } + } + """; + + string afterCode1 = """ + namespace MyNamespace + { + public class MyClass + { + public MyClass() { } + public void MyMethod() { } + } + } + """; + + string afterCode2 = """ + namespace MyNamespace.MySubNamespace + { + public class MySubClass + { + public MySubClass() { } + public void MySubMethod() { } + } + } + """; + + string expectedMarkdown = $@"# {DefaultAssemblyName} + +```diff + namespace MyNamespace + {{ + public class MyClass + {{ ++ public void MyMethod(); + }} + }} + namespace MyNamespace.MySubNamespace + {{ + public class MySubClass + {{ ++ public void MySubMethod(); + }} + }} +``` +"; + + Dictionary beforeAssemblyAndCodeFiles = new() { { DefaultAssemblyName, [beforeCode1, beforeCode2] } }; + Dictionary afterAssemblyAndCodeFiles = new() { { DefaultAssemblyName, [afterCode1, afterCode2] } }; + Dictionary expectedAssemblyMarkdowns = new() { { DefaultAssemblyName, expectedMarkdown } }; + + IDiffGenerator generator = TestDiskShared( + inputFolderPath.DirPath, + DefaultBeforeFriendlyName, + DefaultAfterFriendlyName, + DefaultTableOfContentsTitle, + beforeAssemblyAndCodeFiles, + afterAssemblyAndCodeFiles, + outputFolderPath.DirPath, + writeToDisk: true); + + await generator.RunAsync(); + + VerifyDiskWrite(outputFolderPath.DirPath, DefaultTableOfContentsTitle, ExpectedTableOfContents, expectedAssemblyMarkdowns); + } + + /// + /// This test reads two DLLs, where the only difference between the types in the assembly is one method that is added on the new type. + /// The output is the Results dictionary. + /// + [Fact] + public async Task DiskRead_MemoryWrite() + { + using TempDirectory inputFolderPath = new(); + using TempDirectory outputFolderPath = new(); + + IDiffGenerator generator = TestDiskShared( + inputFolderPath.DirPath, + DefaultBeforeFriendlyName, + DefaultAfterFriendlyName, + DefaultTableOfContentsTitle, + DefaultBeforeAssemblyAndCodeFiles, + DefaultAfterAssemblyAndCodeFiles, + outputFolderPath.DirPath, + writeToDisk: false); + + await generator.RunAsync(); + + string tableOfContentsMarkdownFilePath = Path.Join(outputFolderPath.DirPath, $"{DefaultTableOfContentsTitle}.md"); + Assert.Contains(tableOfContentsMarkdownFilePath, generator.Results.Keys); + + Assert.Equal(ExpectedTableOfContents, generator.Results[tableOfContentsMarkdownFilePath]); + + string myAssemblyMarkdownFilePath = Path.Join(outputFolderPath.DirPath, $"{DefaultTableOfContentsTitle}_{DefaultAssemblyName}.md"); + Assert.Contains(myAssemblyMarkdownFilePath, generator.Results.Keys); + + Assert.Equal(DefaultExpectedMarkdown, generator.Results[myAssemblyMarkdownFilePath]); + } + + /// + /// This test reads two DLLs, where the assembly is explicitly excluded. + /// The output is an empty Results dictionary. + /// + [Fact] + public async Task DiskRead_MemoryWrite_ExcludeAssembly() + { + using TempDirectory root = new(); + + DirectoryInfo inputFolderPath = new(Path.Join(root.DirPath, "inputFolder")); + DirectoryInfo outputFolderPath = new(Path.Join(root.DirPath, "outputFolder")); + + inputFolderPath.Create(); + outputFolderPath.Create(); + + IDiffGenerator generator = TestDiskShared( + inputFolderPath.FullName, + DefaultBeforeFriendlyName, + DefaultAfterFriendlyName, + DefaultTableOfContentsTitle, + DefaultBeforeAssemblyAndCodeFiles, + DefaultAfterAssemblyAndCodeFiles, + outputFolderPath.FullName, + writeToDisk: false, + filesWithAssembliesToExclude: await GetFileWithListsAsync(root, [DefaultAssemblyName])); + + await generator.RunAsync(); + + string tableOfContentsMarkdownFilePath = Path.Join(outputFolderPath.FullName, $"{DefaultTableOfContentsTitle}.md"); + Assert.Contains(tableOfContentsMarkdownFilePath, generator.Results.Keys); + + Assert.Equal(ExpectedEmptyTableOfContents, generator.Results[tableOfContentsMarkdownFilePath]); + + string myAssemblyMarkdownFilePath = Path.Join(outputFolderPath.FullName, $"{DefaultTableOfContentsTitle}_{DefaultAssemblyName}.md"); + Assert.DoesNotContain(myAssemblyMarkdownFilePath, generator.Results.Keys); + } + + private IDiffGenerator TestDiskShared( + string inputFolderPath, + string beforeFriendlyName, + string afterFriendlyName, + string tableOfContentsTitle, + Dictionary beforeAssemblyAndCodeFiles, + Dictionary afterAssemblyAndCodeFiles, + string outputFolderPath, + bool writeToDisk, + FileInfo[]? filesWithAssembliesToExclude = null, + FileInfo[]? filesWithAttributesToExclude = null, + FileInfo[]? filesWithAPIsToExclude = null) + { + string beforeAssembliesFolderPath = Path.Join(inputFolderPath, DefaultBeforeFriendlyName); + string afterAssembliesFolderPath = Path.Join(inputFolderPath, DefaultAfterFriendlyName); + + Directory.CreateDirectory(beforeAssembliesFolderPath); + Directory.CreateDirectory(afterAssembliesFolderPath); + + WriteCodeToAssemblyInDisk(beforeAssembliesFolderPath, beforeAssemblyAndCodeFiles); + WriteCodeToAssemblyInDisk(afterAssembliesFolderPath, afterAssemblyAndCodeFiles); + + Mock log = new(); + + return DiffGeneratorFactory.Create(log.Object, + beforeAssembliesFolderPath, + beforeAssemblyReferencesFolderPath: null, + afterAssembliesFolderPath, + afterAssemblyReferencesFolderPath: null, + outputFolderPath, + beforeFriendlyName, + afterFriendlyName, + tableOfContentsTitle, + filesWithAssembliesToExclude ?? [], + filesWithAttributesToExclude ?? [], + filesWithAPIsToExclude ?? [], + addPartialModifier: false, + writeToDisk, + DiffGeneratorFactory.DefaultDiagnosticOptions); + } + + private void WriteCodeToAssemblyInDisk(string assemblyDirectoryPath, Dictionary assemblyAndCodeFiles) + { + foreach ((string assemblyName, string[] codeFiles) in assemblyAndCodeFiles) + { + List syntaxTrees = new(); + foreach (string codeFile in codeFiles) + { + syntaxTrees.Add(CSharpSyntaxTree.ParseText(codeFile)); + } + + IEnumerable references = AppDomain.CurrentDomain.GetAssemblies() + .Where(a => !a.IsDynamic) + .Select(a => MetadataReference.CreateFromFile(a.Location)) + .Cast(); + + CSharpCompilation compilation = CSharpCompilation.Create( + assemblyName, + syntaxTrees, + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var assemblyPath = Path.Join(assemblyDirectoryPath, $"{assemblyName}.dll"); + EmitResult result = compilation.Emit(assemblyPath); + + if (!result.Success) + { + IEnumerable failures = result.Diagnostics.Where(diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error); + string failureMessages = string.Join(Environment.NewLine, failures.Select(f => $"{f.Id}: {f.GetMessage()}")); + Assert.Fail($"Compilation failed: {failureMessages}"); + } + } + } + + private void VerifyDiskWrite(string outputFolderPath, string tableOfContentsTitle, string expectedTableOfContents, Dictionary expectedAssemblyMarkdowns) + { + string tableOfContentsMarkdownFilePath = Path.Join(outputFolderPath, $"{tableOfContentsTitle}.md"); + Assert.True(File.Exists(tableOfContentsMarkdownFilePath), $"{tableOfContentsMarkdownFilePath} table of contents markdown file does not exist."); + + string actualTableOfContentsText = File.ReadAllText(tableOfContentsMarkdownFilePath); + Assert.Equal(expectedTableOfContents, actualTableOfContentsText); + + foreach ((string expectedAssembly, string expectedMarkdown) in expectedAssemblyMarkdowns) + { + string myAssemblyMarkdownFilePath = Path.Join(outputFolderPath, $"{tableOfContentsTitle}_{expectedAssembly}.md"); + Assert.True(File.Exists(myAssemblyMarkdownFilePath), $"{myAssemblyMarkdownFilePath} assembly markdown file does not exist."); + + string actualCode = File.ReadAllText(myAssemblyMarkdownFilePath); + Assert.Equal(expectedMarkdown, actualCode); + } + } + + private Task GetFileWithListsAsync(TempDirectory root, string[] list) => GetFilesWithListsAsync(root, [list]); + + private async Task GetFilesWithListsAsync(TempDirectory root, List lists) + { + List filesWithLists = []; + + foreach (string[] list in lists) + { + FileInfo file = new(Path.Join(root.DirPath, Path.GetRandomFileName())); + await File.WriteAllLinesAsync(file.FullName, list); + filesWithLists.Add(file); + } + + return [.. filesWithLists]; + } +} diff --git a/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Enum.Tests.cs b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Enum.Tests.cs new file mode 100644 index 000000000000..8b375f7cbf4f --- /dev/null +++ b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Enum.Tests.cs @@ -0,0 +1,404 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.ApiDiff.Tests; + +public class DiffEnumTests : DiffBaseTests +{ + #region Enum types + + [Fact] + public Task EnumTypeAddWithOneMember() => RunTestAsync( + // Dummy record is added to avoid thinking namespace is empty. + beforeCode: """ + namespace MyNamespace + { + public record MyRecord(int x); + } + """, + afterCode: """ + namespace MyNamespace + { + public enum MyEnum + { + Default = 0, + } + public record MyRecord(int x); + } + """, + expectedCode: """ + namespace MyNamespace + { + + public enum MyEnum + + { + + Default = 0, + + } + } + """); + + [Fact] + public Task EnumTypeAddWithMultipleSortedMembers() => RunTestAsync( + // Dummy record is added to avoid thinking namespace is empty. + beforeCode: """ + namespace MyNamespace + { + public record MyRecord(int x); + } + """, + afterCode: """ + namespace MyNamespace + { + public enum MyEnum + { + Default = 0, + Foo = 1, + Bar = 2, + } + public record MyRecord(int x); + } + """, + expectedCode: """ + namespace MyNamespace + { + + public enum MyEnum + + { + + Default = 0, + + Foo = 1, + + Bar = 2, + + } + } + """); + + [Fact] + public Task EnumTypeAddWithMultipleUnsortedMembers() => RunTestAsync( + // Enum members shall show up sorted by value, not alphabetically. + // Dummy record is added to avoid thinking namespace is empty. + beforeCode: """ + namespace MyNamespace + { + public record MyRecord(int x); + } + """, + afterCode: """ + namespace MyNamespace + { + public enum MyEnum + { + What = 3, + Foo = 1, + Bar = 2, + Default = 0, + } + public record MyRecord(int x); + } + """, + expectedCode: """ + namespace MyNamespace + { + + public enum MyEnum + + { + + Default = 0, + + Foo = 1, + + Bar = 2, + + What = 3, + + } + } + """); + + [Fact] + public Task EnumTypeWithOneMemberRemove() => RunTestAsync( + // Dummy record is added to avoid thinking namespace is empty. + beforeCode: """ + namespace MyNamespace + { + public enum MyEnum + { + Default = 0, + } + public record MyRecord(int x); + } + """, + afterCode: """ + namespace MyNamespace + { + public record MyRecord(int x); + } + """, + expectedCode: """ + namespace MyNamespace + { + - public enum MyEnum + - { + - Default = 0, + - } + } + """); + + [Fact] + public Task EnumTypeWithMultipleMembersRemove() => RunTestAsync( + // Dummy record is added to avoid thinking namespace is empty. + beforeCode: """ + namespace MyNamespace + { + public enum MyEnum + { + Default = 0, + Foo = 1, + Bar = 2, + } + public record MyRecord(int x); + } + """, + afterCode: """ + namespace MyNamespace + { + public record MyRecord(int x); + } + """, + expectedCode: """ + namespace MyNamespace + { + - public enum MyEnum + - { + - Default = 0, + - Foo = 1, + - Bar = 2, + - } + } + """); + + #endregion + + #region Enum members + + [Fact] + public Task EnumMemberAddOne() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public enum MyEnum + { + Default = 0, + } + } + """, + afterCode: """ + namespace MyNamespace + { + public enum MyEnum + { + Default = 0, + Other = 1, + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public enum MyEnum + { + + Other = 1, + } + } + """); + + [Fact] + public Task EnumMemberAddMultipleSorted() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public enum MyEnum + { + Default = 0, + } + } + """, + afterCode: """ + namespace MyNamespace + { + public enum MyEnum + { + Default = 0, + Foo = 1, + Bar = 2, + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public enum MyEnum + { + + Foo = 1, + + Bar = 2, + } + } + """); + + [Fact] + public Task EnumMemberAddMultipleUnsorted() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public enum MyEnum + { + Default = 0, + } + } + """, + afterCode: """ + namespace MyNamespace + { + public enum MyEnum + { + Bar = 2, + Default = 0, + Foo = 1, + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public enum MyEnum + { + + Foo = 1, + + Bar = 2, + } + } + """); + + [Fact] + public Task EnumMemberValueChange() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public enum MyEnum + { + Default = 0, + Foo = 1, + } + } + """, + afterCode: """ + namespace MyNamespace + { + public enum MyEnum + { + Default = 1, + Foo = 2, + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public enum MyEnum + { + - Default = 0, + + Default = 1, + - Foo = 1, + + Foo = 2, + } + } + """); + + [Fact] + public Task EnumMemberNameChange() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public enum MyEnum + { + Default = 0, + Foo = 1, + } + } + """, + afterCode: """ + namespace MyNamespace + { + public enum MyEnum + { + Default = 0, + Foo2 = 1, + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public enum MyEnum + { + - Foo = 1, + + Foo2 = 1, + } + } + """); + + [Fact] + public Task EnumMemberRemoveOne() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public enum MyEnum + { + Default = 0, + Other = 1, + } + } + """, + afterCode: """ + namespace MyNamespace + { + public enum MyEnum + { + Default = 0, + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public enum MyEnum + { + - Other = 1, + } + } + """); + + [Fact] + public Task EnumMemberRemoveMultiple() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public enum MyEnum + { + Default = 0, + Foo = 1, + Bar = 2, + } + } + """, + afterCode: """ + namespace MyNamespace + { + public enum MyEnum + { + Foo = 1, + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public enum MyEnum + { + - Default = 0, + - Bar = 2, + } + } + """); + + #endregion +} diff --git a/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Event.Tests.cs b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Event.Tests.cs new file mode 100644 index 000000000000..a99bf84ffde2 --- /dev/null +++ b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Event.Tests.cs @@ -0,0 +1,141 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.ApiDiff.Tests; + +public class DiffEventTests : DiffBaseTests +{ + [Fact] + public Task EventAdd() => RunTestAsync( + beforeCode: """ + using System; + namespace MyNamespace + { + public class MyClass + { + public delegate void MyEventHandler(object sender, EventArgs e); + } + } + """, + afterCode: """ + using System; + namespace MyNamespace + { + public class MyClass + { + public delegate void MyEventHandler(object sender, EventArgs e); + public event MyEventHandler? MyEvent; + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public event MyNamespace.MyClass.MyEventHandler? MyEvent { add; remove; } + } + } + """); + + [Fact] + public Task EventChange() => RunTestAsync( + beforeCode: """ + using System; + namespace MyNamespace + { + public class MyClass + { + public delegate void MyEventHandler(object sender, EventArgs e); + public event MyEventHandler? MyEvent1; + } + } + """, + afterCode: """ + using System; + namespace MyNamespace + { + public class MyClass + { + public delegate void MyEventHandler(object sender, EventArgs e); + public event MyEventHandler? MyEvent2; + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - public event MyNamespace.MyClass.MyEventHandler? MyEvent1 { add; remove; } + + public event MyNamespace.MyClass.MyEventHandler? MyEvent2 { add; remove; } + } + } + """); + + [Fact] + public Task EventRemove() => RunTestAsync( + beforeCode: """ + using System; + namespace MyNamespace + { + public class MyClass + { + public delegate void MyEventHandler(object sender, EventArgs e); + public event MyEventHandler? MyEvent; + } + } + """, + afterCode: """ + using System; + namespace MyNamespace + { + public class MyClass + { + public delegate void MyEventHandler(object sender, EventArgs e); + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - public event MyNamespace.MyClass.MyEventHandler? MyEvent { add; remove; } + } + } + """); + + [Fact] + public Task AbstractEvent() => RunTestAsync( + beforeCode: """ + using System; + namespace MyNamespace + { + public abstract class MyClass + { + public delegate void MyEventHandler(object sender, EventArgs e); + } + } + """, + afterCode: """ + using System; + namespace MyNamespace + { + public abstract class MyClass + { + public delegate void MyEventHandler(object sender, EventArgs e); + public abstract event MyEventHandler MyEvent; + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public abstract class MyClass + { + + public abstract event MyNamespace.MyClass.MyEventHandler MyEvent; + } + } + """); +} diff --git a/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Field.Tests.cs b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Field.Tests.cs new file mode 100644 index 000000000000..857802cb89e2 --- /dev/null +++ b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Field.Tests.cs @@ -0,0 +1,260 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.ApiDiff.Tests; + +public class DiffFieldTests : DiffBaseTests +{ + #region Fields + + [Fact] + public Task FieldAdd() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public int _myField; + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public int _myField; + } + } + """); + + [Fact] + public Task tFieldChange() => RunTestAsync( + // Test both change of type and change of name + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public int _myInt1; + public double _myField; + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public int _myInt2; + public float _myField; + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - public double _myField; + + public float _myField; + - public int _myInt1; + + public int _myInt2; + } + } + """); + + [Fact] + public Task tFieldDelete() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public int _myField; + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - public int _myField; + } + } + """); + + #endregion + + #region Field lists + + [Fact] + public Task FieldListAdd() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public int _myField1, _myField2; + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public int _myField1; + + public int _myField2; + } + } + """); + + [Fact] + public Task FieldListDataTypeChange() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public int _myField1, _myField2; + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public double _myField1, _myField2; + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - public int _myField1; + + public double _myField1; + - public int _myField2; + + public double _myField2; + } + } + """); + + [Fact] + public Task FieldListOrderChange() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public int _myField1, _myField2; + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public int _myField2, _myField1; + } + } + """, + expectedCode: ""); // No change expected + + [Fact] + public Task FieldListNameChange() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public int _myField1, _myField2; + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public int _myField3, _myField4; + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - public int _myField1; + - public int _myField2; + + public int _myField3; + + public int _myField4; + } + } + """); + + [Fact] + public Task FieldVisibilityChange() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public int _myField1, _myField2; + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + protected int _myField1, _myField2; + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - public int _myField1; + + protected int _myField1; + - public int _myField2; + + protected int _myField2; + } + } + """); + + #endregion +} diff --git a/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Interface.Tests.cs b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Interface.Tests.cs new file mode 100644 index 000000000000..3a95aca9988f --- /dev/null +++ b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Interface.Tests.cs @@ -0,0 +1,152 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.ApiDiff.Tests; + +public class DiffInterfaceTests : DiffBaseTests +{ + #region Interfaces + + [Fact] + public Task InterfaceAdd() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public struct MyStruct + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public struct MyStruct + { + } + public interface IMyInterface + { + int MyMethod(); + long MyProperty { get; } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + + public interface IMyInterface + + { + + int MyMethod(); + + long MyProperty { get; } + + } + } + """); + + [Fact] + public Task InterfaceChange() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public interface IMyBeforeInterface + { + int MyMethod(); + long MyProperty { get; } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public interface IMyAfterInterface + { + int MyMethod(); + long MyProperty { get; } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + - public interface IMyBeforeInterface + - { + - int MyMethod(); + - long MyProperty { get; } + - } + + public interface IMyAfterInterface + + { + + int MyMethod(); + + long MyProperty { get; } + + } + } + """); + + [Fact] + public Task InterfaceDelete() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public struct MyStruct + { + } + public interface IMyInterface + { + int MyMethod(); + long MyProperty { get; } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public struct MyStruct + { + } + } + """, + expectedCode: """ + namespace MyNamespace + { + - public interface IMyInterface + - { + - int MyMethod(); + - long MyProperty { get; } + - } + } + """); + + [Fact(Skip = "The resulting inheritance shows more than expected but not wrong, and does not show the nullability constraing")] + // Shows: public interface IMyInterface : System.Collections.Generic.IDictionary, System.Collections.Generic.ICollection>, System.Collections.Generic.IEnumerable>, System.Collections.IEnumerable, System.Collections.Generic.IReadOnlyDictionary, System.Collections.Generic.IReadOnlyCollection> + public Task InterfaceAddWithTypeConstraints() => RunTestAsync( + beforeCode: """ + using System.Collections.Generic; + namespace MyNamespace + { + public struct MyStruct + { + } + } + """, + afterCode: """ + using System.Collections.Generic; + namespace MyNamespace + { + public struct MyStruct + { + } + public interface IMyInterface : IDictionary, IReadOnlyDictionary where TKey : notnull + { + bool ContainsValue(TValue value); + } + } + """, + expectedCode: """ + namespace MyNamespace + { + + public interface IMyInterface : System.Collections.Generic.IDictionary, System.Collections.Generic.IReadOnlyDictionary where TKey : notnull + + { + + bool ContainsValue(TValue value); + + } + } + """); + + #endregion +} diff --git a/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Method.Tests.cs b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Method.Tests.cs new file mode 100644 index 000000000000..90566321108b --- /dev/null +++ b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Method.Tests.cs @@ -0,0 +1,231 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.ApiDiff.Tests; + +public class DiffMethodTests : DiffBaseTests +{ + #region Methods + + [Fact] + public Task MethodAdd() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public void MyMethod() + { + } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public void MyMethod(); + } + } + """); + + [Fact] + public Task MethodChange() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public void MyBeforeMethod() + { + } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public void MyAfterMethod() + { + } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - public void MyBeforeMethod(); + + public void MyAfterMethod(); + } + } + """); + + [Fact] + public Task MethodDelete() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public void MyMethod() + { + } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - public void MyMethod(); + } + } + """); + + [Fact] + public Task MethodReturnChange() => + // The DocID remains the same, but the return type changes + RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public void MyMethod() + { + } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public int MyMethod() + { + return 0; + } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - public void MyMethod(); + + public int MyMethod(); + } + } + """); + + [Fact] + public Task MethodParametersChange() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public void MyMethod() + { + } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public void MyMethod(int x) + { + } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - public void MyMethod(); + + public void MyMethod(int x); + } + } + """); + + #endregion + + #region Exclusions + + [Fact] + + public Task ExcludeModifiedMethod() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public void MyMethod1() { } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public void MyMethod2() { } + } + } + """, + expectedCode: "", + apisToExclude: ["M:MyNamespace.MyClass.MyMethod1", "M:MyNamespace.MyClass.MyMethod2"]); + + [Fact] + public Task ExcludeRemovedMethod() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public void MyMethod() { } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + expectedCode: "", + apisToExclude: ["M:MyNamespace.MyClass.MyMethod"]); + + #endregion +} diff --git a/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Namespace.Tests.cs b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Namespace.Tests.cs new file mode 100644 index 000000000000..ff9def5a0e13 --- /dev/null +++ b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Namespace.Tests.cs @@ -0,0 +1,367 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.ApiDiff.Tests; + +public class DiffNamespaceTests : DiffBaseTests +{ + #region Block-scoped namespaces + + [Fact] + public Task BlockScopedNamespaceAdd() => RunTestAsync( + beforeCode: "", + afterCode: """ + namespace MyAddedNamespace + { + public struct MyStruct + { + } + } + """, + expectedCode: """ + + namespace MyAddedNamespace + + { + + public struct MyStruct + + { + + } + + } + """); + + [Fact] + public Task BlockScopedNamespaceChange() => RunTestAsync( + beforeCode: """ + namespace MyBeforeNamespace + { + public struct MyStruct + { + } + } + """, + afterCode: """ + namespace MyAfterNamespace + { + public struct MyStruct + { + } + } + """, + expectedCode: """ + - namespace MyBeforeNamespace + - { + - public struct MyStruct + - { + - } + - } + + namespace MyAfterNamespace + + { + + public struct MyStruct + + { + + } + + } + """); + + [Fact] + public Task BlockScopedNamespaceDelete() => RunTestAsync( + beforeCode: """ + namespace MyDeletedNamespace + { + public struct MyStruct + { + } + } + """, + afterCode: "", + expectedCode: """ + - namespace MyDeletedNamespace + - { + - public struct MyStruct + - { + - } + - } + """); + + [Fact] + public Task BlockScopedNamespaceSortAlphabetically() => + // The output is block scoped + RunTestAsync( + beforeCode: "", + afterCode: """ + namespace C + { + public struct CType + { + } + } + namespace B + { + public struct BType + { + } + } + namespace D + { + public struct DType + { + } + } + namespace A + { + public struct AType + { + } + } + """, + expectedCode: """ + + namespace A + + { + + public struct AType + + { + + } + + } + + namespace B + + { + + public struct BType + + { + + } + + } + + namespace C + + { + + public struct CType + + { + + } + + } + + namespace D + + { + + public struct DType + + { + + } + + } + """); + + [Fact] + public Task BlockScopedNamespaceUnchanged() => RunTestAsync( + beforeCode: """ + namespace MyAddedNamespace + { + public struct MyStruct + { + } + } + """, + afterCode: """ + namespace MyAddedNamespace + { + public struct MyStruct + { + } + } + """, + expectedCode: ""); // No changes + + #endregion + + #region File-scoped namespaces + + [Fact] + public Task FileScopedNamespaceAdd() => + // The output is block scoped + RunTestAsync( + beforeCode: "", + afterCode: """ + namespace MyAddedNamespace; + public struct MyStruct + { + } + """, + expectedCode: """ + + namespace MyAddedNamespace + + { + + public struct MyStruct + + { + + } + + } + """); + + [Fact] + public Task FileScopedNamespaceChange() => + // The output is block scoped + RunTestAsync( + beforeCode: """ + namespace MyBeforeNamespace; + public struct MyStruct + { + } + """, + afterCode: """ + namespace MyAfterNamespace; + public struct MyStruct + { + } + """, + expectedCode: """ + - namespace MyBeforeNamespace + - { + - public struct MyStruct + - { + - } + - } + + namespace MyAfterNamespace + + { + + public struct MyStruct + + { + + } + + } + """); + + [Fact] + public Task FileScopedNamespaceDelete() => + // The output is block scoped + RunTestAsync( + beforeCode: """ + namespace MyDeletedNamespace; + public struct MyStruct + { + } + """, + afterCode: "", + expectedCode: """ + - namespace MyDeletedNamespace + - { + - public struct MyStruct + - { + - } + - } + """); + + [Fact] + public Task FileScopedNamespaceUnchanged() => + RunTestAsync( + beforeCode: """ + namespace MyAddedNamespace; + public struct MyStruct + { + } + """, + afterCode: """ + namespace MyAddedNamespace; + public struct MyStruct + { + } + """, + expectedCode: ""); // No changes + + #endregion + + #region Exclusions + + [Fact] + public Task ExcludeAddedNamespace() => RunTestAsync( + beforeCode: "", + afterCode: """ + namespace MyNamespace + { + } + """, + expectedCode: "", + apisToExclude: ["N:MyNamespace.MyNamespace"]); + + [Fact] + public Task ExcludeModifiedNamespace() => RunTestAsync( + beforeCode: """ + namespace MyNamespace1 + { + } + """, + afterCode: """ + namespace MyNamespace2 + { + } + """, + expectedCode: "", + apisToExclude: ["N:MyNamespace.MyNamespace1", "N:MyNamespace.MyNamespace2"]); + + [Fact] + public Task ExcludeRemovedNamespace() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + } + """, + afterCode: "", + expectedCode: "", + apisToExclude: ["N:MyNamespace.MyNamespace"]); + + #endregion + + #region Full names + + [Fact] + public Task NamespaceUsingDependencyKeepFullName() => + // If the same assembly contains two APIs in two different namespaces, but the two namespaces + // share a prefix of their name, and a reference to the API from the other namespace is + // excluding part of the namespace, make sure the final result contains the full name. + RunTestAsync( + beforeCode: "", + afterCode: """ + using System.Reflection; + namespace System.MyNamespace + { + public class MyAClass + { + public void MyMethod(Reflection.AssemblyName assemblyName) { } + } + } + """, + expectedCode: """ + + namespace System.MyNamespace + + { + + public class MyAClass + + { + + public MyAClass(); + + public void MyMethod(System.Reflection.AssemblyName assemblyName); + + } + + } + """); + + [Fact] + public Task NamespacesSameAssemblyDependencyKeepFullName() => + // If the same assembly contains two APIs in two different namespaces, but the two namespaces + // share a prefix of their name, and a reference to the API from the other namespace is + // excluding part of the namespace, make sure the final result contains the full name. + RunTestAsync( + beforeCode: "", + afterCode: """ + namespace System.MyNamespaceA + { + public class MyAClass + { + } + } + namespace System.MyNamespaceB + { + public class MyBClass + { + public void MyMethod(MyNamespaceA.MyAClass myAClass) { } + } + } + """, + expectedCode: """ + + namespace System.MyNamespaceA + + { + + public class MyAClass + + { + + public MyAClass(); + + } + + } + + namespace System.MyNamespaceB + + { + + public class MyBClass + + { + + public MyBClass(); + + public void MyMethod(System.MyNamespaceA.MyAClass myAClass); + + } + + } + """); + + #endregion +} diff --git a/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Operator.Tests.cs b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Operator.Tests.cs new file mode 100644 index 000000000000..9da6f43ee628 --- /dev/null +++ b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Operator.Tests.cs @@ -0,0 +1,705 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.ApiDiff.Tests; + +// Since operators are also methods, this class tests more basic things than the methods class. +public class DiffOperatorTests : DiffBaseTests +{ + [Fact] + public Task EqualityOperators() => + // The equality and inequality operators require also adding Equals and GetHashCode overrides + RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public static MyClass operator ==(MyClass a, MyClass b) { throw null; } + public static MyClass operator !=(MyClass a, MyClass b) { throw null; } + public override bool Equals(object? o) { throw null; } + public override int GetHashCode() { throw null; } + } + } + """, + // Note that the order of the methods is different in the expected code + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public override bool Equals(object? o); + + public override int GetHashCode(); + + public static MyNamespace.MyClass operator ==(MyNamespace.MyClass a, MyNamespace.MyClass b); + + public static MyNamespace.MyClass operator !=(MyNamespace.MyClass a, MyNamespace.MyClass b); + } + } + """); + + [Fact] + public Task AdditionOperator() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public static MyClass operator +(MyClass a, MyClass b) { throw null; } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public static MyNamespace.MyClass operator +(MyNamespace.MyClass a, MyNamespace.MyClass b); + } + } + """); + + [Fact] + public Task SubtractionOperator() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public static MyClass operator -(MyClass a, MyClass b) { throw null; } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public static MyNamespace.MyClass operator -(MyNamespace.MyClass a, MyNamespace.MyClass b); + } + } + """); + + [Fact] + public Task MultiplicationOperator() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public static MyClass operator *(MyClass a, MyClass b) { throw null; } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public static MyNamespace.MyClass operator *(MyNamespace.MyClass a, MyNamespace.MyClass b); + } + } + """); + + [Fact] + public Task DivisionOperator() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + // This would've thrown CS8597 but it's disabled by CSharpAssemblyDocumentGenerator + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public static MyClass operator /(MyClass a, MyClass b) { throw null; } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public static MyNamespace.MyClass operator /(MyNamespace.MyClass a, MyNamespace.MyClass b); + } + } + """); + + [Fact] + public Task ModulusOperator() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public static MyClass operator %(MyClass a, MyClass b) { throw null; } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public static MyNamespace.MyClass operator %(MyNamespace.MyClass a, MyNamespace.MyClass b); + } + } + """); + + [Fact] + public Task LessAndGreaterThanOperator() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + // This would've thrown CS8597 but it's disabled by CSharpAssemblyDocumentGenerator + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public static bool operator <(MyClass a, MyClass b) { throw null; } + public static bool operator >(MyClass a, MyClass b) { throw null; } + } + } + """, + // Note that the order of the operators is different in the expected code + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public static bool operator >(MyNamespace.MyClass a, MyNamespace.MyClass b); + + public static bool operator <(MyNamespace.MyClass a, MyNamespace.MyClass b); + } + } + """); + + [Fact] + public Task LessAndGreaterThanOrEqualOperator() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + // This would've thrown CS8597 but it's disabled by CSharpAssemblyDocumentGenerator + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public static bool operator <=(MyClass a, MyClass b) { throw null; } + public static bool operator >=(MyClass a, MyClass b) { throw null; } + } + } + """, + // Note that the order of the operators is different in the expected code + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public static bool operator >=(MyNamespace.MyClass a, MyNamespace.MyClass b); + + public static bool operator <=(MyNamespace.MyClass a, MyNamespace.MyClass b); + } + } + """); + + [Fact] + public Task IncrementOperator() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public static MyClass operator ++(MyClass a) { throw null; } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public static MyNamespace.MyClass operator ++(MyNamespace.MyClass a); + } + } + """); + + [Fact] + public Task DecrementOperator() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public static MyClass operator --(MyClass a) { throw null; } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public static MyNamespace.MyClass operator --(MyNamespace.MyClass a); + } + } + """); + + [Fact] + public Task LogicalNotOperator() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public static MyClass operator !(MyClass a) { throw null; } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public static MyNamespace.MyClass operator !(MyNamespace.MyClass a); + } + } + """); + + [Fact] + public Task BitwiseNotOperator() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public static MyClass operator ~(MyClass a) { throw null; } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public static MyNamespace.MyClass operator ~(MyNamespace.MyClass a); + } + } + """); + + [Fact] + public Task BitwiseAndOperator() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public static MyClass operator &(MyClass a, MyClass b) { throw null; } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public static MyNamespace.MyClass operator &(MyNamespace.MyClass a, MyNamespace.MyClass b); + } + } + """); + + [Fact] + public Task BitwiseOrOperator() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + // This would've thrown CS8597 but it's disabled by CSharpAssemblyDocumentGenerator + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public static MyClass operator |(MyClass a, MyClass b) { throw null; } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public static MyNamespace.MyClass operator |(MyNamespace.MyClass a, MyNamespace.MyClass b); + } + } + """); + + [Fact] + public Task BitwiseXorOperator() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public static MyClass operator ^(MyClass a, MyClass b) { throw null; } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public static MyNamespace.MyClass operator ^(MyNamespace.MyClass a, MyNamespace.MyClass b); + } + } + """); + + [Fact] + public Task LeftShiftOperator() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public static MyClass operator <<(MyClass a, int shift) { throw null; } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public static MyNamespace.MyClass operator <<(MyNamespace.MyClass a, int shift); + } + } + """); + + [Fact] + public Task RightShiftOperator() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public static MyClass operator >>(MyClass a, int shift) { throw null; } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public static MyNamespace.MyClass operator >>(MyNamespace.MyClass a, int shift); + } + } + """); + + [Fact] + public Task ImplicitOperator() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public static implicit operator MyClass(int value) { throw null; } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public static implicit operator MyNamespace.MyClass(int value); + } + } + """); + + [Fact] + public Task ExplicitOperator() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public static explicit operator int(MyClass value) { throw null; } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public static explicit operator int(MyNamespace.MyClass value); + } + } + """); + + [Fact] + public Task ExplicitCheckedOperator() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public static explicit operator byte(MyClass value) => (byte)(MyClass)value; + public static explicit operator checked byte(MyClass value) => checked((byte)(MyClass)value); + } + } + """, // Notice they get sorted + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public static explicit operator checked byte(MyNamespace.MyClass value); + + public static explicit operator byte(MyNamespace.MyClass value); + } + } + """); + + #region Exclusions + + [Fact] + public Task ExcludeAddedOperator() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public static explicit operator int(MyClass value) { throw null; } + } + } + """, + expectedCode: "", + apisToExclude: ["M:MyNamespace.MyClass.op_Explicit(MyNamespace.MyClass)~System.Int32"]); + + [Fact] + public Task ExcludeModifiedOperator() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public static explicit operator int(MyClass value) { throw null; } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public static explicit operator byte(MyClass value) { throw null; } + } + } + """, + expectedCode: "", + apisToExclude: ["M:MyNamespace.MyClass.op_Explicit(MyNamespace.MyClass)~System.Int32", "M:MyNamespace.MyClass.op_Explicit(MyNamespace.MyClass)~System.Byte"]); + + [Fact] + public Task ExcludeRemovedOperator() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public static explicit operator int(MyClass value) { throw null; } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + expectedCode: "", + apisToExclude: ["M:MyNamespace.MyClass.op_Explicit(MyNamespace.MyClass)~System.Int32"]); + + [Fact] + public Task ExcludeUnmodifiedOperator() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public static explicit operator byte(MyClass value) => (byte)(MyClass)value; + public static explicit operator checked byte(MyClass value) => checked((byte)(MyClass)value); + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public static explicit operator byte(MyClass value) => (byte)(MyClass)value; + public static explicit operator checked byte(MyClass value) => checked((byte)(MyClass)value); + } + } + """, + expectedCode: "", + apisToExclude: ["M:MyNamespace.MyClass.op_Explicit(MyNamespace.MyClass)~System.Byte"]); + + #endregion +} diff --git a/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Property.Tests.cs b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Property.Tests.cs new file mode 100644 index 000000000000..810cc2bfa0e3 --- /dev/null +++ b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Property.Tests.cs @@ -0,0 +1,384 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.ApiDiff.Tests; + +public class DiffPropertyTests : DiffBaseTests +{ + [Fact] + public Task PropertyAdd() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public int MyProperty { get { throw null; } set { } } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + + public int MyProperty { get; set; } + } + } + """); + + [Fact] + public Task PropertyChange() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public int MyBeforeProperty { get { throw null; } set { } } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public int MyAfterProperty { get { throw null; } set { } } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - public int MyBeforeProperty { get; set; } + + public int MyAfterProperty { get; set; } + } + } + """); + + [Fact] + public Task PropertyDelete() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public int MyProperty { get { throw null; } set { } } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - public int MyProperty { get; set; } + } + } + """); + + [Fact] + public Task PropertySetAdd() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public int MyProperty { get { throw null; } } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public int MyProperty { get { throw null; } set { } } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - public int MyProperty { get; } + + public int MyProperty { get; set; } + } + } + """); + + [Fact] + public Task PropertySetRemove() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public int MyProperty { get; set; } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public int MyProperty { get; } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - public int MyProperty { get; set; } + + public int MyProperty { get; } + } + } + """); + + [Fact] + public Task PropertySetVisibilityProtected() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public int MyProperty { get { throw null; } set { } } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public int MyProperty { get { throw null; } protected set { } } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - public int MyProperty { get; set; } + + public int MyProperty { get; protected set; } + } + } + """); + + [Fact] + public Task PropertySetVisibilityPrivate() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public int MyProperty { get; set; } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public int MyProperty { get; private set; } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - public int MyProperty { get; set; } + + public int MyProperty { get; } + } + } + """); + + [Fact] + public Task PropertyReturnChange() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public int MyProperty { get; set; } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public float MyProperty { get; set; } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - public int MyProperty { get; set; } + + public float MyProperty { get; set; } + } + } + """); + + [Fact] + public Task PropertyNullabilityAdd() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public int MyProperty { get; set; } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public int? MyProperty { get; set; } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - public int MyProperty { get; set; } + + public int? MyProperty { get; set; } + } + } + """); + + [Fact] + public Task PropertyNullabilityRemove() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public int? MyProperty { get; set; } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public int MyProperty { get; set; } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyClass + { + - public int? MyProperty { get; set; } + + public int MyProperty { get; set; } + } + } + """); + + #region Exclusions + + [Fact] + public Task ExcludeAddedProperty() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public int MyProperty { get; set; } + } + } + """, + expectedCode: "", + apisToExclude: ["P:MyNamespace.MyClass.MyProperty"]); + + [Fact] + public Task ExcludeModifiedProperty() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public int MyProperty1 { get; set; } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public int MyProperty2 { get; set; } + } + } + """, + expectedCode: "", + apisToExclude: ["P:MyNamespace.MyClass.MyProperty1", "P:MyNamespace.MyClass.MyProperty2"]); + + [Fact] + public Task ExcludeRemovedProperty() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + public int MyProperty { get { throw null; } set { } } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + expectedCode: "", + apisToExclude: ["P:MyNamespace.MyClass.MyProperty"]); + + #endregion +} diff --git a/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Record.Tests.cs b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Record.Tests.cs new file mode 100644 index 000000000000..0cc37f5268bf --- /dev/null +++ b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Record.Tests.cs @@ -0,0 +1,152 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.ApiDiff.Tests; + +public class DiffRecordTests : DiffBaseTests +{ + #region Records + + [Fact] + public Task RecordAdd() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + } + public record MyRecord1(int x); + public record MyRecord2 + { + public double Y { get; set; } + } + public record MyRecord3(int x) + { + public double Y { get; set; } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + + public record MyRecord1(int x); + + public record MyRecord2 + + { + + public double Y { get; set; } + + } + + public record MyRecord3(int x) + + { + + public double Y { get; set; } + + } + } + """); + + [Fact] + public Task RecordChange() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + public record MyBeforeRecord1(int a); + public record MyRecord2 + { + public double Y { get; set; } + } + public record MyRecord3(int a) + { + public int Y { get; set; } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + } + public record MyAfterRecord1(int a); + public record MyRecord2 + { + public int X { get; set; } + } + public record MyRecord3(double a) + { + public double Y { get; set; } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + - public record MyBeforeRecord1(int a); + public record MyRecord2 + { + - public double Y { get; set; } + + public int X { get; set; } + } + - public record MyRecord3(int a) + - { + - public int Y { get; set; } + - } + + public record MyAfterRecord1(int a); + + public record MyRecord3(double a) + + { + + public double Y { get; set; } + + } + } + """); // Note they get sorted + + [Fact] + public Task RecordDelete() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + public record MyRecord1(int a); + public record MyRecord2 + { + public double Y { get; set; } + } + public record MyRecord3(int x) + { + public double Y { get; set; } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + expectedCode: """ + namespace MyNamespace + { + - public record MyRecord1(int a); + - public record MyRecord2 + - { + - public double Y { get; set; } + - } + - public record MyRecord3(int x) + - { + - public double Y { get; set; } + - } + } + """); + + #endregion +} diff --git a/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Struct.Tests.cs b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Struct.Tests.cs new file mode 100644 index 000000000000..abed8d55025a --- /dev/null +++ b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Struct.Tests.cs @@ -0,0 +1,101 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.ApiDiff.Tests; + +public class DiffStructTests : DiffBaseTests +{ + #region Structs + + [Fact] + public Task StructAdd() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + } + public struct MyAddedStruct + { + } + } + """, + expectedCode: """ + namespace MyNamespace + { + + public struct MyAddedStruct + + { + + } + } + """); + + [Fact] + public Task StructChange() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public struct MyBeforeStruct + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public struct MyAfterStruct + { + } + } + """, + expectedCode: """ + namespace MyNamespace + { + - public struct MyBeforeStruct + - { + - } + + public struct MyAfterStruct + + { + + } + } + """); + + [Fact] + public Task StructDelete() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + public struct MyDeletedStruct + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + expectedCode: """ + namespace MyNamespace + { + - public struct MyDeletedStruct + - { + - } + } + """); + + #endregion +} diff --git a/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Type.Tests.cs b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Type.Tests.cs new file mode 100644 index 000000000000..2b193148db49 --- /dev/null +++ b/test/Microsoft.DotNet.ApiDiff.Tests/Diff.Type.Tests.cs @@ -0,0 +1,383 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.ApiDiff.Tests; + +public class DiffTypeTests : DiffBaseTests +{ + #region Nested types + + [Fact] + public Task NestedTypeAdd() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyType + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyType + { + public class MyNestedType + { + public MyNestedType() { } + } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyType + { + + public class MyNestedType + + { + + public MyNestedType(); + + } + } + } + """); + + [Fact] + public Task NestedTypeChange() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyType + { + public class MyBeforeNestedType + { + } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyType + { + public class MyAfterNestedType + { + } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyType + { + - public class MyBeforeNestedType + - { + - public MyBeforeNestedType(); + - } + + public class MyAfterNestedType + + { + + public MyAfterNestedType(); + + } + } + } + """); + + [Fact] + public Task NestedTypeRemove() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyType + { + public class MyNestedType + { + public MyNestedType() { } + } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyType + { + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyType + { + - public class MyNestedType + - { + - public MyNestedType(); + - } + } + } + """); + + [Fact] + public Task NestedTypeMemberAdd() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyType + { + public class MyNestedType + { + } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyType + { + public class MyNestedType + { + public void MyMethod() { } + } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyType + { + public class MyNestedType + { + + public void MyMethod(); + } + } + } + """); + + [Fact] + public Task NestedTypeMemberChange() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyType + { + public class MyNestedType + { + public void MyBeforeMethod() { } + } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyType + { + public class MyNestedType + { + public void MyAfterMethod() { } + } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyType + { + public class MyNestedType + { + - public void MyBeforeMethod(); + + public void MyAfterMethod(); + } + } + } + """); + + [Fact] + public Task NestedTypeMemberRemove() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyType + { + public class MyNestedType + { + public void MyMethod() { } + } + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyType + { + public class MyNestedType + { + } + } + } + """, + expectedCode: """ + namespace MyNamespace + { + public class MyType + { + public class MyNestedType + { + - public void MyMethod(); + } + } + } + """); + + #endregion + + #region Exclusions + + [Fact] + public Task ExcludeAddedType() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + } + public struct MyStruct + { + } + } + """, + expectedCode: "", + apisToExclude: ["T:MyNamespace.MyStruct"]); + + [Fact] + public Task ExcludeModifiedType() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public class MyClass + { + } + public struct MyStruct1 + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + } + public struct MyStruct2 + { + } + } + """, + expectedCode: "", + apisToExclude: ["T:MyNamespace.MyStruct1", "T:MyNamespace.MyStruct2"]); + + [Fact] + public Task ExcludeRemovedType() => RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public struct MyStruct + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + } + """, + expectedCode: "", // The removal is not shown + apisToExclude: ["T:MyNamespace.MyStruct"]); + + #endregion + + #region Other + + [Fact] + public Task TypeKindChange() => + // Name remains the same (as well as DocID), but the kind changes + RunTestAsync( + beforeCode: """ + namespace MyNamespace + { + public struct MyType + { + } + } + """, + afterCode: """ + namespace MyNamespace + { + public class MyType + { + } + } + """, + expectedCode: """ + namespace MyNamespace + { + - public struct MyType + - { + - } + + public class MyType + + { + + public MyType(); + + } + } + """); + + [Fact] + public Task ShowPartial() => RunTestAsync( + beforeCode: "", + afterCode: """ + namespace MyNamespace + { + public class MyClass + { + public class MySubClass + { + } + } + public struct MyStruct + { + } + } + """, + expectedCode: """ + + namespace MyNamespace + + { + + public partial class MyClass + + { + + public MyClass(); + + public partial class MySubClass + + { + + public MySubClass(); + + } + + } + + public partial struct MyStruct + + { + + } + + } + """, + addPartialModifier: true); + + #endregion +} diff --git a/test/Microsoft.DotNet.ApiDiff.Tests/Microsoft.DotNet.ApiDiff.Tests.csproj b/test/Microsoft.DotNet.ApiDiff.Tests/Microsoft.DotNet.ApiDiff.Tests.csproj new file mode 100644 index 000000000000..0614262e2f1e --- /dev/null +++ b/test/Microsoft.DotNet.ApiDiff.Tests/Microsoft.DotNet.ApiDiff.Tests.csproj @@ -0,0 +1,30 @@ + + + + $(ToolsetTargetFramework) + Exe + true + + + + + + + + + + + + PreserveNewest + + + + + + + + + + diff --git a/test/Microsoft.DotNet.ApiSymbolExtensions.Tests/AssemblySymbolLoaderTests.cs b/test/Microsoft.DotNet.ApiSymbolExtensions.Tests/AssemblySymbolLoaderTests.cs index 5fb4b0f17947..5da6367d7953 100644 --- a/test/Microsoft.DotNet.ApiSymbolExtensions.Tests/AssemblySymbolLoaderTests.cs +++ b/test/Microsoft.DotNet.ApiSymbolExtensions.Tests/AssemblySymbolLoaderTests.cs @@ -332,6 +332,43 @@ public void LoadAssemblyFromStreamNoWarns() Assert.Equal("MyNamespace.MyClass", types.FirstOrDefault().ToDisplayString()); } + [Fact] + public void TestCreateFromFiles() + { + var assetInfo = GetSimpleTestAsset(); + TestLog log = new(); + (AssemblySymbolLoader loader, Dictionary symbols) = AssemblySymbolLoader.CreateFromFiles( + log, + assembliesPaths: [assetInfo.OutputDirectory], + assemblyReferencesPaths: [], + assembliesToExclude: [assetInfo.TestAsset.TestProject.Name + ".dll"]); + + Assert.Single(symbols); + + IEnumerable types = symbols.FirstOrDefault().Value + .GlobalNamespace + .GetNamespaceMembers() + .FirstOrDefault((n) => n.Name == "MyNamespace") + .GetTypeMembers(); + + Assert.Single(types); + Assert.Equal("MyNamespace.MyClass", types.FirstOrDefault().ToDisplayString()); + } + + [Fact] + public void TestCreateFromFilesExcludeAssembly() + { + var assetInfo = GetSimpleTestAsset(); + TestLog log = new(); + (AssemblySymbolLoader loader, Dictionary symbols) = AssemblySymbolLoader.CreateFromFiles( + log, + assembliesPaths: [assetInfo.OutputDirectory], + assemblyReferencesPaths: [], + assembliesToExclude: [assetInfo.TestAsset.TestProject.Name]); + + Assert.Empty(symbols); + } + private static CommandResult BuildTestAsset(TestAsset testAsset, out string outputDirectory) { BuildCommand buildCommand = new(testAsset); diff --git a/test/Microsoft.DotNet.ApiSymbolExtensions.Tests/SymbolFactory.cs b/test/Microsoft.DotNet.ApiSymbolExtensions.Tests/SymbolFactory.cs index 9f5abe5252bb..640674329811 100644 --- a/test/Microsoft.DotNet.ApiSymbolExtensions.Tests/SymbolFactory.cs +++ b/test/Microsoft.DotNet.ApiSymbolExtensions.Tests/SymbolFactory.cs @@ -90,10 +90,7 @@ private static CSharpCompilation CreateCSharpCompilationFromSyntax(IEnumerable CSharpSyntaxTree.ParseText(syntax, ParseOptions); private static CSharpCompilation CreateCSharpCompilation(string name, bool enableNullable, byte[] publicKey, bool allowUnsafe, IEnumerable> diagnosticOptions = null) { diff --git a/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs b/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs index 231309db0798..7004692dfc2e 100644 --- a/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs +++ b/test/Microsoft.DotNet.GenAPI.Tests/CSharpFileBuilderTests.cs @@ -85,8 +85,8 @@ namespace C.D { public struct Bar {} } } """, expected: $@" - {CSharpFileBuilder.DefaultFileHeader} - namespace A.C.D {{ public partial struct Bar {{}} }} +{CSharpFileBuilder.DefaultFileHeader} +namespace A.C.D {{ public partial struct Bar {{}} }} ", header: null); } diff --git a/test/Microsoft.DotNet.GenAPI.Tests/SyntaxRewriter/SingleLineStatementCSharpSyntaxRewriterTests.cs b/test/Microsoft.DotNet.GenAPI.Tests/SyntaxRewriter/SingleLineStatementCSharpSyntaxRewriterTests.cs index e4b4bc66dd6c..81d328f11992 100644 --- a/test/Microsoft.DotNet.GenAPI.Tests/SyntaxRewriter/SingleLineStatementCSharpSyntaxRewriterTests.cs +++ b/test/Microsoft.DotNet.GenAPI.Tests/SyntaxRewriter/SingleLineStatementCSharpSyntaxRewriterTests.cs @@ -10,7 +10,7 @@ public class SingleLineStatementCSharpSyntaxRewriterTests : CSharpSyntaxRewriter [Fact] public void TestEmptyMethodBody() { - Compare(new SingleLineStatementCSharpSyntaxRewriter(), + Compare(SingleLineStatementCSharpSyntaxRewriter.Singleton, original: """ namespace A { @@ -34,7 +34,7 @@ void Execute() { } [Fact] public void TestMethodBodyWithSingleStatement() { - Compare(new SingleLineStatementCSharpSyntaxRewriter(), + Compare(SingleLineStatementCSharpSyntaxRewriter.Singleton, original: """ namespace A { @@ -60,7 +60,7 @@ class B [Fact] public void TestConstructorPostProcessing() { - Compare(new SingleLineStatementCSharpSyntaxRewriter(), + Compare(SingleLineStatementCSharpSyntaxRewriter.Singleton, original: """ namespace A { @@ -84,7 +84,7 @@ public B() { } [Fact] public void TestMethodBodyWithSingleStatementInOneLine() { - Compare(new SingleLineStatementCSharpSyntaxRewriter(), + Compare(SingleLineStatementCSharpSyntaxRewriter.Singleton, original: """ namespace A { @@ -108,7 +108,7 @@ class B [Fact] public void TestPropertyPostProcessing() { - Compare(new SingleLineStatementCSharpSyntaxRewriter(), + Compare(SingleLineStatementCSharpSyntaxRewriter.Singleton, original: """ namespace A { @@ -138,7 +138,7 @@ int Property4 { get { } } [Fact] public void TestOperatorPostProcessing() { - Compare(new SingleLineStatementCSharpSyntaxRewriter(), + Compare(SingleLineStatementCSharpSyntaxRewriter.Singleton, original: """ namespace A { @@ -164,7 +164,7 @@ class B [Fact] public void TestConversionOperatorPostProcessing() { - Compare(new SingleLineStatementCSharpSyntaxRewriter(), + Compare(SingleLineStatementCSharpSyntaxRewriter.Singleton, original: """ namespace Foo { diff --git a/test/Microsoft.DotNet.GenAPI.Tests/TestAssemblyLoaderFactory.cs b/test/Microsoft.DotNet.GenAPI.Tests/TestAssemblyLoaderFactory.cs index 9bd062acf33a..dce08f5f56b6 100644 --- a/test/Microsoft.DotNet.GenAPI.Tests/TestAssemblyLoaderFactory.cs +++ b/test/Microsoft.DotNet.GenAPI.Tests/TestAssemblyLoaderFactory.cs @@ -11,7 +11,12 @@ namespace Microsoft.DotNet.GenAPI.Tests; public class TestAssemblyLoaderFactory { - public static (IAssemblySymbolLoader, Dictionary) CreateFromTexts(ILog log, (string, string)[] assemblyTexts, IEnumerable>? diagnosticOptions = null, bool respectInternals = false, bool allowUnsafe = false) + public static (IAssemblySymbolLoader, Dictionary) CreateFromTexts( + ILog log, + (string, string)[] assemblyTexts, + bool respectInternals = false, + bool allowUnsafe = false, + IEnumerable>? diagnosticOptions = null) { if (assemblyTexts.Length == 0) { @@ -26,10 +31,11 @@ public static (IAssemblySymbolLoader, Dictionary) Creat Dictionary assemblySymbols = new(); foreach ((string assemblyName, string assemblyText) in assemblyTexts) { + string actualAssemblyName = assemblyName.Replace(".dll", string.Empty); using Stream assemblyStream = SymbolFactory.EmitAssemblyStreamFromSyntax(assemblyText, diagnosticOptions, enableNullable: true, allowUnsafe: allowUnsafe, assemblyName: assemblyName); - if (loader.LoadAssembly(assemblyName, assemblyStream) is IAssemblySymbol assemblySymbol) + if (loader.LoadAssembly(actualAssemblyName, assemblyStream) is IAssemblySymbol assemblySymbol) { - assemblySymbols.Add(assemblyName, assemblySymbol); + assemblySymbols.Add(actualAssemblyName, assemblySymbol); } }