|
// 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.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
using static Microsoft.CodeAnalysis.UseConditionalExpression.UseConditionalExpressionCodeFixHelpers;
namespace Microsoft.CodeAnalysis.UseConditionalExpression;
internal abstract class AbstractUseConditionalExpressionForAssignmentCodeFixProvider<
TStatementSyntax,
TIfStatementSyntax,
TLocalDeclarationStatementSyntax,
TVariableDeclaratorSyntax,
TExpressionSyntax,
TConditionalExpressionSyntax>
: AbstractUseConditionalExpressionCodeFixProvider<TStatementSyntax, TIfStatementSyntax, TExpressionSyntax, TConditionalExpressionSyntax>
where TStatementSyntax : SyntaxNode
where TIfStatementSyntax : TStatementSyntax
where TLocalDeclarationStatementSyntax : TStatementSyntax
where TVariableDeclaratorSyntax : SyntaxNode
where TExpressionSyntax : SyntaxNode
where TConditionalExpressionSyntax : TExpressionSyntax
{
protected abstract TVariableDeclaratorSyntax WithInitializer(TVariableDeclaratorSyntax variable, TExpressionSyntax value);
protected abstract TVariableDeclaratorSyntax GetDeclaratorSyntax(IVariableDeclaratorOperation declarator);
protected abstract TLocalDeclarationStatementSyntax AddSimplificationToType(TLocalDeclarationStatementSyntax updatedLocalDeclaration);
public override ImmutableArray<string> FixableDiagnosticIds
=> [IDEDiagnosticIds.UseConditionalExpressionForAssignmentDiagnosticId];
public override Task RegisterCodeFixesAsync(CodeFixContext context)
{
var (title, key) = context.Diagnostics.First().Properties.ContainsKey(UseConditionalExpressionHelpers.CanSimplifyName)
? (AnalyzersResources.Simplify_check, nameof(AnalyzersResources.Simplify_check))
: (AnalyzersResources.Convert_to_conditional_expression, nameof(AnalyzersResources.Convert_to_conditional_expression));
RegisterCodeFix(context, title, key);
return Task.CompletedTask;
}
/// <summary>
/// Returns 'true' if a multi-line conditional was created, and thus should be
/// formatted specially.
/// </summary>
protected override async Task FixOneAsync(
Document document, Diagnostic diagnostic,
SyntaxEditor editor, SyntaxFormattingOptions formattingOptions, CancellationToken cancellationToken)
{
var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
var ifStatement = diagnostic.AdditionalLocations[0].FindNode(getInnermostNodeForTie: true, cancellationToken);
var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
var ifOperation = (IConditionalOperation)semanticModel.GetOperation(ifStatement, cancellationToken)!;
if (!UseConditionalExpressionForAssignmentHelpers.TryMatchPattern(
syntaxFacts, ifOperation, out var isRef,
out var trueStatement, out var falseStatement,
out var trueAssignment, out var falseAssignment))
{
return;
}
var conditionalExpression = await CreateConditionalExpressionAsync(
document, ifOperation,
trueStatement, falseStatement,
trueAssignment?.Value ?? trueStatement,
falseAssignment?.Value ?? falseStatement,
isRef,
formattingOptions,
cancellationToken).ConfigureAwait(false);
// See if we're assigning to a variable declared directly above the if statement. If so,
// try to inline the conditional directly into the initializer for that variable.
if (TryConvertWhenAssignmentToLocalDeclaredImmediateAbove(
syntaxFacts, editor, ifOperation,
trueAssignment, falseAssignment, conditionalExpression))
{
return;
}
// If not, just replace the if-statement with a single assignment of the new
// conditional.
ConvertOnlyIfToConditionalExpression(
editor, ifOperation, (trueAssignment ?? falseAssignment)!, conditionalExpression);
}
private void ConvertOnlyIfToConditionalExpression(
SyntaxEditor editor,
IConditionalOperation ifOperation,
ISimpleAssignmentOperation assignment,
TExpressionSyntax conditionalExpression)
{
var generator = editor.Generator;
var ifStatement = (TIfStatementSyntax)ifOperation.Syntax;
var expressionStatement = (TStatementSyntax)generator.ExpressionStatement(
generator.AssignmentStatement(
assignment.Target.Syntax,
conditionalExpression)).WithTriviaFrom(ifStatement);
editor.ReplaceNode(
ifOperation.Syntax,
WrapWithBlockIfAppropriate(ifStatement, expressionStatement));
}
private bool TryConvertWhenAssignmentToLocalDeclaredImmediateAbove(
ISyntaxFactsService syntaxFacts, SyntaxEditor editor, IConditionalOperation ifOperation,
ISimpleAssignmentOperation? trueAssignment,
ISimpleAssignmentOperation? falseAssignment,
TExpressionSyntax conditionalExpression)
{
if (!TryFindMatchingLocalDeclarationImmediatelyAbove(
ifOperation, trueAssignment, falseAssignment,
out var localDeclarationOperation, out var declarator))
{
return false;
}
// We found a valid local declaration right above the if-statement.
var localDeclaration = localDeclarationOperation.Syntax;
var variable = GetDeclaratorSyntax(declarator);
// Initialize that variable with the conditional expression.
var updatedVariable = WithInitializer(variable, conditionalExpression);
// Because we merged the initialization and the variable, the variable may now be able
// to use 'var' (c#), or elide its type (vb). Add the simplification annotation
// appropriately so that can happen later down the line.
var updatedLocalDeclaration = localDeclaration.ReplaceNode(variable, updatedVariable);
updatedLocalDeclaration = AddSimplificationToType(
(TLocalDeclarationStatementSyntax)updatedLocalDeclaration);
editor.ReplaceNode(localDeclaration, updatedLocalDeclaration);
editor.RemoveNode(ifOperation.Syntax, GetRemoveOptions(syntaxFacts, ifOperation.Syntax));
return true;
}
private static bool TryFindMatchingLocalDeclarationImmediatelyAbove(
IConditionalOperation ifOperation,
ISimpleAssignmentOperation? trueAssignment,
ISimpleAssignmentOperation? falseAssignment,
[NotNullWhen(true)] out IVariableDeclarationGroupOperation? localDeclaration,
[NotNullWhen(true)] out IVariableDeclaratorOperation? declarator)
{
localDeclaration = null;
declarator = null;
ILocalSymbol? local = null;
if (trueAssignment != null)
{
if (trueAssignment.Target is not ILocalReferenceOperation trueLocal)
return false;
local = trueLocal.Local;
}
if (falseAssignment != null)
{
if (falseAssignment.Target is not ILocalReferenceOperation falseLocal)
return false;
// See if both assignments are to the same local.
if (local != null && !Equals(local, falseLocal.Local))
return false;
local = falseLocal.Local;
}
// We weren't assigning to a local.
if (local == null)
return false;
// If so, see if that local was declared immediately above the if-statement.
if (ifOperation.Parent is not IBlockOperation parentBlock)
{
return false;
}
var ifIndex = parentBlock.Operations.IndexOf(ifOperation);
if (ifIndex <= 0)
{
return false;
}
localDeclaration = parentBlock.Operations[ifIndex - 1] as IVariableDeclarationGroupOperation;
if (localDeclaration == null)
{
return false;
}
if (localDeclaration.IsImplicit)
{
return false;
}
if (localDeclaration.Declarations.Length != 1)
{
return false;
}
var declaration = localDeclaration.Declarations[0];
var declarators = declaration.Declarators;
if (declarators.Length != 1)
{
return false;
}
declarator = declarators[0];
var variable = declarator.Symbol;
if (!Equals(variable, local))
{
// wasn't a declaration of the local we're assigning to.
return false;
}
var variableInitializer = declarator.Initializer ?? declaration.Initializer;
if (variableInitializer?.Value != null)
{
var unwrapped = variableInitializer.Value.UnwrapImplicitConversion();
// the variable has to either not have an initializer, or it needs to be basic
// literal/default expression.
if (unwrapped is not ILiteralOperation and
not IDefaultValueOperation)
{
return false;
}
}
// If the variable is referenced in the condition of the 'if' block, we can't merge the
// declaration and assignments.
return !ReferencesLocalVariable(ifOperation.Condition, variable);
}
private static bool ReferencesLocalVariable(IOperation operation, ILocalSymbol variable)
{
if (operation is ILocalReferenceOperation localReference &&
Equals(variable, localReference.Local))
{
return true;
}
foreach (var child in operation.ChildOperations)
{
if (ReferencesLocalVariable(child, variable))
{
return true;
}
}
return false;
}
}
|