File: Diagnostics\DiagnosticAnalysisResultBuilder.cs
Web Access
Project: src\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// 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.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Workspaces.Diagnostics;
 
/// <summary>
/// We have this builder to avoid creating collections unnecessarily.
/// Expectation is that, most of time, most of analyzers doesn't have any diagnostics. so no need to actually create any objects.
/// </summary>
internal struct DiagnosticAnalysisResultBuilder(Project project, VersionStamp version)
{
    public readonly Project Project = project;
    public readonly VersionStamp Version = version;
 
    private HashSet<DocumentId>? _lazyDocumentsWithDiagnostics = null;
 
    private Dictionary<DocumentId, List<DiagnosticData>>? _lazySyntaxLocals = null;
    private Dictionary<DocumentId, List<DiagnosticData>>? _lazySemanticLocals = null;
    private Dictionary<DocumentId, List<DiagnosticData>>? _lazyNonLocals = null;
 
    private List<DiagnosticData>? _lazyOthers = null;
 
    public readonly ImmutableHashSet<DocumentId> DocumentIds => _lazyDocumentsWithDiagnostics == null ? [] : [.. _lazyDocumentsWithDiagnostics];
    public readonly ImmutableDictionary<DocumentId, ImmutableArray<DiagnosticData>> SyntaxLocals => Convert(_lazySyntaxLocals);
    public readonly ImmutableDictionary<DocumentId, ImmutableArray<DiagnosticData>> SemanticLocals => Convert(_lazySemanticLocals);
    public readonly ImmutableDictionary<DocumentId, ImmutableArray<DiagnosticData>> NonLocals => Convert(_lazyNonLocals);
    public readonly ImmutableArray<DiagnosticData> Others => _lazyOthers == null ? [] : [.. _lazyOthers];
 
    public void AddExternalSyntaxDiagnostics(DocumentId documentId, IEnumerable<Diagnostic> diagnostics)
    {
        AddExternalDiagnostics(ref _lazySyntaxLocals, documentId, diagnostics);
    }
 
    public void AddExternalSemanticDiagnostics(DocumentId documentId, IEnumerable<Diagnostic> diagnostics)
    {
        // this is for diagnostic producer that doesnt use compiler based DiagnosticAnalyzer such as TypeScript.
        Contract.ThrowIfTrue(Project.SupportsCompilation);
 
        AddExternalDiagnostics(ref _lazySemanticLocals, documentId, diagnostics);
    }
 
    private void AddExternalDiagnostics(
        ref Dictionary<DocumentId, List<DiagnosticData>>? lazyLocals, DocumentId documentId, IEnumerable<Diagnostic> diagnostics)
    {
        foreach (var diagnostic in diagnostics)
        {
            // REVIEW: what is our plan for additional locations? 
            switch (diagnostic.Location.Kind)
            {
                case LocationKind.ExternalFile:
                    {
                        var diagnosticDocumentId = Project.GetDocumentForExternalLocation(diagnostic.Location);
                        if (documentId == diagnosticDocumentId)
                        {
                            // local diagnostics to a file
                            AddDocumentDiagnostic(ref lazyLocals, Project.GetTextDocument(diagnosticDocumentId), diagnostic);
                        }
                        else if (diagnosticDocumentId != null)
                        {
                            // non local diagnostics to a file
                            AddDocumentDiagnostic(ref _lazyNonLocals, Project.GetTextDocument(diagnosticDocumentId), diagnostic);
                        }
                        else
                        {
                            // non local diagnostics without location
                            AddOtherDiagnostic(DiagnosticData.Create(Project.Solution, diagnostic, Project));
                        }
 
                        break;
                    }
 
                case LocationKind.None:
                    AddOtherDiagnostic(DiagnosticData.Create(Project.Solution, diagnostic, Project));
                    break;
 
                case LocationKind.SourceFile:
                case LocationKind.MetadataFile:
                case LocationKind.XmlFile:
                    // ignore
                    continue;
 
                case var kind:
                    throw ExceptionUtilities.UnexpectedValue(kind);
            }
        }
    }
 
