diff --git a/ChangeLog.md b/ChangeLog.md index 04c2f0604a..cb24f56a0e 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -9,6 +9,7 @@ * Fix code fix for CS0225 * Put back refactoring SplitLocalDeclarationAndAssignment (RR0194) ([issue](https://github.com/JosefPihrt/Roslynator/issues/881)) * Fix: Get config value from global AnalyzerConfig if available ([issue](https://github.com/JosefPihrt/Roslynator/issues/884)) +* Add refactoring [Deconstruct foreach variable (RR0217)](https://github.com/JosefPihrt/Roslynator/blob/master/docs/refactoring/RR0217.md) ### 4.0.3 (2022-01-29) diff --git a/docs/Configuration.md b/docs/Configuration.md index 0e0c40ca9a..4af59257ab 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -1026,6 +1026,7 @@ roslynator_refactoring.copy_member_declaration.enabled = true roslynator_refactoring.copy_parameter.enabled = true roslynator_refactoring.copy_statement.enabled = true roslynator_refactoring.copy_switch_section.enabled = true +roslynator_refactoring.deconstruct_foreach_variable.enabled = true roslynator_refactoring.expand_coalesce_expression.enabled = true roslynator_refactoring.expand_compound_assignment.enabled = true roslynator_refactoring.expand_event_declaration.enabled = true diff --git a/src/Refactorings/CSharp/RefactoringDescriptors.Generated.cs b/src/Refactorings/CSharp/RefactoringDescriptors.Generated.cs index e95028c33d..87d687f012 100644 --- a/src/Refactorings/CSharp/RefactoringDescriptors.Generated.cs +++ b/src/Refactorings/CSharp/RefactoringDescriptors.Generated.cs @@ -68,6 +68,7 @@ public static class RefactoringDescriptors public static RefactoringDescriptor CopyParameter = new RefactoringDescriptor("RR0032", "roslynator_refactoring.copy_parameter.enabled", isEnabledByDefault: true); public static RefactoringDescriptor CopyStatement = new RefactoringDescriptor("RR0033", "roslynator_refactoring.copy_statement.enabled", isEnabledByDefault: true); public static RefactoringDescriptor CopySwitchSection = new RefactoringDescriptor("RR0212", "roslynator_refactoring.copy_switch_section.enabled", isEnabledByDefault: true); + public static RefactoringDescriptor DeconstructForeachVariable = new RefactoringDescriptor("RR0217", "roslynator_refactoring.deconstruct_foreach_variable.enabled", isEnabledByDefault: true); public static RefactoringDescriptor ExpandCoalesceExpression = new RefactoringDescriptor("RR0035", "roslynator_refactoring.expand_coalesce_expression.enabled", isEnabledByDefault: true); public static RefactoringDescriptor ExpandCompoundAssignment = new RefactoringDescriptor("RR0034", "roslynator_refactoring.expand_compound_assignment.enabled", isEnabledByDefault: true); public static RefactoringDescriptor ExpandEventDeclaration = new RefactoringDescriptor("RR0036", "roslynator_refactoring.expand_event_declaration.enabled", isEnabledByDefault: true); diff --git a/src/Refactorings/CSharp/RefactoringIdentifiers.Generated.cs b/src/Refactorings/CSharp/RefactoringIdentifiers.Generated.cs index e7e6e3b654..4e973c3328 100644 --- a/src/Refactorings/CSharp/RefactoringIdentifiers.Generated.cs +++ b/src/Refactorings/CSharp/RefactoringIdentifiers.Generated.cs @@ -70,6 +70,7 @@ public static partial class RefactoringIdentifiers public const string CopyParameter = Prefix + "0032"; public const string CopyStatement = Prefix + "0033"; public const string CopySwitchSection = Prefix + "0212"; + public const string DeconstructForeachVariable = Prefix + "0217"; public const string ExpandCoalesceExpression = Prefix + "0035"; public const string ExpandCompoundAssignment = Prefix + "0034"; public const string ExpandEventDeclaration = Prefix + "0036"; diff --git a/src/Refactorings/CSharp/Refactorings/DeconstructForeachVariableRefactoring.cs b/src/Refactorings/CSharp/Refactorings/DeconstructForeachVariableRefactoring.cs new file mode 100644 index 0000000000..e04da7437a --- /dev/null +++ b/src/Refactorings/CSharp/Refactorings/DeconstructForeachVariableRefactoring.cs @@ -0,0 +1,204 @@ +// Copyright (c) Josef Pihrt and Contributors. 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; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Roslynator.CSharp.Refactorings +{ + internal static class DeconstructForeachVariableRefactoring + { + public static void ComputeRefactoring( + RefactoringContext context, + ForEachStatementSyntax forEachStatement, + SemanticModel semanticModel) + { + ITypeSymbol typeSymbol = semanticModel.GetTypeSymbol(forEachStatement.Type); + + IMethodSymbol deconstructSymbol = typeSymbol.FindMember( + "Deconstruct", + symbol => + { + if (symbol.DeclaredAccessibility == Accessibility.Public) + { + ImmutableArray parameters = symbol.Parameters; + + return parameters.Any() + && parameters.All(f => f.RefKind == RefKind.Out); + } + + return false; + }); + + if (deconstructSymbol is null) + return; + + ISymbol foreachSymbol = semanticModel.GetDeclaredSymbol(forEachStatement, context.CancellationToken); + + if (foreachSymbol?.IsKind(SymbolKind.Local) != true) + return; + + var walker = new DeconstructForeachVariableWalker( + deconstructSymbol, + foreachSymbol, + forEachStatement.Identifier.ValueText, + semanticModel, + context.CancellationToken); + + walker.Visit(forEachStatement.Statement); + + if (!walker.Success) + return; + + context.RegisterRefactoring( + "Deconstruct foreach variable", + ct => RefactorAsync(context.Document, forEachStatement, deconstructSymbol, foreachSymbol, semanticModel, ct), + RefactoringDescriptors.DeconstructForeachVariable); + } + + private static async Task RefactorAsync( + Document document, + ForEachStatementSyntax forEachStatement, + IMethodSymbol deconstructSymbol, + ISymbol identifierSymbol, + SemanticModel semanticModel, + CancellationToken cancellationToken) + { + DeclarationExpressionSyntax variableExpression = DeclarationExpression( + CSharpFactory.VarType().WithTriviaFrom(forEachStatement.Type), + ParenthesizedVariableDesignation( + deconstructSymbol.Parameters.Select(parameter => + { + return (VariableDesignationSyntax)SingleVariableDesignation( + Identifier( + SyntaxTriviaList.Empty, + parameter.Name, + SyntaxTriviaList.Empty)); + }) + .ToSeparatedSyntaxList()) + .WithTriviaFrom(forEachStatement.Identifier)) + .WithFormatterAnnotation(); + + var rewriter = new DeconstructForeachVariableRewriter(identifierSymbol, semanticModel, cancellationToken); + + var newStatement = (StatementSyntax)rewriter.Visit(forEachStatement.Statement); + + ForEachVariableStatementSyntax newForEachStatement = ForEachVariableStatement( + forEachStatement.AttributeLists, + forEachStatement.AwaitKeyword, + forEachStatement.ForEachKeyword, + forEachStatement.OpenParenToken, + variableExpression.WithFormatterAnnotation(), + forEachStatement.InKeyword, + forEachStatement.Expression, + forEachStatement.CloseParenToken, + newStatement); + + return await document.ReplaceNodeAsync(forEachStatement, newForEachStatement, cancellationToken).ConfigureAwait(false); + } + + private class DeconstructForeachVariableWalker : CSharpSyntaxWalker + { + public DeconstructForeachVariableWalker( + IMethodSymbol deconstructMethod, + ISymbol identifierSymbol, + string identifier, + SemanticModel semanticModel, + CancellationToken cancellationToken) + { + DeconstructMethod = deconstructMethod; + IdentifierSymbol = identifierSymbol; + Identifier = identifier; + SemanticModel = semanticModel; + CancellationToken = cancellationToken; + } + + public IMethodSymbol DeconstructMethod { get; } + + public ISymbol IdentifierSymbol { get; } + + public string Identifier { get; } + + public SemanticModel SemanticModel { get; } + + public CancellationToken CancellationToken { get; } + + public bool Success { get; private set; } = true; + + public override void DefaultVisit(SyntaxNode node) + { + if (Success) + base.DefaultVisit(node); + } + + public override void VisitIdentifierName(IdentifierNameSyntax node) + { + if (node.Identifier.ValueText == IdentifierSymbol.Name + && SymbolEqualityComparer.Default.Equals(SemanticModel.GetSymbol(node, CancellationToken), IdentifierSymbol) + && !IsFixable(node)) + { + Success = false; + } + + base.VisitIdentifierName(node); + + bool IsFixable(IdentifierNameSyntax node) + { + if (node.IsParentKind(SyntaxKind.SimpleMemberAccessExpression)) + { + var memberAccess = (MemberAccessExpressionSyntax)node.Parent; + if (object.ReferenceEquals(memberAccess.Expression, node)) + { + foreach (IParameterSymbol parameter in DeconstructMethod.Parameters) + { + if (string.Equals(parameter.Name, memberAccess.Name.Identifier.ValueText, StringComparison.OrdinalIgnoreCase)) + return true; + } + } + } + + return false; + } + } + } + + private class DeconstructForeachVariableRewriter : CSharpSyntaxRewriter + { + public DeconstructForeachVariableRewriter( + ISymbol identifierSymbol, + SemanticModel semanticModel, + CancellationToken cancellationToken) + { + IdentifierSymbol = identifierSymbol; + SemanticModel = semanticModel; + CancellationToken = cancellationToken; + } + + public ISymbol IdentifierSymbol { get; } + + public SemanticModel SemanticModel { get; } + + public CancellationToken CancellationToken { get; } + + public override SyntaxNode VisitMemberAccessExpression(MemberAccessExpressionSyntax node) + { + if (node.IsKind(SyntaxKind.SimpleMemberAccessExpression) + && node.Expression is IdentifierNameSyntax identifierName + && identifierName.Identifier.ValueText == IdentifierSymbol.Name + && SymbolEqualityComparer.Default.Equals(SemanticModel.GetSymbol(identifierName, CancellationToken), IdentifierSymbol)) + { + return IdentifierName(StringUtility.FirstCharToLower(node.Name.Identifier.ValueText)) + .WithTriviaFrom(identifierName); + } + + return base.VisitMemberAccessExpression(node); + } + } + } +} diff --git a/src/Refactorings/CSharp/Refactorings/ForEachStatementRefactoring.cs b/src/Refactorings/CSharp/Refactorings/ForEachStatementRefactoring.cs index e3b38d373c..c11f0b10fa 100644 --- a/src/Refactorings/CSharp/Refactorings/ForEachStatementRefactoring.cs +++ b/src/Refactorings/CSharp/Refactorings/ForEachStatementRefactoring.cs @@ -6,6 +6,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Rename; +using Microsoft.CodeAnalysis.Text; namespace Roslynator.CSharp.Refactorings { @@ -24,10 +25,12 @@ public static async Task ComputeRefactoringsAsync(RefactoringContext context, Fo if (context.IsRefactoringEnabled(RefactoringDescriptors.RenameIdentifierAccordingToTypeName)) await RenameIdentifierAccordingToTypeNameAsync(context, forEachStatement).ConfigureAwait(false); + SemanticModel semanticModel = null; + if (context.IsAnyRefactoringEnabled(RefactoringDescriptors.ConvertForEachToFor, RefactoringDescriptors.ConvertForEachToForAndReverseLoop) && context.Span.IsEmptyAndContainedInSpanOrBetweenSpans(forEachStatement)) { - SemanticModel semanticModel = await context.GetSemanticModelAsync().ConfigureAwait(false); + semanticModel = await context.GetSemanticModelAsync().ConfigureAwait(false); ITypeSymbol typeSymbol = semanticModel.GetTypeSymbol(forEachStatement.Expression, context.CancellationToken); @@ -51,6 +54,14 @@ public static async Task ComputeRefactoringsAsync(RefactoringContext context, Fo } } + if (context.IsRefactoringEnabled(RefactoringDescriptors.DeconstructForeachVariable) + && TextSpan.FromBounds(forEachStatement.Type.SpanStart, forEachStatement.Identifier.Span.End).Contains(context.Span)) + { + semanticModel ??= await context.GetSemanticModelAsync().ConfigureAwait(false); + + DeconstructForeachVariableRefactoring.ComputeRefactoring(context, forEachStatement, semanticModel); + } + if (context.IsRefactoringEnabled(RefactoringDescriptors.UseEnumeratorExplicitly) && context.Span.IsEmptyAndContainedInSpan(forEachStatement.ForEachKeyword)) { diff --git a/src/Refactorings/Refactorings.xml b/src/Refactorings/Refactorings.xml index 0ec49cc259..1987059cef 100644 --- a/src/Refactorings/Refactorings.xml +++ b/src/Refactorings/Refactorings.xml @@ -2627,4 +2627,32 @@ class Foo : IFoo + + deconstruct_foreach_variable + + foreach statement + + + type or identifier + + + (); + +foreach (var kvp in dic) +{ + var k = kvp.Key; + var v = kvp.Value.ToString(); +} +]]> + (); + +foreach (var (key, value) in dic) +{ + var k = key; + var v = value.ToString(); +} +]]> + + + diff --git a/src/Tests/Refactorings.Tests/RR0217DeconstructForeachVariableTests.cs b/src/Tests/Refactorings.Tests/RR0217DeconstructForeachVariableTests.cs new file mode 100644 index 0000000000..29feaa6e88 --- /dev/null +++ b/src/Tests/Refactorings.Tests/RR0217DeconstructForeachVariableTests.cs @@ -0,0 +1,51 @@ +// Copyright (c) Josef Pihrt and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Roslynator.Testing.CSharp; +using Xunit; + +namespace Roslynator.CSharp.Refactorings.Tests +{ + public class RR0217DeconstructForeachVariableTests : AbstractCSharpRefactoringVerifier + { + public override string RefactoringId { get; } = RefactoringIdentifiers.DeconstructForeachVariable; + + [Fact, Trait(Traits.Refactoring, RefactoringIdentifiers.DeconstructForeachVariable)] + public async Task Test_EmptyObjectInitializer() + { + await VerifyRefactoringAsync(@" +using System.Collections.Generic; + +class C +{ + void M() + { + var dic = new Dictionary(); + + foreach ([||]var kvp in dic) + { + var k = kvp.Key; + var v = kvp.Value.ToString(); + } + } +} +", @" +using System.Collections.Generic; + +class C +{ + void M() + { + var dic = new Dictionary(); + + foreach (var (key, value) in dic) + { + var k = key; + var v = value.ToString(); + } + } +} +", equivalenceKey: EquivalenceKey.Create(RefactoringId)); + } + } +} diff --git a/src/VisualStudioCode/package/src/configurationFiles.generated.ts b/src/VisualStudioCode/package/src/configurationFiles.generated.ts index 9b185615ee..0ad0b7053f 100644 --- a/src/VisualStudioCode/package/src/configurationFiles.generated.ts +++ b/src/VisualStudioCode/package/src/configurationFiles.generated.ts @@ -998,6 +998,7 @@ roslynator_analyzers.enabled_by_default = true|false #roslynator_refactoring.copy_parameter.enabled = true #roslynator_refactoring.copy_statement.enabled = true #roslynator_refactoring.copy_switch_section.enabled = true +#roslynator_refactoring.deconstruct_foreach_variable.enabled = true #roslynator_refactoring.expand_coalesce_expression.enabled = true #roslynator_refactoring.expand_compound_assignment.enabled = true #roslynator_refactoring.expand_event_declaration.enabled = true