File: Shared\Extensions\ISymbolExtensions.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;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Roslyn.Utilities;
 
using static Microsoft.CodeAnalysis.Shared.Utilities.EditorBrowsableHelpers;
 
namespace Microsoft.CodeAnalysis.Shared.Extensions;
 
internal static partial class ISymbolExtensions
{
    /// <summary>
    /// Checks a given symbol for browsability based on its declaration location, attributes explicitly limiting
    /// browsability, and whether showing of advanced members is enabled. The optional editorBrowsableInfo parameters
    /// may be used to specify the symbols of the constructors of the various browsability limiting attributes because
    /// finding these repeatedly over a large list of symbols can be slow. If these are not provided, they will be found
    /// in the compilation.
    /// </summary>
    public static bool IsEditorBrowsable(
        this ISymbol symbol,
        bool hideAdvancedMembers,
        Compilation compilation,
        EditorBrowsableInfo editorBrowsableInfo = default,
        bool includingSourceSymbols = false)
    {
        return IsEditorBrowsableWithState(
            symbol,
            hideAdvancedMembers,
            compilation,
            editorBrowsableInfo,
            includingSourceSymbols).isBrowsable;
    }
 
    // In addition to given symbol's browsability, also returns its EditorBrowsableState if it contains EditorBrowsableAttribute.
    public static (bool isBrowsable, bool isEditorBrowsableStateAdvanced) IsEditorBrowsableWithState(
        this ISymbol symbol,
        bool hideAdvancedMembers,
        Compilation compilation,
        EditorBrowsableInfo editorBrowsableInfo = default,
        bool includingSourceSymbols = false)
    {
        // Namespaces can't have attributes, so just return true here.  This also saves us a 
        // costly check if this namespace has any locations in source (since a merged namespace
        // needs to go collect all the locations).
        if (symbol.Kind == SymbolKind.Namespace)
            return (isBrowsable: true, isEditorBrowsableStateAdvanced: false);
 
        // check for IsImplicitlyDeclared so we don't spend time examining VB's embedded types.
        // This saves a few percent in typing scenarios.  An implicitly declared symbol can't
        // have attributes, so it can't be hidden by them.
        if (symbol.IsImplicitlyDeclared)
            return (isBrowsable: true, isEditorBrowsableStateAdvanced: false);
 
        if (editorBrowsableInfo.IsDefault)
            editorBrowsableInfo = new EditorBrowsableInfo(compilation);
 
        // Ignore browsability limiting attributes if the symbol is declared in source.
        // Check all locations since some of VB's embedded My symbols are declared in 
        // both source and the MyTemplateLocation.
        if (!includingSourceSymbols && symbol.Locations.All(loc => loc.IsInSource))
        {
            // The HideModuleNameAttribute still applies to Modules defined in source
            return (!IsBrowsingProhibitedByHideModuleNameAttribute(symbol, editorBrowsableInfo.HideModuleNameAttribute), isEditorBrowsableStateAdvanced: false);
        }
 
        var (isProhibited, isEditorBrowsableStateAdvanced) = IsBrowsingProhibited(symbol, hideAdvancedMembers, editorBrowsableInfo);
        return (!isProhibited, isEditorBrowsableStateAdvanced);
    }
 
    private static (bool isProhibited, bool isEditorBrowsableStateAdvanced) IsBrowsingProhibited(
        ISymbol symbol,
        bool hideAdvancedMembers,
        EditorBrowsableInfo editorBrowsableInfo)
    {
        var attributes = symbol.GetAttributes();
        if (attributes.Length == 0)
        {
            return (isProhibited: false, isEditorBrowsableStateAdvanced: false);
        }
 
        var (isProhibited, isEditorBrowsableStateAdvanced) = IsBrowsingProhibitedByEditorBrowsableAttribute(attributes, hideAdvancedMembers, editorBrowsableInfo.EditorBrowsableAttributeConstructor);
 
        return ((isProhibited
            || IsBrowsingProhibitedByTypeLibTypeAttribute(attributes, editorBrowsableInfo.TypeLibTypeAttributeConstructors)
            || IsBrowsingProhibitedByTypeLibFuncAttribute(attributes, editorBrowsableInfo.TypeLibFuncAttributeConstructors)
            || IsBrowsingProhibitedByTypeLibVarAttribute(attributes, editorBrowsableInfo.TypeLibVarAttributeConstructors)
            || IsBrowsingProhibitedByHideModuleNameAttribute(symbol, editorBrowsableInfo.HideModuleNameAttribute, attributes)), isEditorBrowsableStateAdvanced);
    }
 