    private void AddDocumentDiagnostic(ref Dictionary<DocumentId, List<DiagnosticData>>? map, TextDocument? document, Diagnostic diagnostic)
    {
        if (document is null || !document.SupportsDiagnostics())
        {
            return;
        }
 
        map ??= [];
        map.GetOrAdd(document.Id, _ => []).Add(DiagnosticData.Create(diagnostic, document));
 
        _lazyDocumentsWithDiagnostics ??= [];
        _lazyDocumentsWithDiagnostics.Add(document.Id);
    }
 
    private void AddOtherDiagnostic(DiagnosticData data)
    {
        _lazyOthers ??= [];
        _lazyOthers.Add(data);
    }
 
    public void AddSyntaxDiagnostics(SyntaxTree tree, IEnumerable<Diagnostic> diagnostics)
        => AddDiagnostics(ref _lazySyntaxLocals, tree, diagnostics);
 
    public void AddDiagnosticTreatedAsLocalSemantic(Diagnostic diagnostic)
        => AddDiagnostic(ref _lazySemanticLocals, diagnostic.Location.SourceTree, diagnostic);
 
    public void AddSemanticDiagnostics(SyntaxTree tree, IEnumerable<Diagnostic> diagnostics)
        => AddDiagnostics(ref _lazySemanticLocals, tree, diagnostics);
 
    public void AddCompilationDiagnostics(IEnumerable<Diagnostic> diagnostics)
    {
        Dictionary<DocumentId, List<DiagnosticData>>? dummy = null;
        AddDiagnostics(ref dummy, tree: null, diagnostics: diagnostics);
 
        // dummy should be always null since tree is null
        Debug.Assert(dummy == null);
    }
 
    private void AddDiagnostic(
        ref Dictionary<DocumentId, List<DiagnosticData>>? lazyLocals, SyntaxTree? tree, Diagnostic diagnostic)
    {
        // REVIEW: what is our plan for additional locations? 
        switch (diagnostic.Location.Kind)
        {
            case LocationKind.ExternalFile:
                var diagnosticDocumentId = Project.GetDocumentForExternalLocation(diagnostic.Location);
                if (diagnosticDocumentId != null)
                {
                    AddDocumentDiagnostic(ref _lazyNonLocals, Project.GetRequiredTextDocument(diagnosticDocumentId), diagnostic);
                }
                else
                {
                    AddOtherDiagnostic(DiagnosticData.Create(Project.Solution, diagnostic, Project));
                }
 
                break;
 
            case LocationKind.None:
                AddOtherDiagnostic(DiagnosticData.Create(Project.Solution, diagnostic, Project));
                break;
 
            case LocationKind.SourceFile:
                var diagnosticTree = diagnostic.Location.SourceTree;
                if (tree != null && diagnosticTree == tree)
                {
                    // local diagnostics to a file
                    AddDocumentDiagnostic(ref lazyLocals, Project.GetDocument(diagnosticTree), diagnostic);
                }
                else if (diagnosticTree != null)
                {
                    // non local diagnostics to a file
                    AddDocumentDiagnostic(ref _lazyNonLocals, Project.GetDocument(diagnosticTree), diagnostic);
                }
                else
                {
                    // non local diagnostics without location
                    AddOtherDiagnostic(DiagnosticData.Create(Project.Solution, diagnostic, Project));
                }
 
                break;
 
            case LocationKind.MetadataFile:
            case LocationKind.XmlFile:
                // ignore
                return;
 
            default:
                throw ExceptionUtilities.UnexpectedValue(diagnostic.Location.Kind);
        }
    }
 
    private void AddDiagnostics(
        ref Dictionary<DocumentId, List<DiagnosticData>>? lazyLocals, SyntaxTree? tree, IEnumerable<Diagnostic> diagnostics)
    {
        foreach (var diagnostic in diagnostics)
            AddDiagnostic(ref lazyLocals, tree, diagnostic);
    }
 
    private static ImmutableDictionary<DocumentId, ImmutableArray<DiagnosticData>> Convert(Dictionary<DocumentId, List<DiagnosticData>>? map)
    {
        return map == null
            ? ImmutableDictionary<DocumentId, ImmutableArray<DiagnosticData>>.Empty
            : map.ToImmutableDictionary(kv => kv.Key, kv => kv.Value.ToImmutableArray());
    }
}