File: ProjectSystem\Logging\RoslynWorkspaceStructureLogger.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices.csproj (Microsoft.VisualStudio.LanguageServices)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Shell.FileDialog;
using System.Xml.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host;
using Microsoft.VisualStudio.ComponentModelHost;
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.Extensions;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Threading;
 
#pragma warning disable CA2007 // We are OK awaiting tasks since we're following Visual Studio threading rules in this file
 
namespace Microsoft.VisualStudio.LanguageServices.ProjectSystem.Logging;
 
internal static class RoslynWorkspaceStructureLogger
{
    private static int s_NextCompilationId;
    private static readonly ConditionalWeakTable<Compilation, StrongBox<int>> s_CompilationIds = new();
 
    public static void ShowSaveDialogAndLog(IServiceProvider serviceProvider)
    {
        ThreadHelper.ThrowIfNotOnUIThread();
 
        var uiShell = (IVsUIShell)serviceProvider.GetService(typeof(SVsUIShell));
        var uiShell2 = (IVsUIShell2)uiShell;
 
        Assumes.Present(uiShell2);
 
        ErrorHandler.ThrowOnFailure(uiShell.GetDialogOwnerHwnd(out var hwnd));
 
        var filters = new DialogFilters(
            new[] { new DialogFilter("Zip Files", "*.zip") },
            defaultFilterIndex: 0);
 
        var path = VsShellUtilities.SelectSaveAsFile(
            uiShell2,
            hwnd,
            title: string.Empty,
            initialDirectory: string.Empty,
            initialFileName: string.Empty,
            filters);
 
        if (string.IsNullOrEmpty(path))
            return;
 
        var threadingContext = serviceProvider.GetMefService<IThreadingContext>();
 
        threadingContext.JoinableTaskFactory.RunAsync(() => LogAsync(serviceProvider, threadingContext, path));
    }
 
