File: Microsoft.NetCore.Analyzers\Runtime\InstantiateArgumentExceptionsCorrectly.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.Globalization;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.NetCore.Analyzers.Runtime
{
    using static MicrosoftNetCoreAnalyzersResources;

    /// <summary>
    /// CA2208: <inheritdoc cref="InstantiateArgumentExceptionsCorrectlyTitle"/>
    /// </summary>
    [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
    public sealed class InstantiateArgumentExceptionsCorrectlyAnalyzer : DiagnosticAnalyzer
    {
        internal const string RuleId = "CA2208";
        internal const string MessagePosition = nameof(MessagePosition);

        private static readonly LocalizableString s_localizableTitle = CreateLocalizableResourceString(nameof(InstantiateArgumentExceptionsCorrectlyTitle));
        private static readonly LocalizableString s_localizableDescription = CreateLocalizableResourceString(nameof(InstantiateArgumentExceptionsCorrectlyDescription));

        internal static readonly DiagnosticDescriptor RuleNoArguments = DiagnosticDescriptorHelper.Create(
            RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(InstantiateArgumentExceptionsCorrectlyMessageNoArguments)),
            DiagnosticCategory.Usage,
            RuleLevel.IdeSuggestion,
            description: s_localizableDescription,
            isPortedFxCopRule: true,
            isDataflowRule: false);

        internal static readonly DiagnosticDescriptor RuleIncorrectMessage = DiagnosticDescriptorHelper.Create(
            RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(InstantiateArgumentExceptionsCorrectlyMessageIncorrectMessage)),
            DiagnosticCategory.Usage,
            RuleLevel.IdeSuggestion,
            description: s_localizableDescription,
            isPortedFxCopRule: true,
            isDataflowRule: false);

        internal static readonly DiagnosticDescriptor RuleIncorrectParameterName = DiagnosticDescriptorHelper.Create(
            RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(InstantiateArgumentExceptionsCorrectlyMessageIncorrectParameterName)),
            DiagnosticCategory.Usage,
            RuleLevel.IdeSuggestion,
            description: s_localizableDescription,
            isPortedFxCopRule: true,
            isDataflowRule: false);

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(RuleNoArguments, RuleIncorrectMessage, RuleIncorrectParameterName);

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

            context.RegisterCompilationStartAction(
                compilationContext =>
                {
                    Compilation compilation = compilationContext.Compilation;
                    ITypeSymbol? argumentExceptionType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemArgumentException);

                    if (argumentExceptionType == null)
                    {
                        return;
                    }

                    compilationContext.RegisterOperationAction(
                        operationContext => AnalyzeObjectCreation(
                            operationContext,
                            operationContext.ContainingSymbol,
                            argumentExceptionType),
                        OperationKind.ObjectCreation);
                });
        }

        private static void AnalyzeObjectCreation(
            OperationAnalysisContext context,
            ISymbol owningSymbol,
            ITypeSymbol argumentExceptionType)
        {
            var creation = (IObjectCreationOperation)context.Operation;
            if (!creation.Type.Inherits(argumentExceptionType) || !MatchesConfiguredVisibility(owningSymbol, context) || !HasParameterNameConstructor(creation.Type))
            {
                return;
            }

            if (creation.Arguments.IsEmpty)
            {
                if (HasParameters(owningSymbol))
                {
                    // Call the {0} constructor that contains a message and/ or paramName parameter
                    context.ReportDiagnostic(context.Operation.Syntax.CreateDiagnostic(RuleNoArguments, creation.Type.Name));
                }
            }
            else
            {
                Diagnostic? diagnosticFound = null;
                foreach (IArgumentOperation argument in creation.Arguments)
                {
                    if (argument.Parameter?.Type.SpecialType != SpecialType.System_String)
                    {
                        continue;
                    }

                    string? value = argument.Value.ConstantValue.HasValue ? argument.Value.ConstantValue.Value as string : null;
                    if (value == null)
                    {
                        continue;
                    }

                    Diagnostic? diagnostic = CheckArgument(owningSymbol, creation, argument.Parameter, value, context);

                    if (diagnostic != null)
                    {
                        diagnosticFound = diagnostic;
                        // RuleIncorrectMessage is the highest priority rule, no need to check other rules
                        if (diagnostic.Descriptor.Equals(RuleIncorrectMessage))
                        {
                            break;
                        }
                    }
                }

                if (diagnosticFound != null)
                {
                    context.ReportDiagnostic(diagnosticFound);
                }
            }
        }

        private static bool MatchesConfiguredVisibility(ISymbol owningSymbol, OperationAnalysisContext context) =>
             context.Options.MatchesConfiguredVisibility(RuleIncorrectParameterName, owningSymbol, context.Compilation,
                 defaultRequiredVisibility: SymbolVisibilityGroup.All);

        private static bool HasParameters(ISymbol owningSymbol) => !owningSymbol.GetParameters().IsEmpty;

        private static Diagnostic? CheckArgument(
            ISymbol targetSymbol,
            IObjectCreationOperation creation,
            IParameterSymbol parameter,
            string stringArgument,
            OperationAnalysisContext context)
        {
            if (IsMessage(parameter) && MatchesParameterStrict(targetSymbol, creation, stringArgument))
            {
                var dictBuilder = ImmutableDictionary.CreateBuilder<string, string?>();
                dictBuilder.Add(MessagePosition, parameter.Ordinal.ToString(CultureInfo.InvariantCulture));
                return context.Operation.CreateDiagnostic(RuleIncorrectMessage, dictBuilder.ToImmutable(), targetSymbol.Name, stringArgument, parameter.Name, creation.Type!.Name);
            }

            if (HasParameters(targetSymbol) && IsParameterName(parameter) && !MatchesParameterRelax(targetSymbol, creation, stringArgument))
            {
                // Allow argument exceptions in accessors to use the associated property symbol name.
                if (!MatchesAssociatedSymbol(targetSymbol, stringArgument))
                {
                    return context.Operation.CreateDiagnostic(RuleIncorrectParameterName, targetSymbol.Name, stringArgument, parameter.Name, creation.Type!.Name);
                }
            }

            return null;
        }

        private static bool IsMessage(IParameterSymbol parameter)
        {
            return parameter.Name == "message";
        }

        private static bool IsParameterName(IParameterSymbol parameter)
        {
            return parameter.Name is "paramName" or "parameterName";
        }

        private static bool HasParameterNameConstructor(ITypeSymbol type)
        {
            foreach (ISymbol member in type.GetMembers())
            {
                if (!member.IsConstructor())
                {
                    continue;
                }

                foreach (IParameterSymbol parameter in member.GetParameters())
                {
                    if (parameter.Type.SpecialType == SpecialType.System_String
                        && IsParameterName(parameter))
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        private static bool MatchesParameterStrict(ISymbol? symbol, IObjectCreationOperation creation, string stringArgumentValue)
        {
            return MatchesParameterCore(symbol, creation, stringArgumentValue, strict: true);
        }

        private static bool MatchesParameterRelax(ISymbol? symbol, IObjectCreationOperation creation, string stringArgumentValue)
        {
            return MatchesParameterCore(symbol, creation, stringArgumentValue, strict: false);
        }

        private static bool MatchesParameterCore(ISymbol? symbol, IObjectCreationOperation creation, string stringArgumentValue, bool strict)
        {
            if (MatchesParameterCore(symbol, stringArgumentValue, strict))
            {
                return true;
            }

            var operation = creation.Parent;
            while (operation != null)
            {
                symbol = null;
                switch (operation.Kind)
                {
                    case OperationKind.LocalFunction:
                        symbol = ((ILocalFunctionOperation)operation).Symbol;
                        break;

                    case OperationKind.AnonymousFunction:
                        symbol = ((IAnonymousFunctionOperation)operation).Symbol;
                        break;
                }

                if (symbol != null && MatchesParameterCore(symbol, stringArgumentValue, strict))
                {
                    return true;
                }

                operation = operation.Parent;
            }

            return false;
        }

        private static bool MatchesParameterCore(ISymbol? symbol, string stringArgumentValue, bool strict)
        {
            foreach (IParameterSymbol parameter in symbol.GetParameters())
            {
                // If the parameter name matches exactly, it's a match.
                if (parameter.Name == stringArgumentValue)
                {
                    return true;
                }

                if (!strict)
                {
                    // If the string argument begins with the parameter name followed by punctuation, it's also considered a match.
                    // e.g. "arg.Length", "arg[0]", etc.
                    if (stringArgumentValue.Length > parameter.Name.Length &&
                        stringArgumentValue.StartsWith(parameter.Name, StringComparison.Ordinal) &&
                        char.IsPunctuation(stringArgumentValue, parameter.Name.Length))
                    {
                        return true;
                    }
                }
            }

            if (symbol is IMethodSymbol method)
            {
                if (method.IsGenericMethod)
                {
                    foreach (ITypeParameterSymbol parameter in method.TypeParameters)
                    {
                        if (parameter.Name == stringArgumentValue)
                        {
                            return true;
                        }
                    }
                }
            }

            return false;
        }

        private static bool MatchesAssociatedSymbol(ISymbol targetSymbol, string stringArgument)
            => targetSymbol.IsAccessorMethod() &&
            ((IMethodSymbol)targetSymbol).AssociatedSymbol?.Name == stringArgument;
    }
}