File: Diagnostics\DiagnosticDataLocation.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.IO;
using System.Runtime.Serialization;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Diagnostics;
 
[DataContract]
internal sealed class DiagnosticDataLocation
{
    /// <summary>
    /// Path to where the diagnostic was originally reported.  May be a path to a document in a project, or the
    /// project file itself. This should only be used by clients that truly need to know the original location a
    /// diagnostic was reported at, ignoring things like <c>#line</c> directives or other systems that would map the
    /// diagnostic to a different file or location.  Most clients should instead use <see cref="MappedFileSpan"/>,
    /// which contains the final location (file and span) that the diagnostic should be considered at.
    /// </summary>
    [DataMember(Order = 0)]
    public readonly FileLinePositionSpan UnmappedFileSpan;
 
    /// <summary>
    /// Document the diagnostic is associated with.  May be null if this is a project diagnostic.
    /// </summary>
    [DataMember(Order = 1)]
    public readonly DocumentId? DocumentId;
 
    /// <summary>
    /// Path and span where the diagnostic has been finally mapped to.  If no mapping happened, this will be equal
    /// to <see cref="UnmappedFileSpan"/>.  The <see cref="FileLinePositionSpan.Path"/> of this value will be the
    /// fully normalized file path where the diagnostic is located at.
    /// </summary>
    [DataMember(Order = 2)]
    public readonly FileLinePositionSpan MappedFileSpan;
 
    public DiagnosticDataLocation(
        FileLinePositionSpan unmappedFileSpan,
        DocumentId? documentId,
        FileLinePositionSpan mappedFileSpan)
        : this(unmappedFileSpan, documentId, mappedFileSpan, forceMappedPath: false)
    {
        // This constructor is used for deserialization, so the arguments must have the same exact order and type
        // as the fields with the [DataMember] attribute.
    }
 
    public DiagnosticDataLocation(
        FileLinePositionSpan unmappedFileSpan,
        DocumentId? documentId = null)
        : this(unmappedFileSpan, documentId, null, forceMappedPath: false)
    {
    }
 
    private DiagnosticDataLocation(
        FileLinePositionSpan unmappedFileSpan,
        DocumentId? documentId,
        FileLinePositionSpan? mappedFileSpan,
        bool forceMappedPath)
    {
        Contract.ThrowIfNull(unmappedFileSpan.Path);
 
        UnmappedFileSpan = unmappedFileSpan;
        DocumentId = documentId;
 
        // If we were passed in a mapped span use it with the original span to determine the true final mapped
        // location. If forceMappedSpan is true, then this is a test which is explicitly making a mapped span that
        // it wants us to not mess with.  In that case, just hold onto that value directly.
        if (mappedFileSpan is { } mappedSpan &&
            (mappedSpan.HasMappedPath || forceMappedPath))
        {
            MappedFileSpan = new FileLinePositionSpan(GetNormalizedFilePath(unmappedFileSpan.Path, mappedSpan.Path), mappedSpan.Span);
        }
        else
        {
            MappedFileSpan = mappedFileSpan ?? unmappedFileSpan;
        }
 
        return;
 
        static string GetNormalizedFilePath(string original, string mapped)
        {
            if (RoslynString.IsNullOrEmpty(mapped))
                return original;
 
            var combined = PathUtilities.CombinePaths(PathUtilities.GetDirectoryName(original), mapped);
            try
            {
                return Path.GetFullPath(combined);
            }
            catch
            {
                return combined;
            }
        }
    }
 
    /// <summary>
    /// Return a new location with the same <see cref="DocumentId"/> as this, but with updated <see
    /// cref="UnmappedFileSpan"/> and <see cref="MappedFileSpan"/> corresponding to the respection locations of
    /// <paramref name="newSourceSpan"/> within <paramref name="tree"/>.
    /// </summary>
    public DiagnosticDataLocation WithSpan(TextSpan newSourceSpan, SyntaxTree tree)
        => new(
            tree.GetLineSpan(newSourceSpan),
            DocumentId,
            tree.GetMappedLineSpan(newSourceSpan));
 
    public static class TestAccessor
    {
        public static DiagnosticDataLocation Create(
            FileLinePositionSpan originalFileSpan,
            DocumentId? documentId,
            FileLinePositionSpan mappedFileSpan,
            bool forceMappedPath)
        {
            return new DiagnosticDataLocation(originalFileSpan, documentId, mappedFileSpan, forceMappedPath);
        }
    }
}