/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Features/CSharp/Portable/ExtractMethod/CSharpSelectionValidator.cs
432 строки
19 KB
Cyrus Najmabadi
remove pointless tracking
31 дек 2024, 00:07
31 дек 2024, 00:07
7e87b4e
Код
Авторство
О чём код?
// 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); } } }