// 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; } }