File: MetaAnalyzers\RegisterActionAnalyzer.cs
Web Access
Project: src\src\RoslynAnalyzers\Microsoft.CodeAnalysis.Analyzers\Core\Microsoft.CodeAnalysis.Analyzers.csproj (Microsoft.CodeAnalysis.Analyzers)
// 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.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis.Analyzers.MetaAnalyzers.Helpers;
using Microsoft.CodeAnalysis.Diagnostics;
 
namespace Microsoft.CodeAnalysis.Analyzers.MetaAnalyzers
{
    using static CodeAnalysisDiagnosticsResources;
 
    /// <summary>
    /// RS1002: <inheritdoc cref="MissingKindArgumentToRegisterActionTitle"/>
    /// RS1003: <inheritdoc cref="UnsupportedSymbolKindArgumentToRegisterActionTitle"/>
    /// RS1006: <inheritdoc cref="InvalidSyntaxKindTypeArgumentTitle"/>
    /// RS1012: <inheritdoc cref="StartActionWithNoRegisteredActionsTitle"/>
    /// RS1013: <inheritdoc cref="StartActionWithOnlyEndActionTitle"/>
    /// </summary>
    public abstract class RegisterActionAnalyzer<TInvocationExpressionSyntax, TArgumentSyntax, TLanguageKindEnum> : DiagnosticAnalyzerCorrectnessAnalyzer
        where TInvocationExpressionSyntax : SyntaxNode
        where TArgumentSyntax : SyntaxNode
        where TLanguageKindEnum : struct
    {
        private static readonly LocalizableString s_localizableTitleMissingKindArgument = CreateLocalizableResourceString(nameof(MissingKindArgumentToRegisterActionTitle));
        private static readonly LocalizableString s_localizableDescriptionMissingKindArgument = CreateLocalizableResourceString(nameof(MissingKindArgumentToRegisterActionDescription));
 
        public static readonly DiagnosticDescriptor MissingSymbolKindArgumentRule = new(
            DiagnosticIds.MissingKindArgumentToRegisterActionRuleId,
            s_localizableTitleMissingKindArgument,
            CreateLocalizableResourceString(nameof(MissingSymbolKindArgumentToRegisterActionMessage)),
            DiagnosticCategory.MicrosoftCodeAnalysisCorrectness,
            DiagnosticSeverity.Warning,
            isEnabledByDefault: true,
            description: s_localizableDescriptionMissingKindArgument,
            customTags: WellKnownDiagnosticTagsExtensions.Telemetry);
 
        public static readonly DiagnosticDescriptor MissingSyntaxKindArgumentRule = new(
            DiagnosticIds.MissingKindArgumentToRegisterActionRuleId,
            s_localizableTitleMissingKindArgument,
            CreateLocalizableResourceString(nameof(MissingSyntaxKindArgumentToRegisterActionMessage)),
            DiagnosticCategory.MicrosoftCodeAnalysisCorrectness,
            DiagnosticSeverity.Warning,
            isEnabledByDefault: true,
            description: s_localizableDescriptionMissingKindArgument,
            customTags: WellKnownDiagnosticTagsExtensions.Telemetry);
 
        public static readonly DiagnosticDescriptor MissingOperationKindArgumentRule = new(
            DiagnosticIds.MissingKindArgumentToRegisterActionRuleId,
            s_localizableTitleMissingKindArgument,
            CreateLocalizableResourceString(nameof(MissingOperationKindArgumentToRegisterActionMessage)),
            DiagnosticCategory.MicrosoftCodeAnalysisCorrectness,
            DiagnosticSeverity.Warning,
            isEnabledByDefault: true,
            description: s_localizableDescriptionMissingKindArgument,
            customTags: WellKnownDiagnosticTagsExtensions.Telemetry);
 
        public static readonly DiagnosticDescriptor UnsupportedSymbolKindArgumentRule = new(
            DiagnosticIds.UnsupportedSymbolKindArgumentRuleId,
            CreateLocalizableResourceString(nameof(UnsupportedSymbolKindArgumentToRegisterActionTitle)),
            CreateLocalizableResourceString(nameof(UnsupportedSymbolKindArgumentToRegisterActionMessage)),
            DiagnosticCategory.MicrosoftCodeAnalysisCorrectness,
            DiagnosticSeverity.Warning,
            isEnabledByDefault: true,
            customTags: WellKnownDiagnosticTagsExtensions.Telemetry);
 
        public static readonly DiagnosticDescriptor InvalidSyntaxKindTypeArgumentRule = new(
            DiagnosticIds.InvalidSyntaxKindTypeArgumentRuleId,
            CreateLocalizableResourceString(nameof(InvalidSyntaxKindTypeArgumentTitle)),
            CreateLocalizableResourceString(nameof(InvalidSyntaxKindTypeArgumentMessage)),
            DiagnosticCategory.MicrosoftCodeAnalysisCorrectness,
            DiagnosticSeverity.Warning,
            isEnabledByDefault: true,
            description: CreateLocalizableResourceString(nameof(InvalidSyntaxKindTypeArgumentDescription), nameof(DiagnosticWellKnownNames.TLanguageKindEnumName)),
            customTags: WellKnownDiagnosticTagsExtensions.Telemetry);
 
        private static readonly LocalizableString s_localizableDescriptionStatefulAnalyzerRegisterActionsDescription = CreateLocalizableResourceString(nameof(StatefulAnalyzerRegisterActionsDescription), nameof(DiagnosticWellKnownNames.TLanguageKindEnumName));
 
        public static readonly DiagnosticDescriptor StartActionWithNoRegisteredActionsRule = new(
            DiagnosticIds.StartActionWithNoRegisteredActionsRuleId,
            CreateLocalizableResourceString(nameof(StartActionWithNoRegisteredActionsTitle)),
            CreateLocalizableResourceString(nameof(StartActionWithNoRegisteredActionsMessage)),
            DiagnosticCategory.MicrosoftCodeAnalysisPerformance,
            DiagnosticSeverity.Warning,
            isEnabledByDefault: true,
            description: s_localizableDescriptionStatefulAnalyzerRegisterActionsDescription,
            customTags: WellKnownDiagnosticTagsExtensions.Telemetry);
 
        public static readonly DiagnosticDescriptor StartActionWithOnlyEndActionRule = new(
            DiagnosticIds.StartActionWithOnlyEndActionRuleId,
            CreateLocalizableResourceString(nameof(StartActionWithOnlyEndActionTitle)),
            CreateLocalizableResourceString(nameof(StartActionWithOnlyEndActionMessage)),
            DiagnosticCategory.MicrosoftCodeAnalysisPerformance,
            DiagnosticSeverity.Warning,
            isEnabledByDefault: true,
            description: s_localizableDescriptionStatefulAnalyzerRegisterActionsDescription,
            customTags: WellKnownDiagnosticTagsExtensions.Telemetry);
 
        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(
            MissingSymbolKindArgumentRule,
            MissingSyntaxKindArgumentRule,
            MissingOperationKindArgumentRule,
            UnsupportedSymbolKindArgumentRule,
            InvalidSyntaxKindTypeArgumentRule,
            StartActionWithNoRegisteredActionsRule,
            StartActionWithOnlyEndActionRule);
 
        protected override DiagnosticAnalyzerSymbolAnalyzer? GetDiagnosticAnalyzerSymbolAnalyzer(CompilationStartAnalysisContext compilationContext, INamedTypeSymbol diagnosticAnalyzer, INamedTypeSymbol diagnosticAnalyzerAttribute)
        {
            Compilation compilation = compilationContext.Compilation;
 
            INamedTypeSymbol? analysisContext = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftCodeAnalysisDiagnosticsAnalysisContext);
            if (analysisContext == null)
            {
                return null;
            }
 
            INamedTypeSymbol? compilationStartAnalysisContext = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftCodeAnalysisDiagnosticsCompilationStartAnalysisContext);
            if (compilationStartAnalysisContext == null)
            {
                return null;
            }
 
