File: Utilities\Documentation\XmlDocumentationProvider.cs
Web Access
Project: src\roslyn\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.

#nullable disable

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Xml;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis;

/// <summary>
/// A class used to provide XML documentation to the compiler for members from metadata from an XML document source.
/// </summary>
public abstract class XmlDocumentationProvider : DocumentationProvider
{
    private readonly SemaphoreSlim _gate = new(initialCount: 1);
    private Dictionary<string, string> _docComments;

    /// <summary>
    /// Gets the source stream for the XML document.
    /// </summary>
    /// <param name="cancellationToken">The cancellation token.</param>
    protected abstract Stream GetSourceStream(CancellationToken cancellationToken);

    /// <summary>
    /// Creates an <see cref="XmlDocumentationProvider"/> from bytes representing XML documentation data.
    /// </summary>
    /// <param name="xmlDocCommentBytes">The XML document bytes.</param>
    /// <returns>An <see cref="XmlDocumentationProvider"/>.</returns>
    public static XmlDocumentationProvider CreateFromBytes(byte[] xmlDocCommentBytes)
        => new ContentBasedXmlDocumentationProvider(xmlDocCommentBytes);

    private static XmlDocumentationProvider DefaultXmlDocumentationProvider { get; } = new NullXmlDocumentationProvider();

    /// <summary>
    /// Creates an <see cref="XmlDocumentationProvider"/> from an XML documentation file.
    /// </summary>
    /// <param name="xmlDocCommentFilePath">The path to the XML file.</param>
    /// <returns>An <see cref="XmlDocumentationProvider"/>.</returns>
    public static XmlDocumentationProvider CreateFromFile(string xmlDocCommentFilePath)
    {
        if (!File.Exists(xmlDocCommentFilePath))
        {
            return DefaultXmlDocumentationProvider;
        }

        return new FileBasedXmlDocumentationProvider(xmlDocCommentFilePath);
    }

    protected override string GetDocumentationForSymbol(string documentationMemberID, CultureInfo preferredCulture, CancellationToken cancellationToken = default)
    {
        if (_docComments == null)
        {
            using (_gate.DisposableWait(cancellationToken))
            {
                try
                {
                    using (var stream = GetSourceStream(cancellationToken))
                    using (var xmlReader = XmlReader.Create(stream, s_xmlSettings))
                    {
                        var comments = new Dictionary<string, string>();
                        while (xmlReader.Read())
                        {
                            if (xmlReader is { NodeType: XmlNodeType.Element, Name: "member" })
                            {
                                var name = xmlReader.GetAttribute("name");
                                if (name != null)
                                {
                                    // Read the entire <member> element as a string
                                    var memberXml = xmlReader.ReadOuterXml();
                                    comments[name] = memberXml;
                                }
                            }
                        }

                        _docComments = comments;
                    }
                }
                catch (Exception)
                {
                    _docComments = [];
                }
            }
        }

        return _docComments.TryGetValue(documentationMemberID, out var docComment) ? docComment : "";
    }

    private static readonly XmlReaderSettings s_xmlSettings = new()
    {
        DtdProcessing = DtdProcessing.Prohibit,
    };

    private sealed class ContentBasedXmlDocumentationProvider : XmlDocumentationProvider
    {
        private readonly byte[] _xmlDocCommentBytes;

        public ContentBasedXmlDocumentationProvider(byte[] xmlDocCommentBytes)
        {
            Contract.ThrowIfNull(xmlDocCommentBytes);

            _xmlDocCommentBytes = xmlDocCommentBytes;
        }

        protected override Stream GetSourceStream(CancellationToken cancellationToken)
            => SerializableBytes.CreateReadableStream(_xmlDocCommentBytes);

        public override bool Equals(object obj)
        {
            return obj is ContentBasedXmlDocumentationProvider other && EqualsHelper(other);
        }

        private bool EqualsHelper(ContentBasedXmlDocumentationProvider other)
        {
            // Check for reference equality first
            if (this == other || _xmlDocCommentBytes == other._xmlDocCommentBytes)
            {
                return true;
            }

            // Compare byte sequences
            if (_xmlDocCommentBytes.Length != other._xmlDocCommentBytes.Length)
            {
                return false;
            }

            for (var i = 0; i < _xmlDocCommentBytes.Length; i++)
            {
                if (_xmlDocCommentBytes[i] != other._xmlDocCommentBytes[i])
                {
                    return false;
                }
            }

            return true;
        }

        public override int GetHashCode()
            => Hash.CombineValues(_xmlDocCommentBytes);
    }

    private sealed class FileBasedXmlDocumentationProvider : XmlDocumentationProvider
    {
        private readonly string _filePath;

        public FileBasedXmlDocumentationProvider(string filePath)
        {
            Contract.ThrowIfNull(filePath);
            Debug.Assert(PathUtilities.IsAbsolute(filePath));

            _filePath = filePath;
        }

        protected override Stream GetSourceStream(CancellationToken cancellationToken)
            => new FileStream(_filePath, FileMode.Open, FileAccess.Read);

        public override bool Equals(object obj)
        {
            return obj is FileBasedXmlDocumentationProvider other && _filePath == other._filePath;
        }

        public override int GetHashCode()
            => _filePath.GetHashCode();
    }

    /// <summary>
    /// A trivial XmlDocumentationProvider which never returns documentation.
    /// </summary>
    private sealed class NullXmlDocumentationProvider : XmlDocumentationProvider
    {
        protected override string GetDocumentationForSymbol(string documentationMemberID, CultureInfo preferredCulture, CancellationToken cancellationToken = default)
            => "";

        protected override Stream GetSourceStream(CancellationToken cancellationToken)
            => new MemoryStream();

        public override bool Equals(object obj)
        {
            // Only one instance is expected to exist, so reference equality is fine.
            return ReferenceEquals(this, obj);
        }

        public override int GetHashCode()
            => 0;
    }
}