| File: src\roslyn\src\Analyzers\CSharp\Analyzers\UseCompoundAssignment\CSharpUseCompoundCoalesceAssignmentDiagnosticAnalyzer.cs | Web Access |
| Project: src\roslyn\src\CodeStyle\CSharp\Analyzers\Microsoft.CodeAnalysis.CSharp.CodeStyle.csproj (Microsoft.CodeAnalysis.CSharp.CodeStyle) |
// 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.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 sealed class CSharpUseCompoundCoalesceAssignmentDiagnosticAnalyzer() : AbstractBuiltInCodeStyleDiagnosticAnalyzer( 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 || ShouldSkipAnalysis(context, option.Notification)) 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, context.Options, additionalLocations: [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 || ShouldSkipAnalysis(context, option.Notification)) return; if (ifStatement.Else != null) return; if (!GetWhenTrueAssignment(ifStatement, out var whenTrue, 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; } // 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 (whenTrue.GetLeadingTrivia().Any(static t => t.GetStructure() is ConditionalDirectiveTriviaSyntax)) return; // pointers cannot use ??= if (semanticModel.GetTypeInfo(testedExpression, cancellationToken).Type is IPointerTypeSymbol or IFunctionPointerTypeSymbol) return; // Good match. context.ReportDiagnostic(DiagnosticHelper.Create( Descriptor, ifStatement.IfKeyword.GetLocation(), option.Notification, context.Options, additionalLocations: [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 is { Name: nameof(ReferenceEquals), ContainingType.SpecialType: SpecialType.System_Object }) { testedExpression = arg0.Kind() == SyntaxKind.NullLiteralExpression ? arg1 : arg0; } } } return testedExpression != null; } }