|
// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.LanguageService;
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 CSharpSelectionValidator(
SemanticDocument document,
TextSpan textSpan,
bool localFunction) : SelectionValidator<CSharpSelectionResult, StatementSyntax>(document, textSpan)
{
private readonly bool _localFunction = localFunction;
public override async Task<(CSharpSelectionResult, OperationStatus)> GetValidSelectionAsync(CancellationToken cancellationToken)
{
if (!ContainsValidSelection)
return (null, OperationStatus.FailedWithUnknownReason);
var text = SemanticDocument.Text;
var root = SemanticDocument.Root;
var model = SemanticDocument.SemanticModel;
var doc = SemanticDocument;
// go through pipe line and calculate information about the user selection
var selectionInfo = GetInitialSelectionInfo(root, text);
selectionInfo = AssignInitialFinalTokens(selectionInfo, root, cancellationToken);
selectionInfo = AdjustFinalTokensBasedOnContext(selectionInfo, model, cancellationToken);
selectionInfo = AssignFinalSpan(selectionInfo, text);
selectionInfo = ApplySpecialCases(selectionInfo, text, SemanticDocument.SyntaxTree.Options, _localFunction);
selectionInfo = CheckErrorCasesAndAppendDescriptions(selectionInfo, root);
// there was a fatal error that we couldn't even do negative preview, return error result
if (selectionInfo.Status.Failed)
return (null, selectionInfo.Status);
var controlFlowSpan = GetControlFlowSpan(selectionInfo);
if (!selectionInfo.SelectionInExpression)
{
var statementRange = GetStatementRangeContainedInSpan<StatementSyntax>(root, controlFlowSpan, cancellationToken);
if (statementRange == null)
{
selectionInfo = selectionInfo.WithStatus(s => s.With(succeeded: false, CSharpFeaturesResources.Cannot_determine_valid_range_of_statements_to_extract));
return (null, selectionInfo.Status);
}
var isFinalSpanSemanticallyValid = IsFinalSpanSemanticallyValidSpan(model, controlFlowSpan, statementRange.Value, cancellationToken);
if (!isFinalSpanSemanticallyValid)
{
// check control flow only if we are extracting statement level, not expression
// level. you can not have goto that moves control out of scope in expression level
// (even in lambda)
selectionInfo = selectionInfo.WithStatus(s => s.With(succeeded: true, FeaturesResources.Not_all_code_paths_return));
}
}
var selectionChanged = selectionInfo.FirstTokenInOriginalSpan != selectionInfo.FirstTokenInFinalSpan || selectionInfo.LastTokenInOriginalSpan != selectionInfo.LastTokenInFinalSpan;
var result = await CSharpSelectionResult.CreateAsync(
selectionInfo.OriginalSpan,
selectionInfo.FinalSpan,
selectionInfo.SelectionInExpression,
doc,
selectionInfo.FirstTokenInFinalSpan,
selectionInfo.LastTokenInFinalSpan,
selectionChanged,
cancellationToken).ConfigureAwait(false);
return (result, selectionInfo.Status);
}
private SelectionInfo ApplySpecialCases(SelectionInfo selectionInfo, SourceText text, ParseOptions options, bool localFunction)
{
if (selectionInfo.Status.Failed)
return selectionInfo;
// 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 selectionInfo.WithStatus(s => s.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 selectionInfo.WithStatus(s => s.With(succeeded: false, CSharpFeaturesResources.Selection_cannot_include_global_statements));
}
if (_localFunction)
{
foreach (var ancestor in selectionInfo.CommonRootFromOriginalSpan.AncestorsAndSelf())
{
if (ancestor.Kind() is SyntaxKind.BaseConstructorInitializer or SyntaxKind.ThisConstructorInitializer)
return selectionInfo.WithStatus(s => s.With(succeeded: false, CSharpFeaturesResources.Selection_cannot_be_in_constructor_initializer));
if (ancestor is AnonymousFunctionExpressionSyntax)
break;
}
}
if (!selectionInfo.SelectionInExpression)
return selectionInfo;
var expressionNode = selectionInfo.FirstTokenInFinalSpan.GetCommonRoot(selectionInfo.LastTokenInFinalSpan);
if (expressionNode is not AssignmentExpressionSyntax assign)
return selectionInfo;
// make sure there is a visible token at right side expression
if (assign.Right.GetLastToken().Kind() == SyntaxKind.None)
return selectionInfo;
return AssignFinalSpan(selectionInfo
.With(s => s.FirstTokenInFinalSpan = assign.Right.GetFirstToken(includeZeroWidth: true))
.With(s => s.LastTokenInFinalSpan = assign.Right.GetLastToken(includeZeroWidth: true)), text);
bool IsCodeInGlobalLevel()
{
for (var current = selectionInfo.CommonRootFromOriginalSpan; 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 TextSpan GetControlFlowSpan(SelectionInfo selectionInfo)
=> TextSpan.FromBounds(selectionInfo.FirstTokenInFinalSpan.SpanStart, selectionInfo.LastTokenInFinalSpan.Span.End);
private static SelectionInfo AdjustFinalTokensBasedOnContext(
SelectionInfo 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.SelectionInExpression && !selectionInfo.SelectionInSingleStatement)
{
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.WithStatus(s => new OperationStatus(succeeded: false, CSharpFeaturesResources.Selection_does_not_contain_a_valid_node))
.With(s => s.FirstTokenInFinalSpan = default)
.With(s => s.LastTokenInFinalSpan = default);
}
firstValidNode = (firstValidNode.Parent is ExpressionStatementSyntax) ? firstValidNode.Parent : firstValidNode;
return selectionInfo.With(s => s.SelectionInExpression = firstValidNode is ExpressionSyntax)
.With(s => s.SelectionInSingleStatement = firstValidNode is StatementSyntax)
.With(s => s.FirstTokenInFinalSpan = firstValidNode.GetFirstToken(includeZeroWidth: true))
.With(s => s.LastTokenInFinalSpan = firstValidNode.GetLastToken(includeZeroWidth: true));
}
private SelectionInfo GetInitialSelectionInfo(SyntaxNode root, SourceText text)
{
var adjustedSpan = GetAdjustedSpan(text, 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 new SelectionInfo { Status = new OperationStatus(succeeded: false, FeaturesResources.Invalid_selection), OriginalSpan = adjustedSpan };
}
if (firstTokenInSelection.SpanStart > lastTokenInSelection.Span.End)
{
return new SelectionInfo
{
Status = new OperationStatus(succeeded: false, FeaturesResources.Selection_does_not_contain_a_valid_token),
OriginalSpan = adjustedSpan,
FirstTokenInOriginalSpan = firstTokenInSelection,
LastTokenInOriginalSpan = lastTokenInSelection
};
}
if (!UnderValidContext(firstTokenInSelection) || !UnderValidContext(lastTokenInSelection))
{
return new SelectionInfo
{
Status = new OperationStatus(succeeded: false, FeaturesResources.No_valid_selection_to_perform_extraction),
OriginalSpan = adjustedSpan,
FirstTokenInOriginalSpan = firstTokenInSelection,
LastTokenInOriginalSpan = lastTokenInSelection
};
}
var commonRoot = firstTokenInSelection.GetCommonRoot(lastTokenInSelection);
if (commonRoot == null)
{
return new SelectionInfo
{
Status = new OperationStatus(succeeded: false, FeaturesResources.No_common_root_node_for_extraction),
OriginalSpan = adjustedSpan,
FirstTokenInOriginalSpan = firstTokenInSelection,
LastTokenInOriginalSpan = lastTokenInSelection
};
}
if (!commonRoot.ContainedInValidType())
{
return new SelectionInfo
{
Status = new OperationStatus(succeeded: false, FeaturesResources.Selection_not_contained_inside_a_type),
OriginalSpan = adjustedSpan,
FirstTokenInOriginalSpan = firstTokenInSelection,
LastTokenInOriginalSpan = lastTokenInSelection
};
}
var selectionInExpression = commonRoot is ExpressionSyntax;
if (!selectionInExpression && !commonRoot.UnderValidContext())
{
return new SelectionInfo
{
Status = new OperationStatus(succeeded: false, FeaturesResources.No_valid_selection_to_perform_extraction),
OriginalSpan = adjustedSpan,
FirstTokenInOriginalSpan = firstTokenInSelection,
LastTokenInOriginalSpan = lastTokenInSelection
};
}
return new SelectionInfo
{
Status = OperationStatus.SucceededStatus,
OriginalSpan = adjustedSpan,
CommonRootFromOriginalSpan = commonRoot,
SelectionInExpression = selectionInExpression,
FirstTokenInOriginalSpan = firstTokenInSelection,
LastTokenInOriginalSpan = lastTokenInSelection
};
}
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 SelectionInfo CheckErrorCasesAndAppendDescriptions(
SelectionInfo selectionInfo,
SyntaxNode root)
{
if (selectionInfo.Status.Failed)
return selectionInfo;
if (selectionInfo.FirstTokenInFinalSpan.IsMissing || selectionInfo.LastTokenInFinalSpan.IsMissing)
{
selectionInfo = selectionInfo.WithStatus(s => s.With(succeeded: false, CSharpFeaturesResources.Contains_invalid_selection));
}
// get the node that covers the selection
var commonNode = selectionInfo.FirstTokenInFinalSpan.GetCommonRoot(selectionInfo.LastTokenInFinalSpan);
if ((selectionInfo.SelectionInExpression || selectionInfo.SelectionInSingleStatement) && commonNode.HasDiagnostics())
{
selectionInfo = selectionInfo.WithStatus(s => s.With(succeeded: false, CSharpFeaturesResources.The_selection_contains_syntactic_errors));
}
var tokens = root.DescendantTokens(selectionInfo.FinalSpan);
if (tokens.ContainPreprocessorCrossOver(selectionInfo.FinalSpan))
{
selectionInfo = selectionInfo.WithStatus(s => s.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.WithStatus(s => s.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.WithStatus(s => s.With(succeeded: true, CSharpFeaturesResources.Selection_can_not_contain_throw_statement));
}
if (selectionInfo.SelectionInExpression && commonNode.PartOfConstantInitializerExpression())
{
selectionInfo = selectionInfo.WithStatus(s => s.With(succeeded: false, CSharpFeaturesResources.Selection_can_not_be_part_of_constant_initializer_expression));
}
if (commonNode.IsUnsafeContext())
{
selectionInfo = selectionInfo.WithStatus(s => s.With(s.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.WithStatus(s => s.With(succeeded: false, CSharpFeaturesResources.Selection_can_not_contain_a_pattern_expression));
}
return selectionInfo;
}
private static SelectionInfo AssignInitialFinalTokens(SelectionInfo selectionInfo, SyntaxNode root, CancellationToken cancellationToken)
{
if (selectionInfo.Status.Failed)
return selectionInfo;
if (selectionInfo.SelectionInExpression)
{
// simple expression case
return selectionInfo.With(s => s.FirstTokenInFinalSpan = s.CommonRootFromOriginalSpan.GetFirstToken(includeZeroWidth: true))
.With(s => s.LastTokenInFinalSpan = s.CommonRootFromOriginalSpan.GetLastToken(includeZeroWidth: true));
}
var range = GetStatementRangeContainingSpan<StatementSyntax>(
CSharpSyntaxFacts.Instance,
root, TextSpan.FromBounds(selectionInfo.FirstTokenInOriginalSpan.SpanStart, selectionInfo.LastTokenInOriginalSpan.Span.End),
cancellationToken);
if (range == null)
{
return selectionInfo.WithStatus(s => s.With(succeeded: false, CSharpFeaturesResources.No_valid_statement_range_to_extract));
}
var statement1 = range.Value.Item1;
var statement2 = range.Value.Item2;
if (statement1 == statement2)
{
// check one more time to see whether it is an expression case
var expression = selectionInfo.CommonRootFromOriginalSpan.GetAncestor<ExpressionSyntax>();
if (expression != null && statement1.Span.Contains(expression.Span))
{
return selectionInfo.With(s => s.SelectionInExpression = true)
.With(s => s.FirstTokenInFinalSpan = expression.GetFirstToken(includeZeroWidth: true))
.With(s => s.LastTokenInFinalSpan = expression.GetLastToken(includeZeroWidth: true));
}
// single statement case
return selectionInfo.With(s => s.SelectionInSingleStatement = true)
.With(s => s.FirstTokenInFinalSpan = statement1.GetFirstToken(includeZeroWidth: true))
.With(s => s.LastTokenInFinalSpan = statement1.GetLastToken(includeZeroWidth: true));
}
// move only statements inside of the block
return selectionInfo.With(s => s.FirstTokenInFinalSpan = statement1.GetFirstToken(includeZeroWidth: true))
.With(s => s.LastTokenInFinalSpan = statement2.GetLastToken(includeZeroWidth: true));
}
private static SelectionInfo AssignFinalSpan(SelectionInfo selectionInfo, SourceText text)
{
if (selectionInfo.Status.Failed)
return selectionInfo;
// set final span
var start = (selectionInfo.FirstTokenInOriginalSpan == selectionInfo.FirstTokenInFinalSpan)
? Math.Min(selectionInfo.FirstTokenInOriginalSpan.SpanStart, selectionInfo.OriginalSpan.Start)
: selectionInfo.FirstTokenInFinalSpan.FullSpan.Start;
var end = (selectionInfo.LastTokenInOriginalSpan == selectionInfo.LastTokenInFinalSpan)
? Math.Max(selectionInfo.LastTokenInOriginalSpan.Span.End, selectionInfo.OriginalSpan.End)
: selectionInfo.LastTokenInFinalSpan.FullSpan.End;
return selectionInfo.With(s => s.FinalSpan = GetAdjustedSpan(text, TextSpan.FromBounds(start, end)));
}
public override bool ContainsNonReturnExitPointsStatements(IEnumerable<SyntaxNode> jumpsOutOfRegion)
=> jumpsOutOfRegion.Where(n => n is not ReturnStatementSyntax).Any();
public override IEnumerable<SyntaxNode> GetOuterReturnStatements(SyntaxNode commonRoot, IEnumerable<SyntaxNode> jumpsOutOfRegion)
{
var returnStatements = jumpsOutOfRegion.Where(s => s is ReturnStatementSyntax);
var container = commonRoot.GetAncestorsOrThis<SyntaxNode>().Where(a => a.IsReturnableConstruct()).FirstOrDefault();
if (container == null)
return [];
var returnableConstructPairs = returnStatements.Select(r => (r, r.GetAncestors<SyntaxNode>().Where(a => a.IsReturnableConstruct()).FirstOrDefault()))
.Where(p => p.Item2 != null);
// now filter return statements to only include the one under outmost container
return returnableConstructPairs.Where(p => p.Item2 == container).Select(p => p.Item1);
}
public override bool IsFinalSpanSemanticallyValidSpan(
SyntaxNode root, TextSpan textSpan,
IEnumerable<SyntaxNode> returnStatements, CancellationToken cancellationToken)
{
// return statement shouldn't contain any return value
if (returnStatements.Cast<ReturnStatementSyntax>().Any(r => r.Expression != null))
{
return false;
}
var lastToken = root.FindToken(textSpan.End);
if (lastToken.Kind() == SyntaxKind.None)
{
return false;
}
var container = lastToken.GetAncestors<SyntaxNode>().FirstOrDefault(n => n.IsReturnableConstruct());
if (container == null)
{
return false;
}
var body = container.GetBlockBody();
if (body == null)
{
return false;
}
// make sure that next token of the last token in the selection is the close braces of containing block
if (body.CloseBraceToken != lastToken.GetNextToken(includeZeroWidth: true))
{
return false;
}
// alright, for these constructs, it must be okay to be extracted
switch (container.Kind())
{
case SyntaxKind.AnonymousMethodExpression:
case SyntaxKind.SimpleLambdaExpression:
case SyntaxKind.ParenthesizedLambdaExpression:
return true;
}
// now, only method is okay to be extracted out
if (body.Parent is not MethodDeclarationSyntax method)
{
return false;
}
// make sure this method doesn't have return type.
return method.ReturnType is PredefinedTypeSyntax p &&
p.Keyword.Kind() == SyntaxKind.VoidKeyword;
}
private static TextSpan GetAdjustedSpan(SourceText text, TextSpan textSpan)
{
// 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);
}
}
|