Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement CodeFix for CA2246 #3694

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// 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.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Analyzer.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

#nullable enable
Youssef1313 marked this conversation as resolved.
Show resolved Hide resolved

namespace Microsoft.CodeQuality.Analyzers.QualityGuidelines
Youssef1313 marked this conversation as resolved.
Show resolved Hide resolved
{
[ExportCodeFixProvider(LanguageNames.CSharp, Name = AssigningSymbolAndItsMemberInSameStatement.RuleId), Shared]
public sealed class AssigningSymbolAndItsMemberInSameStatementFixer : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(AssigningSymbolAndItsMemberInSameStatement.RuleId);

public override Task RegisterCodeFixesAsync(CodeFixContext context)
{
// For something like `a.x = a = b;`, we offer 3 code fixes:
// First:
// a = b;
// a.x = b;
// Second:
// a = b;
// a.x = a;
// Third: (Not currently implemented)
// var temp = a;
// a = b;
// temp.x = b;

var title = MicrosoftCodeQualityAnalyzersResources.AssigningSymbolAndItsMemberInSameStatementTitle;
context.RegisterCodeFix(new MyCodeAction(title,
async ct => await SplitAssignmentFirstOption(context.Document, context.Span, ct).ConfigureAwait(false),
equivalenceKey: title + "0"),
context.Diagnostics);
context.RegisterCodeFix(new MyCodeAction(title,
async ct => await SplitAssignmentSecondOption(context.Document, context.Span, ct).ConfigureAwait(false),
equivalenceKey: title + "1"),
context.Diagnostics);
Youssef1313 marked this conversation as resolved.
Show resolved Hide resolved
return Task.CompletedTask;
}

private static async Task<Document> SplitAssignmentFirstOption(Document document, TextSpan span, CancellationToken cancellationToken)
{
// This method splits `a.x = a = b` to:
// a = b;
// a.x = b;
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

// a.x = a = b;
var parentAssignment = root.FindNode(span).Parent;

if (!TryGetAssignmentExpressionParts(parentAssignment, out _, out var right) ||
!TryGetAssignmentExpressionParts(right, out _, out var rightOfRight))
{
return document;
}

// a = b;
right = GetExpressionFromAssignment(right).WithTriviaFrom(parentAssignment.Parent);

// a.x = b;
var firstEqualsLastAssignment = GetExpressionFromAssignment(GetAssignmentWithRight(parentAssignment, rightOfRight));

root = root.ReplaceNode(parentAssignment.Parent, new[] { right, firstEqualsLastAssignment });
return document.WithSyntaxRoot(root);
}

private static async Task<Document> SplitAssignmentSecondOption(Document document, TextSpan span, CancellationToken cancellationToken)
Youssef1313 marked this conversation as resolved.
Show resolved Hide resolved
{
// This method splits `a.x = a = b` to:
// a = b;
// a.x = a;
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

// a.x = a = b;
var parentAssignment = root.FindNode(span).Parent;
Youssef1313 marked this conversation as resolved.
Show resolved Hide resolved

if (!TryGetAssignmentExpressionParts(parentAssignment, out _, out var right) ||
!TryGetAssignmentExpressionParts(right, out var leftOfRight, out _))
{
return document;
}

// a = b;
right = GetExpressionFromAssignment(right).WithTriviaFrom(parentAssignment.Parent);

// a.x = a;
var firstEqualsSecondAssignment = GetExpressionFromAssignment(GetAssignmentWithRight(parentAssignment, leftOfRight));

root = root.ReplaceNode(parentAssignment.Parent, new[] { right, firstEqualsSecondAssignment });
return document.WithSyntaxRoot(root);
}

private static SyntaxNode GetAssignmentWithRight(SyntaxNode assignmentExpression, SyntaxNode newRight)
{
return ((AssignmentExpressionSyntax)assignmentExpression).WithRight((ExpressionSyntax)newRight);
Youssef1313 marked this conversation as resolved.
Show resolved Hide resolved
}

private static SyntaxNode GetExpressionFromAssignment(SyntaxNode assignmentExpression)
{
return SyntaxFactory.ExpressionStatement((AssignmentExpressionSyntax)assignmentExpression);
Youssef1313 marked this conversation as resolved.
Show resolved Hide resolved
}

private static bool TryGetAssignmentExpressionParts(SyntaxNode assignmentExpression, [NotNullWhen(true)] out SyntaxNode? left, [NotNullWhen(true)] out SyntaxNode? right)
{
if (assignmentExpression is AssignmentExpressionSyntax assignment)
{
left = assignment.Left;
right = assignment.Right;
return true;
}
left = null;
right = null;
return false;
Youssef1313 marked this conversation as resolved.
Show resolved Hide resolved
}

public override FixAllProvider GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
Youssef1313 marked this conversation as resolved.
Show resolved Hide resolved
}

private class MyCodeAction : DocumentChangeAction
{
public MyCodeAction(string title, Func<CancellationToken, Task<Document>> createChangedDocument, string equivalenceKey)
: base(title, createChangedDocument, equivalenceKey)
{
}
}
}
}