File: Debugging\AbstractBreakpointResolver.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Debugging;
 
internal abstract partial class AbstractBreakpointResolver
{
    // I believe this is a close approximation of the IVsDebugName string format produced 
    // by the native language service implementations:
    //
    //   C#: csharp\radmanaged\DebuggerInteraction\BreakpointNameResolver.cs
    //   VB: vb\Language\VsEditor\Debugging\VsLanguageDebugInfo.vb
    //
    // The one clear deviation from the native implementation is VB properties.  Resolving
    // the name of a property in VB used to return all the accessor methods (using their
    // metadata names) because setting a breakpoint directly on a property isn't supported
    // in VB.  In Roslyn, we'll keep things consistent and just return the property name.
    // This means that VB users won't be able to set breakpoints on property accessors using
    // Ctrl+B, but it would seem that a better solution to this problem would be to simply
    // enable setting breakpoints on all accessors by setting a breakpoint on the property
    // declaration (same as C# behavior).
    private static readonly SymbolDisplayFormat s_vsDebugNameFormat =
        new(
            globalNamespaceStyle:
                SymbolDisplayGlobalNamespaceStyle.Omitted,
            typeQualificationStyle:
                SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
            genericsOptions:
                SymbolDisplayGenericsOptions.IncludeTypeParameters,
            memberOptions:
                SymbolDisplayMemberOptions.IncludeContainingType |
                SymbolDisplayMemberOptions.IncludeParameters,
            parameterOptions:
                SymbolDisplayParameterOptions.IncludeOptionalBrackets |
                SymbolDisplayParameterOptions.IncludeType,
            propertyStyle:
                SymbolDisplayPropertyStyle.NameOnly,
            miscellaneousOptions:
                SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers |
                SymbolDisplayMiscellaneousOptions.UseSpecialTypes);
 
    protected readonly string Text;
    private readonly string _language;
    private readonly Solution _solution;
    private readonly IEqualityComparer<string> _identifierComparer;
 
    protected AbstractBreakpointResolver(
        Solution solution,
        string text,
        string language,
        IEqualityComparer<string> identifierComparer)
    {
        _solution = solution;
        Text = text;
        _language = language;
        _identifierComparer = identifierComparer;
    }
 
    protected abstract void ParseText(out IList<NameAndArity> nameParts, out int? parameterCount);
    protected abstract IEnumerable<ISymbol> GetMembers(INamedTypeSymbol type, string name);
    protected abstract bool HasMethodBody(IMethodSymbol method, CancellationToken cancellationToken);
 
    private BreakpointResolutionResult CreateBreakpoint(ISymbol methodSymbol)
    {
        var location = methodSymbol.Locations.First(loc => loc.IsInSource);
 
        var document = _solution.GetDocument(location.SourceTree);
        var textSpan = new TextSpan(location.SourceSpan.Start, 0);
        var vsDebugName = methodSymbol.ToDisplayString(s_vsDebugNameFormat);
 
        return BreakpointResolutionResult.CreateSpanResult(document, textSpan, vsDebugName);
    }
 
