File: Microsoft.NetCore.Analyzers\Performance\ConstantExpectedAnalyzer.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;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.NetCore.Analyzers.Performance
{
    using static MicrosoftNetCoreAnalyzersResources;
    public abstract partial class ConstantExpectedAnalyzer : DiagnosticAnalyzer
    {
        protected const string ConstantExpectedAttribute = nameof(ConstantExpectedAttribute);
        protected const string ConstantExpected = nameof(ConstantExpected);
        protected const string ConstantExpectedMin = "Min";
        protected const string ConstantExpectedMax = "Max";
        private static readonly LocalizableString s_localizableApplicationTitle = CreateLocalizableResourceString(nameof(ConstantExpectedApplicationTitle));
        private static readonly LocalizableString s_localizableApplicationDescription = CreateLocalizableResourceString(nameof(ConstantExpectedApplicationDescription));
        private static readonly LocalizableString s_localizableUsageTitle = CreateLocalizableResourceString(nameof(ConstantExpectedUsageTitle));
        private static readonly LocalizableString s_localizableUsageDescription = CreateLocalizableResourceString(nameof(ConstantExpectedUsageDescription));

        internal static class CA1856
        {
            internal const string Id = nameof(CA1856);
            internal const RuleLevel Level = RuleLevel.BuildError;
            internal static readonly DiagnosticDescriptor UnsupportedTypeRule = DiagnosticDescriptorHelper.Create(
                Id,
                s_localizableApplicationTitle,
                CreateLocalizableResourceString(nameof(ConstantExpectedNotSupportedMessage)),
                DiagnosticCategory.Performance,
                Level,
                description: s_localizableApplicationDescription,
                isPortedFxCopRule: false,
                isDataflowRule: false);

            internal static readonly DiagnosticDescriptor IncompatibleConstantTypeRule = DiagnosticDescriptorHelper.Create(
                Id,
                s_localizableApplicationTitle,
                CreateLocalizableResourceString(nameof(ConstantExpectedIncompatibleConstantTypeMessage)),
                DiagnosticCategory.Performance,
                Level,
                description: s_localizableApplicationDescription,
                isPortedFxCopRule: false,
                isDataflowRule: false);

            internal static readonly DiagnosticDescriptor InvalidBoundsRule = DiagnosticDescriptorHelper.Create(
                Id,
                s_localizableApplicationTitle,
                CreateLocalizableResourceString(nameof(ConstantExpectedInvalidBoundsMessage)),
                DiagnosticCategory.Performance,
                Level,
                description: s_localizableApplicationDescription,
                isPortedFxCopRule: false,
                isDataflowRule: false);

            internal static readonly DiagnosticDescriptor InvertedRangeRule = DiagnosticDescriptorHelper.Create(
                Id,
                s_localizableApplicationTitle,
                CreateLocalizableResourceString(nameof(ConstantExpectedInvertedRangeMessage)),
                DiagnosticCategory.Performance,
                Level,
                description: s_localizableApplicationDescription,
                isPortedFxCopRule: false,
                isDataflowRule: false);
        }

        internal static class CA1857
        {
            internal const string Id = nameof(CA1857);
            internal const RuleLevel Level = RuleLevel.BuildWarning;

            internal static readonly DiagnosticDescriptor ConstantOutOfBoundsRule = DiagnosticDescriptorHelper.Create(
                Id,
                s_localizableUsageTitle,
                CreateLocalizableResourceString(nameof(ConstantExpectedOutOfBoundsMessage)),
                DiagnosticCategory.Performance,
                Level,
                description: s_localizableUsageDescription,
                isPortedFxCopRule: false,
                isDataflowRule: false);

            internal static readonly DiagnosticDescriptor ConstantNotConstantRule = DiagnosticDescriptorHelper.Create(
                Id,
                s_localizableUsageTitle,
                CreateLocalizableResourceString(nameof(ConstantExpectedNotConstantMessage)),
                DiagnosticCategory.Performance,
                Level,
                description: s_localizableUsageDescription,
                isPortedFxCopRule: false,
                isDataflowRule: false);

            internal static readonly DiagnosticDescriptor ConstantInvalidConstantRule = DiagnosticDescriptorHelper.Create(
                Id,
                s_localizableUsageTitle,
                CreateLocalizableResourceString(nameof(ConstantExpectedInvalidMessage)),
                DiagnosticCategory.Performance,
                Level,
                description: s_localizableUsageDescription,
                isPortedFxCopRule: false,
                isDataflowRule: false);

            internal static readonly DiagnosticDescriptor AttributeExpectedRule = DiagnosticDescriptorHelper.Create(
                Id,
                s_localizableUsageTitle,
                CreateLocalizableResourceString(nameof(ConstantExpectedAttributExpectedMessage)),
                DiagnosticCategory.Performance,
                Level,
                description: s_localizableUsageDescription,
                isPortedFxCopRule: false,
                isDataflowRule: false);
        }
        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(
            CA1856.UnsupportedTypeRule, CA1856.IncompatibleConstantTypeRule,
            CA1856.InvalidBoundsRule, CA1856.InvertedRangeRule,
            CA1857.ConstantOutOfBoundsRule, CA1857.ConstantInvalidConstantRule,
            CA1857.ConstantNotConstantRule, CA1857.AttributeExpectedRule);

        protected abstract DiagnosticHelper Helper { get; }

        public override void Initialize(AnalysisContext context)
        {
            context.EnableConcurrentExecution();
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
            context.RegisterCompilationStartAction(OnCompilationStart);
        }

        private void OnCompilationStart(CompilationStartAnalysisContext context)
        {
            if (!ConstantExpectedContext.TryCreate(context.Compilation, out var constantExpectedContext))
            {
                return;
            }

            context.RegisterOperationAction(context => OnInvocation(context, constantExpectedContext), OperationKind.Invocation);
            context.RegisterSymbolAction(context => OnMethodSymbol(context, constantExpectedContext), SymbolKind.Method);
            RegisterAttributeSyntax(context, constantExpectedContext);
        }

        private static void OnMethodSymbol(SymbolAnalysisContext context, ConstantExpectedContext constantExpectedContext)
        {
            var methodSymbol = (IMethodSymbol)context.Symbol;
            if (methodSymbol.ExplicitInterfaceImplementations
                    .FirstOrDefault(methodSymbol.IsImplementationOfInterfaceMember) is { } explicitInterfaceMethod)
            {
                CheckParameters(methodSymbol.Parameters, explicitInterfaceMethod.Parameters);
            }
            else if (methodSymbol.OverriddenMethod is not null)
            {
                CheckParameters(methodSymbol.Parameters, methodSymbol.OverriddenMethod.Parameters);
            }
            else if (methodSymbol.IsImplementationOfAnyImplicitInterfaceMember(out IMethodSymbol interfaceMethodSymbol))
            {
                CheckParameters(methodSymbol.Parameters, interfaceMethodSymbol.Parameters);
            }

            void CheckParameters(ImmutableArray<IParameterSymbol> parameters, ImmutableArray<IParameterSymbol> baseParameters)
            {
                if (constantExpectedContext.ValidatesAttributeImplementedFromParent(parameters, baseParameters, out var diagnostics))
                {
                    return;
                }

                foreach (var diagnostic in diagnostics)
                {
                    context.ReportDiagnostic(diagnostic);
                }
            }
        }

        private static void OnInvocation(OperationAnalysisContext context, ConstantExpectedContext constantExpectedContext)
        {
            var invocation = (IInvocationOperation)context.Operation;

            foreach (var argument in invocation.Arguments)
            {
                if (!constantExpectedContext.TryCreateConstantExpectedParameter(argument.Parameter, out var argConstantParameter))
                {
                    continue;
                }

                var v = argument.Value.WalkDownConversion();
                if (v is IParameterReferenceOperation parameterReference &&
                    constantExpectedContext.TryCreateConstantExpectedParameter(parameterReference.Parameter, out var currConstantParameter))
                {
                    if (!argConstantParameter.ValidateParameterIsWithinRange(currConstantParameter, argument, out var parameterCheckDiagnostic))
                    {
                        context.ReportDiagnostic(parameterCheckDiagnostic);
                    }

                    continue;
                }

                if (v.ConstantValue is { } constantValue &&
                    !argConstantParameter.ValidateValue(argument, constantValue, out var valueDiagnostic))
                {
                    context.ReportDiagnostic(valueDiagnostic);
                }
            }
        }

        protected abstract void RegisterAttributeSyntax(CompilationStartAnalysisContext context, ConstantExpectedContext constantExpectedContext);

        protected void OnParameterWithConstantExpectedAttribute(IParameterSymbol parameter, ConstantExpectedContext constantExpectedContext, Action<Diagnostic> reportAction)
        {
            if (!constantExpectedContext.ValidateConstantExpectedParameter(parameter, Helper, out ImmutableArray<Diagnostic> diagnostics))
            {
                foreach (var diagnostic in diagnostics)
                {
                    reportAction(diagnostic);
                }
            }
        }

        protected sealed class ConstantExpectedContext
        {
            public INamedTypeSymbol AttributeSymbol { get; }

            public ConstantExpectedContext(INamedTypeSymbol attributeSymbol)
            {
                AttributeSymbol = attributeSymbol;
            }
            /// <summary>
            /// Validates for ConstantExpected attribute in base parameter and returns AttributeExpectedRule if the coresponding implementation parameter does not have it
            /// </summary>
            /// <param name="parameters"></param>
            /// <param name="baseParameters"></param>
            /// <param name="diagnostics">Non empty when method returns false</param>
            /// <returns></returns>
            public bool ValidatesAttributeImplementedFromParent(ImmutableArray<IParameterSymbol> parameters, ImmutableArray<IParameterSymbol> baseParameters, out ImmutableArray<Diagnostic> diagnostics)
            {
                var arraybuilder = ImmutableArray.CreateBuilder<Diagnostic>();
                for (var i = 0; i < parameters.Length; i++)
                {
                    var parameter = parameters[i];
                    if (!IsConstantCompatible(parameter.Type))
                    {
                        continue;
                    }

                    var baseParameter = baseParameters[i];
                    if (HasConstantExpectedAttributeData(baseParameter) && !HasConstantExpectedAttributeData(parameter))
                    {
                        // mark the parameter including the type and name
                        var diagnostic = parameter.DeclaringSyntaxReferences[0].GetSyntax().CreateDiagnostic(CA1857.AttributeExpectedRule);
                        arraybuilder.Add(diagnostic);
                    }
                }

                diagnostics = arraybuilder.ToImmutable();
                return diagnostics.Length is 0;
            }

            private static bool IsConstantCompatible(ITypeSymbol type)
            {
                return type.SpecialType switch
                {
                    SpecialType.System_Char => true,
                    SpecialType.System_Byte => true,
                    SpecialType.System_UInt16 => true,
                    SpecialType.System_UInt32 => true,
                    SpecialType.System_UInt64 => true,
                    SpecialType.System_SByte => true,
                    SpecialType.System_Int16 => true,
                    SpecialType.System_Int32 => true,
                    SpecialType.System_Int64 => true,
                    SpecialType.System_Single => true,
                    SpecialType.System_Double => true,
                    SpecialType.System_Boolean => true,
                    SpecialType.System_String => true,
                    SpecialType.None when type.TypeKind == TypeKind.TypeParameter => true,
                    _ => false,
                };
            }

            /// <summary>
            /// Tries to create a ConstantExpectedParameter to represent the ConstantExpected attribute application
            /// </summary>
            /// <param name="parameterSymbol"></param>
            /// <param name="parameter"></param>
            /// <returns></returns>
            public bool TryCreateConstantExpectedParameter([NotNullWhen(true)] IParameterSymbol? parameterSymbol, [NotNullWhen(true)] out ConstantExpectedParameter? parameter)
            {
                var underlyingType = GetUnderlyingType(parameterSymbol);

                if (underlyingType == null ||
                    !TryGetConstantExpectedAttributeData(parameterSymbol, out var attributeData))
                {
                    parameter = null;
                    return false;
                }

                switch (underlyingType.SpecialType)
                {
                    case SpecialType.System_Char:
                        return UnmanagedHelper<char>.TryCreate(parameterSymbol, attributeData, char.MinValue, char.MaxValue, out parameter);
                    case SpecialType.System_Byte:
                        return UnmanagedHelper<ulong>.TryCreate(parameterSymbol, attributeData, byte.MinValue, byte.MaxValue, out parameter);
                    case SpecialType.System_UInt16:
                        return UnmanagedHelper<ulong>.TryCreate(parameterSymbol, attributeData, ushort.MinValue, ushort.MaxValue, out parameter);
                    case SpecialType.System_UInt32:
                        return UnmanagedHelper<ulong>.TryCreate(parameterSymbol, attributeData, uint.MinValue, uint.MaxValue, out parameter);
                    case SpecialType.System_UInt64:
                        return UnmanagedHelper<ulong>.TryCreate(parameterSymbol, attributeData, ulong.MinValue, ulong.MaxValue, out parameter);
                    case SpecialType.System_SByte:
                        return UnmanagedHelper<long>.TryCreate(parameterSymbol, attributeData, sbyte.MinValue, sbyte.MaxValue, out parameter);
                    case SpecialType.System_Int16:
                        return UnmanagedHelper<long>.TryCreate(parameterSymbol, attributeData, short.MinValue, short.MaxValue, out parameter);
                    case SpecialType.System_Int32:
                        return UnmanagedHelper<long>.TryCreate(parameterSymbol, attributeData, int.MinValue, int.MaxValue, out parameter);
                    case SpecialType.System_Int64:
                        return UnmanagedHelper<long>.TryCreate(parameterSymbol, attributeData, long.MinValue, long.MaxValue, out parameter);
                    case SpecialType.System_Single:
                        return UnmanagedHelper<float>.TryCreate(parameterSymbol, attributeData, float.MinValue, float.MaxValue, out parameter);
                    case SpecialType.System_Double:
                        return UnmanagedHelper<double>.TryCreate(parameterSymbol, attributeData, double.MinValue, double.MaxValue, out parameter);
                    case SpecialType.System_Boolean:
                        return UnmanagedHelper<bool>.TryCreate(parameterSymbol, attributeData, false, true, out parameter);
                    case SpecialType.System_String:
                        return StringConstantExpectedParameter.TryCreate(parameterSymbol, attributeData, out parameter);
                    default:
                        parameter = null;
                        return false;
                }
            }

            /// <summary>
            /// Validates that the parameter has a valid application of the ConstantExpected attributes. Returns diagnostics otherwise
            /// </summary>
            /// <param name="parameterSymbol"></param>
            /// <param name="helper"></param>
            /// <param name="diagnostics">not empty when method returns false</param>
            /// <returns></returns>
            public bool ValidateConstantExpectedParameter(IParameterSymbol parameterSymbol, DiagnosticHelper helper, out ImmutableArray<Diagnostic> diagnostics)
            {
                var underlyingType = GetUnderlyingType(parameterSymbol);

                if (underlyingType == null ||
                    !TryGetConstantExpectedAttributeData(parameterSymbol, out var attributeData))
                {
                    diagnostics = ImmutableArray<Diagnostic>.Empty;
                    return false;
                }

                switch (underlyingType.SpecialType)
                {
                    case SpecialType.System_Char:
                        return UnmanagedHelper<char>.Validate(parameterSymbol, attributeData, char.MinValue, char.MaxValue, helper, out diagnostics);
                    case SpecialType.System_Byte:
                        return UnmanagedHelper<ulong>.Validate(parameterSymbol, attributeData, byte.MinValue, byte.MaxValue, helper, out diagnostics);
                    case SpecialType.System_UInt16:
                        return UnmanagedHelper<ulong>.Validate(parameterSymbol, attributeData, ushort.MinValue, ushort.MaxValue, helper, out diagnostics);
                    case SpecialType.System_UInt32:
                        return UnmanagedHelper<ulong>.Validate(parameterSymbol, attributeData, uint.MinValue, uint.MaxValue, helper, out diagnostics);
                    case SpecialType.System_UInt64:
                        return UnmanagedHelper<ulong>.Validate(parameterSymbol, attributeData, ulong.MinValue, ulong.MaxValue, helper, out diagnostics);
                    case SpecialType.System_SByte:
                        return UnmanagedHelper<long>.Validate(parameterSymbol, attributeData, sbyte.MinValue, sbyte.MaxValue, helper, out diagnostics);
                    case SpecialType.System_Int16:
                        return UnmanagedHelper<long>.Validate(parameterSymbol, attributeData, short.MinValue, short.MaxValue, helper, out diagnostics);
                    case SpecialType.System_Int32:
                        return UnmanagedHelper<long>.Validate(parameterSymbol, attributeData, int.MinValue, int.MaxValue, helper, out diagnostics);
                    case SpecialType.System_Int64:
                        return UnmanagedHelper<long>.Validate(parameterSymbol, attributeData, long.MinValue, long.MaxValue, helper, out diagnostics);
                    case SpecialType.System_Single:
                        return UnmanagedHelper<float>.Validate(parameterSymbol, attributeData, float.MinValue, float.MaxValue, helper, out diagnostics);
                    case SpecialType.System_Double:
                        return UnmanagedHelper<double>.Validate(parameterSymbol, attributeData, double.MinValue, double.MaxValue, helper, out diagnostics);
                    case SpecialType.System_Boolean:
                        return UnmanagedHelper<bool>.Validate(parameterSymbol, attributeData, false, true, helper, out diagnostics);
                    case SpecialType.System_String:
                        return ValidateMinMaxIsNull(parameterSymbol, attributeData, helper, out diagnostics);
                    case SpecialType.None when parameterSymbol.Type.TypeKind == TypeKind.TypeParameter:
                        return ValidateMinMaxIsNull(parameterSymbol, attributeData, helper, out diagnostics);
                    default:
                        var syntax = attributeData.ApplicationSyntaxReference?.GetSyntax() ?? parameterSymbol.DeclaringSyntaxReferences[0].GetSyntax();
                        diagnostics = DiagnosticHelper.ParameterIsInvalid(parameterSymbol.Type.ToDisplayString(), syntax);
                        return false;
                }

                static bool ValidateMinMaxIsNull(IParameterSymbol parameterSymbol, AttributeData attributeData, DiagnosticHelper helper, out ImmutableArray<Diagnostic> diagnostics)
                {
                    ErrorKind errorFlags = 0;

                    foreach (var namedArg in attributeData.NamedArguments)
                    {
                        if (namedArg.Key.Equals(ConstantExpectedMin, StringComparison.Ordinal)
                            && !namedArg.Value.IsNull)
                        {
                            errorFlags |= ErrorKind.MinIsIncompatible;
                        }
                        else if (namedArg.Key.Equals(ConstantExpectedMax, StringComparison.Ordinal)
                            && !namedArg.Value.IsNull)
                        {
                            errorFlags |= ErrorKind.MaxIsIncompatible;
                        }
                    }

                    if (errorFlags is not 0)
                    {
                        var syntax = attributeData.ApplicationSyntaxReference?.GetSyntax() ?? parameterSymbol.DeclaringSyntaxReferences[0].GetSyntax();
                        diagnostics = helper.GetError(errorFlags, parameterSymbol, syntax, "null", "null");
                        return false;
                    }

                    diagnostics = ImmutableArray<Diagnostic>.Empty;
                    return true;
                }
            }

            private static ITypeSymbol? GetUnderlyingType(IParameterSymbol? parameterSymbol)
            {
                if (parameterSymbol?.Type.TypeKind is TypeKind.Enum)
                {
                    var enumType = (INamedTypeSymbol)parameterSymbol.Type;
                    return enumType.EnumUnderlyingType;
                }
                else
                {
                    return parameterSymbol?.Type;
                }
            }

            public bool TryGetConstantExpectedAttributeData([NotNullWhen(true)] IParameterSymbol? parameter, [NotNullWhen(true)] out AttributeData? attributeData)
            {
                attributeData = parameter?.GetAttribute(AttributeSymbol);
                return attributeData is not null;
            }

            private bool HasConstantExpectedAttributeData(IParameterSymbol parameter)
            {
                return parameter.HasAnyAttribute(AttributeSymbol);
            }

            public static bool TryCreate(Compilation compilation, [NotNullWhen(true)] out ConstantExpectedContext? constantExpectedContext)
            {
                if (!compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemDiagnosticsCodeAnalysisConstantExpectedAttribute, out var attributeSymbol))
                {
                    constantExpectedContext = null;
                    return false;
                }

                constantExpectedContext = new ConstantExpectedContext(attributeSymbol);
                return true;
            }
        }

        /// <summary>
        /// Encodes a parameter with its ConstantExpected attribute rules
        /// </summary>
        protected abstract class ConstantExpectedParameter
        {
            protected ConstantExpectedParameter(IParameterSymbol parameter)
            {
                Parameter = parameter;
            }
            /// <summary>
            /// Parameter with the ConstantExpected attribute
            /// </summary>
            public IParameterSymbol Parameter { get; }

            /// <summary>
            /// Validates the provided constant value is within the constraints of ConstantExpected attribute set
            /// </summary>
            /// <param name="argument"></param>
            /// <param name="constant"></param>
            /// <param name="validationDiagnostics">Non empty when method returns false</param>
            /// <returns></returns>
            public abstract bool ValidateValue(IArgumentOperation argument, Optional<object?> constant, [NotNullWhen(false)] out Diagnostic? validationDiagnostics);

            public static bool ValidateConstant(IArgumentOperation argument, Optional<object?> constant, [NotNullWhen(false)] out Diagnostic? validationDiagnostics)
            {
                if (!constant.HasValue)
                {
                    validationDiagnostics = argument.CreateDiagnostic(CA1857.ConstantNotConstantRule);
                    return false;
                }

                validationDiagnostics = null;
                return true;
            }

            public abstract bool ValidateParameterIsWithinRange(ConstantExpectedParameter subsetCandidate, IArgumentOperation argument, [NotNullWhen(false)] out Diagnostic? validationDiagnostics);
            protected Diagnostic CreateConstantInvalidConstantRuleDiagnostic(IArgumentOperation argument) => argument.CreateDiagnostic(CA1857.ConstantInvalidConstantRule, Parameter.Type.ToDisplayString());
            protected static Diagnostic CreateConstantOutOfBoundsRuleDiagnostic(IArgumentOperation argument, string minText, string maxText) => argument.CreateDiagnostic(CA1857.ConstantOutOfBoundsRule, minText, maxText);
        }

        private sealed class StringConstantExpectedParameter : ConstantExpectedParameter
        {
            public StringConstantExpectedParameter(IParameterSymbol parameter) : base(parameter) { }

            public override bool ValidateParameterIsWithinRange(ConstantExpectedParameter subsetCandidate, IArgumentOperation argument, [NotNullWhen(false)] out Diagnostic? validationDiagnostics)
            {
                if (subsetCandidate is not StringConstantExpectedParameter)
                {
                    validationDiagnostics = CreateConstantInvalidConstantRuleDiagnostic(argument);
                    return false;
                }

                validationDiagnostics = null;
                return true;
            }

            public override bool ValidateValue(IArgumentOperation argument, Optional<object?> constant, [NotNullWhen(false)] out Diagnostic? validationDiagnostics)
            {
                if (!ValidateConstant(argument, constant, out validationDiagnostics))
                {
                    return false;
                }

                if (constant.Value is not string and not null)
                {
                    validationDiagnostics = CreateConstantInvalidConstantRuleDiagnostic(argument);
                    return false;
                }

                validationDiagnostics = null;
                return true;
            }

            public static bool TryCreate(IParameterSymbol parameterSymbol, AttributeData attributeData, [NotNullWhen(true)] out ConstantExpectedParameter? parameter)
            {
                var ac = AttributeConstant.Get(attributeData);
                if (ac.Min is not null || ac.Max is not null)
                {
                    parameter = null;
                    return false;
                }

                parameter = new StringConstantExpectedParameter(parameterSymbol);
                return true;
            }
        }

#pragma warning disable CA1815 // Override equals and operator equals on value types
        private readonly struct AttributeConstant
#pragma warning restore CA1815 // Override equals and operator equals on value types
        {
            public object? Min { get; }
            public object? Max { get; }

            public AttributeConstant(object? min, object? max)
            {
                Min = min;
                Max = max;
            }

            public static AttributeConstant Get(AttributeData attributeData)
            {
                object? minConstant = null;
                object? maxConstant = null;

                foreach (var namedArg in attributeData.NamedArguments)
                {
                    if (namedArg.Key.Equals(ConstantExpectedMin, StringComparison.Ordinal))
                    {
                        minConstant = ToObject(namedArg.Value);
                    }
                    else if (namedArg.Key.Equals(ConstantExpectedMax, StringComparison.Ordinal))
                    {
                        maxConstant = ToObject(namedArg.Value);
                    }
                }

                return new AttributeConstant(minConstant, maxConstant);

                static object? ToObject(TypedConstant typedConstant)
                {
                    if (typedConstant.IsNull)
                    {
                        return null;
                    }

                    return typedConstant.Kind == TypedConstantKind.Array ? typedConstant.Values : typedConstant.Value;
                }
            }
        }

        protected abstract class DiagnosticHelper
        {
            public abstract Location? GetMinLocation(SyntaxNode attributeSyntax);
            public abstract Location? GetMaxLocation(SyntaxNode attributeSyntax);

            public static ImmutableArray<Diagnostic> ParameterIsInvalid(string expectedTypeName, SyntaxNode attributeSyntax) => ImmutableArray.Create(Diagnostic.Create(CA1856.UnsupportedTypeRule, attributeSyntax.GetLocation(), expectedTypeName));

            public Diagnostic MinIsIncompatible(string expectedTypeName, SyntaxNode attributeSyntax) => Diagnostic.Create(CA1856.IncompatibleConstantTypeRule, GetMinLocation(attributeSyntax)!, ConstantExpectedMin, expectedTypeName);

            public Diagnostic MaxIsIncompatible(string expectedTypeName, SyntaxNode attributeSyntax) => Diagnostic.Create(CA1856.IncompatibleConstantTypeRule, GetMaxLocation(attributeSyntax)!, ConstantExpectedMax, expectedTypeName);

            public Diagnostic MinIsOutOfRange(SyntaxNode attributeSyntax, string typeMinValue, string typeMaxValue) => Diagnostic.Create(CA1856.InvalidBoundsRule, GetMinLocation(attributeSyntax)!, ConstantExpectedMin, typeMinValue, typeMaxValue);

            public Diagnostic MaxIsOutOfRange(SyntaxNode attributeSyntax, string typeMinValue, string typeMaxValue) => Diagnostic.Create(CA1856.InvalidBoundsRule, GetMaxLocation(attributeSyntax)!, ConstantExpectedMax, typeMinValue, typeMaxValue);

            public static Diagnostic MinMaxIsInverted(SyntaxNode attributeSyntax) => Diagnostic.Create(CA1856.InvertedRangeRule, attributeSyntax.GetLocation());

            public ImmutableArray<Diagnostic> GetError(ErrorKind errorFlags, IParameterSymbol parameterSymbol, SyntaxNode attributeSyntax, string typeMinValue, string typeMaxValue)
            {
                switch (errorFlags)
                {
                    case ErrorKind.MinIsIncompatible:
                        return ImmutableArray.Create(MinIsIncompatible(parameterSymbol.Type.ToDisplayString(), attributeSyntax));
                    case ErrorKind.MaxIsIncompatible:
                        return ImmutableArray.Create(MaxIsIncompatible(parameterSymbol.Type.ToDisplayString(), attributeSyntax));
                    case ErrorKind.MinIsIncompatible | ErrorKind.MaxIsIncompatible:
                        var expectedTypeName = parameterSymbol.Type.ToDisplayString();
                        return ImmutableArray.Create(MinIsIncompatible(expectedTypeName, attributeSyntax), MaxIsIncompatible(expectedTypeName, attributeSyntax));
                    case ErrorKind.MinIsOutOfRange:
                        return ImmutableArray.Create(MinIsOutOfRange(attributeSyntax, typeMinValue, typeMaxValue));
                    case ErrorKind.MaxIsOutOfRange:
                        return ImmutableArray.Create(MaxIsOutOfRange(attributeSyntax, typeMinValue, typeMaxValue));
                    case ErrorKind.MinIsOutOfRange | ErrorKind.MaxIsOutOfRange:
                        return ImmutableArray.Create(MinIsOutOfRange(attributeSyntax, typeMinValue, typeMaxValue), MaxIsOutOfRange(attributeSyntax, typeMinValue, typeMaxValue));
                    case ErrorKind.MinIsOutOfRange | ErrorKind.MaxIsIncompatible:
                        return ImmutableArray.Create(MinIsOutOfRange(attributeSyntax, typeMinValue, typeMaxValue), MaxIsIncompatible(parameterSymbol.Type.ToDisplayString(), attributeSyntax));
                    case ErrorKind.MinIsIncompatible | ErrorKind.MaxIsOutOfRange:
                        return ImmutableArray.Create(MinIsIncompatible(parameterSymbol.Type.ToDisplayString(), attributeSyntax), MaxIsOutOfRange(attributeSyntax, typeMinValue, typeMaxValue));
                    case ErrorKind.MinMaxInverted:
                        return ImmutableArray.Create(MinMaxIsInverted(attributeSyntax));
                    default:
                        throw new ArgumentOutOfRangeException(nameof(errorFlags));
                }
            }
        }

        [Flags]
        protected enum ErrorKind
        {
            None = 0,
            /// <summary>
            /// mutually exclusive with MinIsIncompatible and MinMaxInverted
            /// </summary>
            MinIsOutOfRange = 1,
            /// <summary>
            /// mutually exclusive with MinIsOutOfRange and MinMaxInverted
            /// </summary>
            MinIsIncompatible = 1 << 2,
            /// <summary>
            /// mutually exclusive with MaxIsIncompatible and MinMaxInverted
            /// </summary>
            MaxIsOutOfRange = 1 << 3,
            /// <summary>
            /// mutually exclusive with MaxIsOutOfRange and MinMaxInverted
            /// </summary>
            MaxIsIncompatible = 1 << 4,
            /// <summary>
            /// mutually exclusive
            /// </summary>
            MinMaxInverted = 1 << 5,
        }
    }
}