Skip to content

Commit

Permalink
Add refactoring DeconstructForeachVariable (RR0217)
Browse files Browse the repository at this point in the history
  • Loading branch information
josefpihrt committed Mar 25, 2022
1 parent ecbcf34 commit 2eb1238
Show file tree
Hide file tree
Showing 9 changed files with 300 additions and 1 deletion.
1 change: 1 addition & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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<IMethodSymbol>(
"Deconstruct",
symbol =>
{
if (symbol.DeclaredAccessibility == Accessibility.Public)
{
ImmutableArray<IParameterSymbol> 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<Document> 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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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);

Expand All @@ -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))
{
Expand Down
28 changes: 28 additions & 0 deletions src/Refactorings/Refactorings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2627,4 +2627,32 @@ class Foo : IFoo
</Sample>
</Samples>
</Refactoring>
<Refactoring Id="RR0217" Identifier="DeconstructForeachVariable" Title="Deconstruct foreach variable">
<OptionKey>deconstruct_foreach_variable</OptionKey>
<Syntaxes>
<Syntax>foreach statement</Syntax>
</Syntaxes>
<Span></Span>
<Summary>type or identifier</Summary>
<Samples>
<Sample>
<Before><![CDATA[var dic = new Dictionary<string, object>();
foreach (var kvp in dic)
{
var k = kvp.Key;
var v = kvp.Value.ToString();
}
]]></Before>
<After><![CDATA[var dic = new Dictionary<string, object>();
foreach (var (key, value) in dic)
{
var k = key;
var v = value.ToString();
}
]]></After>
</Sample>
</Samples>
</Refactoring>
</Refactorings>
Original file line number Diff line number Diff line change
@@ -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<object, object>();
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<object, object>();
foreach (var (key, value) in dic)
{
var k = key;
var v = value.ToString();
}
}
}
", equivalenceKey: EquivalenceKey.Create(RefactoringId));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 2eb1238

Please sign in to comment.