File: FileOutputDiffGenerator.cs
Web Access
Project: ..\..\..\src\Compatibility\ApiDiff\Microsoft.DotNet.ApiDiff\Microsoft.DotNet.ApiDiff.csproj (Microsoft.DotNet.ApiDiff)
// 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;
 
/// <summary>
/// Generates a markdown diff of two different versions of the same assembly.
/// </summary>
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<KeyValuePair<string, ReportDiagnostic>>? _diagnosticOptions;
    private readonly Dictionary<string, string> _results;
 
    /// <summary>
    /// Initializes a new instance of the <see cref="FileOutputDiffGenerator"/> class.
    /// </summary>
    /// <param name="log">The logger to use for logging messages.</param>
    /// <param name="beforeAssembliesFolderPath">The folder path containing the assemblies before the changes.</param>
    /// <param name="beforeAssemblyReferencesFolderPath">The folder path containing the assembly references before the changes.</param>
    /// <param name="afterAssembliesFolderPath">The folder path containing the assemblies after the changes.</param>
    /// <param name="afterAssemblyReferencesFolderPath">The folder path containing the assembly references after the changes.</param>
    /// <param name="outputFolderPath">The folder path where the output files will be written.</param>
    /// <param name="beforeFriendlyName">The friendly name for the before version of the assemblies.</param>
    /// <param name="afterFriendlyName">The friendly name for the after version of the assemblies.</param>
    /// <param name="tableOfContentsTitle">The title for the table of contents.</param>
    /// <param name="filesWithAssembliesToExclude">An optional array of filepaths each containing a list of assemblies to avoid showing in the diff.</param>
    /// <param name="filesWithAttributesToExclude">An optional array of filepaths each containing a list of attributes to avoid showing in the diff.</param>
    /// <param name="filesWithApisToExclude">An optional array of filepaths each containing a list of APIs to avoid showing in the diff.</param>
    /// <param name="addPartialModifier">A value indicating whether to add the partial modifier to types.</param>
    /// <param name="writeToDisk">If <see langword="true"/>, when calling <see cref="RunAsync"/>, the generated markdown files get written to disk, and no item is added to the <see cref="RunAsync"/> dictionary. If <see langword="false"/>, when calling <see cref="RunAsync"/>, the generated markdown files get added to the <see cref="RunAsync"/> dictionary (with the file path as the dictionary key) and none of them is written to disk. This is meant for testing purposes.</param>
    /// <param name="diagnosticOptions">An optional set of diagnostic options.</param>
    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<KeyValuePair<string, ReportDiagnostic>>? 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 = [];
    }
 
    /// <inheritdoc/>
    public IReadOnlyDictionary<string, string> Results => _results.AsReadOnly();
 
    /// <inheritdoc/>
    public async Task RunAsync(CancellationToken cancellationToken)
    {
        Debug.Assert(_beforeAssembliesFolderPaths.Length == 1);
        Debug.Assert(_afterAssembliesFolderPaths.Length == 1);
 
        cancellationToken.ThrowIfCancellationRequested();
 
        (IAssemblySymbolLoader beforeLoader, Dictionary<string, IAssemblySymbol> beforeAssemblySymbols) =
            AssemblySymbolLoader.CreateFromFiles(
                _log,
                assembliesPaths: _beforeAssembliesFolderPaths,
                assemblyReferencesPaths: _beforeAssemblyReferencesFolderPaths,
                assembliesToExclude: _assembliesToExclude,
                diagnosticOptions: _diagnosticOptions);
 
        (IAssemblySymbolLoader afterLoader, Dictionary<string, IAssemblySymbol> 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(cancellationToken).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))
        {
            cancellationToken.ThrowIfCancellationRequested();
 
            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}'.");
        }
 
        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<string> 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()];
    }
}