File: TaskList\ProjectExternalErrorReporter.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_avk53qfa_wpftmp.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.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.Extensions;
using Microsoft.VisualStudio.LanguageServices.Implementation.Venus;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.TextManager.Interop;
using Roslyn.Utilities;
 
namespace Microsoft.VisualStudio.LanguageServices.Implementation.TaskList;
 
internal sealed class ProjectExternalErrorReporter : IVsReportExternalErrors, IVsLanguageServiceBuildErrorReporter2
{
    internal static readonly ImmutableArray<string> CustomTags = [WellKnownDiagnosticTags.Telemetry];
    internal static readonly ImmutableArray<string> CompilerDiagnosticCustomTags = [WellKnownDiagnosticTags.Compiler, WellKnownDiagnosticTags.Telemetry];
 
    private readonly ProjectId _projectId;
 
    /// <summary>
    /// Passed to the error reporting service to allow current project error list filters to work.
    /// </summary>
    private readonly Guid _projectHierarchyGuid;
    private readonly string _errorCodePrefix;
    private readonly string _language;
 
    private readonly VisualStudioWorkspaceImpl _workspace;
 
    private DiagnosticAnalyzerInfoCache AnalyzerInfoCache => _workspace.ExternalErrorDiagnosticUpdateSource.AnalyzerInfoCache;
 
    public ProjectExternalErrorReporter(ProjectId projectId, Guid projectHierarchyGuid, string errorCodePrefix, string language, VisualStudioWorkspaceImpl workspace)
    {
        Debug.Assert(projectId != null);
        Debug.Assert(errorCodePrefix != null);
        Debug.Assert(workspace != null);
 
        _projectId = projectId;
        _projectHierarchyGuid = projectHierarchyGuid;
        _errorCodePrefix = errorCodePrefix;
        _language = language;
        _workspace = workspace;
    }
 
    private ExternalErrorDiagnosticUpdateSource DiagnosticProvider => _workspace.ExternalErrorDiagnosticUpdateSource;
 
    private bool CanHandle(string errorId)
    {
        // make sure we have error id, otherwise, we simple don't support
        // this error
        if (errorId == null)
        {
            // record NFW to see who violates contract.
            FatalError.ReportAndCatch(new Exception("errorId is null"));
            return false;
        }
 
        // we accept all compiler diagnostics
        if (errorId.StartsWith(_errorCodePrefix))
        {
            return true;
        }
 
        return DiagnosticProvider.IsSupportedDiagnosticId(_projectId, errorId);
    }
 
    public int AddNewErrors(IVsEnumExternalErrors pErrors)
    {
        using var _ = ArrayBuilder<DiagnosticData>.GetInstance(out var allDiagnostics);
 
        var errors = new ExternalError[1];
        var project = _workspace.CurrentSolution.GetProject(_projectId);
        while (pErrors.Next(1, errors, out var fetched) == VSConstants.S_OK && fetched == 1)
        {
            var error = errors[0];
            if (error.bstrFileName != null)
            {
                var diagnostic = TryCreateDocumentDiagnosticItem(error);
                if (diagnostic != null)
                {
                    allDiagnostics.Add(diagnostic);
                    continue;
                }
            }
 
            allDiagnostics.Add(GetDiagnosticData(
                documentId: null,
                _projectId,
                _workspace,
                GetErrorId(error),
                error.bstrText,
                GetDiagnosticSeverity(error),
                _language,
                new FileLinePositionSpan(project.FilePath ?? "", span: default),
                AnalyzerInfoCache));
        }
 
        DiagnosticProvider.AddNewErrors(_projectId, _projectHierarchyGuid, allDiagnostics.ToImmutableAndClear());
        return VSConstants.S_OK;
    }
 
    public int ClearAllErrors()
    {
        DiagnosticProvider.ClearErrors(_projectId);
        return VSConstants.S_OK;
    }
 
    public int GetErrors(out IVsEnumExternalErrors pErrors)
    {
        pErrors = null;
        Debug.Fail("This is not implemented, because no one called it.");
        return VSConstants.E_NOTIMPL;
    }
 
