File: Microsoft.CodeQuality.Analyzers\QualityGuidelines\MarkMembersAsStatic.cs
Web Access
Project: src\sdk\src\Microsoft.CodeAnalysis.NetAnalyzers\src\Microsoft.CodeAnalysis.NetAnalyzers\Microsoft.CodeAnalysis.NetAnalyzers.csproj (Microsoft.CodeAnalysis.NetAnalyzers)
// 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 System.Linq;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Analyzer.Utilities.PooledObjects;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.CodeQuality.Analyzers.QualityGuidelines
{
    using static MicrosoftCodeQualityAnalyzersResources;

    /// <summary>
    /// CA1822: <inheritdoc cref="MarkMembersAsStaticTitle"/>
    /// </summary>
    [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
    public sealed class MarkMembersAsStaticAnalyzer : DiagnosticAnalyzer
    {
        internal const string RuleId = "CA1822";

        internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create(
            RuleId,
            CreateLocalizableResourceString(nameof(MarkMembersAsStaticTitle)),
            CreateLocalizableResourceString(nameof(MarkMembersAsStaticMessage)),
            DiagnosticCategory.Performance,
            RuleLevel.IdeSuggestion,
            description: CreateLocalizableResourceString(nameof(MarkMembersAsStaticDescription)),
            isPortedFxCopRule: true,
            isDataflowRule: false);

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);

        public override void Initialize(AnalysisContext context)
        {
            context.EnableConcurrentExecution();

            // Don't report in generated code since that's not actionable.
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

            context.RegisterCompilationStartAction(context =>
            {
                var wellKnownTypeProvider = WellKnownTypeProvider.GetOrCreate(context.Compilation);

                // Get the list of all method' attributes for which the rule shall not be triggered.
                ImmutableArray<INamedTypeSymbol> skippedAttributes = GetSkippedAttributes(wellKnownTypeProvider);

                var isWebProject = context.Compilation.IsWebProject(context.Options);

                context.RegisterSymbolStartAction(
                    context => OnSymbolStart(context, wellKnownTypeProvider, skippedAttributes, isWebProject),
                    SymbolKind.NamedType);
            });

            return;

            static void OnSymbolStart(
                SymbolStartAnalysisContext context,
                WellKnownTypeProvider wellKnownTypeProvider,
                ImmutableArray<INamedTypeSymbol> skippedAttributes,
                bool isWebProject)
            {
                // Since property/event accessors cannot be marked static themselves and the associated symbol (property/event)
                // has to be marked static, we want to report the diagnostic on the property/event.
                // So we make a note of the property/event symbols which have at least one accessor with no instance access.
                // At symbol end, we report candidate property/event symbols whose all accessors are candidates to be marked static.
                var propertyOrEventCandidates = TemporarySet<ISymbol>.Empty;
                var accessorCandidates = TemporarySet<IMethodSymbol>.Empty;

                var methodCandidates = TemporarySet<IMethodSymbol>.Empty;

                // Do not flag methods that are used as delegates: https://github.com/dotnet/roslyn-analyzers/issues/1511
                var methodsUsedAsDelegates = TemporarySet<IMethodSymbol>.Empty;

                context.RegisterOperationAction(OnMethodReference, OperationKind.MethodReference);
                context.RegisterOperationBlockStartAction(OnOperationBlockStart);
                context.RegisterSymbolEndAction(OnSymbolEnd);

                return;

                void OnMethodReference(OperationAnalysisContext context)
                {
                    var methodReference = (IMethodReferenceOperation)context.Operation;
                    methodsUsedAsDelegates.Add(methodReference.Method, context.CancellationToken);
                }

                void OnOperationBlockStart(OperationBlockStartAnalysisContext context)
                {
                    if (context.OwningSymbol is not IMethodSymbol methodSymbol)
                    {
                        return;
                    }

                    // Don't run any other check for this method if it isn't a valid analysis context
                    if (!ShouldAnalyze(methodSymbol, wellKnownTypeProvider, skippedAttributes, isWebProject, context))
                    {
                        return;
                    }

                    bool isInstanceReferenced = false;

                    context.RegisterOperationAction(context =>
                    {
                        var operation = (IInstanceReferenceOperation)context.Operation;
                        if (operation.ReferenceKind == InstanceReferenceKind.ContainingTypeInstance
                            && (context.Operation.Parent is not IInvocationOperation invocation || !invocation.TargetMethod.Equals(methodSymbol, SymbolEqualityComparer.Default)))
                        {
                            isInstanceReferenced = true;
                        }
                    }, OperationKind.InstanceReference);

                    // Workaround for https://github.com/dotnet/roslyn/issues/27564
                    context.RegisterOperationAction(context =>
                    {
                        if (!context.Operation.IsOperationNoneRoot())
                        {
                            isInstanceReferenced = true;
                        }
                    }, OperationKind.None);

                    context.RegisterOperationAction(context =>
                    {
                        var operation = (IParameterReferenceOperation)context.Operation;
                        if (operation.Parameter.ContainingSymbol is IMethodSymbol { MethodKind: MethodKind.Constructor })
                        {
                            // we're referencing a parameter not from our actual method, but from a type constructor.
                            // This must be a primary constructor scenario, and we're capturing the parameter here.  
                            // This member cannot be made static.
                            isInstanceReferenced = true;
                        }
                        else if (operation.Parameter.ContainingSymbol is ITypeSymbol { TypeKind: ITypeSymbolExtensions.ExtensionTypeKind })
                        {
                            // We're referencing an extension receiver.
                            isInstanceReferenced = true;
                        }
                    }, OperationKind.ParameterReference);

                    context.RegisterOperationBlockEndAction(context =>
                    {
                        if (!isInstanceReferenced)
                        {
                            if (methodSymbol.IsAccessorMethod())
                            {
                                accessorCandidates.Add(methodSymbol, context.CancellationToken);
                                propertyOrEventCandidates.Add(methodSymbol.AssociatedSymbol!, context.CancellationToken);
                            }
                            else if (methodSymbol.IsExternallyVisible())
                            {
                                if (!IsOnObsoleteMemberChain(methodSymbol, wellKnownTypeProvider))
                                {
                                    context.ReportDiagnostic(methodSymbol.CreateDiagnostic(Rule, methodSymbol.Name));
                                }
                            }
                            else
                            {
                                methodCandidates.Add(methodSymbol, context.CancellationToken);
                            }
                        }
                    });
                }

                void OnSymbolEnd(SymbolAnalysisContext context)
                {
                    foreach (var candidate in methodCandidates.NonConcurrentEnumerable)
                    {
                        if (methodsUsedAsDelegates.Contains_NonConcurrent(candidate))
                        {
                            continue;
                        }

                        if (!IsOnObsoleteMemberChain(candidate, wellKnownTypeProvider))
                        {
                            context.ReportDiagnostic(candidate.CreateDiagnostic(Rule, candidate.Name));
                        }
                    }

                    foreach (var candidatePropertyOrEvent in propertyOrEventCandidates.NonConcurrentEnumerable)
                    {
                        var allAccessorsAreCandidates = true;
                        foreach (var accessor in candidatePropertyOrEvent.GetAccessors())
                        {
                            if (!accessorCandidates.Contains_NonConcurrent(accessor) ||
                                IsOnObsoleteMemberChain(accessor, wellKnownTypeProvider))
                            {
                                allAccessorsAreCandidates = false;
                                break;
                            }
                        }

                        if (allAccessorsAreCandidates)
                        {
                            context.ReportDiagnostic(candidatePropertyOrEvent.CreateDiagnostic(Rule, candidatePropertyOrEvent.Name));
                        }
                    }

                    propertyOrEventCandidates.Free(context.CancellationToken);
                    accessorCandidates.Free(context.CancellationToken);
                    methodCandidates.Free(context.CancellationToken);
                    methodsUsedAsDelegates.Free(context.CancellationToken);
                }
            }
        }

        private static bool ShouldAnalyze(
            IMethodSymbol methodSymbol,
            WellKnownTypeProvider wellKnownTypeProvider,
            ImmutableArray<INamedTypeSymbol> skippedAttributes,
            bool isWebProject,
#pragma warning disable RS1012 // Start action has no registered actions
            OperationBlockStartAnalysisContext context)
#pragma warning restore RS1012 // Start action has no registered actions
        {
            // Modifiers that we don't care about
            // 'PartialImplementationPart is not null' means the method is partial, and the
            // symbol we have is the "definition", not the "implementation".
            // We should only analyze the implementation.
            if (methodSymbol.IsStatic || methodSymbol.IsOverride || methodSymbol.IsVirtual ||
                methodSymbol.IsExtern || methodSymbol.IsAbstract || methodSymbol.PartialImplementationPart is not null ||
                methodSymbol.IsImplementationOfAnyInterfaceMember())
            {
                return false;
            }

            // Do not analyze constructors, finalizers, and indexers.
            if (methodSymbol.IsConstructor() || methodSymbol.IsFinalizer() || methodSymbol.AssociatedSymbol.IsIndexer())
            {
                return false;
            }

            // Don't report methods which have a single throw statement
            // with NotImplementedException or NotSupportedException
            if (context.IsMethodNotImplementedOrSupported())
            {
                return false;
            }

            if (methodSymbol.IsExternallyVisible())
            {
                // Do not analyze public APIs for web projects
                // See https://github.com/dotnet/roslyn-analyzers/issues/3835 for details.
                if (isWebProject)
                {
                    return false;
                }

                // CA1000 says one shouldn't declare static members on generic types. So don't flag such cases.
                if (methodSymbol.ContainingType.IsGenericType)
                {
                    return false;
                }
            }

            // We consider that auto-property have the intent to always be instance members so we want to workaround this issue.
            if (methodSymbol.IsAutoPropertyAccessor())
            {
                return false;
            }

            // Awaitable-awaiter pattern members should not be marked as static.
            // There is no need to check for INotifyCompletion or ICriticalNotifyCompletion members as they are already excluded.
            if (wellKnownTypeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemRuntimeCompilerServicesINotifyCompletion, out var inotifyCompletionType)
                && wellKnownTypeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemRuntimeCompilerServicesICriticalNotifyCompletion, out var icriticalNotifyCompletionType))
            {
                if (methodSymbol.IsGetAwaiterFromAwaitablePattern(inotifyCompletionType, icriticalNotifyCompletionType)
                    || methodSymbol.IsGetResultFromAwaiterPattern(inotifyCompletionType, icriticalNotifyCompletionType))
                {
                    return false;
                }

                if (methodSymbol.AssociatedSymbol is IPropertySymbol property
                    && property.IsIsCompletedFromAwaiterPattern(inotifyCompletionType, icriticalNotifyCompletionType))
                {
                    return false;
                }
            }

            var attributes = methodSymbol.GetAttributes();
            if (methodSymbol.AssociatedSymbol != null)
            {
                // For accessors we want to also check the attributes of the associated symbol
                attributes = attributes.AddRange(methodSymbol.AssociatedSymbol.GetAttributes());
            }

            // FxCop doesn't check for the fully qualified name for these attributes - so we'll do the same.
            if (attributes.Any(static (attribute, skippedAttributes) => skippedAttributes.Any(static (attr, attribute) => attribute.AttributeClass.Inherits(attr), attribute), skippedAttributes))
            {
                return false;
            }

            // If this looks like an event handler don't flag such cases.
            // However, we do want to consider EventRaise accessor as a candidate
            // so we can flag the associated event if none of it's accessors need instance reference.
            if (methodSymbol.HasEventHandlerSignature(wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemEventArgs)) &&
                methodSymbol.MethodKind != MethodKind.EventRaise)
            {
                return false;
            }

            if (IsExplicitlyVisibleFromCom(methodSymbol, wellKnownTypeProvider))
            {
                return false;
            }

            var hasCorrectVisibility = context.Options.MatchesConfiguredVisibility(Rule, methodSymbol, wellKnownTypeProvider.Compilation,
                defaultRequiredVisibility: SymbolVisibilityGroup.All);
            if (!hasCorrectVisibility)
            {
                return false;
            }

            return true;
        }

        private static bool IsExplicitlyVisibleFromCom(IMethodSymbol methodSymbol, WellKnownTypeProvider wellKnownTypeProvider)
        {
            if (!methodSymbol.IsExternallyVisible() || methodSymbol.IsGenericMethod)
            {
                return false;
            }

            var comVisibleAttribute = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemRuntimeInteropServicesComVisibleAttribute);
            if (comVisibleAttribute == null)
            {
                return false;
            }

            if (methodSymbol.HasAnyAttribute(comVisibleAttribute) ||
                methodSymbol.ContainingType.HasAnyAttribute(comVisibleAttribute))
            {
                return true;
            }

            return false;
        }

        private static ImmutableArray<INamedTypeSymbol> GetSkippedAttributes(WellKnownTypeProvider wellKnownTypeProvider)
        {
            ImmutableArray<INamedTypeSymbol>.Builder? builder = null;

            void Add(INamedTypeSymbol? symbol)
            {
                if (symbol != null)
                {
                    builder ??= ImmutableArray.CreateBuilder<INamedTypeSymbol>();
                    builder.Add(symbol);
                }
            }

            // MSTest attributes
            Add(wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingTestInitializeAttribute));
            Add(wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingTestMethodAttribute));
            Add(wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingDataTestMethodAttribute));
            Add(wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingTestCleanupAttribute));

            // XUnit attributes
            Add(wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.XunitFactAttribute));

            // NUnit Attributes
            Add(wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.NUnitFrameworkSetUpAttribute));
            Add(wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.NUnitFrameworkInterfacesITestBuilder));
            Add(wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.NUnitFrameworkOneTimeSetUpAttribute));
            Add(wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.NUnitFrameworkOneTimeTearDownAttribute));
            Add(wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.NUnitFrameworkTestAttribute));
            Add(wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.NUnitFrameworkTearDownAttribute));

            return builder?.ToImmutable() ?? ImmutableArray<INamedTypeSymbol>.Empty;
        }

        private static bool IsOnObsoleteMemberChain(ISymbol symbol, WellKnownTypeProvider wellKnownTypeProvider)
        {
            var obsoleteAttributeType = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemObsoleteAttribute);
            if (obsoleteAttributeType is null)
            {
                return false;
            }

            while (symbol != null)
            {
                if (symbol.HasAnyAttribute(obsoleteAttributeType))
                    return true;

                symbol = symbol is IMethodSymbol method && method.AssociatedSymbol != null
                    ? method.AssociatedSymbol :
                    symbol.ContainingSymbol;
            }

            return false;
        }
    }
}