|
// 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.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Debugging;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Shared.Extensions;
namespace Microsoft.CodeAnalysis.CSharp.Debugging;
/// <summary>
/// Given a position in a source file, returns the expressions in close proximity that should
/// show up in the debugger 'autos' window. In general, the expressions we place into the autos
/// window are things that appear to be 'side effect free'. Note: because we only use the syntax
/// tree for this, it's possible for us to get this wrong. However, this should only happen in
/// code that behaves unexpectedly. For example, we will assume that "a + b" is side effect free
/// (when in practice it may not be).
///
/// The general tactic we take is to add the expressions for the statements on the
/// line the debugger is currently at. We will also try to find the 'previous' statement as well
/// to add the expressions from that. The 'previous' statement is a bit of an interesting beast.
/// Consider, for example, if the user has just jumped out of a switch and is the statement
/// directly following it. What is the previous statement? Without keeping state, there's no way
/// to know. So, in this case, we treat all 'exit points' (i.e. the last statement of a switch
/// section) of the switch statement as the 'previous statement'. There are many cases like this
/// we need to handle. Basically anything that might have nested statements/blocks might
/// contribute to the 'previous statement'
/// </summary>
[ExportLanguageService(typeof(IProximityExpressionsService), LanguageNames.CSharp), Shared]
internal sealed partial class CSharpProximityExpressionsService : IProximityExpressionsService
{
[ImportingConstructor]
[SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
public CSharpProximityExpressionsService()
{
}
public async Task<bool> IsValidAsync(
Document document,
int position,
string expressionValue,
CancellationToken cancellationToken)
{
var expression = SyntaxFactory.ParseExpression(expressionValue);
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var token = root.FindToken(position);
if (token.Kind() == SyntaxKind.CloseBraceToken && token.GetPreviousToken().Kind() != SyntaxKind.None)
{
token = token.GetPreviousToken();
}
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
var info = semanticModel.GetSpeculativeSymbolInfo(token.SpanStart, expression, SpeculativeBindingOption.BindAsExpression);
if (info.Symbol == null)
{
return false;
}
// We seem to have bound successfully. However, if it bound to a local, then make
// sure that that local isn't after the statement that we're currently looking at.
if (info.Symbol.Kind == SymbolKind.Local)
{
var statement = info.Symbol.Locations.First().FindToken(cancellationToken).GetAncestor<StatementSyntax>();
if (statement != null && position < statement.SpanStart)
{
return false;
}
}
return true;
}
/// <summary>
/// Returns null indicating a failure.
/// </summary>
public async Task<IList<string>> GetProximityExpressionsAsync(
Document document,
int position,
CancellationToken cancellationToken)
{
try
{
var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
return GetProximityExpressions(tree, position, cancellationToken);
}
catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken))
{
return null;
}
}
public static IList<string> GetProximityExpressions(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken)
=> new Worker(syntaxTree, position).Do(cancellationToken);
[Obsolete($"Use {nameof(GetProximityExpressions)}.")]
private static IList<string> Do(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken)
=> new Worker(syntaxTree, position).Do(cancellationToken);
private static void AddRelevantExpressions(
StatementSyntax statement,
IList<string> expressions,
bool includeDeclarations)
{
new RelevantExpressionsCollector(includeDeclarations, expressions).Visit(statement);
}
}
|