File: ConditionalAccessAnalyzer.cs
Web Access
Project: src\src\Analyzers\Microsoft.Analyzers.Extra\Microsoft.Analyzers.Extra.csproj (Microsoft.Analyzers.Extra)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.Extensions.ExtraAnalyzers.Utilities;
 
namespace Microsoft.Extensions.ExtraAnalyzers;
 
/// <summary>
/// C# analyzer that recommends removing superfluous uses of ?.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class ConditionalAccessAnalyzer : DiagnosticAnalyzer
{
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(DiagDescriptors.ConditionalAccess);
 
    public override void Initialize(AnalysisContext context)
    {
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.EnableConcurrentExecution();
 
        context.RegisterCompilationStartAction(compilationStartContext =>
        {
            // only report diagnostics on .NET 6 or above since previous runtimes don't have good enough nullability annotations
            if (compilationStartContext.Compilation.IsNet6OrGreater())
            {
                var maybeNull = compilationStartContext.Compilation.GetTypeByMetadataName("System.Diagnostics.CodeAnalysis.MaybeNullAttribute");
 
                compilationStartContext.RegisterOperationAction(operationAnalysisContext =>
                {
                    var op = (IConditionalAccessOperation)operationAnalysisContext.Operation;
 
                    ITypeSymbol? type;
                    switch (op.Operation.Kind)
                    {
                        case OperationKind.PropertyReference:
                        {
                            var propRef = (IPropertyReferenceOperation)op.Operation;
                            if (MaybeNull(propRef.Property.GetAttributes()))
                            {
                                // property can be null, independent of its type signature
                                return;
                            }
 
                            type = propRef.Property.Type;
                            break;
                        }
 
                        case OperationKind.FieldReference:
                        {
                            var fieldRef = (IFieldReferenceOperation)op.Operation;
                            if (MaybeNull(fieldRef.Field.GetAttributes()))
                            {
                                return;
                            }
 
                            type = fieldRef.Field.Type;
                            break;
                        }
 
                        case OperationKind.Invocation:
                        {
                            var invocation = (IInvocationOperation)op.Operation;
                            if (MaybeNull(invocation.TargetMethod.GetReturnTypeAttributes()))
                            {
                                return;
                            }
 
                            type = op.Operation.Type;
                            break;
                        }
 
                        default:
                        {
                            type = op.Operation.Type;
                            break;
                        }
                    }
 
                    if (type != null)
                    {
                        if (type is ITypeParameterSymbol tp)
                        {
                            if (!tp.HasNotNullConstraint)
                            {
                                // a generic type without a notnull constraint can potentially hold null values
                                return;
                            }
                        }
 
                        // if the type of the operand is not nullable, then we have a candidate
                        if (type.NullableAnnotation == NullableAnnotation.NotAnnotated)
                        {
                            // if the operand is a parameter on a public method or interface method, then don't report it
                            if (op.Operation.Kind == OperationKind.ParameterReference)
                            {
                                var pr = (IParameterReferenceOperation)op.Operation;
                                var method = pr.Parameter.ContainingSymbol as IMethodSymbol;
 
                                if (pr.Parameter.ContainingSymbol.IsExternallyVisible()
                                    || (method != null && method.ImplementsPublicInterface()))
                                {
                                    // this is a ? applied to a parameter of a public method, let it slide...
                                    return;
                                }
                            }
 
                            var diagnostic = Diagnostic.Create(DiagDescriptors.ConditionalAccess, op.Syntax.GetLocation());
                            operationAnalysisContext.ReportDiagnostic(diagnostic);
                        }
                    }
                }, OperationKind.ConditionalAccess);
 
                bool MaybeNull(ImmutableArray<AttributeData> attrs)
                {
                    foreach (var attr in attrs)
                    {
                        if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, maybeNull))
                        {
                            return true;
                        }
                    }
 
                    return false;
                }
            }
        });
    }
}