    private DocumentId TryGetDocumentId(string filePath)
    {
        return _workspace.CurrentSolution.GetDocumentIdsWithFilePath(filePath)
                         .Where(f => f.ProjectId == _projectId)
                         .FirstOrDefault();
    }
 
    private DiagnosticData TryCreateDocumentDiagnosticItem(ExternalError error)
    {
        var documentId = TryGetDocumentId(error.bstrFileName);
        if (documentId == null)
        {
            return null;
        }
 
        var line = error.iLine;
        var column = error.iCol;
 
        // something we should move to document service one day. but until then, we keep the old way.
        // build basically output error location on surface buffer and we map it back to
        // subject buffer for contained document. so that contained document can map
        // it back to surface buffer when navigate. whole problem comes in due to the mapped span.
        // unlike live error, build outputs mapped span and we save it as original span (since we
        // have no idea whether it is mapped or not). for contained document case, we do know it is
        // mapped span, so we calculate original span and put that in original span.
        var containedDocument = ContainedDocument.TryGetContainedDocument(documentId);
        if (containedDocument != null)
        {
            var span = new TextManager.Interop.TextSpan
            {
                iStartLine = line,
                iStartIndex = column,
                iEndLine = line,
                iEndIndex = column,
            };
 
            var spans = new TextManager.Interop.TextSpan[1];
            Marshal.ThrowExceptionForHR(containedDocument.BufferCoordinator.MapPrimaryToSecondarySpan(
                span,
                spans));
 
            line = spans[0].iStartLine;
            column = spans[0].iStartIndex;
        }
 
        // save error line/column (surface buffer location) as mapped line/column so that we can display
        // right location on closed Venus file.
        return GetDiagnosticData(
            documentId,
            _projectId,
            _workspace,
            GetErrorId(error),
            error.bstrText,
            GetDiagnosticSeverity(error),
            _language,
            new FileLinePositionSpan(error.bstrFileName,
                new LinePosition(line, column),
                new LinePosition(line, column)),
                AnalyzerInfoCache);
    }
 
    public int ReportError(string bstrErrorMessage, string bstrErrorId, [ComAliasName("VsShell.VSTASKPRIORITY")] VSTASKPRIORITY nPriority, int iLine, int iColumn, string bstrFileName)
    {
        ReportError2(bstrErrorMessage, bstrErrorId, nPriority, iLine, iColumn, iLine, iColumn, bstrFileName);
        return VSConstants.S_OK;
    }
 
    // TODO: Use PreserveSig instead of throwing these exceptions for common cases.
    public void ReportError2(string bstrErrorMessage, string bstrErrorId, [ComAliasName("VsShell.VSTASKPRIORITY")] VSTASKPRIORITY nPriority, int iStartLine, int iStartColumn, int iEndLine, int iEndColumn, string bstrFileName)
    {
        // first we check whether given error is something we can take care.
        if (!CanHandle(bstrErrorId))
        {
            // it is not, let project system take care.
            throw new NotImplementedException();
        }
 
        if ((iEndLine >= 0 && iEndColumn >= 0) &&
           ((iEndLine < iStartLine) ||
            (iEndLine == iStartLine && iEndColumn < iStartColumn)))
        {
            throw new ArgumentException(ServicesVSResources.End_position_must_be_start_position);
        }
 
        var severity = nPriority switch
        {
            VSTASKPRIORITY.TP_HIGH => DiagnosticSeverity.Error,
            VSTASKPRIORITY.TP_NORMAL => DiagnosticSeverity.Warning,
            VSTASKPRIORITY.TP_LOW => DiagnosticSeverity.Info,
            _ => throw new ArgumentException(ServicesVSResources.Not_a_valid_value, nameof(nPriority))
        };
 
        DocumentId documentId;
        if (bstrFileName == null || iStartLine < 0 || iStartColumn < 0)
        {
            documentId = null;
            iStartLine = iStartColumn = iEndLine = iEndColumn = 0;
        }
        else
        {
            documentId = TryGetDocumentId(bstrFileName);
        }
 
        if (iEndLine < 0)
            iEndLine = iStartLine;
        if (iEndColumn < 0)
            iEndColumn = iStartColumn;
 
        var diagnostic = GetDiagnosticData(
            documentId,
            _projectId,
            _workspace,
            bstrErrorId,
            bstrErrorMessage,
            severity,
            _language,
            new FileLinePositionSpan(
                bstrFileName,
                new LinePosition(iStartLine, iStartColumn),
                new LinePosition(iEndLine, iEndColumn)),
                AnalyzerInfoCache);
 
        DiagnosticProvider.AddNewErrors(_projectId, _projectHierarchyGuid, [diagnostic]);
    }
 
