File: src\Analyzers\CSharp\Analyzers\UseNameofInNullableAttribute\CSharpUseNameofInNullableAttributeDiagnosticAnalyzer.cs
Web Access
Project: src\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.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Simplification;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.UseNameofInAttribute;
 
/// <summary>
/// Analyzer that looks for things like `NotNullIfNotNull("param")` and offers to use `NotNullIfNotNull(nameof(param))` instead.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal sealed class CSharpUseNameofInAttributeDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer
{
    public const string NameKey = nameof(NameKey);
 
    public CSharpUseNameofInAttributeDiagnosticAnalyzer()
        : base(IDEDiagnosticIds.UseNameofInAttributeDiagnosticId,
               EnforceOnBuildValues.UseNameofInAttribute,
               option: null,
               new LocalizableResourceString(
                   nameof(CSharpAnalyzersResources.Use_nameof), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)))
    {
    }
 
    public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
        => DiagnosticAnalyzerCategory.SemanticSpanAnalysis;
 
    protected override void InitializeWorker(AnalysisContext context)
    {
        context.RegisterCompilationStartAction(context =>
        {
            if (context.Compilation.LanguageVersion() >= LanguageVersion.CSharp11)
                context.RegisterSyntaxNodeAction(AnalyzeAttribute, SyntaxKind.Attribute);
        });
    }
 
    private void AnalyzeAttribute(SyntaxNodeAnalysisContext context)
    {
        if (ShouldSkipAnalysis(context, notification: null))
            return;
 
        var cancellationToken = context.CancellationToken;
        var attribute = (AttributeSyntax)context.Node;
        var semanticModel = context.SemanticModel;
 
        if (attribute.ArgumentList is null)
            return;
 
        var attributeName = attribute.Name.GetRightmostName()?.Identifier.ValueText;
        if (attributeName is null)
            return;
 
        if (attributeName
                is not "NotNullIfNotNull"
                and not "NotNullIfNotNullAttribute"
                and not "MemberNotNull"
                and not "MemberNotNullAttribute"
                and not "MemberNotNullWhen"
                and not "MemberNotNullWhenAttribute"
                and not "CallerArgumentExpression"
                and not "CallerArgumentExpressionAttribute")
        {
            return;
        }
 
        INamedTypeSymbol? containingType = null;
        foreach (var argument in attribute.ArgumentList.Arguments)
        {
            if (argument.Expression is not LiteralExpressionSyntax(SyntaxKind.StringLiteralExpression) and not InterpolatedStringExpressionSyntax)
                continue;
 
            var constantValue = semanticModel.GetConstantValue(argument.Expression, cancellationToken);
            if (constantValue.Value is not string stringValue)
                continue;
 
            if (stringValue == "")
                continue;
 
            var position = argument.Expression.SpanStart;
 
            containingType ??= semanticModel.GetEnclosingNamedType(position, cancellationToken);
            if (containingType is null)
                return;
 
            // Now, see if there are any parameters in scope with this same name.  If so, we can now suggest the user
            // just use `nameof(param)` instead of `"param"` in the attribute.
            var symbols = semanticModel.LookupSymbols(argument.Expression.SpanStart, name: stringValue);
            if (symbols.Any(s => s.IsAccessibleWithin(containingType)) ||
                MatchesParameterOnContainer(attribute, stringValue))
            {
                context.ReportDiagnostic(DiagnosticHelper.Create(
                    this.Descriptor,
                    argument.Expression.GetLocation(),
                    NotificationOption2.Suggestion,
                    context.Options,
                    additionalLocations: null,
                    ImmutableDictionary<string, string?>.Empty.Add(NameKey, stringValue)));
            }
        }
    }
 
    private static bool MatchesParameterOnContainer(AttributeSyntax attribute, string stringValue)
    {
        var attributeList = attribute.Parent as AttributeListSyntax;
        var container = attributeList?.Parent;
 
        if (container is ParameterSyntax)
        {
            var parameterList = container.Parent as BaseParameterListSyntax;
            container = parameterList?.Parent;
        }
 
        if (container is null)
            return false;
 
        var parameters = container.GetParameterList();
        if (parameters is null)
            return false;
 
        foreach (var parameter in parameters.Parameters)
        {
            if (parameter.Identifier.ValueText == stringValue)
                return true;
        }
 
        return false;
    }
}