File: Compiler\DocumentationCommentCompiler.cs
Web Access
Project: src\src\Compilers\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.csproj (Microsoft.CodeAnalysis.CSharp)
// 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.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Resources;
using System.Text;
using System.Threading;
using System.Xml;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp
{
    /// <summary>
    /// Traverses the symbol table processing XML documentation comments and optionally writing them to
    /// a provided stream.
    /// </summary>
    internal partial class DocumentationCommentCompiler : CSharpSymbolVisitor
    {
        private readonly string _assemblyName;
        private readonly CSharpCompilation _compilation;
        private readonly TextWriter _writer; //never write directly - always use a helper
        private readonly SyntaxTree _filterTree; //if not null, limit analysis to types residing in this tree
        private readonly TextSpan? _filterSpanWithinTree; //if filterTree and filterSpanWithinTree is not null, limit analysis to types residing within this span in the filterTree.
        private readonly bool _processIncludes;
        private readonly bool _isForSingleSymbol; //minor differences in behavior between batch case and API case.
        private readonly BindingDiagnosticBag _diagnostics;
        private readonly CancellationToken _cancellationToken;
 
        private SyntaxNodeLocationComparer _lazyComparer;
        private DocumentationCommentIncludeCache _includedFileCache;
 
        private int _indentDepth;
 
        private Stack<TemporaryStringBuilder> _temporaryStringBuilders;
 
        private DocumentationCommentCompiler(
            string assemblyName,
            CSharpCompilation compilation,
            TextWriter writer,
            SyntaxTree filterTree,
            TextSpan? filterSpanWithinTree,
            bool processIncludes,
            bool isForSingleSymbol,
            BindingDiagnosticBag diagnostics,
            CancellationToken cancellationToken)
        {
            _assemblyName = assemblyName;
 
            _compilation = compilation;
            _writer = writer;
            _filterTree = filterTree;
            _filterSpanWithinTree = filterSpanWithinTree;
            _processIncludes = processIncludes;
            _isForSingleSymbol = isForSingleSymbol;
            _diagnostics = diagnostics;
            _cancellationToken = cancellationToken;
        }
 
        /// <summary>
        /// Traverses the symbol table processing XML documentation comments and optionally writing them to
        /// a provided stream.
        /// </summary>
        /// <param name="compilation">Compilation that owns the symbol table.</param>
        /// <param name="assemblyName">Assembly name override, if specified. Otherwise the <see cref="ISymbol.Name"/> of the source assembly is used.</param>
        /// <param name="xmlDocStream">Stream to which XML will be written, if specified.</param>
        /// <param name="diagnostics">Will be supplemented with documentation comment diagnostics.</param>
        /// <param name="cancellationToken">To stop traversing the symbol table early.</param>
        /// <param name="filterTree">Only report diagnostics from this syntax tree, if non-null.</param>
        /// <param name="filterSpanWithinTree">If <paramref name="filterTree"/> and filterSpanWithinTree is non-null, report diagnostics within this span in the <paramref name="filterTree"/>.</param>
#nullable enable
        public static void WriteDocumentationCommentXml(CSharpCompilation compilation, string? assemblyName, Stream? xmlDocStream, BindingDiagnosticBag diagnostics, CancellationToken cancellationToken, SyntaxTree? filterTree = null, TextSpan? filterSpanWithinTree = null)
#nullable disable
        {
            StreamWriter writer = null;
            if (xmlDocStream != null && xmlDocStream.CanWrite)
            {
                writer = new StreamWriter(
                    stream: xmlDocStream,
                    encoding: new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: false),
                    bufferSize: 0x400, // Default.
                    leaveOpen: true); // Don't close caller's stream.
            }
 
            try
            {
                using (writer)
                {
                    var compiler = new DocumentationCommentCompiler(assemblyName ?? compilation.SourceAssembly.Name, compilation, writer, filterTree, filterSpanWithinTree,
                        processIncludes: true, isForSingleSymbol: false, diagnostics: diagnostics, cancellationToken: cancellationToken);
                    compiler.Visit(compilation.SourceAssembly.GlobalNamespace);
                    Debug.Assert(compiler._indentDepth == 0);
                    writer?.Flush();
                }
            }
            catch (Exception e)
            {
                diagnostics.Add(ErrorCode.ERR_DocFileGen, Location.None, e.Message);
            }
 
            if (diagnostics.DiagnosticBag is DiagnosticBag diagnosticBag)
            {
                if (filterTree != null)
                {
                    // Will respect the DocumentationMode.
                    UnprocessedDocumentationCommentFinder.ReportUnprocessed(filterTree, filterSpanWithinTree, diagnosticBag, cancellationToken);
                }
                else
                {
                    foreach (SyntaxTree tree in compilation.SyntaxTrees)
                    {
                        // Will respect the DocumentationMode.
                        UnprocessedDocumentationCommentFinder.ReportUnprocessed(tree, null, diagnosticBag, cancellationToken);
                    }
                }
            }
        }
 
        /// <summary>
        /// Gets the XML that would be written to the documentation comment file for this assembly.
        /// </summary>
        /// <param name="symbol">The symbol for which to retrieve documentation comments.</param>
        /// <param name="processIncludes">True to treat includes as semantically meaningful (pull in contents from other files and bind crefs, etc).</param>
        /// <param name="cancellationToken">To stop traversing the symbol table early.</param>
        internal static string GetDocumentationCommentXml(Symbol symbol, bool processIncludes, CancellationToken cancellationToken)
        {
            Debug.Assert(
                symbol.Kind == SymbolKind.Event ||
                symbol.Kind == SymbolKind.Field ||
                symbol.Kind == SymbolKind.Method ||
                symbol.Kind == SymbolKind.NamedType ||
                symbol.Kind == SymbolKind.Property);
 
            CSharpCompilation compilation = symbol.DeclaringCompilation;
            Debug.Assert(compilation != null);
 
            PooledStringBuilder pooled = PooledStringBuilder.GetInstance();
            StringWriter writer = new StringWriter(pooled.Builder);
 
            var compiler = new DocumentationCommentCompiler(
                assemblyName: null,
                compilation: compilation,
                writer: writer,
                filterTree: null,
                filterSpanWithinTree: null,
                processIncludes: processIncludes,
                isForSingleSymbol: true,
                diagnostics: BindingDiagnosticBag.Discarded,
                cancellationToken: cancellationToken);
            compiler.Visit(symbol);
            Debug.Assert(compiler._indentDepth == 0);
 
            writer.Dispose();
            return pooled.ToStringAndFree();
        }
 
        /// <summary>
        /// Write header, descend into members, and write footer.
        /// </summary>
        public override void VisitNamespace(NamespaceSymbol symbol)
        {
            _cancellationToken.ThrowIfCancellationRequested();
 
            if (symbol.IsGlobalNamespace)
            {
                Debug.Assert(_assemblyName != null);
 
                WriteLine("<?xml version=\"1.0\"?>");
                WriteLine("<doc>");
                Indent();
 
                if (!_compilation.Options.OutputKind.IsNetModule())
                {
                    WriteLine("<assembly>");
                    Indent();
                    WriteLine("<name>{0}</name>", _assemblyName);
                    Unindent();
                    WriteLine("</assembly>");
                }
 
                WriteLine("<members>");
                Indent();
            }
 
            Debug.Assert(!_isForSingleSymbol);
            foreach (var s in symbol.GetMembers())
            {
                _cancellationToken.ThrowIfCancellationRequested();
                s.Accept(this);
            }
 
            if (symbol.IsGlobalNamespace)
            {
                Unindent();
                WriteLine("</members>");
                Unindent();
                WriteLine("</doc>");
            }
        }
 
        /// <summary>
        /// Write own documentation comments and then descend into members.
        /// </summary>
        public override void VisitNamedType(NamedTypeSymbol symbol)
        {
            _cancellationToken.ThrowIfCancellationRequested();
 
            if (_filterTree != null && !symbol.IsDefinedInSourceTree(_filterTree, _filterSpanWithinTree))
            {
                return;
            }
 
            DefaultVisit(symbol);
 
            if (!_isForSingleSymbol)
            {
                foreach (Symbol member in symbol.GetMembers())
                {
                    _cancellationToken.ThrowIfCancellationRequested();
                    member.Accept(this);
                }
            }
        }
 
#nullable enable
 
        /// <summary>
        /// Compile documentation comments on the symbol and write them to the stream if one is provided.
        /// </summary>
        public override void DefaultVisit(Symbol symbol)
        {
            _cancellationToken.ThrowIfCancellationRequested();
 
            if (ShouldSkip(symbol))
            {
                return;
            }
 
            if (_filterTree != null && !symbol.IsDefinedInSourceTree(_filterTree, _filterSpanWithinTree))
            {
                return;
            }
 
            bool shouldSkipPartialDefinitionComments = false;
            if (symbol.IsPartialDefinition())
            {
                Symbol? implementationPart = symbol switch
                {
                    MethodSymbol method => method.PartialImplementationPart,
                    SourcePropertySymbol property => property.PartialImplementationPart,
                    _ => null
                };
 
                if (implementationPart is not null)
                {
                    Visit(implementationPart);
 
                    foreach (var trivia in implementationPart.GetNonNullSyntaxNode().GetLeadingTrivia())
                    {
                        if (trivia.Kind() is SyntaxKind.SingleLineDocumentationCommentTrivia or SyntaxKind.MultiLineDocumentationCommentTrivia)
                        {
                            // If the partial method implementation has doc comments,
                            // we will not emit any doc comments found on the definition,
                            // regardless of whether the partial implementation doc comments are valid.
                            shouldSkipPartialDefinitionComments = true;
                            break;
                        }
                    }
                }
                else
                {
                    // The partial method has no implementation. Since it won't be present in the
                    // output assembly, it shouldn't be included in the documentation file.
                    shouldSkipPartialDefinitionComments = !_isForSingleSymbol;
                }
            }
 
            // synthesized record property: emit the matching param doc on containing type as the summary doc of the property.
            var symbolForDocComments = symbol is SynthesizedRecordPropertySymbol ? symbol.ContainingType : symbol;
            if (!TryGetDocumentationCommentNodes(symbolForDocComments, out var maxDocumentationMode, out var docCommentNodes))
            {
                // If the XML in any of the doc comments is invalid, skip all further processing (for this symbol) and 
                // just write a comment saying that info was lost for this symbol.
                string message = ErrorFacts.GetMessage(MessageID.IDS_XMLIGNORED, CultureInfo.CurrentUICulture);
                WriteLine(string.Format(CultureInfo.CurrentUICulture, message, symbol.GetDocumentationCommentId()));
                return;
            }
 
            // If there are no doc comments, then no further work is required (other than to report a diagnostic if one is required).
            if (docCommentNodes.IsEmpty)
            {
                if (maxDocumentationMode >= DocumentationMode.Diagnose
                    && RequiresDocumentationComment(symbol)
                    // We never give a missing doc comment warning on a partial method
                    // implementation, and we skip the missing doc comment warning on a partial
                    // definition whose documentation we were not going to output anyway.
                    && !symbol.IsPartialImplementation()
                    && !shouldSkipPartialDefinitionComments)
                {
                    // Report the error at a location in the tree that was parsing doc comments.
                    Location location = GetLocationInTreeReportingDocumentationCommentDiagnostics(symbol);
                    if (location != null)
                    {
                        _diagnostics.Add(ErrorCode.WRN_MissingXMLComment, location, symbol);
                    }
                }
                return;
            }
 
            _cancellationToken.ThrowIfCancellationRequested();
 
            string withUnprocessedIncludes;
            bool haveParseError;
            HashSet<TypeParameterSymbol> documentedTypeParameters;
            HashSet<ParameterSymbol> documentedParameters;
            ImmutableArray<CSharpSyntaxNode> includeElementNodes;
            if (!TryProcessDocumentationCommentTriviaNodes(
                    symbol,
                    shouldSkipPartialDefinitionComments,
                    docCommentNodes,
                    out withUnprocessedIncludes,
                    out haveParseError,
                    out documentedTypeParameters,
                    out documentedParameters,
                    out includeElementNodes))
            {
                return;
            }
 
            if (haveParseError)
            {
                // If the XML in any of the doc comments is invalid, skip all further processing (for this symbol) and 
                // just write a comment saying that info was lost for this symbol.
                string message = ErrorFacts.GetMessage(MessageID.IDS_XMLIGNORED, CultureInfo.CurrentUICulture);
                WriteLine(string.Format(CultureInfo.CurrentUICulture, message, symbol.GetDocumentationCommentId()));
                return;
            }
 
            // If there are no include elements, then there's nothing to expand.
            if (!includeElementNodes.IsDefaultOrEmpty)
            {
                _cancellationToken.ThrowIfCancellationRequested();
 
                // NOTE: we are expanding include elements AFTER formatting the comment, since the included text is pure
                // XML, not XML mixed with documentation comment trivia (e.g. ///).  If we expanded them before formatting,
                // the formatting engine would have trouble determining what prefix to remove from each line.
                TextWriter? expanderWriter = shouldSkipPartialDefinitionComments ? null : _writer; // Don't actually write partial method definition parts.
                IncludeElementExpander.ProcessIncludes(withUnprocessedIncludes, symbol, includeElementNodes,
                    _compilation, ref documentedParameters, ref documentedTypeParameters, ref _includedFileCache, expanderWriter, _diagnostics, _cancellationToken);
            }
            else if (_writer != null && !shouldSkipPartialDefinitionComments)
            {
                // CONSIDER: The output would look a little different if we ran the XDocument through an XmlWriter.  In particular, 
                // formatting inside tags (e.g. <__tag___attr__=__"value"__>) would be normalized.  Whitespace in elements would
                // (or should) not be affected.  If we decide that this difference matters, we can run the XDocument through an XmlWriter.
                // Otherwise, just writing out the string saves a bunch of processing and does a better job of preserving whitespace.
                Write(withUnprocessedIncludes);
            }
 
            bool reportParameterOrTypeParameterDiagnostics = GetLocationInTreeReportingDocumentationCommentDiagnostics(symbol) != null;
            if (reportParameterOrTypeParameterDiagnostics)
            {
                _cancellationToken.ThrowIfCancellationRequested();
 
                if (documentedParameters != null)
                {
                    foreach (ParameterSymbol parameter in GetParameters(symbol))
                    {
                        if (!documentedParameters.Contains(parameter))
                        {
                            Location location = parameter.GetFirstLocation();
                            Debug.Assert(location.SourceTree!.ReportDocumentationCommentDiagnostics()); //Should be the same tree as for the symbol.
 
                            // NOTE: parameter name, since the parameter would be displayed as just its type.
                            _diagnostics.Add(ErrorCode.WRN_MissingParamTag, location, parameter.Name, symbol);
                        }
                    }
                }
 
                if (documentedTypeParameters != null)
                {
                    foreach (TypeParameterSymbol typeParameter in GetTypeParameters(symbol))
                    {
                        if (!documentedTypeParameters.Contains(typeParameter))
                        {
                            Location location = typeParameter.GetFirstLocation();
                            Debug.Assert(location.SourceTree!.ReportDocumentationCommentDiagnostics()); //Should be the same tree as for the symbol.
 
                            _diagnostics.Add(ErrorCode.WRN_MissingTypeParamTag, location, typeParameter, symbol);
                        }
                    }
                }
            }
        }
 
        private static bool ShouldSkip(Symbol symbol)
        {
            return symbol.IsImplicitlyDeclared ||
                symbol.IsAccessor() ||
                symbol is SynthesizedSimpleProgramEntryPointSymbol;
        }
 
        private bool TryProcessRecordPropertyDocumentation(
            SynthesizedRecordPropertySymbol recordPropertySymbol,
            ImmutableArray<DocumentationCommentTriviaSyntax> docCommentNodes,
            [NotNullWhen(true)] out string? withUnprocessedIncludes,
            out ImmutableArray<CSharpSyntaxNode> includeElementNodes)
        {
            _cancellationToken.ThrowIfCancellationRequested();
 
            if (getMatchingParamTags(recordPropertySymbol.Name, docCommentNodes) is not { } paramTags)
            {
                withUnprocessedIncludes = null;
                includeElementNodes = default;
                return false;
            }
 
            Debug.Assert(paramTags.Count > 0);
 
            BeginTemporaryString();
            WriteLine("<member name=\"{0}\">", recordPropertySymbol.GetDocumentationCommentId());
            Indent();
            var substitutedTextBuilder = PooledStringBuilder.GetInstance();
            var includeElementNodesBuilder = _processIncludes ? ArrayBuilder<CSharpSyntaxNode>.GetInstance() : null;
            DocumentationCommentWalker.GetSubstitutedText(_compilation, recordPropertySymbol, paramTags, includeElementNodesBuilder, substitutedTextBuilder.Builder);
            string substitutedText = substitutedTextBuilder.ToStringAndFree();
            string formattedXml = FormatComment(substitutedText);
            Write(formattedXml);
            Unindent();
            WriteLine("</member>");
 
            withUnprocessedIncludes = GetAndEndTemporaryString();
            includeElementNodes = includeElementNodesBuilder?.ToImmutableAndFree() ?? default;
 
            paramTags.Free();
            return true;
 
            static ArrayBuilder<XmlElementSyntax>? getMatchingParamTags(string propertyName, ImmutableArray<DocumentationCommentTriviaSyntax> docCommentNodes)
            {
                ArrayBuilder<XmlElementSyntax>? result = null;
                foreach (var trivia in docCommentNodes)
                {
                    foreach (var contentItem in trivia.Content)
                    {
                        if (contentItem is XmlElementSyntax elementSyntax)
                        {
                            foreach (var attribute in elementSyntax.StartTag.Attributes)
                            {
                                if (attribute is XmlNameAttributeSyntax nameAttribute
                                    && nameAttribute.GetElementKind() == XmlNameAttributeElementKind.Parameter
                                    && string.Equals(nameAttribute.Identifier.Identifier.ValueText, propertyName, StringComparison.Ordinal))
                                {
                                    result ??= ArrayBuilder<XmlElementSyntax>.GetInstance();
                                    result.Add(elementSyntax);
                                    break;
                                }
                            }
                        }
                    }
                }
                return result;
            }
        }
 
#nullable disable
 
        /// <summary>
        /// Loop over the DocumentationCommentTriviaSyntaxes.  Gather
        ///   1) concatenated XML, as a string;
        ///   2) whether or not the XML is valid;
        ///   3) set of type parameters covered by &lt;typeparam&gt; elements;
        ///   4) set of parameters covered by &lt;param&gt; elements;
        ///   5) list of &lt;include&gt; elements, as SyntaxNodes.
        /// </summary>
        /// <returns>True, if at least one documentation comment was processed; false, otherwise.</returns>
        /// <remarks>This was factored out for clarity, not because it's reusable.</remarks>
        private bool TryProcessDocumentationCommentTriviaNodes(
            Symbol symbol,
            bool shouldSkipPartialDefinitionComments,
            ImmutableArray<DocumentationCommentTriviaSyntax> docCommentNodes,
            out string withUnprocessedIncludes,
            out bool haveParseError,
            out HashSet<TypeParameterSymbol> documentedTypeParameters,
            out HashSet<ParameterSymbol> documentedParameters,
            out ImmutableArray<CSharpSyntaxNode> includeElementNodes)
        {
            Debug.Assert(!docCommentNodes.IsDefaultOrEmpty);
 
            bool processedDocComment = false; // Even if there are DocumentationCommentTriviaSyntax, we may not need to process any of them.
 
            ArrayBuilder<CSharpSyntaxNode> includeElementNodesBuilder = null;
 
            documentedParameters = null;
            documentedTypeParameters = null;
 
            // Saw an XmlException while parsing one of the DocumentationCommentTriviaSyntax nodes.
            haveParseError = false;
 
            if (symbol is SynthesizedRecordPropertySymbol recordProperty)
            {
                return TryProcessRecordPropertyDocumentation(recordProperty, docCommentNodes, out withUnprocessedIncludes, out includeElementNodes);
            }
 
            // We're doing substitution and formatting per-trivia, rather than per-symbol,
            // because a single symbol can have both single-line and multi-line style
            // doc comments.
            foreach (DocumentationCommentTriviaSyntax trivia in docCommentNodes)
            {
                _cancellationToken.ThrowIfCancellationRequested();
 
                bool reportDiagnosticsForCurrentTrivia = trivia.SyntaxTree.ReportDocumentationCommentDiagnostics();
 
                if (!processedDocComment)
                {
                    // Since we have to throw away all the parts if any part is bad, we need to write to an intermediate temp.
                    BeginTemporaryString();
 
                    if (_processIncludes)
                    {
                        includeElementNodesBuilder = ArrayBuilder<CSharpSyntaxNode>.GetInstance();
                    }
 
                    // We DO want to write out partial method definition parts if we're processing includes
                    // because we need to have XML to process.
                    if (!shouldSkipPartialDefinitionComments || _processIncludes)
                    {
                        WriteLine("<member name=\"{0}\">", symbol.GetDocumentationCommentId());
                        Indent();
                    }
 
                    processedDocComment = true;
                }
 
                // Will respect the DocumentationMode.
                string substitutedText = DocumentationCommentWalker.GetSubstitutedText(_compilation, _diagnostics, symbol, trivia,
                    includeElementNodesBuilder, ref documentedParameters, ref documentedTypeParameters);
 
                string formattedXml = FormatComment(substitutedText);
 
                // It would be preferable to just parse the concatenated XML at the end of the loop (we wouldn't have
                // to wrap it in a root element and we wouldn't have to reparse in the IncludeElementExpander), but
                // then we wouldn't know whether or where to report a diagnostic.
                XmlException e = XmlDocumentationCommentTextReader.ParseAndGetException(formattedXml);
                if (e != null)
                {
                    haveParseError = true;
                    if (reportDiagnosticsForCurrentTrivia)
                    {
                        Location location = new SourceLocation(trivia.SyntaxTree, new TextSpan(trivia.SpanStart, 0));
                        _diagnostics.Add(ErrorCode.WRN_XMLParseError, location, GetDescription(e));
                    }
                }
 
                // For partial methods, all parts are validated, but only the implementation part is written to the XML stream.
                if (!shouldSkipPartialDefinitionComments || _processIncludes)
                {
                    // This string already has indentation and line breaks, so don't call WriteLine - just write the text directly.
                    Write(formattedXml);
                }
            }
 
            if (!processedDocComment)
            {
                withUnprocessedIncludes = null;
                includeElementNodes = default(ImmutableArray<CSharpSyntaxNode>);
 
                return false;
            }
 
            if (!shouldSkipPartialDefinitionComments || _processIncludes)
            {
                Unindent();
                WriteLine("</member>");
            }
 
            // Free the temp.
            withUnprocessedIncludes = GetAndEndTemporaryString();
 
            // Free the builder, even if there was an error.
            includeElementNodes = _processIncludes ? includeElementNodesBuilder.ToImmutableAndFree() : default(ImmutableArray<CSharpSyntaxNode>);
 
            return true;
        }
 
        private static Location GetLocationInTreeReportingDocumentationCommentDiagnostics(Symbol symbol)
        {
            foreach (Location location in symbol.Locations)
            {
                if (location.SourceTree.ReportDocumentationCommentDiagnostics())
                {
                    return location;
                }
            }
            return null;
        }
 
        /// <remarks>
        /// Similar to SymbolExtensions.GetParameters, but returns empty for unsupported symbols
        /// and handles delegates.
        /// </remarks>
        private static ImmutableArray<ParameterSymbol> GetParameters(Symbol symbol)
        {
            switch (symbol.Kind)
            {
                case SymbolKind.NamedType:
                    MethodSymbol delegateInvoke = ((NamedTypeSymbol)symbol).DelegateInvokeMethod;
                    if ((object)delegateInvoke != null)
                    {
                        return delegateInvoke.Parameters;
                    }
                    break;
                case SymbolKind.Method:
                case SymbolKind.Property:
                case SymbolKind.Event:
                    return symbol.GetParameters();
            }
 
            return ImmutableArray<ParameterSymbol>.Empty;
        }
 
        /// <remarks>
        /// Similar to SymbolExtensions.GetMemberTypeParameters, but returns empty for unsupported symbols.
        /// </remarks>
        private static ImmutableArray<TypeParameterSymbol> GetTypeParameters(Symbol symbol)
        {
            switch (symbol.Kind)
            {
                case SymbolKind.Method:
                case SymbolKind.NamedType:
                case SymbolKind.ErrorType:
                    return symbol.GetMemberTypeParameters();
            }
 
            return ImmutableArray<TypeParameterSymbol>.Empty;
        }
 
        /// <summary>
        /// A symbol requires a documentation comment if it was explicitly declared and
        /// will be visible outside the current assembly (ignoring InternalsVisibleTo).
        /// Exception: accessors do not require doc comments.
        /// </summary>
        private static bool RequiresDocumentationComment(Symbol symbol)
        {
            Debug.Assert((object)symbol != null);
 
            if (ShouldSkip(symbol))
            {
                return false;
            }
 
            while ((object)symbol != null)
            {
                switch (symbol.DeclaredAccessibility)
                {
                    case Accessibility.Public:
                    case Accessibility.Protected:
                    case Accessibility.ProtectedOrInternal:
                        symbol = symbol.ContainingType;
                        break;
                    default:
                        return false;
                }
            }
 
            return true;
        }
 
        /// <summary>
        /// Get all of the DocumentationCommentTriviaSyntax associated with any declaring syntax of the
        /// given symbol (except for partial methods, which only consider the part with the body).
        /// </summary>
        /// <returns>True if the nodes are all valid XML.</returns>
        private bool TryGetDocumentationCommentNodes(Symbol symbol, out DocumentationMode maxDocumentationMode, out ImmutableArray<DocumentationCommentTriviaSyntax> nodes)
        {
            maxDocumentationMode = DocumentationMode.None;
            nodes = default(ImmutableArray<DocumentationCommentTriviaSyntax>);
 
            ArrayBuilder<DocumentationCommentTriviaSyntax> builder = null;
            var diagnosticBag = _diagnostics.DiagnosticBag ?? DiagnosticBag.GetInstance();
 
            foreach (SyntaxReference reference in symbol.DeclaringSyntaxReferences)
            {
                DocumentationMode currDocumentationMode = reference.SyntaxTree.Options.DocumentationMode;
                maxDocumentationMode = currDocumentationMode > maxDocumentationMode ? currDocumentationMode : maxDocumentationMode;
 
                ImmutableArray<DocumentationCommentTriviaSyntax> triviaList = SourceDocumentationCommentUtils.GetDocumentationCommentTriviaFromSyntaxNode((CSharpSyntaxNode)reference.GetSyntax(), diagnosticBag);
                foreach (var trivia in triviaList)
                {
                    if (ContainsXmlParseDiagnostic(trivia))
                    {
                        if (builder != null)
                        {
                            builder.Free();
                        }
                        return false;
                    }
 
                    if (builder == null)
                    {
                        builder = ArrayBuilder<DocumentationCommentTriviaSyntax>.GetInstance();
                    }
                    builder.Add(trivia);
                }
            }
 
            if (diagnosticBag != _diagnostics.DiagnosticBag)
            {
                diagnosticBag.Free();
            }
 
            if (builder == null)
            {
                nodes = ImmutableArray<DocumentationCommentTriviaSyntax>.Empty;
            }
            else
            {
                builder.Sort(Comparer);
                nodes = builder.ToImmutableAndFree();
            }
 
            return true;
        }
 
        private static bool ContainsXmlParseDiagnostic(DocumentationCommentTriviaSyntax node)
        {
            if (!node.ContainsDiagnostics)
            {
                return false;
            }
 
            foreach (Diagnostic diag in node.GetDiagnostics())
            {
                if ((ErrorCode)diag.Code == ErrorCode.WRN_XMLParseError)
                {
                    return true;
                }
            }
 
            return false;
        }
 
        private static readonly string[] s_newLineSequences = new[] { "\r\n", "\r", "\n" };
 
        /// <summary>
        /// Given the full text of a documentation comment, strip off the comment punctuation (///, /**, etc)
        /// and add appropriate indentations.
        /// </summary>
        private string FormatComment(string substitutedText)
        {
            BeginTemporaryString();
 
            if (TrimmedStringStartsWith(substitutedText, "///"))
            {
                //Debug.Assert(lines.Take(numLines).All(line => TrimmedStringStartsWith(line, "///")));
                WriteFormattedSingleLineComment(substitutedText);
            }
            else
            {
                string[] lines = substitutedText.Split(s_newLineSequences, StringSplitOptions.None);
 
                int numLines = lines.Length;
                Debug.Assert(numLines > 0);
 
                if (string.IsNullOrEmpty(lines[numLines - 1]))
                {
                    numLines--;
                    Debug.Assert(numLines > 0);
                }
 
                // We may use multi-line formatting in a "fragment" scenario.
                //     /** <summary>The record</summary>
                //     <param name="P">The parameter</param>
                //     */
                //     record Rec(int P);
                // When formatting docs for property 'Rec.P' we may have just the line with '<param ...>' as input to this method.
                WriteFormattedMultiLineComment(lines, numLines);
            }
 
            return GetAndEndTemporaryString();
        }
 
        /// <summary>
        /// Given a string, find the index of the first non-whitespace char.
        /// </summary>
        /// <param name="str">The string to search</param>
        /// <returns>The index of the first non-whitespace char in the string</returns>
        private static int GetIndexOfFirstNonWhitespaceChar(string str)
        {
            return GetIndexOfFirstNonWhitespaceChar(str, 0, str.Length);
        }
 
        /// <summary>
        /// Find the first non-whitespace character in a given substring.
        /// </summary>
        /// <param name="str">The string to search</param>
        /// <param name="start">The start index</param>
        /// <param name="end">The last index (non-inclusive)</param>
        /// <returns>The index of the first non-whitespace char after index start in the string up to, but not including the end index</returns>
        private static int GetIndexOfFirstNonWhitespaceChar(string str, int start, int end)
        {
            Debug.Assert(start >= 0);
            Debug.Assert(start <= str.Length);
            Debug.Assert(end >= 0);
            Debug.Assert(end <= str.Length);
            Debug.Assert(end >= start);
 
            for (; start < end; start++)
            {
                if (!SyntaxFacts.IsWhitespace(str[start]))
                {
                    break;
                }
            }
 
            return start;
        }
 
        /// <summary>
        /// Determine if the given string starts with the given prefix if whitespace
        /// is first trimmed from the beginning.
        /// </summary>
        /// <param name="str">The string to search</param>
        /// <param name="prefix">The prefix</param>
        /// <returns>true if str.TrimStart().StartsWith(prefix)</returns>
        private static bool TrimmedStringStartsWith(string str, string prefix)
        {
            // PERF: Avoid calling string.Trim() because that allocates a new substring
            int start = GetIndexOfFirstNonWhitespaceChar(str);
            int len = str.Length - start;
            if (len < prefix.Length)
            {
                return false;
            }
 
            for (int i = 0; i < prefix.Length; i++)
            {
                if (prefix[i] != str[i + start])
                {
                    return false;
                }
            }
 
            return true;
        }
 
        /// <summary>
        /// Given a string which may contain newline sequences, get the index of the first newline
        /// sequence beginning at the given starting index.
        /// </summary>
        /// <param name="str">The string to split.</param>
        /// <param name="start">The starting index within the string.</param>
        /// <param name="newLineLength">The length of the newline sequence discovered. 0 if the end of the string was reached, otherwise either 1 or 2 chars</param>
        /// <returns>The index of the start of the first newline sequence following the start index</returns>
        private static int IndexOfNewLine(string str, int start, out int newLineLength)
        {
            for (; start < str.Length; start++)
            {
                switch (str[start])
                {
                    case '\r':
                        if ((start + 1) < str.Length && str[start + 1] == '\n')
                        {
                            newLineLength = 2;
                        }
                        else
                        {
                            newLineLength = 1;
                        }
                        return start;
 
                    case '\n':
                        newLineLength = 1;
                        return start;
                }
            }
 
            newLineLength = 0;
            return start;
        }
 
        /// <summary>
        /// Given the full text of a single-line style documentation comment, for each line, strip off
        /// the comment punctuation (///) and add appropriate indentations.
        /// </summary>
        private void WriteFormattedSingleLineComment(string text)
        {
            // PERF: Avoid allocating intermediate strings e.g. via Split, Trim or Substring
            bool skipSpace = true;
            for (int start = 0; start < text.Length;)
            {
                int newLineLength;
                int end = IndexOfNewLine(text, start, out newLineLength);
                int trimStart = GetIndexOfFirstNonWhitespaceChar(text, start, end);
                int trimmedLength = end - trimStart;
                if (trimmedLength < 4 || !SyntaxFacts.IsWhitespace(text[trimStart + 3]))
                {
                    skipSpace = false;
                    break;
                }
 
                start = end + newLineLength;
            }
 
            int substringStart = skipSpace ? 4 : 3;
 
            for (int start = 0; start < text.Length;)
            {
                int newLineLength;
                int end = IndexOfNewLine(text, start, out newLineLength);
                int trimStart = GetIndexOfFirstNonWhitespaceChar(text, start, end) + substringStart;
                WriteSubStringLine(text, trimStart, end - trimStart);
                start = end + newLineLength;
            }
        }
 
        /// <summary>
        /// Given the full text of a multi-line style documentation comment, broken into lines, strip off
        /// the comment punctuation (/**, */, etc) and add appropriate indentations.
        /// </summary>
        private void WriteFormattedMultiLineComment(string[] lines, int numLines)
        {
            bool skipFirstLine = lines[0].Trim() == "/**";
            bool skipLastLine = lines[numLines - 1].Trim() == "*/";
 
            if (skipLastLine)
            {
                numLines--;
                Debug.Assert(numLines > 0);
            }
 
            int skipLength = 0;
            if (numLines > 1)
            {
                string pattern = FindMultiLineCommentPattern(lines[1]);
 
                if (pattern != null)
                {
                    bool allMatch = true;
 
                    for (int i = 2; i < numLines; i++)
                    {
                        string currentLinePattern = LongestCommonPrefix(pattern, lines[i]);
                        if (string.IsNullOrWhiteSpace(currentLinePattern))
                        {
                            allMatch = false;
                            break;
                        }
                        Debug.Assert(pattern.StartsWith(currentLinePattern, StringComparison.Ordinal));
                        pattern = currentLinePattern;
                    }
 
                    if (allMatch)
                    {
                        skipLength = pattern.Length;
                    }
                }
            }
 
            if (!skipFirstLine)
            {
                string trimmed = lines[0].TrimStart(null);
                if (!skipLastLine && numLines == 1)
                {
                    trimmed = TrimEndOfMultiLineComment(trimmed);
                }
                WriteLine(trimmed.Substring(
                    trimmed.StartsWith("/** ") ? 4 :
                    trimmed.StartsWith("/**") ? 3 :
                    trimmed.StartsWith("* ") ? 2 :
                    trimmed.StartsWith("*") ? 1 :
                    0));
            }
 
            for (int i = 1; i < numLines; i++)
            {
                string trimmed = lines[i].Substring(skipLength);
 
                // If we've already skipped the last line, this can't happen.
                if (!skipLastLine && i == numLines - 1)
                {
                    trimmed = TrimEndOfMultiLineComment(trimmed);
                }
 
                WriteLine(trimmed);
            }
        }
 
        /// <summary>
        /// Remove "*/" and any following text, if it is present.
        /// </summary>
        private static string TrimEndOfMultiLineComment(string trimmed)
        {
            int index = trimmed.IndexOf("*/", StringComparison.Ordinal);
            if (index >= 0)
            {
                trimmed = trimmed.Substring(0, index);
            }
            return trimmed;
        }
 
        /// <summary>
        /// Return the longest prefix matching [whitespace]*[*][whitespace]*.
        /// </summary>
        private static string FindMultiLineCommentPattern(string line)
        {
            int length = 0;
 
            bool seenStar = false;
            foreach (char ch in line)
            {
                if (SyntaxFacts.IsWhitespace(ch))
                {
                    length++;
                }
                else if (!seenStar && ch == '*')
                {
                    length++;
                    seenStar = true;
                }
                else
                {
                    break;
                }
            }
 
            return seenStar ? line.Substring(0, length) : null;
        }
 
        /// <summary>
        /// Return the longest common prefix of two strings
        /// </summary>
        private static string LongestCommonPrefix(string str1, string str2)
        {
            int pos = 0;
            int minLength = Math.Min(str1.Length, str2.Length);
 
            for (; pos < minLength && str1[pos] == str2[pos]; pos++)
            {
            }
 
            return str1.Substring(0, pos);
        }
 
        /// <summary>
        /// Bind a CrefSyntax and unwrap the result if it's an alias.
        /// </summary>
        /// <remarks>
        /// Does not respect DocumentationMode, so use a temporary bag if diagnostics are not desired.
        /// </remarks>
        private static string GetDocumentationCommentId(CrefSyntax crefSyntax, Binder binder, BindingDiagnosticBag diagnostics)
        {
            if (crefSyntax.ContainsDiagnostics)
            {
                return ToBadCrefString(crefSyntax);
            }
 
            Symbol ambiguityWinner;
            ImmutableArray<Symbol> symbols = binder.BindCref(crefSyntax, out ambiguityWinner, diagnostics);
 
            Symbol symbol;
            switch (symbols.Length)
            {
                case 0:
                    return ToBadCrefString(crefSyntax);
                case 1:
                    symbol = symbols[0];
                    break;
                default:
                    symbol = ambiguityWinner;
                    Debug.Assert((object)symbol != null);
                    break;
            }
 
            if (symbol.Kind == SymbolKind.Alias)
            {
                symbol = ((AliasSymbol)symbol).GetAliasTarget(basesBeingResolved: null);
            }
 
            if (symbol is NamespaceSymbol ns)
            {
                Debug.Assert(!ns.IsGlobalNamespace);
                diagnostics.AddAssembliesUsedByNamespaceReference(ns);
            }
            else
            {
                diagnostics.AddDependencies(symbol as TypeSymbol ?? symbol.ContainingType);
            }
 
            return symbol.OriginalDefinition.GetDocumentationCommentId();
        }
 
        /// <summary>
        /// Given a cref syntax that cannot be resolved, get the string that will be written to
        /// the documentation file in place of a documentation comment ID.
        /// </summary>
        private static string ToBadCrefString(CrefSyntax cref)
        {
            using (StringWriter tmp = new StringWriter(CultureInfo.InvariantCulture))
            {
                cref.WriteTo(tmp);
                return "!:" + tmp.ToString().Replace("{", "&lt;").Replace("}", "&gt;");
            }
        }
 
        /// <summary>
        /// Bind an XmlNameAttributeSyntax and update the sets of documented parameters and type parameters.
        /// </summary>
        /// <remarks>
        /// Does not respect DocumentationMode, so do not call unless diagnostics are desired.
        /// </remarks>
        private static void BindName(
            XmlNameAttributeSyntax syntax,
            Binder binder,
            Symbol memberSymbol,
            ref HashSet<ParameterSymbol> documentedParameters,
            ref HashSet<TypeParameterSymbol> documentedTypeParameters,
            BindingDiagnosticBag diagnostics)
        {
            XmlNameAttributeElementKind elementKind = syntax.GetElementKind();
 
            // NOTE: We want the corresponding hash set to be non-null if we saw
            // any <param>/<typeparam> elements, even if they didn't bind (for
            // WRN_MissingParamTag and WRN_MissingTypeParamTag).
            if (elementKind == XmlNameAttributeElementKind.Parameter)
            {
                if (documentedParameters == null)
                {
                    documentedParameters = new HashSet<ParameterSymbol>();
                }
            }
            else if (elementKind == XmlNameAttributeElementKind.TypeParameter)
            {
                if (documentedTypeParameters == null)
                {
                    documentedTypeParameters = new HashSet<TypeParameterSymbol>();
                }
            }
 
            IdentifierNameSyntax identifier = syntax.Identifier;
 
            if (identifier.ContainsDiagnostics)
            {
                return;
            }
 
            CompoundUseSiteInfo<AssemblySymbol> useSiteInfo = binder.GetNewCompoundUseSiteInfo(diagnostics);
            ImmutableArray<Symbol> referencedSymbols = binder.BindXmlNameAttribute(syntax, ref useSiteInfo);
            diagnostics.Add(syntax, useSiteInfo);
 
            if (referencedSymbols.IsEmpty)
            {
                switch (elementKind)
                {
                    case XmlNameAttributeElementKind.Parameter:
                        diagnostics.Add(ErrorCode.WRN_UnmatchedParamTag, identifier.Location, identifier);
                        break;
                    case XmlNameAttributeElementKind.ParameterReference:
                        diagnostics.Add(ErrorCode.WRN_UnmatchedParamRefTag, identifier.Location, identifier, memberSymbol);
                        break;
                    case XmlNameAttributeElementKind.TypeParameter:
                        diagnostics.Add(ErrorCode.WRN_UnmatchedTypeParamTag, identifier.Location, identifier);
                        break;
                    case XmlNameAttributeElementKind.TypeParameterReference:
                        diagnostics.Add(ErrorCode.WRN_UnmatchedTypeParamRefTag, identifier.Location, identifier, memberSymbol);
                        break;
                    default:
                        throw ExceptionUtilities.UnexpectedValue(elementKind);
                }
            }
            else
            {
                foreach (Symbol referencedSymbol in referencedSymbols)
                {
                    if (elementKind == XmlNameAttributeElementKind.Parameter)
                    {
                        Debug.Assert(referencedSymbol.Kind == SymbolKind.Parameter);
                        Debug.Assert(documentedParameters != null);
 
                        // Restriction preserved from dev11: don't report this for the "value" parameter.
                        // Here, we detect that case by checking the containing symbol - only "value"
                        // parameters are contained by accessors, others are on the corresponding property/event.
                        ParameterSymbol parameter = (ParameterSymbol)referencedSymbol;
                        if (!parameter.ContainingSymbol.IsAccessor() && !documentedParameters.Add(parameter))
                        {
                            diagnostics.Add(ErrorCode.WRN_DuplicateParamTag, syntax.Location, identifier);
                        }
                    }
                    else if (elementKind == XmlNameAttributeElementKind.TypeParameter)
                    {
                        Debug.Assert(referencedSymbol.Kind == SymbolKind.TypeParameter);
                        Debug.Assert(documentedTypeParameters != null);
 
                        if (!documentedTypeParameters.Add((TypeParameterSymbol)referencedSymbol))
                        {
                            diagnostics.Add(ErrorCode.WRN_DuplicateTypeParamTag, syntax.Location, identifier);
                        }
                    }
                }
            }
        }
 
        private IComparer<CSharpSyntaxNode> Comparer
        {
            get
            {
                if (_lazyComparer == null)
                {
                    _lazyComparer = new SyntaxNodeLocationComparer(_compilation);
                }
                return _lazyComparer;
            }
        }
 
        private void BeginTemporaryString()
        {
            if (_temporaryStringBuilders == null)
            {
                _temporaryStringBuilders = new Stack<TemporaryStringBuilder>();
            }
 
            _temporaryStringBuilders.Push(new TemporaryStringBuilder(_indentDepth));
        }
 
        private string GetAndEndTemporaryString()
        {
            TemporaryStringBuilder t = _temporaryStringBuilders.Pop();
            RoslynDebug.Assert(_indentDepth == t.InitialIndentDepth, $"Temporary strings should be indent-neutral (was {t.InitialIndentDepth}, is {_indentDepth})");
            _indentDepth = t.InitialIndentDepth;
            return t.Pooled.ToStringAndFree();
        }
 
        private void Indent()
        {
            _indentDepth++;
        }
 
        private void Unindent()
        {
            _indentDepth--;
            Debug.Assert(_indentDepth >= 0);
        }
 
        private void Write(string indentedAndWrappedString)
        {
            if (_temporaryStringBuilders != null && _temporaryStringBuilders.Count > 0)
            {
                StringBuilder builder = _temporaryStringBuilders.Peek().Pooled.Builder;
                builder.Append(indentedAndWrappedString);
            }
            else if (_writer != null)
            {
                _writer.Write(indentedAndWrappedString);
            }
        }
 
        private void WriteLine(string message)
        {
            if (_temporaryStringBuilders?.Count > 0)
            {
                StringBuilder builder = _temporaryStringBuilders.Peek().Pooled.Builder;
                builder.Append(MakeIndent(_indentDepth));
                builder.AppendLine(message);
            }
            else if (_writer != null)
            {
                _writer.Write(MakeIndent(_indentDepth));
                _writer.WriteLine(message);
            }
        }
 
        private void WriteSubStringLine(string message, int start, int length)
        {
            if (_temporaryStringBuilders?.Count > 0)
            {
                StringBuilder builder = _temporaryStringBuilders.Peek().Pooled.Builder;
                builder.Append(MakeIndent(_indentDepth));
                builder.Append(message, start, length);
                builder.AppendLine();
            }
            else if (_writer != null)
            {
                _writer.Write(MakeIndent(_indentDepth));
                for (int i = 0; i < length; i++)
                {
                    _writer.Write(message[start + i]);
                }
                _writer.WriteLine();
            }
        }
 
        private void WriteLine(string format, params object[] args)
        {
            WriteLine(string.Format(format, args));
        }
 
        private static string MakeIndent(int depth)
        {
            Debug.Assert(depth >= 0);
 
            // Since we know a lot about the structure of the output,
            // we should be able to do this without constructing any
            // new string objects.
            switch (depth)
            {
                case 0:
                    return "";
                case 1:
                    return "    ";
                case 2:
                    return "        ";
                case 3:
                    return "            ";
                default:
                    Debug.Assert(false, "Didn't expect nesting to reach depth " + depth);
                    return new string(' ', depth * 4);
            }
        }
 
        /// <remarks>
        /// WORKAROUND:
        /// We're taking a dependency on the location and structure of a framework assembly resource.  This is not a robust solution.
        /// 
        /// Possible alternatives:
        /// 1) Polish our XML parser until it matches MSXML.  We don't want to reinvent the wheel.
        /// 2) Build a map that lets us go from XML string positions back to source positions.  
        /// This is what the native compiler did, and it was a lot of work.  We'd also still need to modify the message.
        /// 3) Do not report a diagnostic.  This is very unhelpful.
        /// 4) Report a vague diagnostic (i.e. there's a problem somewhere in this doc comment).  This is relatively unhelpful.
        /// 5) Always report the message in English, so that we can pull it apart without needing to consume resource files.
        /// This engenders a lot of ill will.
        /// 6) Report the exception message without modification and (optionally) include the text with respect to which the
        /// position is specified.  This would not look sufficiently polished.
        /// </remarks>
        private static string GetDescription(XmlException e)
        {
            string message = e.Message;
            try
            {
                ResourceManager manager = new ResourceManager("System.Xml", typeof(XmlException).GetTypeInfo().Assembly);
                string locationTemplate = manager.GetString("Xml_MessageWithErrorPosition");
                string locationString = string.Format(locationTemplate, "", e.LineNumber, e.LinePosition); // first arg is where the problem description goes
                int position = message.IndexOf(locationString, StringComparison.Ordinal); // Expect exact match
                return position < 0
                    ? message
                    : message.Remove(position, locationString.Length);
            }
            catch
            {
                Debug.Assert(false, "If we hit this, then we might need to think about a different workaround " +
                    "for stripping the location out the message.");
 
                // If anything at all goes wrong, just return the message verbatim.  It probably
                // contains an invalid position, but it's better than nothing.
                return message;
            }
        }
 
        private readonly struct TemporaryStringBuilder
        {
            public readonly PooledStringBuilder Pooled;
            public readonly int InitialIndentDepth;
 
            public TemporaryStringBuilder(int indentDepth)
            {
                this.InitialIndentDepth = indentDepth;
                this.Pooled = PooledStringBuilder.GetInstance();
            }
        }
    }
}