    private static bool IsBrowsingProhibitedByHideModuleNameAttribute(
        ISymbol symbol, INamedTypeSymbol? hideModuleNameAttribute, ImmutableArray<AttributeData> attributes = default)
    {
        if (hideModuleNameAttribute == null || !symbol.IsModuleType())
        {
            return false;
        }
 
        attributes = attributes.IsDefault ? symbol.GetAttributes() : attributes;
 
        foreach (var attribute in attributes)
        {
            if (Equals(attribute.AttributeClass, hideModuleNameAttribute))
            {
                return true;
            }
        }
 
        return false;
    }
 
    private static (bool isProhibited, bool isEditorBrowsableStateAdvanced) IsBrowsingProhibitedByEditorBrowsableAttribute(
        ImmutableArray<AttributeData> attributes, bool hideAdvancedMembers, IMethodSymbol? constructor)
    {
        if (constructor == null)
            return (isProhibited: false, isEditorBrowsableStateAdvanced: false);
 
        foreach (var attribute in attributes)
        {
            if (Equals(attribute.AttributeConstructor, constructor) &&
                attribute.ConstructorArguments is [{ Value: int value }])
            {
                var state = (EditorBrowsableState)value;
                if (EditorBrowsableState.Never == state)
                    return (isProhibited: true, isEditorBrowsableStateAdvanced: false);
 
                if (EditorBrowsableState.Advanced == state)
                    return (isProhibited: hideAdvancedMembers, isEditorBrowsableStateAdvanced: true);
            }
        }
 
        return (isProhibited: false, isEditorBrowsableStateAdvanced: false);
    }
 
    private static bool IsBrowsingProhibitedByTypeLibTypeAttribute(
        ImmutableArray<AttributeData> attributes, ImmutableArray<IMethodSymbol> constructors)
    {
        return IsBrowsingProhibitedByTypeLibAttributeWorker(
            attributes,
            constructors,
            TypeLibTypeFlagsFHidden);
    }
 
    private static bool IsBrowsingProhibitedByTypeLibFuncAttribute(
        ImmutableArray<AttributeData> attributes, ImmutableArray<IMethodSymbol> constructors)
    {
        return IsBrowsingProhibitedByTypeLibAttributeWorker(
            attributes,
            constructors,
            TypeLibFuncFlagsFHidden);
    }
 
    private static bool IsBrowsingProhibitedByTypeLibVarAttribute(
        ImmutableArray<AttributeData> attributes, ImmutableArray<IMethodSymbol> constructors)
    {
        return IsBrowsingProhibitedByTypeLibAttributeWorker(
            attributes,
            constructors,
            TypeLibVarFlagsFHidden);
    }
 
    private const int TypeLibTypeFlagsFHidden = 0x0010;
    private const int TypeLibFuncFlagsFHidden = 0x0040;
    private const int TypeLibVarFlagsFHidden = 0x0040;
 
    private static bool IsBrowsingProhibitedByTypeLibAttributeWorker(
        ImmutableArray<AttributeData> attributes, ImmutableArray<IMethodSymbol> attributeConstructors, int hiddenFlag)
    {
        foreach (var attribute in attributes)
        {
            if (attribute.ConstructorArguments.Length == 1)
            {
                foreach (var constructor in attributeConstructors)
                {
                    if (Equals(attribute.AttributeConstructor, constructor))
                    {
                        // Check for both constructor signatures. The constructor that takes a TypeLib*Flags reports an int argument.
                        var argumentValue = attribute.ConstructorArguments.First().Value;
 
                        int actualFlags;
                        if (argumentValue is int i)
                        {
                            actualFlags = i;
                        }
                        else if (argumentValue is short sh)
                        {
                            actualFlags = sh;
                        }
                        else
                        {
                            continue;
                        }
 
                        if ((actualFlags & hiddenFlag) == hiddenFlag)
                        {
                            return true;
                        }
                    }
                }
            }
        }
 
        return false;
    }
 
