|
// 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,
};
}
|