            INamedTypeSymbol? codeBlockStartAnalysisContext = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftCodeAnalysisDiagnosticsCodeBlockStartAnalysisContext1);
            if (codeBlockStartAnalysisContext == null)
            {
                return null;
            }
 
            INamedTypeSymbol? operationBlockStartAnalysisContext = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftCodeAnalysisDiagnosticsOperationBlockStartAnalysisContext);
            if (operationBlockStartAnalysisContext == null)
            {
                return null;
            }
 
            INamedTypeSymbol? symbolKind = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftCodeAnalysisSymbolKind);
            if (symbolKind == null)
            {
                return null;
            }
 
            compilationContext.RegisterCodeBlockStartAction<TLanguageKindEnum>(codeBlockContext =>
            {
                RegisterActionCodeBlockAnalyzer analyzer = GetCodeBlockAnalyzer(compilation, analysisContext, compilationStartAnalysisContext,
                    codeBlockStartAnalysisContext, operationBlockStartAnalysisContext, symbolKind);
 
                analyzer.CodeBlockStartAction(codeBlockContext);
            });
 
            // We don't want to analyze DiagnosticAnalyzer type symbols, just the code blocks.
            return null;
        }
 
        protected abstract RegisterActionCodeBlockAnalyzer GetCodeBlockAnalyzer(
            Compilation compilation,
            INamedTypeSymbol analysisContext,
            INamedTypeSymbol compilationStartAnalysisContext,
            INamedTypeSymbol codeBlockStartAnalysisContext,
            INamedTypeSymbol operationBlockStartAnalysisContext,
            INamedTypeSymbol symbolKind);
 
        protected abstract class RegisterActionCodeBlockAnalyzer
        {
            private readonly INamedTypeSymbol _analysisContext;
            private readonly INamedTypeSymbol _compilationStartAnalysisContext;
            private readonly INamedTypeSymbol _codeBlockStartAnalysisContext;
            private readonly INamedTypeSymbol _operationBlockStartAnalysisContext;
            private readonly INamedTypeSymbol _symbolKind;
 
            private static readonly ImmutableHashSet<string> s_supportedSymbolKinds =
                ImmutableHashSet.Create(
                    nameof(SymbolKind.Event),
                    nameof(SymbolKind.Field),
                    nameof(SymbolKind.Method),
                    nameof(SymbolKind.NamedType),
                    nameof(SymbolKind.Namespace),
                    nameof(SymbolKind.Parameter),
                    nameof(SymbolKind.Property));
 
#pragma warning disable CA1815 // Override equals and operator equals on value types
            private struct NodeAndSymbol
#pragma warning restore CA1815 // Override equals and operator equals on value types
            {
                public TInvocationExpressionSyntax Invocation { get; set; }
                public IMethodSymbol Method { get; set; }
            }
 
            /// <summary>
            /// Map from declared analysis context type parameters to invocations of Register methods on them.
            /// </summary>
            private Dictionary<IParameterSymbol, List<NodeAndSymbol>>? _nestedActionsMap;
 
            /// <summary>
            /// Set of declared start analysis context parameters that need to be analyzed for <see cref="StartActionWithNoRegisteredActionsRule"/> and <see cref="StartActionWithOnlyEndActionRule"/>.
            /// </summary>
            private HashSet<IParameterSymbol>? _declaredStartAnalysisContextParams;
 
            /// <summary>
            /// Set of declared start analysis context parameters that need to be skipped for <see cref="StartActionWithNoRegisteredActionsRule"/> and <see cref="StartActionWithOnlyEndActionRule"/>.
            /// This is to avoid false positives where context types are passed as arguments to a different invocation, and hence the registration responsibility is not on the current method.
            /// </summary>
            private HashSet<IParameterSymbol>? _startAnalysisContextParamsToSkip;
 
            protected RegisterActionCodeBlockAnalyzer(
                INamedTypeSymbol analysisContext,
                INamedTypeSymbol compilationStartAnalysisContext,
                INamedTypeSymbol codeBlockStartAnalysisContext,
                INamedTypeSymbol operationBlockStartAnalysisContext,
                INamedTypeSymbol symbolKind)
            {
                _analysisContext = analysisContext;
                _compilationStartAnalysisContext = compilationStartAnalysisContext;
                _codeBlockStartAnalysisContext = codeBlockStartAnalysisContext;
                _operationBlockStartAnalysisContext = operationBlockStartAnalysisContext;
                _symbolKind = symbolKind;
 
                _nestedActionsMap = null;
                _declaredStartAnalysisContextParams = null;
                _startAnalysisContextParamsToSkip = null;
            }
 
            protected abstract IEnumerable<SyntaxNode>? GetArgumentExpressions(TInvocationExpressionSyntax invocation);
            protected abstract SyntaxNode GetArgumentExpression(TArgumentSyntax argument);
            protected abstract SyntaxNode GetInvocationExpression(TInvocationExpressionSyntax invocation);
            protected abstract SyntaxNode? GetInvocationReceiver(TInvocationExpressionSyntax invocation);
            protected abstract bool IsSyntaxKind(ITypeSymbol type);
            protected abstract TLanguageKindEnum InvocationExpressionKind { get; }
            protected abstract TLanguageKindEnum ArgumentSyntaxKind { get; }
            protected abstract TLanguageKindEnum ParameterSyntaxKind { get; }
 
            internal void CodeBlockStartAction(CodeBlockStartAnalysisContext<TLanguageKindEnum> codeBlockContext)
            {
                var method = codeBlockContext.OwningSymbol as IMethodSymbol;
                if (!ShouldAnalyze(method))
                {
                    return;
                }
 
                foreach (IParameterSymbol param in method.Parameters)
                {
                    AnalyzeParameterDeclaration(param);
                }
 
                // Analyze all the Register action invocation expressions.
                codeBlockContext.RegisterSyntaxNodeAction(AnalyzeInvocation, InvocationExpressionKind);
 
                // Analyze all the arguments to invocations.
                codeBlockContext.RegisterSyntaxNodeAction(AnalyzeArgumentSyntax, ArgumentSyntaxKind);
 
                // Analyze all the lambda parameters in the method body, if any.
                codeBlockContext.RegisterSyntaxNodeAction(AnalyzerParameterSyntax, ParameterSyntaxKind);
 
                // Report diagnostics based on the final state.
                codeBlockContext.RegisterCodeBlockEndAction(CodeBlockEndAction);
            }
 
            private bool ShouldAnalyze([NotNullWhen(returnValue: true)] IMethodSymbol? method)
            {
                if (method == null)
                {
                    return false;
                }
 
                // Only analyze this method if declares a parameter with one of the allowed analysis context types.
                foreach (IParameterSymbol parameter in method.Parameters)
                {
                    if (parameter.Type is INamedTypeSymbol namedType &&
    IsContextType(namedType, _analysisContext, _codeBlockStartAnalysisContext, _operationBlockStartAnalysisContext, _compilationStartAnalysisContext))
                    {
                        return true;
                    }
                }
 
                return false;
            }
 
            private static bool IsContextType(ITypeSymbol type, params INamedTypeSymbol[] allowedContextTypes)
            {
                INamedTypeSymbol? namedType = (type as INamedTypeSymbol)?.OriginalDefinition;
                if (namedType != null)
                {
                    foreach (INamedTypeSymbol contextType in allowedContextTypes)
                    {
                        if (SymbolEqualityComparer.Default.Equals(namedType, contextType))
                        {
                            return true;
                        }
                    }
                }
 
                return false;
            }
 
            private static bool IsRegisterAction(string expectedName, IMethodSymbol method, params INamedTypeSymbol[] allowedContainingTypes)
            {
                return method.Name.Equals(expectedName, StringComparison.Ordinal) &&
                    IsContextType(method.ContainingType, allowedContainingTypes);
            }
 
            private void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
            {
                var invocation = (TInvocationExpressionSyntax)context.Node;
                SemanticModel semanticModel = context.SemanticModel;
 
                ISymbol? symbol = semanticModel.GetSymbolInfo(invocation, context.CancellationToken).Symbol;
                if (symbol == null || symbol.Kind != SymbolKind.Method || !symbol.Name.StartsWith("Register", StringComparison.Ordinal))
                {
                    return;
                }
 
                var method = (IMethodSymbol)symbol;
                NoteRegisterActionInvocation(method, invocation, semanticModel, context.CancellationToken);
 
                bool isRegisterSymbolAction = IsRegisterAction(DiagnosticWellKnownNames.RegisterSymbolActionName, method, _analysisContext, _compilationStartAnalysisContext);
                bool isRegisterSyntaxNodeAction = IsRegisterAction(DiagnosticWellKnownNames.RegisterSyntaxNodeActionName, method, _analysisContext, _compilationStartAnalysisContext, _codeBlockStartAnalysisContext);
                bool isRegisterCodeBlockStartAction = IsRegisterAction(DiagnosticWellKnownNames.RegisterCodeBlockStartActionName, method, _analysisContext, _compilationStartAnalysisContext);
                bool isRegisterOperationAction = IsRegisterAction(DiagnosticWellKnownNames.RegisterOperationActionName, method, _analysisContext, _compilationStartAnalysisContext, _operationBlockStartAnalysisContext);
 
                if ((isRegisterSymbolAction || isRegisterSyntaxNodeAction || isRegisterOperationAction) &&
                    method.Parameters.Length == 2 && method.Parameters[1].IsParams)
                {
                    IEnumerable<SyntaxNode>? arguments = GetArgumentExpressions(invocation);
                    if (arguments != null)
                    {
                        int argumentCount = arguments.Count();
                        if (argumentCount >= 1)
                        {
                            ITypeSymbol? type = semanticModel.GetTypeInfo(arguments.First(), context.CancellationToken).ConvertedType;
                            if (type == null || type.Name.Equals(nameof(Action), StringComparison.Ordinal))
                            {
                                if (argumentCount == 1)
                                {
                                    DiagnosticDescriptor rule;
                                    if (isRegisterSymbolAction)
                                    {
                                        rule = MissingSymbolKindArgumentRule;
                                    }
                                    else if (isRegisterOperationAction)
                                    {
                                        rule = MissingOperationKindArgumentRule;
                                    }
                                    else
                                    {
                                        rule = MissingSyntaxKindArgumentRule;
                                    }
 
                                    SyntaxNode invocationExpression = GetInvocationExpression(invocation);
                                    Diagnostic diagnostic = invocationExpression.CreateDiagnostic(rule);
                                    context.ReportDiagnostic(diagnostic);
                                }
                                else if (isRegisterSymbolAction)
                                {
                                    foreach (SyntaxNode argument in arguments.Skip(1))
                                    {
                                        symbol = semanticModel.GetSymbolInfo(argument, context.CancellationToken).Symbol;
                                        if (symbol != null &&
                                            symbol.Kind == SymbolKind.Field &&
                                            SymbolEqualityComparer.Default.Equals(_symbolKind, symbol.ContainingType) &&
                                            !s_supportedSymbolKinds.Contains(symbol.Name))
                                        {
                                            Diagnostic diagnostic = argument.CreateDiagnostic(UnsupportedSymbolKindArgumentRule, symbol.Name);
                                            context.ReportDiagnostic(diagnostic);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
 
                if (!method.TypeParameters.IsEmpty &&
                    (isRegisterSyntaxNodeAction || isRegisterCodeBlockStartAction))
                {
                    ITypeSymbol? typeArgument = null;
                    if (method.TypeParameters.Length == 1)
                    {
                        if (method.TypeParameters[0].Name == DiagnosticWellKnownNames.TLanguageKindEnumName)
                        {
                            typeArgument = method.TypeArguments[0];
                        }
                    }
                    else
                    {
                        ITypeParameterSymbol? typeParam = method.TypeParameters.FirstOrDefault(t => t.Name == DiagnosticWellKnownNames.TLanguageKindEnumName);
                        if (typeParam != null)
                        {
                            int index = method.TypeParameters.IndexOf(typeParam);
                            typeArgument = method.TypeArguments[index];
                        }
                    }
 
                    if (typeArgument != null &&
                        typeArgument.TypeKind != TypeKind.TypeParameter &&
                        typeArgument.TypeKind != TypeKind.Error &&
                        !IsSyntaxKind(typeArgument))
                    {
                        Location location = typeArgument.Locations[0];
                        if (!location.IsInSource)
                        {
                            SyntaxNode invocationExpression = GetInvocationExpression(invocation);
                            location = invocationExpression.GetLocation();
                        }
 
                        Diagnostic diagnostic = Diagnostic.Create(InvalidSyntaxKindTypeArgumentRule, location, typeArgument.Name, DiagnosticWellKnownNames.TLanguageKindEnumName, method.Name);
                        context.ReportDiagnostic(diagnostic);
                    }
                }
            }
 
            private void AnalyzeArgumentSyntax(SyntaxNodeAnalysisContext context)
            {
                SyntaxNode argumentExpression = GetArgumentExpression((TArgumentSyntax)context.Node);
                if (argumentExpression != null &&
                    context.SemanticModel.GetSymbolInfo(argumentExpression, context.CancellationToken).Symbol is IParameterSymbol parameter)
                {
                    AnalyzeParameterReference(parameter);
                }
            }
 
            private void AnalyzerParameterSyntax(SyntaxNodeAnalysisContext context)
            {
                if (context.SemanticModel.GetDeclaredSymbol(context.Node, context.CancellationToken) is IParameterSymbol parameter)
                {
                    AnalyzeParameterDeclaration(parameter);
                }
            }
 
            private void AnalyzeParameterDeclaration(IParameterSymbol parameter)
            {
                if (IsContextType(parameter.Type, _compilationStartAnalysisContext, _codeBlockStartAnalysisContext, _operationBlockStartAnalysisContext))
                {
                    _declaredStartAnalysisContextParams ??= new HashSet<IParameterSymbol>();
                    _declaredStartAnalysisContextParams.Add(parameter);
                }
            }
 
            private void AnalyzeParameterReference(IParameterSymbol parameter)
            {
                // We skip analysis for context parameters that are passed as arguments to any invocation.
                // This is to avoid false positives, as the registration responsibility is not on the current method.
                if (IsContextType(parameter.Type, _compilationStartAnalysisContext, _codeBlockStartAnalysisContext, _operationBlockStartAnalysisContext))
                {
                    _startAnalysisContextParamsToSkip ??= new HashSet<IParameterSymbol>();
                    _startAnalysisContextParamsToSkip.Add(parameter);
                }
            }
 
            private void NoteRegisterActionInvocation(IMethodSymbol method, TInvocationExpressionSyntax invocation, SemanticModel model, CancellationToken cancellationToken)
            {
                if (SymbolEqualityComparer.Default.Equals(method.ContainingType, _analysisContext))
                {
                    // Not a nested action.
                    return;
                }
 
                SyntaxNode? receiver = GetInvocationReceiver(invocation);
                if (receiver == null)
                {
                    return;
                }
 
                // Get the context parameter on which we are registering an action.
                if (model.GetSymbolInfo(receiver, cancellationToken).Symbol is not IParameterSymbol contextParameter)
                {
                    return;
                }
 
                // Check if we are bailing out on this context parameter.
                if (_startAnalysisContextParamsToSkip != null && _startAnalysisContextParamsToSkip.Contains(contextParameter))
                {
                    return;
                }
 
                _nestedActionsMap ??= new Dictionary<IParameterSymbol, List<NodeAndSymbol>>();
                if (!_nestedActionsMap.TryGetValue(contextParameter, out List<NodeAndSymbol> registerInvocations))
                {
                    registerInvocations = new List<NodeAndSymbol>();
                }
 
                registerInvocations.Add(new NodeAndSymbol { Invocation = invocation, Method = method });
                _nestedActionsMap[contextParameter] = registerInvocations;
            }
 
            private void CodeBlockEndAction(CodeBlockAnalysisContext codeBlockContext)
            {
                if (_declaredStartAnalysisContextParams == null)
                {
                    // No declared context parameters to analyze.
                    return;
                }
 
                foreach (IParameterSymbol contextParameter in _declaredStartAnalysisContextParams)
                {
                    // Check if we must bail out on this context parameter.
                    if (_startAnalysisContextParamsToSkip != null && _startAnalysisContextParamsToSkip.Contains(contextParameter))
                    {
                        continue;
                    }
 
                    var hasEndAction = false;
                    var hasNonEndAction = false;
 
                    if (_nestedActionsMap != null && _nestedActionsMap.TryGetValue(contextParameter, out List<NodeAndSymbol> registeredActions))
                    {
                        foreach (NodeAndSymbol invocationInfo in registeredActions)
                        {
                            switch (invocationInfo.Method.Name)
                            {
                                case DiagnosticWellKnownNames.RegisterCompilationEndActionName:
                                case DiagnosticWellKnownNames.RegisterCodeBlockEndActionName:
                                case DiagnosticWellKnownNames.RegisterOperationBlockEndActionName:
                                    hasEndAction = true;
                                    break;
 
                                default:
                                    hasNonEndAction = true;
                                    break;
                            }
                        }
                    }
 
                    // Report a diagnostic if no non-end actions were registered on start analysis context parameter.
                    if (!hasNonEndAction)
                    {
                        ReportDiagnostic(codeBlockContext, contextParameter, hasEndAction);
                    }
                }
            }
 
            private void ReportDiagnostic(CodeBlockAnalysisContext codeBlockContext, IParameterSymbol contextParameter, bool hasEndAction)
            {
                Debug.Assert(IsContextType(contextParameter.Type, _codeBlockStartAnalysisContext, _compilationStartAnalysisContext, _operationBlockStartAnalysisContext));
                bool isCompilationStartAction = SymbolEqualityComparer.Default.Equals(contextParameter.Type.OriginalDefinition, _compilationStartAnalysisContext.OriginalDefinition);
                bool isOperationBlockStartAction = !isCompilationStartAction && SymbolEqualityComparer.Default.Equals(contextParameter.Type.OriginalDefinition, _operationBlockStartAnalysisContext.OriginalDefinition);
 
                Location location = contextParameter.DeclaringSyntaxReferences.First()
                        .GetSyntax(codeBlockContext.CancellationToken).GetLocation();
 
                string parameterName = contextParameter.Name;
                string endActionName;
                string statelessActionName;
                string parentRegisterMethodName;
                if (isCompilationStartAction)
                {
                    endActionName = "CompilationEndAction";
                    statelessActionName = DiagnosticWellKnownNames.RegisterCompilationActionName;
                    parentRegisterMethodName = "Initialize";
                }
                else if (isOperationBlockStartAction)
                {
                    endActionName = "OperationBlockEndAction";
                    statelessActionName = DiagnosticWellKnownNames.RegisterOperationBlockActionName;
                    parentRegisterMethodName = "Initialize, CompilationStartAction";
                }
                else
                {
                    endActionName = "CodeBlockEndAction";
                    statelessActionName = DiagnosticWellKnownNames.RegisterCodeBlockActionName;
                    parentRegisterMethodName = "Initialize, CompilationStartAction";
                }
 
                Diagnostic diagnostic;
                if (!hasEndAction)
                {
                    diagnostic = Diagnostic.Create(StartActionWithNoRegisteredActionsRule, location, parameterName, parentRegisterMethodName);
                }
                else
                {
                    diagnostic = Diagnostic.Create(StartActionWithOnlyEndActionRule, location, parameterName, endActionName, statelessActionName, parentRegisterMethodName);
                }
 
                codeBlockContext.ReportDiagnostic(diagnostic);
            }
        }
    }
}