    public static DocumentationComment GetDocumentationComment(this ISymbol symbol, Compilation compilation, CultureInfo? preferredCulture = null, bool expandIncludes = false, bool expandInheritdoc = false, CancellationToken cancellationToken = default)
        => GetDocumentationComment(symbol, visitedSymbols: null, compilation, preferredCulture, expandIncludes, expandInheritdoc, cancellationToken);
 
    private static DocumentationComment GetDocumentationComment(ISymbol symbol, HashSet<ISymbol>? visitedSymbols, Compilation compilation, CultureInfo? preferredCulture, bool expandIncludes, bool expandInheritdoc, CancellationToken cancellationToken)
    {
        var xmlText = symbol.GetDocumentationCommentXml(preferredCulture, expandIncludes, cancellationToken);
        if (expandInheritdoc)
        {
            if (string.IsNullOrEmpty(xmlText))
            {
                if (IsEligibleForAutomaticInheritdoc(symbol))
                {
                    xmlText = $@"<doc><inheritdoc/></doc>";
                }
                else
                {
                    return DocumentationComment.Empty;
                }
            }
 
            try
            {
                var element = XElement.Parse(xmlText, LoadOptions.PreserveWhitespace);
                element.ReplaceNodes(RewriteMany(symbol, visitedSymbols, compilation, element.Nodes().ToArray(), cancellationToken));
                xmlText = element.ToString(SaveOptions.DisableFormatting);
            }
            catch (XmlException)
            {
                // Malformed documentation comments will produce an exception during parsing. This is not directly
                // actionable, so avoid the overhead of telemetry reporting for it.
                // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1385578
            }
            catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken))
            {
            }
        }
 
        return RoslynString.IsNullOrEmpty(xmlText) ? DocumentationComment.Empty : DocumentationComment.FromXmlFragment(xmlText);
 
        static bool IsEligibleForAutomaticInheritdoc(ISymbol symbol)
        {
            // Only the following symbols are eligible to inherit documentation without an <inheritdoc/> element:
            //
            // * Members that override an inherited member
            // * Members that implement an interface member
            if (symbol.IsOverride)
            {
                return true;
            }
 
            if (symbol.ContainingType is null)
            {
                // Observed with certain implicit operators, such as operator==(void*, void*).
                return false;
            }
 
            switch (symbol.Kind)
            {
                case SymbolKind.Method:
                case SymbolKind.Property:
                case SymbolKind.Event:
                    if (symbol.ExplicitOrImplicitInterfaceImplementations().Any())
                    {
                        return true;
                    }
 
                    break;
 
                default:
                    break;
            }
 
            return false;
        }
    }
 
    private static XNode[] RewriteInheritdocElements(ISymbol symbol, HashSet<ISymbol>? visitedSymbols, Compilation compilation, XNode node, CancellationToken cancellationToken)
    {
        if (node.NodeType == XmlNodeType.Element)
        {
            var element = (XElement)node;
            if (ElementNameIs(element, DocumentationCommentXmlNames.InheritdocElementName))
            {
                var rewritten = RewriteInheritdocElement(symbol, visitedSymbols, compilation, element, cancellationToken);
                if (rewritten is object)
                {
                    return rewritten;
                }
            }
        }
 
        var container = node as XContainer;
        if (container == null)
        {
            return [Copy(node, copyAttributeAnnotations: false)];
        }
 
        var oldNodes = container.Nodes();
 
        // Do this after grabbing the nodes, so we don't see copies of them.
        container = Copy(container, copyAttributeAnnotations: false);
 
        // WARN: don't use node after this point - use container since it's already been copied.
 
        if (oldNodes != null)
        {
            var rewritten = RewriteMany(symbol, visitedSymbols, compilation, oldNodes.ToArray(), cancellationToken);
            container.ReplaceNodes(rewritten);
        }
 
        return [container];
    }
 
    private static XNode[] RewriteMany(ISymbol symbol, HashSet<ISymbol>? visitedSymbols, Compilation compilation, XNode[] nodes, CancellationToken cancellationToken)
    {
        var result = new List<XNode>();
        foreach (var child in nodes)
        {
            result.AddRange(RewriteInheritdocElements(symbol, visitedSymbols, compilation, child, cancellationToken));
        }
 
        return [.. result];
    }
 
    private static XNode[]? RewriteInheritdocElement(ISymbol memberSymbol, HashSet<ISymbol>? visitedSymbols, Compilation compilation, XElement element, CancellationToken cancellationToken)
    {
        var crefAttribute = element.Attribute(XName.Get(DocumentationCommentXmlNames.CrefAttributeName));
        var pathAttribute = element.Attribute(XName.Get(DocumentationCommentXmlNames.PathAttributeName));
 
        var candidate = GetCandidateSymbol(memberSymbol);
        var hasCandidateCref = candidate is object;
 
        var hasCrefAttribute = crefAttribute is object;
        var hasPathAttribute = pathAttribute is object;
        if (!hasCrefAttribute && !hasCandidateCref)
        {
            // No cref available
            return null;
        }
 
        ISymbol? symbol;
        if (crefAttribute is null)
        {
            Contract.ThrowIfNull(candidate);
            symbol = candidate;
        }
        else
        {
            var crefValue = crefAttribute.Value;
            symbol = DocumentationCommentId.GetFirstSymbolForDeclarationId(crefValue, compilation);
            if (symbol is null)
            {
                return null;
            }
        }
 
        visitedSymbols ??= [];
        if (!visitedSymbols.Add(symbol))
        {
            // Prevent recursion
            return null;
        }
 
        try
        {
            var inheritedDocumentation = GetDocumentationComment(symbol, visitedSymbols, compilation, preferredCulture: null, expandIncludes: true, expandInheritdoc: true, cancellationToken);
            if (inheritedDocumentation == DocumentationComment.Empty)
            {
                return [];
            }
 
            var document = XDocument.Parse(inheritedDocumentation.FullXmlFragment, LoadOptions.PreserveWhitespace);
            string xpathValue;
            if (string.IsNullOrEmpty(pathAttribute?.Value))
            {
                xpathValue = BuildXPathForElement(element.Parent!);
            }
            else
            {
                xpathValue = pathAttribute!.Value;
                if (xpathValue.StartsWith("/"))
                {
                    // Account for the root <doc> or <member> element
                    xpathValue = "/*" + xpathValue;
                }
            }
 
            // Consider the following code, we want Test<int>.Clone to say "Clones a Test<int>" instead of "Clones a int", thus
            // we rewrite `typeparamref`s as cref pointing to the correct type:
            /*
                public class Test<T> : ICloneable<Test<T>>
                {
                    /// <inheritdoc/>
                    public Test<T> Clone() => new();
                }
 
                /// <summary>A type that has clonable instances.</summary>
                /// <typeparam name="T">The type of instances that can be cloned.</typeparam>
                public interface ICloneable<T>
                {
                    /// <summary>Clones a <typeparamref name="T"/>.</summary>
                    public T Clone();
                }
            */
            // Note: there is no way to cref an instantiated generic type. See https://github.com/dotnet/csharplang/issues/401
            var typeParameterRefs = document.Descendants(DocumentationCommentXmlNames.TypeParameterReferenceElementName).ToImmutableArray();
            foreach (var typeParameterRef in typeParameterRefs)
            {
                if (typeParameterRef.Attribute(DocumentationCommentXmlNames.NameAttributeName) is XAttribute typeParamName)
                {
                    var index = symbol.OriginalDefinition.GetAllTypeParameters().IndexOf(p => p.Name == typeParamName.Value);
                    if (index >= 0)
                    {
                        var typeArgs = symbol.GetAllTypeArguments();
                        if (index < typeArgs.Length)
                        {
                            var docId = typeArgs[index].GetDocumentationCommentId();
                            if (docId != null && !docId.StartsWith("!"))
                            {
                                var replacement = new XElement(DocumentationCommentXmlNames.SeeElementName);
                                replacement.SetAttributeValue(DocumentationCommentXmlNames.CrefAttributeName, docId);
                                typeParameterRef.ReplaceWith(replacement);
                            }
                        }
                    }
                }
            }
 
            var loadedElements = TrySelectNodes(document, xpathValue);
            return loadedElements ?? [];
        }
        catch (XmlException)
        {
            return [];
        }
        finally
        {
            visitedSymbols.Remove(symbol);
        }
 
        // Local functions
        static ISymbol? GetCandidateSymbol(ISymbol memberSymbol)
        {
            if (memberSymbol.ExplicitInterfaceImplementations().Any())
            {
                return memberSymbol.ExplicitInterfaceImplementations().First();
            }
            else if (memberSymbol.IsOverride)
            {
                return memberSymbol.GetOverriddenMember();
            }
 
            if (memberSymbol is IMethodSymbol methodSymbol)
            {
                if (methodSymbol.MethodKind is MethodKind.Constructor or MethodKind.StaticConstructor)
                {
                    var baseType = memberSymbol.ContainingType.BaseType;
#nullable disable // Can 'baseType' be null here? https://github.com/dotnet/roslyn/issues/39166
                    return baseType.Constructors.Where(c => IsSameSignature(methodSymbol, c)).FirstOrDefault();
#nullable enable
                }
                else
                {
                    // check for implicit interface
                    return methodSymbol.ExplicitOrImplicitInterfaceImplementations().FirstOrDefault();
                }
            }
            else if (memberSymbol is INamedTypeSymbol typeSymbol)
            {
                if (typeSymbol.TypeKind == TypeKind.Class)
                {
                    // Classes use the base type as the default inheritance candidate. A different target (e.g. an
                    // interface) can be provided via the 'path' attribute.
                    return typeSymbol.BaseType;
                }
                else if (typeSymbol.TypeKind == TypeKind.Interface)
                {
                    return typeSymbol.Interfaces.FirstOrDefault();
                }
                else
                {
                    // This includes structs, enums, and delegates as mentioned in the inheritdoc spec
                    return null;
                }
            }
 
            return memberSymbol.ExplicitOrImplicitInterfaceImplementations().FirstOrDefault();
        }
 
        static bool IsSameSignature(IMethodSymbol left, IMethodSymbol right)
        {
            if (left.Parameters.Length != right.Parameters.Length)
            {
                return false;
            }
 
            if (left.IsStatic != right.IsStatic)
            {
                return false;
            }
 
            if (!left.ReturnType.Equals(right.ReturnType))
            {
                return false;
            }
 
            for (var i = 0; i < left.Parameters.Length; i++)
            {
                if (!left.Parameters[i].Type.Equals(right.Parameters[i].Type))
                {
                    return false;
                }
            }
 
            return true;
        }
 
        static string BuildXPathForElement(XElement element)
        {
            if (ElementNameIs(element, "member") || ElementNameIs(element, "doc"))
            {
                // Avoid string concatenation allocations for inheritdoc as a top-level element
                return "/*/node()[not(self::overloads)]";
            }
 
            var path = "/node()[not(self::overloads)]";
            for (var current = element; current != null; current = current.Parent)
            {
                var currentName = current.Name.ToString();
                if (ElementNameIs(current, "member") || ElementNameIs(current, "doc"))
                {
                    // Allow <member> and <doc> to be used interchangeably
                    currentName = "*";
                }
 
                path = "/" + currentName + path;
            }
 
            return path;
        }
    }
 
    private static TNode Copy<TNode>(TNode node, bool copyAttributeAnnotations)
        where TNode : XNode
    {
        XNode copy;
 
        // Documents can't be added to containers, so our usual copy trick won't work.
        if (node.NodeType == XmlNodeType.Document)
        {
            copy = new XDocument(((XDocument)(object)node));
        }
        else
        {
            XContainer temp = new XElement("temp");
            temp.Add(node);
            copy = temp.LastNode!;
            temp.RemoveNodes();
        }
 
        Debug.Assert(copy != node);
        Debug.Assert(copy.Parent == null); // Otherwise, when we give it one, it will be copied.
 
        // Copy annotations, the above doesn't preserve them.
        // We need to preserve Location annotations as well as line position annotations.
        CopyAnnotations(node, copy);
 
        // We also need to preserve line position annotations for all attributes
        // since we report errors with attribute locations.
        if (copyAttributeAnnotations && node.NodeType == XmlNodeType.Element)
        {
            var sourceElement = (XElement)(object)node;
            var targetElement = (XElement)copy;
 
            var sourceAttributes = sourceElement.Attributes().GetEnumerator();
            var targetAttributes = targetElement.Attributes().GetEnumerator();
            while (sourceAttributes.MoveNext() && targetAttributes.MoveNext())
            {
                Debug.Assert(sourceAttributes.Current.Name == targetAttributes.Current.Name);
                CopyAnnotations(sourceAttributes.Current, targetAttributes.Current);
            }
        }
 
        return (TNode)copy;
    }
 
    private static void CopyAnnotations(XObject source, XObject target)
    {
        foreach (var annotation in source.Annotations<object>())
        {
            target.AddAnnotation(annotation);
        }
    }
 
    private static XNode[]? TrySelectNodes(XNode node, string xpath)
    {
        try
        {
            var xpathResult = (IEnumerable)System.Xml.XPath.Extensions.XPathEvaluate(node, xpath);
 
            // Throws InvalidOperationException if the result of the XPath is an XDocument:
            return xpathResult?.Cast<XNode>().ToArray();
        }
        catch (InvalidOperationException)
        {
            return null;
        }
        catch (XPathException)
        {
            return null;
        }
    }
 
    private static bool ElementNameIs(XElement element, string name)
        => string.IsNullOrEmpty(element.Name.NamespaceName) && DocumentationCommentXmlNames.ElementEquals(element.Name.LocalName, name);
 
    /// <summary>
    /// First, remove symbols from the set if they are overridden by other symbols in the set.
    /// If a symbol is overridden only by symbols outside of the set, then it is not removed. 
    /// This is useful for filtering out symbols that cannot be accessed in a given context due
    /// to the existence of overriding members. Second, remove remaining symbols that are
    /// unsupported (e.g. pointer types in VB) or not editor browsable based on the EditorBrowsable
    /// attribute.
    /// </summary>
    public static ImmutableArray<T> FilterToVisibleAndBrowsableSymbols<T>(
        this ImmutableArray<T> symbols, bool hideAdvancedMembers, Compilation compilation) where T : ISymbol
    {
        symbols = symbols.RemoveOverriddenSymbolsWithinSet();
 
        // Since all symbols are from the same compilation, find the required attribute
        // constructors once and reuse.
        var editorBrowsableInfo = new EditorBrowsableInfo(compilation);
 
        // PERF: HasUnsupportedMetadata may require recreating the syntax tree to get the base class, so first
        // check to see if we're referencing a symbol defined in source.
        return symbols.WhereAsArray((s, arg) =>
            // Check if symbol is namespace (which is always visible) first to avoid realizing all locations
            // of each namespace symbol, which might end up allocating in LOH
            (s.IsNamespace() || s.Locations.Any(static loc => loc.IsInSource) || !s.HasUnsupportedMetadata) &&
            !s.IsDestructor() &&
            s.IsEditorBrowsable(
                arg.hideAdvancedMembers,
                arg.editorBrowsableInfo.Compilation,
                arg.editorBrowsableInfo),
            (hideAdvancedMembers, editorBrowsableInfo));
    }
 
    private static ImmutableArray<T> RemoveOverriddenSymbolsWithinSet<T>(this ImmutableArray<T> symbols) where T : ISymbol
    {
        var overriddenSymbols = new MetadataUnifyingSymbolHashSet();
 
        foreach (var symbol in symbols)
        {
            var overriddenMember = symbol.GetOverriddenMember();
            if (overriddenMember != null && !overriddenSymbols.Contains(overriddenMember))
                overriddenSymbols.Add(overriddenMember);
        }
 
        return symbols.WhereAsArray(s => !overriddenSymbols.Contains(s));
    }
 
    public static ImmutableArray<T> FilterToVisibleAndBrowsableSymbolsAndNotUnsafeSymbols<T>(
        this ImmutableArray<T> symbols, bool hideAdvancedMembers, Compilation compilation) where T : ISymbol
    {
        return symbols.FilterToVisibleAndBrowsableSymbols(hideAdvancedMembers, compilation)
            .WhereAsArray(s => !s.RequiresUnsafeModifier());
    }
}