|
// 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;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.LanguageService;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.UseCompoundAssignment;
namespace Microsoft.CodeAnalysis.CSharp.UseCompoundAssignment
{
/// <summary>
/// Looks for expressions of the form:
/// <list type="number">
/// <item><c>expr ?? (expr = value)</c> and converts it to <c>expr ??= value</c>.</item>
/// <item><c>if (expr is null) expr = value</c> and converts it to <c>expr ??= value</c>.</item>
/// </list>
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal class CSharpUseCompoundCoalesceAssignmentDiagnosticAnalyzer
: AbstractBuiltInCodeStyleDiagnosticAnalyzer
{
public CSharpUseCompoundCoalesceAssignmentDiagnosticAnalyzer()
: base(IDEDiagnosticIds.UseCoalesceCompoundAssignmentDiagnosticId,
EnforceOnBuildValues.UseCoalesceCompoundAssignment,
CodeStyleOptions2.PreferCompoundAssignment,
new LocalizableResourceString(nameof(AnalyzersResources.Use_compound_assignment), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)))
{
}
public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
=> DiagnosticAnalyzerCategory.SemanticSpanAnalysis;
protected override void InitializeWorker(AnalysisContext context)
{
context.RegisterCompilationStartAction(context =>
{
if (context.Compilation.LanguageVersion() < LanguageVersion.CSharp8)
return;
context.RegisterSyntaxNodeAction(AnalyzeCoalesceExpression, SyntaxKind.CoalesceExpression);
context.RegisterSyntaxNodeAction(AnalyzeIfStatement, SyntaxKind.IfStatement);
});
}
private void AnalyzeCoalesceExpression(SyntaxNodeAnalysisContext context)
{
var cancellationToken = context.CancellationToken;
var semanticModel = context.SemanticModel;
var coalesceExpression = (BinaryExpressionSyntax)context.Node;
var option = context.GetAnalyzerOptions().PreferCompoundAssignment;
// Bail immediately if the user has disabled this feature.
if (!option.Value)
return;
var coalesceLeft = coalesceExpression.Left;
var coalesceRight = coalesceExpression.Right;
if (coalesceRight is not ParenthesizedExpressionSyntax parenthesizedExpr)
return;
if (parenthesizedExpr.Expression is not AssignmentExpressionSyntax assignment)
return;
if (assignment.Kind() != SyntaxKind.SimpleAssignmentExpression)
return;
// have x ?? (y = z)
// ensure that 'x' and 'y' are suitably equivalent.
var syntaxFacts = CSharpSyntaxFacts.Instance;
if (!syntaxFacts.AreEquivalent(coalesceLeft, assignment.Left))
return;
// Syntactically looks promising. But we can only safely do this if 'expr'
// is side-effect-free since we will be changing the number of times it is
// executed from twice to once.
if (!UseCompoundAssignmentUtilities.IsSideEffectFree(
syntaxFacts, coalesceLeft, semanticModel, cancellationToken))
{
return;
}
// Good match.
context.ReportDiagnostic(DiagnosticHelper.Create(
Descriptor,
coalesceExpression.OperatorToken.GetLocation(),
option.Notification.Severity,
additionalLocations: ImmutableArray.Create(coalesceExpression.GetLocation()),
properties: null));
}
public static bool GetWhenTrueAssignment(
IfStatementSyntax ifStatement,
[NotNullWhen(true)] out StatementSyntax? whenTrue,
[NotNullWhen(true)] out AssignmentExpressionSyntax? assignment)
{
whenTrue = ifStatement.Statement is BlockSyntax block
? block.Statements.Count == 1 ? block.Statements[0] : null
: ifStatement.Statement;
assignment = whenTrue is ExpressionStatementSyntax { Expression: AssignmentExpressionSyntax(SyntaxKind.SimpleAssignmentExpression) assignmentTemp }
? assignmentTemp
: null;
return whenTrue != null && assignment != null;
}
private void AnalyzeIfStatement(SyntaxNodeAnalysisContext context)
{
var cancellationToken = context.CancellationToken;
var semanticModel = context.SemanticModel;
var ifStatement = (IfStatementSyntax)context.Node;
var option = context.GetAnalyzerOptions().PreferCompoundAssignment;
// Bail immediately if the user has disabled this feature.
if (!option.Value)
return;
if (ifStatement.Else != null)
return;
if (!GetWhenTrueAssignment(ifStatement, out _, out var assignment))
return;
if (!IsReferenceEqualsNullCheck(semanticModel, ifStatement.Condition, cancellationToken, out var testedExpression))
return;
// have if (x == null) x = y;
// ensure that 'x' in both locations are suitably equivalent.
var syntaxFacts = CSharpSyntaxFacts.Instance;
if (!syntaxFacts.AreEquivalent(testedExpression, assignment.Left))
return;
// Syntactically looks promising. But we can only safely do this if 'expr'
// is side-effect-free since we will be changing the number of times it is
// executed from twice to once.
if (!UseCompoundAssignmentUtilities.IsSideEffectFree(
syntaxFacts, testedExpression, semanticModel, cancellationToken))
{
return;
}
if (ifStatement.Statement is BlockSyntax block)
{
// Single is safe here as GetWhenTrueAssignment will return null if we have a block without a single
// statement in it.
var firstStatement = block.Statements.Single();
// Don't want to offer anything if our if-statement body has any conditional directives in it. That
// means there's some other code that may run under some other conditions, that we do not want to now
// run conditionally outside of the 'if' statement itself.
if (firstStatement.GetLeadingTrivia().Any(t => t.HasStructure && t.GetStructure() is ConditionalDirectiveTriviaSyntax))
return;
}
if (semanticModel.GetTypeInfo(testedExpression, cancellationToken).Type is IPointerTypeSymbol or IFunctionPointerTypeSymbol)
{
// pointers cannot use ??=
return;
}
// Good match.
context.ReportDiagnostic(DiagnosticHelper.Create(
Descriptor,
ifStatement.IfKeyword.GetLocation(),
option.Notification.Severity,
additionalLocations: ImmutableArray.Create(ifStatement.GetLocation()),
properties: null));
}
private bool IsReferenceEqualsNullCheck(
SemanticModel semanticModel,
ExpressionSyntax condition,
CancellationToken cancellationToken,
[NotNullWhen(true)] out ExpressionSyntax? testedExpression)
{
testedExpression = null;
if (condition is BinaryExpressionSyntax(SyntaxKind.EqualsExpression) { Right: LiteralExpressionSyntax(SyntaxKind.NullLiteralExpression) } binaryExpression)
{
// Ensure that if we are using `==` that it's not an overloaded operator. One known exception is
// System.String. Even though `==` is overloaded, it has the same semantics as ReferenceEquals(null) so
// it's safe to convert.
var symbol = semanticModel.GetSymbolInfo(binaryExpression, cancellationToken).Symbol;
if (symbol is null || !symbol.IsUserDefinedOperator() || symbol.ContainingType.SpecialType == SpecialType.System_String)
testedExpression = binaryExpression.Left;
}
else if (condition is IsPatternExpressionSyntax { Pattern: ConstantPatternSyntax { Expression: LiteralExpressionSyntax(SyntaxKind.NullLiteralExpression) } } isPattern)
{
// x is null. always a valid null check.
testedExpression = isPattern.Expression;
}
else if (condition is InvocationExpressionSyntax { ArgumentList.Arguments.Count: 2 } invocation)
{
var arg0 = invocation.ArgumentList.Arguments[0].Expression;
var arg1 = invocation.ArgumentList.Arguments[1].Expression;
if (arg0.Kind() == SyntaxKind.NullLiteralExpression ||
arg1.Kind() == SyntaxKind.NullLiteralExpression)
{
var symbol = semanticModel.GetSymbolInfo(invocation, cancellationToken).Symbol;
if (symbol?.Name == nameof(ReferenceEquals) &&
symbol.ContainingType?.SpecialType == SpecialType.System_Object)
{
testedExpression = arg0.Kind() == SyntaxKind.NullLiteralExpression ? arg1 : arg0;
}
}
}
return testedExpression != null;
}
}
}
|