    public int ClearErrors()
    {
        DiagnosticProvider.ClearErrors(_projectId);
        return VSConstants.S_OK;
    }
 
    private static DiagnosticData GetDiagnosticData(
        DocumentId documentId,
        ProjectId projectId,
        Workspace workspace,
        string errorId,
        string message,
        DiagnosticSeverity severity,
        string language,
        FileLinePositionSpan unmappedSpan,
        DiagnosticAnalyzerInfoCache analyzerInfoCache)
    {
        string title, description, category, helpLink;
        DiagnosticSeverity defaultSeverity;
        bool isEnabledByDefault;
        ImmutableArray<string> customTags;
 
        if (analyzerInfoCache != null && analyzerInfoCache.TryGetDescriptorForDiagnosticId(errorId, out var descriptor))
        {
            title = descriptor.Title.ToString(CultureInfo.CurrentUICulture);
            description = descriptor.Description.ToString(CultureInfo.CurrentUICulture);
            category = descriptor.Category;
            defaultSeverity = descriptor.DefaultSeverity;
            isEnabledByDefault = descriptor.IsEnabledByDefault;
            customTags = descriptor.CustomTags.AsImmutableOrEmpty();
            helpLink = descriptor.HelpLinkUri;
        }
        else
        {
            title = message;
            description = message;
            category = WellKnownDiagnosticTags.Build;
            defaultSeverity = severity;
            isEnabledByDefault = true;
            customTags = IsCompilerDiagnostic(errorId) ? CompilerDiagnosticCustomTags : CustomTags;
            helpLink = null;
        }
 
        var diagnostic = new DiagnosticData(
            id: errorId,
            category: category,
            message: message,
            title: title,
            description: description,
            severity: severity,
            defaultSeverity: defaultSeverity,
            isEnabledByDefault: isEnabledByDefault,
            warningLevel: (severity == DiagnosticSeverity.Error) ? 0 : 1,
            customTags: customTags,
            properties: DiagnosticData.PropertiesForBuildDiagnostic,
            projectId: projectId,
            location: new DiagnosticDataLocation(
                unmappedSpan,
                documentId),
            language: language,
            helpLink: helpLink);
 
        if (workspace.CurrentSolution.GetDocument(documentId) is Document document &&
            document.SupportsSyntaxTree)
        {
            var tree = document.GetSyntaxTreeSynchronously(CancellationToken.None);
            var text = tree.GetText();
            var span = diagnostic.DataLocation.UnmappedFileSpan.GetClampedTextSpan(text);
            var location = diagnostic.DataLocation.WithSpan(span, tree);
            return diagnostic.WithLocations(location, additionalLocations: default);
        }
 
        return diagnostic;
    }
 
    private static bool IsCompilerDiagnostic(string errorId)
    {
        if (!string.IsNullOrEmpty(errorId) && errorId.Length > 2)
        {
            var prefix = errorId[..2];
            if (prefix.Equals("CS", StringComparison.OrdinalIgnoreCase) || prefix.Equals("BC", StringComparison.OrdinalIgnoreCase))
            {
                var suffix = errorId[2..];
                return int.TryParse(suffix, out _);
            }
        }
 
        return false;
    }
 
    private string GetErrorId(ExternalError error)
        => string.Format("{0}{1:0000}", _errorCodePrefix, error.iErrorID);
 
    private static DiagnosticSeverity GetDiagnosticSeverity(ExternalError error)
        => error.fError != 0 ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning;
}