    public static async Task LogAsync(IServiceProvider serviceProvider, IThreadingContext threadingContext, string path)
    {
        var componentModel = (IComponentModel)serviceProvider.GetService(typeof(SComponentModel));
        Assumes.Present(componentModel);
 
        var workspace = componentModel.GetService<VisualStudioWorkspace>();
        var solution = workspace.CurrentSolution;
 
        // Start a threaded wait dialog
        await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync();
        var dialogFactory = (IVsThreadedWaitDialogFactory)serviceProvider.GetService(typeof(SVsThreadedWaitDialogFactory));
        Assumes.Present(dialogFactory);
        using var session = dialogFactory.StartWaitDialog(
            ServicesVSResources.Visual_Studio,
            new ThreadedWaitDialogProgressData(
                ServicesVSResources.Logging_Roslyn_Workspace_structure,
                progressText: null,
                statusBarText: null,
                isCancelable: true,
                currentStep: 0,
                totalSteps: solution.ProjectIds.Count),
            delayToShowDialog: TimeSpan.Zero);
        var cancellationToken = session.UserCancellationToken;
 
        // Now switch to the background thread while we're working
        await TaskScheduler.Default;
 
        try
        {
            var document = new XDocument();
            var workspaceElement = new XElement("workspace");
            workspaceElement.SetAttributeValue("kind", workspace.Kind);
            document.Add(workspaceElement);
 
            var projectsProcessed = 0;
 
            foreach (var project in solution.GetProjectDependencyGraph().GetTopologicallySortedProjects(cancellationToken).Select(solution.GetProject))
            {
                if (project is null)
                    continue;
 
                // Dump basic project attributes
                var projectElement = new XElement("project");
                workspaceElement.Add(projectElement);
 
                projectElement.SetAttributeValue("id", SanitizePath(project.Id.ToString()));
                projectElement.SetAttributeValue("name", project.Name);
                projectElement.SetAttributeValue("assemblyName", project.AssemblyName);
                projectElement.SetAttributeValue("language", project.Language);
                projectElement.SetAttributeValue("path", SanitizePath(project.FilePath ?? "(none)"));
                projectElement.SetAttributeValue("outputPath", SanitizePath(project.OutputFilePath ?? "(none)"));
 
                var hasSuccessfullyLoaded = await project.HasSuccessfullyLoadedAsync(cancellationToken);
                projectElement.SetAttributeValue("hasSuccessfullyLoaded", hasSuccessfullyLoaded);
 
                // Dump MSBuild <Reference> nodes
                var msbuildReferencesElement = CreateMsBuildReferencesElement(project);
                if (msbuildReferencesElement != null)
                    projectElement.Add(msbuildReferencesElement);
 
                // Dump DTE references
                var dteReferencesElement = await CreateDteReferencesElementAsync(serviceProvider, threadingContext, project);
                if (dteReferencesElement != null)
                    projectElement.Add(dteReferencesElement);
 
                // Dump the actual metadata references in the workspace
                var workspaceReferencesElement = new XElement("workspaceReferences");
                projectElement.Add(workspaceReferencesElement);
 
                foreach (var metadataReference in project.MetadataReferences)
                {
                    workspaceReferencesElement.Add(CreateElementForPortableExecutableReference(metadataReference));
                }
 
                // Dump project references in the workspace
                foreach (var projectReference in project.AllProjectReferences)
                {
                    var referenceElement = new XElement("projectReference", new XAttribute("id", SanitizePath(projectReference.ProjectId.ToString())));
 
                    if (!project.ProjectReferences.Contains(projectReference))
                        referenceElement.SetAttributeValue("missingInSolution", "true");
 
                    workspaceReferencesElement.Add(referenceElement);
                }
 
                projectElement.Add(new XElement("workspaceDocuments", await CreateElementsForDocumentCollectionAsync(project.Documents, "document", cancellationToken)));
                projectElement.Add(new XElement("workspaceAdditionalDocuments", await CreateElementsForDocumentCollectionAsync(project.AdditionalDocuments, "additionalDocuments", cancellationToken)));
 
                projectElement.Add(new XElement("workspaceAnalyzerConfigDocuments", await CreateElementsForDocumentCollectionAsync(project.AnalyzerConfigDocuments, "analyzerConfigDocument", cancellationToken)));
 
                // Dump references from the compilation; this should match the workspace but can help rule out
                // cross-language reference bugs or other issues like that
                var compilation = await project.GetCompilationAsync(cancellationToken);
 
                if (compilation != null)
                {
                    var compilationReferencesElement = new XElement("compilationReferences");
                    projectElement.Add(compilationReferencesElement);
 
                    foreach (var reference in compilation.References)
                    {
                        compilationReferencesElement.Add(CreateElementForPortableExecutableReference(reference));
                    }
 
                    projectElement.Add(CreateElementForCompilation(compilation));
 
                    // Dump all diagnostics
                    var diagnosticsElement = new XElement("diagnostics");
                    projectElement.Add(diagnosticsElement);
 
                    foreach (var diagnostic in compilation.GetDiagnostics(cancellationToken))
                    {
                        diagnosticsElement.Add(
                            new XElement("diagnostic",
                                new XAttribute("id", diagnostic.Id),
                                new XAttribute("severity", diagnostic.Severity.ToString()),
                                new XAttribute("path", SanitizePath(diagnostic.Location.GetLineSpan().Path ?? "(none)")),
                                diagnostic.GetMessage()));
                    }
                }
 
                projectsProcessed++;
                session.Progress.Report(new ThreadedWaitDialogProgressData(
                    ServicesVSResources.Logging_Roslyn_Workspace_structure,
                    progressText: null,
                    statusBarText: null,
                    isCancelable: true,
                    currentStep: projectsProcessed,
                    totalSteps: solution.ProjectIds.Count));
            }
 
            File.Delete(path);
 
            using (var zipFile = ZipFile.Open(path, ZipArchiveMode.Create))
            {
                var zipFileEntry = zipFile.CreateEntry("Workspace.xml", CompressionLevel.Fastest);
                using (var stream = zipFileEntry.Open())
                {
                    document.Save(stream);
                }
            }
        }
        catch (OperationCanceledException)
        {
            // They cancelled
        }
    }
 
    private static XElement? CreateMsBuildReferencesElement(Project project)
    {
        if (project.FilePath == null)
            return null;
 
        var msbuildProject = XDocument.Load(project.FilePath);
        var msbuildNamespace = XNamespace.Get("http://schemas.microsoft.com/developer/msbuild/2003");
 
        var msbuildReferencesElement = new XElement("msbuildReferences");
 
        msbuildReferencesElement.Add(msbuildProject.Descendants(msbuildNamespace + "ProjectReference"));
        msbuildReferencesElement.Add(msbuildProject.Descendants(msbuildNamespace + "Reference"));
        msbuildReferencesElement.Add(msbuildProject.Descendants(msbuildNamespace + "ReferencePath"));
 
        return msbuildReferencesElement;
    }
 