    public async Task<IEnumerable<BreakpointResolutionResult>> DoAsync(CancellationToken cancellationToken)
    {
        try
        {
            ParseText(out var nameParts, out var parameterCount);
 
            // Notes:  In C#, indexers can't be resolved by any name.  This is acceptable, because the old language
            //         service wasn't able to resolve them either.  In VB, parameterized properties will work in
            //         the same way as any other property.
            //         Destructors in C# can be resolved using the method name "Finalize". The resulting string
            //         representation will use C# language format ("C.~C()").  I verified that this works with
            //         "Break at Function" (breakpoint is correctly set and can be hit), so I don't see a reason
            //         to prohibit this (even though the old language service didn't support it).
            var members = await FindMembersAsync(nameParts, cancellationToken).ConfigureAwait(false);
 
            // Filter down the list of symbols to "applicable methods", specifically:
            // - "regular" methods
            // - constructors
            // - destructors
            // - properties
            // - operators?
            // - conversions?
            // where "applicable" means that the method or property represents a valid place to set a breakpoint
            // and that it has the expected number of parameters
            return members.Where(m => IsApplicable(m, parameterCount, cancellationToken)).
                Select(CreateBreakpoint).ToImmutableArrayOrEmpty();
        }
        catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken))
        {
            return [];
        }
    }
 
    private async Task<IEnumerable<ISymbol>> FindMembersAsync(
        IList<NameAndArity> nameParts, CancellationToken cancellationToken)
    {
        try
        {
            switch (nameParts.Count)
            {
                case 0:
                    // If there were no name parts, then we don't have any members to return.
                    // We only expect to hit this condition when the name provided does not parse.
                    return [];
 
                case 1:
                    // They're just searching for a method name.  Have to look through every type to find
                    // it.
                    return FindMembers(await GetAllTypesAsync(cancellationToken).ConfigureAwait(false), nameParts[0]);
 
                case 2:
                    // They have a type name and a method name.  Find a type with a matching name and a
                    // method in that type.
                    var types = await GetAllTypesAsync(cancellationToken).ConfigureAwait(false);
                    types = types.Where(t => MatchesName(t, nameParts[0], _identifierComparer));
                    return FindMembers(types, nameParts[1]);
 
                default:
                    // They have a namespace or nested type qualified name.  Walk up to the root namespace trying to match.
                    var containers = await _solution.GetGlobalNamespacesAsync(cancellationToken).ConfigureAwait(false);
                    return FindMembers(containers, [.. nameParts]);
            }
        }
        catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken))
        {
            return [];
        }
    }
 
    private static bool MatchesName(INamespaceOrTypeSymbol typeOrNamespace, NameAndArity nameAndArity, IEqualityComparer<string> comparer)
    {
        switch (typeOrNamespace)
        {
            case INamespaceSymbol namespaceSymbol:
                return comparer.Equals(namespaceSymbol.Name, nameAndArity.Name) && nameAndArity.Arity == 0;
            case INamedTypeSymbol typeSymbol:
                return comparer.Equals(typeSymbol.Name, nameAndArity.Name) &&
                    (nameAndArity.Arity == 0 || nameAndArity.Arity == typeSymbol.TypeArguments.Length);
            default:
                return false;
        }
    }
 
    private static bool MatchesNames(INamedTypeSymbol type, NameAndArity[] names, IEqualityComparer<string> comparer)
    {
        Debug.Assert(type != null);
        Debug.Assert(names.Length >= 2);
 
        INamespaceOrTypeSymbol container = type;
 
        // The last element in "names" is the method/property name, but we're only matching against types here,
        // so we'll skip the last one.
        for (var i = names.Length - 2; i >= 0; i--)
        {
            if (!MatchesName(container, names[i], comparer))
            {
                return false;
            }
 
            container = ((INamespaceOrTypeSymbol)container.ContainingType) ?? container.ContainingNamespace;
 
            // We ran out of containers to match against before we matched all the names, so this type isn't a match.
            if (container == null && i > 0)
            {
                return false;
            }
        }
 
        return true;
    }
 
    private IEnumerable<ISymbol> FindMembers(IEnumerable<INamespaceOrTypeSymbol> containers, params NameAndArity[] names)
    {
        // Recursively expand the list of containers to include all types in all nested containers, then filter down to a
        // set of candidate types by walking the up the enclosing containers matching by simple name.
        var types = containers.SelectMany(GetTypeMembersRecursive).Where(t => MatchesNames(t, names, _identifierComparer));
 
        var lastName = names.Last();
 
        return FindMembers(types, lastName);
    }
 
    private IEnumerable<ISymbol> FindMembers(IEnumerable<INamedTypeSymbol> types, NameAndArity nameAndArity)
    {
        // Get the matching members from all types (including constructors and explicit interface
        // implementations).  If there is a partial method/property, prefer returning the implementation over
        // the definition (since the definition will not be a candidate for setting a breakpoint).
        var members = types.SelectMany(t => GetMembers(t, nameAndArity.Name))
                           .Select(s => GetPartialImplementationPartOrNull(s) ?? s);
 
        return nameAndArity.Arity == 0
            ? members
            : members.OfType<IMethodSymbol>().Where(m => m.TypeParameters.Length == nameAndArity.Arity);
    }
 
    private async Task<IEnumerable<INamedTypeSymbol>> GetAllTypesAsync(CancellationToken cancellationToken)
    {
        var namespaces = await _solution.GetGlobalNamespacesAsync(cancellationToken).ConfigureAwait(false);
        return namespaces.GetAllTypes(cancellationToken);
    }
 
    private static ISymbol GetPartialImplementationPartOrNull(ISymbol symbol) => symbol.Kind switch
    {
        SymbolKind.Method => ((IMethodSymbol)symbol).PartialImplementationPart,
        SymbolKind.Property => ((IPropertySymbol)symbol).PartialImplementationPart,
        _ => null
    };
 
    /// <summary>
    /// Is this method or property a valid place to set a breakpoint and does it match the expected parameter count?
    /// </summary>
    private bool IsApplicable(ISymbol methodOrProperty, int? parameterCount, CancellationToken cancellationToken)
    {
        // You can only set a breakpoint on methods (including constructors/destructors) and properties.
        var kind = methodOrProperty.Kind;
        if (kind is not (SymbolKind.Method or SymbolKind.Property))
        {
            return false;
        }
 
        // You can't set a breakpoint on an abstract method or property.
        if (methodOrProperty.IsAbstract)
        {
            return false;
        }
 
        // If parameters were provided, check to make sure the method or property has the expected number
        // of parameters (but we don't actually validate the type or name of the supplied parameters).
        if (parameterCount != null)
        {
            var mismatch = IsMismatch(methodOrProperty, parameterCount);
 
            if (mismatch)
            {
                return false;
            }
        }
 
        // Finally, check to make sure we have source, and if we've got a method symbol, make sure it
        // has a body to set a breakpoint on.
        if ((methodOrProperty.Language == _language) && methodOrProperty.Locations.Any(static location => location.IsInSource))
        {
            if (methodOrProperty.IsKind(SymbolKind.Method))
            {
                return HasMethodBody((IMethodSymbol)methodOrProperty, cancellationToken);
            }
 
            // Non-abstract properties are always applicable, because you can set a breakpoint on the
            // accessor methods (get and/or set).
            return true;
        }
 
        return false;
    }
 
    private static bool IsMismatch(ISymbol methodOrProperty, int? parameterCount)
        => methodOrProperty switch
        {
            IMethodSymbol method => method.Parameters.Length != parameterCount,
            IPropertySymbol property => property.Parameters.Length != parameterCount,
            _ => false,
        };
 
    private static IEnumerable<INamedTypeSymbol> GetTypeMembersRecursive(INamespaceOrTypeSymbol container)
        => container switch
        {
            INamespaceSymbol namespaceSymbol => namespaceSymbol.GetMembers().SelectMany(GetTypeMembersRecursive),
            INamedTypeSymbol typeSymbol => typeSymbol.GetTypeMembers().SelectMany(GetTypeMembersRecursive).Concat(typeSymbol),
            _ => null,
        };
}