|
// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.ExtractMethod;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod;
internal sealed partial class CSharpExtractMethodService
{
internal sealed partial class CSharpSelectionValidator(
SemanticDocument document,
TextSpan textSpan,
bool localFunction) : SelectionValidator(document, textSpan)
{
private readonly bool _localFunction = localFunction;
protected override InitialSelectionInfo GetInitialSelectionInfo(CancellationToken cancellationToken)
{
var root = this.SemanticDocument.Root;
var adjustedSpan = GetAdjustedSpan(OriginalSpan);
var firstTokenInSelection = root.FindTokenOnRightOfPosition(adjustedSpan.Start, includeSkipped: false);
var lastTokenInSelection = root.FindTokenOnLeftOfPosition(adjustedSpan.End, includeSkipped: false);
if (firstTokenInSelection.Kind() == SyntaxKind.None || lastTokenInSelection.Kind() == SyntaxKind.None)
return InitialSelectionInfo.Failure(FeaturesResources.Invalid_selection);
var commonRoot = firstTokenInSelection.GetCommonRoot(lastTokenInSelection);
var selectionInExpression = commonRoot is ExpressionSyntax;
var statusReason = CheckSpan();
if (statusReason is not null)
return InitialSelectionInfo.Failure(statusReason);
return CreateInitialSelectionInfo(
selectionInExpression, firstTokenInSelection, lastTokenInSelection, cancellationToken);
string? CheckSpan()
{
if (firstTokenInSelection.SpanStart > lastTokenInSelection.Span.End)
return FeaturesResources.Selection_does_not_contain_a_valid_token;
if (!UnderValidContext(firstTokenInSelection) || !UnderValidContext(lastTokenInSelection))
return FeaturesResources.No_valid_selection_to_perform_extraction;
if (commonRoot == null)
return FeaturesResources.No_common_root_node_for_extraction;
if (!commonRoot.ContainedInValidType())
return FeaturesResources.Selection_not_contained_inside_a_type;
if (!selectionInExpression && !commonRoot.UnderValidContext())
return FeaturesResources.No_valid_selection_to_perform_extraction;
return null;
}
}
protected override FinalSelectionInfo UpdateSelectionInfo(
InitialSelectionInfo initialSelectionInfo,
CancellationToken cancellationToken)
{
var root = SemanticDocument.Root;
var model = SemanticDocument.SemanticModel;
// go through pipe line and calculate information about the user selection
var selectionInfo = AssignInitialFinalTokens(initialSelectionInfo);
selectionInfo = AdjustFinalTokensBasedOnContext(selectionInfo, model, cancellationToken);
selectionInfo = AssignFinalSpan(initialSelectionInfo, selectionInfo);
selectionInfo = ApplySpecialCases(initialSelectionInfo, selectionInfo, SemanticDocument.SyntaxTree.Options, _localFunction);
selectionInfo = CheckErrorCasesAndAppendDescriptions(selectionInfo, root);
return selectionInfo;
}
protected override async Task<SelectionResult> CreateSelectionResultAsync(
FinalSelectionInfo selectionInfo, CancellationToken cancellationToken)
{
Contract.ThrowIfFalse(ContainsValidSelection);
Contract.ThrowIfFalse(selectionInfo.Status.Succeeded);
return await CSharpSelectionResult.CreateAsync(
SemanticDocument, selectionInfo, cancellationToken).ConfigureAwait(false);
}
private FinalSelectionInfo ApplySpecialCases(
InitialSelectionInfo initialSelectionInfo, FinalSelectionInfo finalSelectionInfo, ParseOptions options, bool localFunction)
{
if (finalSelectionInfo.Status.Failed)
return finalSelectionInfo;
// If we're under a global statement (and not inside an inner lambda/local-function) then there are restrictions
// on if we can extract a method vs a local function.
if (IsCodeInGlobalLevel())
{
// Cannot extract a method from a top-level statement in normal code
if (!localFunction && options is { Kind: SourceCodeKind.Regular })
return finalSelectionInfo with { Status = finalSelectionInfo.Status.With(succeeded: false, CSharpFeaturesResources.Selection_cannot_include_top_level_statements) };
// Cannot extract a local function from a global statement in script code
if (localFunction && options is { Kind: SourceCodeKind.Script })
return finalSelectionInfo with { Status = finalSelectionInfo.Status.With(succeeded: false, CSharpFeaturesResources.Selection_cannot_include_global_statements) };
}
if (_localFunction)
{
foreach (var ancestor in initialSelectionInfo.CommonRoot.AncestorsAndSelf())
{
if (ancestor.Kind() is SyntaxKind.BaseConstructorInitializer or SyntaxKind.ThisConstructorInitializer)
return finalSelectionInfo with { Status = finalSelectionInfo.Status.With(succeeded: false, CSharpFeaturesResources.Selection_cannot_be_in_constructor_initializer) };
if (ancestor is AnonymousFunctionExpressionSyntax)
break;
}
}
if (!finalSelectionInfo.SelectionInExpression)
return finalSelectionInfo;
var expressionNode = finalSelectionInfo.FirstTokenInFinalSpan.GetCommonRoot(finalSelectionInfo.LastTokenInFinalSpan);
if (expressionNode is not AssignmentExpressionSyntax assign)
return finalSelectionInfo;
// make sure there is a visible token at right side expression
if (assign.Right.GetLastToken().Kind() == SyntaxKind.None)
return finalSelectionInfo;
return AssignFinalSpan(initialSelectionInfo, finalSelectionInfo with
{
FirstTokenInFinalSpan = assign.Right.GetFirstToken(includeZeroWidth: true),
LastTokenInFinalSpan = assign.Right.GetLastToken(includeZeroWidth: true),
});
bool IsCodeInGlobalLevel()
{
for (var current = initialSelectionInfo.CommonRoot; current != null; current = current.Parent)
{
if (current is CompilationUnitSyntax)
return true;
if (current is GlobalStatementSyntax)
return true;
if (current is AnonymousFunctionExpressionSyntax or LocalFunctionStatementSyntax or MemberDeclarationSyntax)
return false;
}
throw ExceptionUtilities.Unreachable();
}
}
private static FinalSelectionInfo AdjustFinalTokensBasedOnContext(
FinalSelectionInfo selectionInfo,
SemanticModel semanticModel,
CancellationToken cancellationToken)
{
if (selectionInfo.Status.Failed)
return selectionInfo;
// don't need to adjust anything if it is multi-statements case
if (selectionInfo.GetSelectionType() == SelectionType.MultipleStatements)
return selectionInfo;
// get the node that covers the selection
var node = selectionInfo.FirstTokenInFinalSpan.GetCommonRoot(selectionInfo.LastTokenInFinalSpan);
var validNode = Check(semanticModel, node, cancellationToken);
if (validNode)
{
return selectionInfo;
}
var firstValidNode = node.GetAncestors<SyntaxNode>().FirstOrDefault(n => Check(semanticModel, n, cancellationToken));
if (firstValidNode == null)
{
// couldn't find any valid node
return selectionInfo with
{
Status = new(succeeded: false, CSharpFeaturesResources.Selection_does_not_contain_a_valid_node),
FirstTokenInFinalSpan = default,
LastTokenInFinalSpan = default,
};
}
firstValidNode = (firstValidNode.Parent is ExpressionStatementSyntax) ? firstValidNode.Parent : firstValidNode;
return selectionInfo with
{
SelectionInExpression = firstValidNode is ExpressionSyntax,
FirstTokenInFinalSpan = firstValidNode.GetFirstToken(includeZeroWidth: true),
LastTokenInFinalSpan = firstValidNode.GetLastToken(includeZeroWidth: true),
};
}
private static bool UnderValidContext(SyntaxToken token)
=> token.GetAncestors<SyntaxNode>().Any(n => CheckTopLevel(n, token.Span));
private static bool CheckTopLevel(SyntaxNode node, TextSpan span)
{
switch (node)
{
case BlockSyntax block:
return ContainsInBlockBody(block, span);
case ArrowExpressionClauseSyntax expressionBodiedMember:
return ContainsInExpressionBodiedMemberBody(expressionBodiedMember, span);
case FieldDeclarationSyntax field:
{
foreach (var variable in field.Declaration.Variables)
{
if (variable.Initializer != null && variable.Initializer.Span.Contains(span))
{
return true;
}
}
break;
}
case GlobalStatementSyntax:
return true;
case ConstructorInitializerSyntax constructorInitializer:
return constructorInitializer.ContainsInArgument(span);
case PrimaryConstructorBaseTypeSyntax primaryConstructorBaseType:
return primaryConstructorBaseType.ArgumentList.Arguments.FullSpan.Contains(span);
}
return false;
}
private static bool ContainsInBlockBody(BlockSyntax block, TextSpan textSpan)
{
if (block == null)
{
return false;
}
var blockSpan = TextSpan.FromBounds(block.OpenBraceToken.Span.End, block.CloseBraceToken.SpanStart);
return blockSpan.Contains(textSpan);
}
private static bool ContainsInExpressionBodiedMemberBody(ArrowExpressionClauseSyntax expressionBodiedMember, TextSpan textSpan)
{
if (expressionBodiedMember == null)
{
return false;
}
var expressionBodiedMemberBody = TextSpan.FromBounds(expressionBodiedMember.Expression.SpanStart, expressionBodiedMember.Expression.Span.End);
return expressionBodiedMemberBody.Contains(textSpan);
}
private static FinalSelectionInfo CheckErrorCasesAndAppendDescriptions(
FinalSelectionInfo selectionInfo,
SyntaxNode root)
{
if (selectionInfo.Status.Failed)
return selectionInfo;
if (selectionInfo.FirstTokenInFinalSpan.IsMissing || selectionInfo.LastTokenInFinalSpan.IsMissing)
{
selectionInfo = selectionInfo with
{
Status = selectionInfo.Status.With(succeeded: false, CSharpFeaturesResources.Contains_invalid_selection)
};
}
// get the node that covers the selection
var commonNode = selectionInfo.FirstTokenInFinalSpan.GetCommonRoot(selectionInfo.LastTokenInFinalSpan);
if (selectionInfo.GetSelectionType() != SelectionType.MultipleStatements && commonNode.HasDiagnostics())
{
selectionInfo = selectionInfo with
{
Status = selectionInfo.Status.With(succeeded: false, CSharpFeaturesResources.The_selection_contains_syntactic_errors),
};
}
var tokens = root.DescendantTokens(selectionInfo.FinalSpan);
if (tokens.ContainPreprocessorCrossOver(selectionInfo.FinalSpan))
{
selectionInfo = selectionInfo with
{
Status = selectionInfo.Status.With(succeeded: true, CSharpFeaturesResources.Selection_can_not_cross_over_preprocessor_directives),
};
}
// TODO : check whether this can be handled by control flow analysis engine
if (tokens.Any(t => t.Kind() == SyntaxKind.YieldKeyword))
{
selectionInfo = selectionInfo with
{
Status = selectionInfo.Status.With(succeeded: true, CSharpFeaturesResources.Selection_can_not_contain_a_yield_statement),
};
}
// TODO : check behavior of control flow analysis engine around exception and exception handling.
if (tokens.ContainArgumentlessThrowWithoutEnclosingCatch(selectionInfo.FinalSpan))
{
selectionInfo = selectionInfo with
{
Status = selectionInfo.Status.With(succeeded: true, CSharpFeaturesResources.Selection_can_not_contain_throw_statement),
};
}
if (selectionInfo.SelectionInExpression && commonNode.PartOfConstantInitializerExpression())
{
selectionInfo = selectionInfo with
{
Status = selectionInfo.Status.With(succeeded: false, CSharpFeaturesResources.Selection_can_not_be_part_of_constant_initializer_expression),
};
}
if (commonNode.IsUnsafeContext())
{
selectionInfo = selectionInfo with
{
Status = selectionInfo.Status.With(selectionInfo.Status.Succeeded, CSharpFeaturesResources.The_selected_code_is_inside_an_unsafe_context),
};
}
// For now patterns are being blanket disabled for extract method. This issue covers designing extractions for them
// and re-enabling this.
// https://github.com/dotnet/roslyn/issues/9244
if (commonNode.Kind() == SyntaxKind.IsPatternExpression)
{
selectionInfo = selectionInfo with
{
Status = selectionInfo.Status.With(succeeded: false, CSharpFeaturesResources.Selection_can_not_contain_a_pattern_expression),
};
}
return selectionInfo;
}
private static FinalSelectionInfo AssignInitialFinalTokens(
InitialSelectionInfo selectionInfo)
{
if (selectionInfo.SelectionInExpression)
{
// simple expression case
return new()
{
Status = selectionInfo.Status,
SelectionInExpression = true,
FirstTokenInFinalSpan = selectionInfo.CommonRoot.GetFirstToken(includeZeroWidth: true),
LastTokenInFinalSpan = selectionInfo.CommonRoot.GetLastToken(includeZeroWidth: true),
};
}
var (firstStatement, lastStatement) = (selectionInfo.FirstStatement, selectionInfo.LastStatement);
Contract.ThrowIfNull(firstStatement);
Contract.ThrowIfNull(lastStatement);
if (firstStatement == lastStatement)
{
// check one more time to see whether it is an expression case
var expression = selectionInfo.CommonRoot.GetAncestor<ExpressionSyntax>();
if (expression != null && firstStatement.Span.Contains(expression.Span))
{
return new()
{
Status = selectionInfo.Status,
SelectionInExpression = true,
FirstTokenInFinalSpan = expression.GetFirstToken(includeZeroWidth: true),
LastTokenInFinalSpan = expression.GetLastToken(includeZeroWidth: true),
};
}
// single statement case
return new()
{
Status = selectionInfo.Status,
FirstTokenInFinalSpan = firstStatement.GetFirstToken(includeZeroWidth: true),
LastTokenInFinalSpan = firstStatement.GetLastToken(includeZeroWidth: true),
};
}
// move only statements inside of the block
return new()
{
Status = selectionInfo.Status,
FirstTokenInFinalSpan = firstStatement.GetFirstToken(includeZeroWidth: true),
LastTokenInFinalSpan = lastStatement.GetLastToken(includeZeroWidth: true),
};
}
protected override TextSpan GetAdjustedSpan(TextSpan textSpan)
{
var text = this.SemanticDocument.Text;
// beginning of a file
if (textSpan.IsEmpty || textSpan.End == 0)
{
return textSpan;
}
// if it is a start of new line, make it belong to previous line
var line = text.Lines.GetLineFromPosition(textSpan.End);
if (line.Start != textSpan.End)
{
return textSpan;
}
// get previous line
Contract.ThrowIfFalse(line.LineNumber > 0);
var previousLine = text.Lines[line.LineNumber - 1];
// if the span is past the end of the line (ie, in whitespace) then
// return to the end of the line including whitespace
if (textSpan.Start > previousLine.End)
{
return TextSpan.FromBounds(textSpan.Start, previousLine.EndIncludingLineBreak);
}
return TextSpan.FromBounds(textSpan.Start, previousLine.End);
}
}
}
|