File: TaskList\ProjectExternalErrorReporter.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_ibebqmp4_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.
 
using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
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(
    ProjectId projectId,
    Guid projectHierarchyGuid,
    string errorCodePrefix,
    string language,
    VisualStudioWorkspaceImpl workspace) : IVsReportExternalErrors, IVsLanguageServiceBuildErrorReporter2
{
    internal static readonly ImmutableArray<string> CustomTags = [WellKnownDiagnosticTags.Telemetry];
    internal static readonly ImmutableArray<string> CompilerDiagnosticCustomTags = [WellKnownDiagnosticTags.Compiler, WellKnownDiagnosticTags.Telemetry];
 
    private readonly ProjectId _projectId = projectId;
 
    /// <summary>
    /// Passed to the error reporting service to allow current project error list filters to work.
    /// </summary>
    private readonly Guid _projectHierarchyGuid = projectHierarchyGuid;
    private readonly string _errorCodePrefix = errorCodePrefix;
    private readonly string _language = language;
 
    private readonly VisualStudioWorkspaceImpl _workspace = workspace;
 
    private ExternalErrorDiagnosticUpdateSource DiagnosticProvider => _workspace.ExternalErrorDiagnosticUpdateSource;
 
    private static ImmutableArray<ExternalError> GetExternalErrors(IVsEnumExternalErrors pErrors)
    {
        using var _ = ArrayBuilder<ExternalError>.GetInstance(out var allErrors);
 
        var errors = new ExternalError[1];
        while (pErrors.Next(1, errors, out var fetched) == VSConstants.S_OK && fetched == 1)
        {
            var error = errors[0];
            allErrors.Add(error);
        }
 
        return allErrors.ToImmutableAndClear();
    }
 
    public int AddNewErrors(IVsEnumExternalErrors pErrors)
    {
        var solution = _workspace.CurrentSolution;
        var project = solution.GetProject(_projectId);
        if (project != null)
        {
            AddNewErrors(project, GetExternalErrors(pErrors));
        }
 
        return VSConstants.S_OK;
    }
 
    private void AddNewErrors(Project project, ImmutableArray<ExternalError> allErrors)
    {
        using var _ = ArrayBuilder<DiagnosticData>.GetInstance(out var allDiagnostics);
 
        var solution = project.Solution;
        var errorIds = allErrors.Select(e => e.iErrorID).Distinct().Select(GetErrorId).ToImmutableArray();
 
        foreach (var error in allErrors)
        {
            if (error.bstrFileName != null)
            {
                var diagnostic = TryCreateDocumentDiagnosticItem(project, error);
                if (diagnostic != null)
                {
                    allDiagnostics.Add(diagnostic);
                    continue;
                }
            }
 
            allDiagnostics.Add(GetDiagnosticData(
                project,
                documentId: null,
                GetErrorId(error),
                error.bstrText,
                GetDiagnosticSeverity(error),
                _language,
                new FileLinePositionSpan(project.FilePath ?? "", span: default)));
        }
 
        DiagnosticProvider.AddNewErrors(_projectId, _projectHierarchyGuid, allDiagnostics.ToImmutableAndClear());
    }
 
    public int ClearAllErrors()
        => ClearErrorsWorker();
 
    public int ClearErrors()
        => ClearErrorsWorker();
 
    private int ClearErrorsWorker()
    {
        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(
        Project project,
        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(
            project,
            documentId,
            GetErrorId(error),
            error.bstrText,
            GetDiagnosticSeverity(error),
            _language,
            new FileLinePositionSpan(error.bstrFileName,
                new LinePosition(line, column),
                new LinePosition(line, column)));
    }
 
    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)
    {
 
        // make sure we have error id, otherwise, we simple don't support
        // this error
        if (bstrErrorId == null)
        {
            // record NFW to see who violates contract.
            FatalError.ReportAndCatch(new Exception("errorId is null"));
            return;
        }
 
        if (!bstrErrorId.StartsWith(_errorCodePrefix))
            return;
 
        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 (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 solution = _workspace.CurrentSolution;
        var project = solution.GetProject(_projectId);
        if (project is null)
            return;
 
        var diagnostic = GetDiagnosticData(
            project,
            documentId,
            bstrErrorId,
            bstrErrorMessage,
            severity,
            _language,
            new FileLinePositionSpan(
                bstrFileName,
                new LinePosition(iStartLine, iStartColumn),
                new LinePosition(iEndLine, iEndColumn)));
 
        DiagnosticProvider.AddNewErrors(_projectId, _projectHierarchyGuid, [diagnostic]);
    }
 
    private static DiagnosticData GetDiagnosticData(
        Project project,
        DocumentId? documentId,
        string errorId,
        string message,
        DiagnosticSeverity severity,
        string language,
        FileLinePositionSpan unmappedSpan)
    {
        return new DiagnosticData(
            id: errorId,
            category: WellKnownDiagnosticTags.Build,
            message: message,
            title: message,
            description: message,
            severity: severity,
            defaultSeverity: severity,
            isEnabledByDefault: true,
            warningLevel: severity == DiagnosticSeverity.Error ? 0 : 1,
            customTags: IsCompilerDiagnostic(errorId) ? CompilerDiagnosticCustomTags : CustomTags,
            properties: DiagnosticData.PropertiesForBuildDiagnostic,
            projectId: project.Id,
            location: new DiagnosticDataLocation(
                unmappedSpan,
                documentId),
            language: language);
    }
 
    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)
        => GetErrorId(error.iErrorID);
 
    private string GetErrorId(int errorId)
        => string.Format("{0}{1:0000}", _errorCodePrefix, errorId);
 
    private static DiagnosticSeverity GetDiagnosticSeverity(ExternalError error)
        => error.fError != 0 ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning;
}