// 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.Threading; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; namespace Microsoft.CodeAnalysis.UseNullPropagation; internal abstract partial class AbstractUseNullPropagationDiagnosticAnalyzer< TSyntaxKind, TExpressionSyntax, TStatementSyntax, TConditionalExpressionSyntax, TBinaryExpressionSyntax, TInvocationExpressionSyntax, TConditionalAccessExpressionSyntax, TElementAccessExpressionSyntax, TMemberAccessExpressionSyntax, TIfStatementSyntax, TExpressionStatementSyntax> { protected abstract bool TryGetPartsOfIfStatement( TIfStatementSyntax ifStatement, [NotNullWhen(true)] out TExpressionSyntax? condition, out ImmutableArray<TStatementSyntax> trueStatements); private void AnalyzeIfStatementAndReportDiagnostic( SyntaxNodeAnalysisContext context, IMethodSymbol? referenceEqualsMethod) { var cancellationToken = context.CancellationToken; var option = context.GetAnalyzerOptions().PreferNullPropagation; if (!option.Value || ShouldSkipAnalysis(context, option.Notification)) return; var ifStatement = (TIfStatementSyntax)context.Node; var analysisResultOpt = AnalyzeIfStatement( context.SemanticModel, referenceEqualsMethod, ifStatement, cancellationToken); if (analysisResultOpt is not IfStatementAnalysisResult analysisResult) return; context.ReportDiagnostic(DiagnosticHelper.Create( Descriptor, ifStatement.GetFirstToken().GetLocation(), option.Notification, context.Options, additionalLocations: [ifStatement.GetLocation()], properties: analysisResult.Properties)); } public virtual IfStatementAnalysisResult? AnalyzeIfStatement( SemanticModel semanticModel, IMethodSymbol? referenceEqualsMethod, TIfStatementSyntax ifStatement, CancellationToken cancellationToken) { var syntaxFacts = this.SyntaxFacts; // The true-statement if the if-statement has to be a statement of the form `<expr1>.Name(...)`; if (!TryGetPartsOfIfStatement(ifStatement, out var condition, out var trueStatement, out var nullAssignmentOpt)) return null; if (trueStatement is not TExpressionStatementSyntax expressionStatement) return null; if (nullAssignmentOpt is not null and not TExpressionStatementSyntax) return null; // Now see if the `if (<condition>)` looks like an appropriate null check. if (!TryAnalyzeCondition( semanticModel, referenceEqualsMethod, condition, out var conditionPartToCheck, out var isEquals, cancellationToken)) { return null; } // Ok, we have `if (<expr2> == null)` or `if (<expr2> != null)` (or some similar form of that. `conditionPartToCheck` will be `<expr2>` here. // We only support `if (<expr2> != null)`. Fail out if we have the alternate form. if (isEquals) return null; if (nullAssignmentOpt != null) { // If we have a second statement in the if-statement, it must be `<expr> = null;`. // This is fine to convert to a null-conditional access. Here's why: say we started with: // // `if (<expr> != null) { <expr>.Method(); <expr> = null; }` // // If 'expr' is not null, then we execute the body and then end up with expr being null. So `expr?.Method(); expr = null;` // preserves those semantics. Simialarly, if is expr is null, then `expr?.Method();` does nothing, and `expr = null` keeps it // the same as well. So this is a valid conversion in all cases. if (!syntaxFacts.IsSimpleAssignmentStatement(nullAssignmentOpt)) return null; syntaxFacts.GetPartsOfAssignmentStatement(nullAssignmentOpt, out var assignLeft, out _, out var assignRight); if (!syntaxFacts.AreEquivalent(assignLeft, conditionPartToCheck) || !syntaxFacts.IsNullLiteralExpression(assignRight)) { return null; } // Looks good. we can convert this block. } var whenPartMatch = GetWhenPartMatch( syntaxFacts, semanticModel, conditionPartToCheck, (TExpressionSyntax)syntaxFacts.GetExpressionOfExpressionStatement(expressionStatement), cancellationToken); if (whenPartMatch == null) return null; // If we have: // // D D { get; } // // public void Test() // { // if (D != null) // { // D.Method(D); // } // } // // Then `D.Method` is actually an access of a static member, and cannot be converted to `D?.Method`. if (whenPartMatch.Parent is TMemberAccessExpressionSyntax memberAccess) { var memberSymbol = semanticModel.GetSymbolInfo(memberAccess, cancellationToken).GetAnySymbol(); if (memberSymbol?.IsStatic is true) return null; } // can't use ?. on a pointer var whenPartType = semanticModel.GetTypeInfo(whenPartMatch, cancellationToken).Type; if (whenPartType is IPointerTypeSymbol or IFunctionPointerTypeSymbol) return null; var whenPartIsNullable = semanticModel.GetTypeInfo(whenPartMatch, cancellationToken).Type?.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T; var properties = whenPartIsNullable ? s_whenPartIsNullableProperties : ImmutableDictionary<string, string?>.Empty; return new(trueStatement, whenPartMatch, nullAssignmentOpt, properties); } private bool TryGetPartsOfIfStatement( TIfStatementSyntax ifStatement, [NotNullWhen(true)] out TExpressionSyntax? condition, [NotNullWhen(true)] out TStatementSyntax? trueStatement, out TStatementSyntax? nullAssignmentOpt) { trueStatement = null; nullAssignmentOpt = null; if (!this.TryGetPartsOfIfStatement(ifStatement, out condition, out var trueStatements)) return false; if (trueStatements.Length is < 1 or > 2) return false; trueStatement = trueStatements[0]; if (trueStatements.Length == 2) nullAssignmentOpt = trueStatements[1]; return true; } }