diff --git a/src/Features/CSharp/Portable/SplitIntoNestedIfStatements/CSharpMergeNestedIfStatementsCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/SplitIntoNestedIfStatements/CSharpMergeNestedIfStatementsCodeRefactoringProvider.cs new file mode 100644 index 0000000000000000000000000000000000000000..a1a4865c31d1ece7aea0556eabdc6eb327ff341b --- /dev/null +++ b/src/Features/CSharp/Portable/SplitIntoNestedIfStatements/CSharpMergeNestedIfStatementsCodeRefactoringProvider.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using System.Composition; +using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.SplitIntoNestedIfStatements; + +namespace Microsoft.CodeAnalysis.CSharp.SplitIntoNestedIfStatements +{ + [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.MergeNestedIfStatements), Shared] + internal sealed class CSharpMergeNestedIfStatementsCodeRefactoringProvider + : AbstractMergeNestedIfStatementsCodeRefactoringProvider + { + protected override string IfKeywordText => SyntaxFacts.GetText(SyntaxKind.IfKeyword); + + protected override bool IsTokenOfIfStatement(SyntaxToken token, out IfStatementSyntax ifStatement) + { + if (token.Parent is IfStatementSyntax s) + { + ifStatement = s; + return true; + } + + ifStatement = null; + return false; + } + + protected override bool IsFirstStatementOfIfStatement(SyntaxNode statement, out IfStatementSyntax ifStatement) + { + if (statement.Parent is IfStatementSyntax s1 && s1.Statement == statement) + { + ifStatement = s1; + return true; + } + + if (statement.Parent is BlockSyntax block && block.Statements.FirstOrDefault() == statement && + block.Parent is IfStatementSyntax s2 && s2.Statement == block) + { + ifStatement = s2; + return true; + } + + ifStatement = null; + return false; + } + + protected override ImmutableArray GetElseClauses(IfStatementSyntax ifStatement) + { + return ImmutableArray.Create(ifStatement.Else); + } + + protected override IfStatementSyntax MergeIfStatements(IfStatementSyntax outerIfStatement, IfStatementSyntax innerIfStatement, SyntaxGenerator generator) + { + var newCondition = SyntaxFactory.BinaryExpression( + SyntaxKind.LogicalAndExpression, + (ExpressionSyntax)generator.AddParentheses(outerIfStatement.Condition), + (ExpressionSyntax)generator.AddParentheses(innerIfStatement.Condition)); + return outerIfStatement.WithCondition(newCondition).WithStatement(innerIfStatement.Statement); + } + } +} diff --git a/src/Features/Core/Portable/CodeRefactorings/PredefinedCodeRefactoringProviderNames.cs b/src/Features/Core/Portable/CodeRefactorings/PredefinedCodeRefactoringProviderNames.cs index 073ab8471771ae615d85e2c1fa9a7ae04d10c0d9..bfd8494708322283b77b7cf4f3421cee586fb3c5 100644 --- a/src/Features/Core/Portable/CodeRefactorings/PredefinedCodeRefactoringProviderNames.cs +++ b/src/Features/Core/Portable/CodeRefactorings/PredefinedCodeRefactoringProviderNames.cs @@ -26,6 +26,7 @@ internal static class PredefinedCodeRefactoringProviderNames public const string MoveDeclarationNearReference = "Move Declaration Near Reference Code Action Provider"; public const string SimplifyLambda = "Simplify Lambda Code Action Provider"; public const string ConvertToInterpolatedString = "Convert To Interpolated String Code Action Provider"; + public const string MergeNestedIfStatements = "Merge Nested If Statements Code Action Provider"; public const string MoveTypeToFile = "Move Type To File Code Action Provider"; public const string ReplaceDocCommentTextWithTag = "Replace Documentation Comment Text With Tag Code Action Provider"; public const string SplitIntoNestedIfStatements = "Split Into Nested If Statements Code Action Provider"; diff --git a/src/Features/Core/Portable/FeaturesResources.Designer.cs b/src/Features/Core/Portable/FeaturesResources.Designer.cs index e2d6a14262495bcceeae56fde6ce381eb315dde4..c35f763d1becd42bc82d77afa7af299cd2bbac0c 100644 --- a/src/Features/Core/Portable/FeaturesResources.Designer.cs +++ b/src/Features/Core/Portable/FeaturesResources.Designer.cs @@ -2233,6 +2233,15 @@ internal class FeaturesResources { } } + /// + /// Looks up a localized string similar to Merge nested '{0}' statements. + /// + internal static string Merge_nested_0_statements { + get { + return ResourceManager.GetString("Merge_nested_0_statements", resourceCulture); + } + } + /// /// Looks up a localized string similar to method. /// diff --git a/src/Features/Core/Portable/FeaturesResources.resx b/src/Features/Core/Portable/FeaturesResources.resx index f4bdee5093e7b6d170a1a42477422f3de3a27d23..5fdaabd61a22cd808d6d40b0746acb6865111052 100644 --- a/src/Features/Core/Portable/FeaturesResources.resx +++ b/src/Features/Core/Portable/FeaturesResources.resx @@ -1472,4 +1472,7 @@ This version used in: {2} Split into nested '{0}' statements + + Merge nested '{0}' statements + \ No newline at end of file diff --git a/src/Features/Core/Portable/SplitIntoNestedIfStatements/AbstractMergeNestedIfStatementsCodeRefactoringProvider.cs b/src/Features/Core/Portable/SplitIntoNestedIfStatements/AbstractMergeNestedIfStatementsCodeRefactoringProvider.cs new file mode 100644 index 0000000000000000000000000000000000000000..4cfb0d2e63dc9f54ec8e1285768d838ed6f69126 --- /dev/null +++ b/src/Features/Core/Portable/SplitIntoNestedIfStatements/AbstractMergeNestedIfStatementsCodeRefactoringProvider.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.LanguageServices; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.Utilities; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.SplitIntoNestedIfStatements +{ + internal abstract class AbstractMergeNestedIfStatementsCodeRefactoringProvider< + TIfStatementSyntax> : CodeRefactoringProvider + where TIfStatementSyntax : SyntaxNode + { + protected abstract string IfKeywordText { get; } + + protected abstract bool IsTokenOfIfStatement(SyntaxToken token, out TIfStatementSyntax ifStatement); + + protected abstract bool IsFirstStatementOfIfStatement(SyntaxNode statement, out TIfStatementSyntax ifStatement); + + protected abstract ImmutableArray GetElseClauses(TIfStatementSyntax ifStatement); + + protected abstract TIfStatementSyntax MergeIfStatements(TIfStatementSyntax outerIfStatement, TIfStatementSyntax innerIfStatement, SyntaxGenerator generator); + + public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var token = root.FindToken(context.Span.Start); + + if (context.Span.Length > 0 && + context.Span != token.Span) + { + return; + } + + if (IsTokenOfIfStatement(token, out var ifStatement) && + IsFirstStatementOfIfStatement(ifStatement, out var parentIfStatement) && + await CanBeMergedAsync(context.Document, parentIfStatement, ifStatement, context.CancellationToken)) + { + context.RegisterRefactoring( + new MyCodeAction( + c => FixAsync(context.Document, context.Span, c), + IfKeywordText)); + } + } + + private async Task FixAsync(Document document, TextSpan span, CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var token = root.FindToken(span.Start); + + Contract.ThrowIfFalse(IsTokenOfIfStatement(token, out var ifStatement)); + Contract.ThrowIfFalse(IsFirstStatementOfIfStatement(ifStatement, out var parentIfStatement)); + + var newIfStatement = MergeIfStatements(parentIfStatement, ifStatement, document.GetLanguageService()); + + var newRoot = root.ReplaceNode(parentIfStatement, newIfStatement.WithAdditionalAnnotations(Formatter.Annotation)); + return document.WithSyntaxRoot(newRoot); + } + + private async Task CanBeMergedAsync( + Document document, + TIfStatementSyntax outerIfStatement, + TIfStatementSyntax innerIfStatement, + CancellationToken cancellationToken) + { + var syntaxFacts = document.GetLanguageService(); + + if (!GetElseClauses(outerIfStatement).SequenceEqual(GetElseClauses(innerIfStatement), syntaxFacts.AreEquivalent)) + { + return false; + } + + var statements = syntaxFacts.GetStatementContainerStatements(innerIfStatement.Parent); + if (statements.Count == 1) + { + // There are no other statements below the inner if statement. Merging is OK. + return true; + } + else + { + // There are statements below the inner if statement. We can merge if + // 1. these statements exist below the outer if as well, and + // 2. control flow can't reach after the end of these statements (otherwise, they would get executed twice). + // This will typically look like a single return, break, continue or a throw statement. + + if (!syntaxFacts.IsStatementContainer(outerIfStatement.Parent)) + { + // This shouldn't happen, but let's be cautious. + return false; + } + + var outerStatements = syntaxFacts.GetStatementContainerStatements(outerIfStatement.Parent); + var outerIfStatementIndex = outerStatements.IndexOf(outerIfStatement); + + var remainingStatements = statements.Skip(1); + var remainingOuterStatements = outerStatements.Skip(outerIfStatementIndex + 1); + + if (!remainingStatements.SequenceEqual(remainingOuterStatements.Take(statements.Count - 1), syntaxFacts.AreEquivalent)) + { + return false; + } + + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var controlFlow = semanticModel.AnalyzeControlFlow(statements.First(), statements.Last()); + + return !controlFlow.EndPointIsReachable; + } + } + + private sealed class MyCodeAction : CodeAction.DocumentChangeAction + { + public MyCodeAction(Func> createChangedDocument, string ifKeywordText) + : base(string.Format(FeaturesResources.Merge_nested_0_statements, ifKeywordText), createChangedDocument) + { + } + } + } +} diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf index ca18803a23c1617e06380239ca81f3f44eced427..684876e6bbf97ccaaf94322b114e4cbb2c4b009e 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf @@ -107,6 +107,11 @@ Introduce query variable + + Merge nested '{0}' statements + Merge nested '{0}' statements + + Private member '{0}' can be removed as the value assigned to it is never read. Private member '{0}' can be removed as the value assigned to it is never read. diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf index 03796a2b27bb0911ae8c752ff443d0cee091a911..f377b0b256d2c711d61f0eee7ee4c2a374394c72 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf @@ -107,6 +107,11 @@ Introduce query variable + + Merge nested '{0}' statements + Merge nested '{0}' statements + + Private member '{0}' can be removed as the value assigned to it is never read. Private member '{0}' can be removed as the value assigned to it is never read. diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf index 19db20bff2fad012f151cfcbea9838a9bc0a96d0..e36206426a147fe0b43c6ccc6f61f76b77b3a95a 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf @@ -107,6 +107,11 @@ Introduce query variable + + Merge nested '{0}' statements + Merge nested '{0}' statements + + Private member '{0}' can be removed as the value assigned to it is never read. Private member '{0}' can be removed as the value assigned to it is never read. diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf index 4b184e9c28bc923b9aa4e06b5005c692a23b7816..d480b58ac396e3220e7b29acda6d2caf6b631101 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf @@ -107,6 +107,11 @@ Introduce query variable + + Merge nested '{0}' statements + Merge nested '{0}' statements + + Private member '{0}' can be removed as the value assigned to it is never read. Private member '{0}' can be removed as the value assigned to it is never read. diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf index 59ff49ee6cc312c78fa1c0ae70c157ba8e81ff2b..42c5dd60fb58cc12dd2729860831973c4afe1354 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf @@ -107,6 +107,11 @@ Introduce query variable + + Merge nested '{0}' statements + Merge nested '{0}' statements + + Private member '{0}' can be removed as the value assigned to it is never read. Private member '{0}' can be removed as the value assigned to it is never read. diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf index 17b9deb5bf691f6c9e3555e3078cb3125f7cdb31..4df2699e961bee4be3744375553787e765dee7a6 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf @@ -107,6 +107,11 @@ Introduce query variable + + Merge nested '{0}' statements + Merge nested '{0}' statements + + Private member '{0}' can be removed as the value assigned to it is never read. Private member '{0}' can be removed as the value assigned to it is never read. diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf index 53940ad5aeadff1ecfe517d5f293933968b24903..b100ed73442402e08fdd2bf12b83767c515040f9 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf @@ -107,6 +107,11 @@ Introduce query variable + + Merge nested '{0}' statements + Merge nested '{0}' statements + + Private member '{0}' can be removed as the value assigned to it is never read. Private member '{0}' can be removed as the value assigned to it is never read. diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf index 7105740303d320ececfa06870ecd181c3ddcea9f..7b8f2ebe24edb996cbe79bd4386f715198783e7b 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf @@ -107,6 +107,11 @@ Introduce query variable + + Merge nested '{0}' statements + Merge nested '{0}' statements + + Private member '{0}' can be removed as the value assigned to it is never read. Private member '{0}' can be removed as the value assigned to it is never read. diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf index f869d3502cfbc7c0f7e975056f7fb252e5581be4..3085b28a1244ef335297396853cb3670ea766b7f 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf @@ -107,6 +107,11 @@ Introduce query variable + + Merge nested '{0}' statements + Merge nested '{0}' statements + + Private member '{0}' can be removed as the value assigned to it is never read. Private member '{0}' can be removed as the value assigned to it is never read. diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf index 01643680f7ffca8ca89381fecfc89cd139227744..99e3c6b267099ae05d8da080d614e32e5055faf4 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf @@ -107,6 +107,11 @@ Introduce query variable + + Merge nested '{0}' statements + Merge nested '{0}' statements + + Private member '{0}' can be removed as the value assigned to it is never read. Private member '{0}' can be removed as the value assigned to it is never read. diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf index 24125afb0af1cc92015b386b6d6f554ca1e4c816..2765bf3681428892d5e403c5e2b68b716082b08c 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf @@ -107,6 +107,11 @@ Introduce query variable + + Merge nested '{0}' statements + Merge nested '{0}' statements + + Private member '{0}' can be removed as the value assigned to it is never read. Private member '{0}' can be removed as the value assigned to it is never read. diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf index 6e861d7fcacf6d3984157f6dd10b2a2f39e3ee4e..1889f2d5ed40e23754b2c5a0f7cf0521276816e5 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf @@ -107,6 +107,11 @@ Introduce query variable + + Merge nested '{0}' statements + Merge nested '{0}' statements + + Private member '{0}' can be removed as the value assigned to it is never read. Private member '{0}' can be removed as the value assigned to it is never read. diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf index b96dfed7870be036a84b2106af51bb1feac79367..da54fff810cc46f2107b23cc694166edff076e3c 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf @@ -107,6 +107,11 @@ Introduce query variable + + Merge nested '{0}' statements + Merge nested '{0}' statements + + Private member '{0}' can be removed as the value assigned to it is never read. Private member '{0}' can be removed as the value assigned to it is never read. diff --git a/src/Features/VisualBasic/Portable/SplitIntoNestedIfStatements/VisualBasicMergeNestedIfStatementsCodeRefactoringProvider.vb b/src/Features/VisualBasic/Portable/SplitIntoNestedIfStatements/VisualBasicMergeNestedIfStatementsCodeRefactoringProvider.vb new file mode 100644 index 0000000000000000000000000000000000000000..995784e3018920497cb3ed202d5f1502ee93ac5d --- /dev/null +++ b/src/Features/VisualBasic/Portable/SplitIntoNestedIfStatements/VisualBasicMergeNestedIfStatementsCodeRefactoringProvider.vb @@ -0,0 +1,49 @@ +' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +Imports System.Collections.Immutable +Imports System.Composition +Imports Microsoft.CodeAnalysis.CodeRefactorings +Imports Microsoft.CodeAnalysis.Editing +Imports Microsoft.CodeAnalysis.SplitIntoNestedIfStatements +Imports Microsoft.CodeAnalysis.VisualBasic.Syntax + +Namespace Microsoft.CodeAnalysis.VisualBasic.SplitIntoNestedIfStatements + + Friend NotInheritable Class VisualBasicMergeNestedIfStatementsCodeRefactoringProvider + Inherits AbstractMergeNestedIfStatementsCodeRefactoringProvider(Of MultiLineIfBlockSyntax) + + Protected Overrides ReadOnly Property IfKeywordText As String = SyntaxFacts.GetText(SyntaxKind.IfKeyword) + + Protected Overrides Function IsTokenOfIfStatement(token As SyntaxToken, ByRef ifStatement As MultiLineIfBlockSyntax) As Boolean + If TypeOf token.Parent Is IfStatementSyntax AndAlso TypeOf token.Parent.Parent Is MultiLineIfBlockSyntax Then + ifStatement = DirectCast(token.Parent.Parent, MultiLineIfBlockSyntax) + Return True + End If + + ifStatement = Nothing + Return False + End Function + + Protected Overrides Function IsFirstStatementOfIfStatement(statement As SyntaxNode, ByRef ifStatement As MultiLineIfBlockSyntax) As Boolean + If TypeOf statement.Parent Is MultiLineIfBlockSyntax Then + ifStatement = DirectCast(statement.Parent, MultiLineIfBlockSyntax) + Return ifStatement.Statements.FirstOrDefault() Is statement + End If + + ifStatement = Nothing + Return False + End Function + + Protected Overrides Function GetElseClauses(ifStatement As MultiLineIfBlockSyntax) As ImmutableArray(Of SyntaxNode) + Return ImmutableArray.ToImmutableArray(Of SyntaxNode)(ifStatement.ElseIfBlocks).Add(ifStatement.ElseBlock) + End Function + + Protected Overrides Function MergeIfStatements(outerIfBlock As MultiLineIfBlockSyntax, innerIfBlock As MultiLineIfBlockSyntax, generator As SyntaxGenerator) As MultiLineIfBlockSyntax + Dim newCondition = SyntaxFactory.BinaryExpression(SyntaxKind.AndAlsoExpression, + DirectCast(generator.AddParentheses(outerIfBlock.IfStatement.Condition), ExpressionSyntax), + SyntaxFactory.Token(SyntaxKind.AndAlsoKeyword), + DirectCast(generator.AddParentheses(innerIfBlock.IfStatement.Condition), ExpressionSyntax)) + Return outerIfBlock.WithIfStatement(outerIfBlock.IfStatement.WithCondition(newCondition)).WithStatements(innerIfBlock.Statements) + End Function + End Class +End Namespace diff --git a/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs b/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs index 341a32cedf31960b0aae9f062dd3a7ab0d964c29..58415a812855274b5f65e638808b74c42064d7af 100644 --- a/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs +++ b/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs @@ -1895,14 +1895,31 @@ public SyntaxNode GetValueOfEqualsValueClause(SyntaxNode node) => ((EqualsValueClauseSyntax)node)?.Value; public bool IsExecutableBlock(SyntaxNode node) - => node.IsKind(SyntaxKind.Block); + => node.IsKind(SyntaxKind.Block, SyntaxKind.SwitchSection); public SyntaxList GetExecutableBlockStatements(SyntaxNode node) - => ((BlockSyntax)node).Statements; + { + switch (node) + { + case BlockSyntax block: + return block.Statements; + case SwitchSectionSyntax switchSection: + return switchSection.Statements; + default: + throw ExceptionUtilities.UnexpectedValue(node); + } + } public SyntaxNode FindInnermostCommonExecutableBlock(IEnumerable nodes) => nodes.FindInnermostCommonBlock(); + public bool IsStatementContainer(SyntaxNode node) + => IsExecutableBlock(node) || node.IsEmbeddedStatementOwner(); + + public IReadOnlyList GetStatementContainerStatements(SyntaxNode node) => IsExecutableBlock(node) + ? GetExecutableBlockStatements(node) + : (IReadOnlyList)ImmutableArray.Create(node.GetEmbeddedStatement()); + public bool IsCastExpression(SyntaxNode node) => node is CastExpressionSyntax; diff --git a/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs b/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs index d81ad489d4890f9264e3bf82f8dd839bb841654b..ca73b126435a5ef86d25b5d05de2313974ca47fa 100644 --- a/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs +++ b/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs @@ -290,6 +290,9 @@ internal interface ISyntaxFactsService : ILanguageService SyntaxList GetExecutableBlockStatements(SyntaxNode node); SyntaxNode FindInnermostCommonExecutableBlock(IEnumerable nodes); + bool IsStatementContainer(SyntaxNode node); + IReadOnlyList GetStatementContainerStatements(SyntaxNode node); + bool AreEquivalent(SyntaxToken token1, SyntaxToken token2); bool AreEquivalent(SyntaxNode node1, SyntaxNode node2); diff --git a/src/Workspaces/Core/Portable/Utilities/IReadOnlyListExtensions.cs b/src/Workspaces/Core/Portable/Utilities/IReadOnlyListExtensions.cs index b15e7de4427d1833cb973162d4abaf4dd8a8e468..289345068e87ce316e3b79288d2fd40be97963c1 100644 --- a/src/Workspaces/Core/Portable/Utilities/IReadOnlyListExtensions.cs +++ b/src/Workspaces/Core/Portable/Utilities/IReadOnlyListExtensions.cs @@ -20,6 +20,19 @@ public static T Last(this IReadOnlyList list) return list[list.Count - 1]; } + public static int IndexOf(this IReadOnlyList list, T item) + { + for (int i = 0; i < list.Count; ++i) + { + if (list[i].Equals(item)) + { + return i; + } + } + + return -1; + } + private class ReadOnlyList : IReadOnlyList { private readonly IList _list; diff --git a/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb b/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb index e070657d0f3ba5a6c39db535653a6f133cfdd53a..af94157a8b723c6de9c710b2746d574a39106f3f 100644 --- a/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb +++ b/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb @@ -1824,6 +1824,14 @@ Namespace Microsoft.CodeAnalysis.VisualBasic Return nodes.FindInnermostCommonExecutableBlock() End Function + Public Function IsStatementContainer(node As SyntaxNode) As Boolean Implements ISyntaxFactsService.IsStatementContainer + Return IsExecutableBlock(node) + End Function + + Public Function GetStatementContainerStatements(node As SyntaxNode) As IReadOnlyList(Of SyntaxNode) Implements ISyntaxFactsService.GetStatementContainerStatements + Return GetExecutableBlockStatements(node) + End Function + Private Function ISyntaxFactsService_GetLeadingBlankLines(node As SyntaxNode) As ImmutableArray(Of SyntaxTrivia) Implements ISyntaxFactsService.GetLeadingBlankLines Return MyBase.GetLeadingBlankLines(node) End Function