    private static async Task<XElement?> CreateDteReferencesElementAsync(IServiceProvider serviceProvider, IThreadingContext threadingContext, Project project)
    {
        await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync();
 
        var dte = (EnvDTE.DTE)serviceProvider.GetService(typeof(SDTE));
 
        VSLangProj.VSProject? langProjProject = null;
        foreach (EnvDTE.Project p in dte.Solution.Projects)
        {
            try
            {
                if (string.Equals(p.FullName, project.FilePath, StringComparison.OrdinalIgnoreCase))
                {
                    langProjProject = p.Object as VSLangProj.VSProject;
                    if (langProjProject is not null)
                        break;
                }
            }
            catch (NotImplementedException)
            {
                // Some EnvDTE.Projects will throw on p.FullName, so just bail in that case.
            }
        }
 
        if (langProjProject == null)
            return null;
 
        var dteReferences = new XElement("dteReferences");
 
        foreach (var reference in langProjProject.References.Cast<VSLangProj.Reference>())
        {
            if (reference.SourceProject != null)
            {
                dteReferences.Add(new XElement("projectReference", new XAttribute("projectName", reference.SourceProject.Name)));
            }
            else
            {
                dteReferences.Add(new XElement("metadataReference",
                    reference.Path != null ? new XAttribute("path", SanitizePath(reference.Path)) : null,
                    new XAttribute("name", reference.Name)));
            }
        }
 
        return dteReferences;
    }
 
    private static string SanitizePath(string s)
    {
        return ReplacePathComponent(s, Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "%USERPROFILE%");
    }
 
    /// <summary>
    /// Equivalent to string.Replace, but uses OrdinalIgnoreCase for matching.
    /// </summary>
    private static string ReplacePathComponent(string s, string oldValue, string newValue)
    {
        while (true)
        {
            var index = s.IndexOf(oldValue, StringComparison.OrdinalIgnoreCase);
            if (index == -1)
                return s;
 
            s = s.Substring(0, index) + newValue + s.Substring(index + oldValue.Length);
        }
    }
 
    private static XElement CreateElementForPortableExecutableReference(MetadataReference reference)
    {
        var aliasesAttribute = new XAttribute("aliases", string.Join(",", reference.Properties.Aliases));
 
        if (reference is CompilationReference compilationReference)
        {
            return new XElement("compilationReference",
                aliasesAttribute,
                CreateElementForCompilation(compilationReference.Compilation));
        }
        else if (reference is PortableExecutableReference portableExecutableReference)
        {
            return new XElement("peReference",
                new XAttribute("file", SanitizePath(portableExecutableReference.FilePath ?? "(none)")),
                new XAttribute("display", SanitizePath(portableExecutableReference.Display ?? "(none)")),
                aliasesAttribute);
        }
        else
        {
            return new XElement("metadataReference", new XAttribute("display", SanitizePath(reference.Display ?? "(none)")));
        }
    }
 
    private static XElement CreateElementForCompilation(Compilation compilation)
    {
        StrongBox<int> compilationId;
        if (!s_CompilationIds.TryGetValue(compilation, out compilationId))
        {
            compilationId = new StrongBox<int>(s_NextCompilationId++);
            s_CompilationIds.Add(compilation, compilationId);
        }
 
        var namespaces = new Queue<INamespaceSymbol>();
        var typesElement = new XElement("types");
 
        namespaces.Enqueue(compilation.Assembly.GlobalNamespace);
 
        while (namespaces.Count > 0)
        {
            var @ns = namespaces.Dequeue();
 
            foreach (var type in @ns.GetTypeMembers())
            {
                typesElement.Add(new XElement("type", new XAttribute("name", type.ToDisplayString())));
            }
 
            foreach (var childNamespace in @ns.GetNamespaceMembers())
            {
                namespaces.Enqueue(childNamespace);
            }
        }
 
        return new XElement("compilation",
            new XAttribute("objectId", compilationId.Value),
            new XAttribute("assemblyIdentity", compilation.Assembly.Identity.ToString()),
            typesElement);
    }
 
    public static async Task<IEnumerable<XElement>> CreateElementsForDocumentCollectionAsync(IEnumerable<TextDocument> documents, string elementName, CancellationToken cancellationToken)
    {
        var elements = new List<XElement>();
 
        foreach (var document in documents)
        {
            var documentElement = new XElement(elementName, new XAttribute("path", SanitizePath(document.FilePath ?? "(none)")));
 
            var clientName = document.DocumentServiceProvider.GetService<DocumentPropertiesService>()?.DiagnosticsLspClientName;
            if (clientName != null)
                documentElement.SetAttributeValue("clientName", clientName);
 
            var loadDiagnostic = await document.State.GetFailedToLoadExceptionMessageAsync(cancellationToken);
 
            if (loadDiagnostic != null)
                documentElement.Add(new XElement("loadDiagnostic", loadDiagnostic));
 
            elements.Add(documentElement);
        }
 
        return elements;
    }
}