File: Microsoft.CodeQuality.Analyzers\ApiDesignGuidelines\DoNotRaiseExceptionsInUnexpectedLocations.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.Generic;
using System.Collections.Immutable;
using System.Linq;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

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

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

        private static readonly LocalizableString s_localizableTitle = CreateLocalizableResourceString(nameof(DoNotRaiseExceptionsInUnexpectedLocationsTitle));
        private static readonly LocalizableString s_localizableDescription = CreateLocalizableResourceString(nameof(DoNotRaiseExceptionsInUnexpectedLocationsDescription));

        internal static readonly DiagnosticDescriptor PropertyGetterRule = DiagnosticDescriptorHelper.Create(
            RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(DoNotRaiseExceptionsInUnexpectedLocationsMessagePropertyGetter)),
            DiagnosticCategory.Design,
            RuleLevel.Disabled, // Could consider Suggestion level if we could exclude test code by default.
            description: s_localizableDescription,
            isPortedFxCopRule: true,
            isDataflowRule: false);

        internal static readonly DiagnosticDescriptor HasAllowedExceptionsRule = DiagnosticDescriptorHelper.Create(
            RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(DoNotRaiseExceptionsInUnexpectedLocationsMessageHasAllowedExceptions)),
            DiagnosticCategory.Design,
            RuleLevel.Disabled, // Could consider Suggestion level if we could exclude test code by default.
            description: s_localizableDescription,
            isPortedFxCopRule: true,
            isDataflowRule: false);

        internal static readonly DiagnosticDescriptor NoAllowedExceptionsRule = DiagnosticDescriptorHelper.Create(
            RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(DoNotRaiseExceptionsInUnexpectedLocationsMessageNoAllowedExceptions)),
            DiagnosticCategory.Design,
            RuleLevel.Disabled, // Could consider Suggestion level if we could exclude test code by default.
            description: s_localizableDescription,
            isPortedFxCopRule: true,
            isDataflowRule: false);

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
            ImmutableArray.Create(PropertyGetterRule, HasAllowedExceptionsRule, NoAllowedExceptionsRule);

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

            context.RegisterCompilationStartAction(compilationStartContext =>
            {
                Compilation compilation = compilationStartContext.Compilation;
                INamedTypeSymbol? exceptionType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemException);
                INamedTypeSymbol? unreachableExceptionType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemDiagnosticsUnreachableException);
                if (exceptionType == null)
                {
                    return;
                }

                // Get a list of interesting categories of methods to analyze.
                List<MethodCategory> methodCategories = GetMethodCategories(compilation);

                compilationStartContext.RegisterOperationBlockStartAction(operationBlockContext =>
                {
                    if (operationBlockContext.OwningSymbol is not IMethodSymbol methodSymbol)
                    {
                        return;
                    }

                    // Find out if this given method is one of the interesting categories of methods.
                    // For example, certain Equals methods or certain accessors etc.
                    MethodCategory? methodCategory = methodCategories.FirstOrDefault(l => l.IsMatch(methodSymbol, compilation));
                    if (methodCategory == null)
                    {
                        return;
                    }

                    // For the interesting methods, register an operation action to catch all
                    // Throw statements.
                    operationBlockContext.RegisterOperationAction(operationContext =>
                    {
                        var throwOperation = (IThrowOperation)operationContext.Operation;
                        if (throwOperation.TryGetContainingAnonymousFunctionOrLocalFunction() is not null)
                        {
                            return;
                        }

                        // Get ThrowOperation's ExceptionType
                        if (throwOperation.GetThrownExceptionType() is INamedTypeSymbol thrownExceptionType &&
                            thrownExceptionType.DerivesFrom(exceptionType) &&
                            !SymbolEqualityComparer.Default.Equals(thrownExceptionType, unreachableExceptionType))
                        {
                            // If no exceptions are allowed or if the thrown exceptions is not an allowed one..
                            if (methodCategory.AllowedExceptions.IsEmpty || !methodCategory.AllowedExceptions.Any(n => thrownExceptionType.IsAssignableTo(n, compilation)))
                            {
                                operationContext.ReportDiagnostic(
                                    operationContext.Operation.Syntax.CreateDiagnostic(methodCategory.Rule, methodSymbol.Name, thrownExceptionType.Name));
                            }
                        }
                    }, OperationKind.Throw);
                });
            });
        }

        /// <summary>
        /// This object describes a class of methods where exception throwing statements should be analyzed.
        /// </summary>
        private class MethodCategory
        {
            /// <summary>
            /// Function used to determine whether a given method symbol falls into this category.
            /// </summary>
            private readonly Func<IMethodSymbol, Compilation, bool> _matchFunction;

            /// <summary>
            /// Determines if we should analyze non-public methods of a given type.
            /// </summary>
            private readonly bool _analyzeOnlyPublicMethods;

            /// <summary>
            /// The rule that should be fired if there is an exception in this kind of method.
            /// </summary>
            public DiagnosticDescriptor Rule { get; }

            /// <summary>
            /// List of exception types which are allowed to be thrown inside this category of method.
            /// This list will be empty if no exceptions are allowed.
            /// </summary>
            public ImmutableHashSet<ITypeSymbol> AllowedExceptions { get; }

            public MethodCategory(Func<IMethodSymbol, Compilation, bool> matchFunction, bool analyzeOnlyPublicMethods, DiagnosticDescriptor rule, params ITypeSymbol?[] allowedExceptionTypes)
            {
                _matchFunction = matchFunction;
                _analyzeOnlyPublicMethods = analyzeOnlyPublicMethods;
                this.Rule = rule;
                AllowedExceptions = allowedExceptionTypes.WhereNotNull().ToImmutableHashSet();
            }

            /// <summary>
            /// Checks if the given method belong this category
            /// </summary>
            public bool IsMatch(IMethodSymbol method, Compilation compilation)
            {
                // If we are supposed to analyze only public methods get the resultant visibility
                // i.e public method inside an internal class is not considered public.
                if (_analyzeOnlyPublicMethods && !method.IsExternallyVisible())
                {
                    return false;
                }

                return _matchFunction(method, compilation);
            }
        }

        private static List<MethodCategory> GetMethodCategories(Compilation compilation)
        {
            var methodCategories = new List<MethodCategory> {
                new MethodCategory(IsPropertyGetter, true,
                    PropertyGetterRule,
                    compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemInvalidOperationException),
                    compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemNotSupportedException)),

                new MethodCategory(IsIndexerGetter, true,
                    PropertyGetterRule,
                    compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemInvalidOperationException),
                    compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemNotSupportedException),
                    compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemArgumentException),
                    compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsGenericKeyNotFoundException)),

                new MethodCategory(IsEventAccessor, true,
                    HasAllowedExceptionsRule,
                    compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemInvalidOperationException),
                    compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemNotSupportedException),
                    compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemArgumentException)),

                new MethodCategory(IsGetHashCodeInterfaceImplementation, false,
                    HasAllowedExceptionsRule,
                    compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemArgumentException)),

                new MethodCategory(IsEqualsOverrideOrInterfaceImplementation, false,
                    NoAllowedExceptionsRule),

                new MethodCategory(IsComparisonOperator, true,
                    NoAllowedExceptionsRule),

                new MethodCategory(IsGetHashCodeOverride, true,
                    NoAllowedExceptionsRule),

                new MethodCategory(IsToString, true,
                    NoAllowedExceptionsRule),

                new MethodCategory(IsImplicitCastOperator, true,
                    NoAllowedExceptionsRule),

                new MethodCategory(IsStaticConstructor, false,
                    NoAllowedExceptionsRule),

                new MethodCategory(IsFinalizer, false,
                    NoAllowedExceptionsRule),

                new MethodCategory(IMethodSymbolExtensions.IsDisposeImplementation, true,
                    NoAllowedExceptionsRule),
            };

            return methodCategories;
        }

        private static bool IsPropertyGetter(IMethodSymbol method, Compilation compilation)
        {
            return method.IsPropertyGetter();
        }

        private static bool IsIndexerGetter(IMethodSymbol method, Compilation compilation)
        {
            return method.IsIndexerGetter();
        }

        private static bool IsEventAccessor(IMethodSymbol method, Compilation compilation)
        {
            return method.IsEventAccessor();
        }

        private static bool IsEqualsOverrideOrInterfaceImplementation(IMethodSymbol method, Compilation compilation)
        {
            return method.IsObjectEqualsOverride() || IsEqualsInterfaceImplementation(method, compilation);
        }

        /// <summary>
        /// Checks if a given method implements IEqualityComparer.Equals or IEquatable.Equals.
        /// </summary>
        private static bool IsEqualsInterfaceImplementation(IMethodSymbol method, Compilation compilation)
        {
            if (method.Name != WellKnownMemberNames.ObjectEquals &&
                method.ExplicitInterfaceImplementations.Length == 0)
            {
                return false;
            }

            int paramCount = method.Parameters.Length;
            if (method.ReturnType.SpecialType == SpecialType.System_Boolean &&
                (paramCount == 1 || paramCount == 2))
            {
                // Substitute the type of the first parameter of Equals in the generic interface and then check if that
                // interface method is implemented by the given method.
                INamedTypeSymbol? iEqualityComparer = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsGenericIEqualityComparer1);
                if (method.IsImplementationOfInterfaceMethod(method.Parameters.First().Type, iEqualityComparer, WellKnownMemberNames.ObjectEquals))
                {
                    return true;
                }

                // Substitute the type of the first parameter of Equals in the generic interface and then check if that
                // interface method is implemented by the given method.
                INamedTypeSymbol? iEquatable = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemIEquatable1);
                if (method.IsImplementationOfInterfaceMethod(method.Parameters.First().Type, iEquatable, WellKnownMemberNames.ObjectEquals))
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Checks if a given method implements IEqualityComparer.GetHashCode or IHashCodeProvider.GetHashCode.
        /// </summary>
        /// <param name="method"></param>
        /// <param name="compilation"></param>
        /// <returns></returns>
        private static bool IsGetHashCodeInterfaceImplementation(IMethodSymbol method, Compilation compilation)
        {
            if (method.Name != WellKnownMemberNames.ObjectGetHashCode &&
                method.ExplicitInterfaceImplementations.Length == 0)
            {
                return false;
            }

            if (method.ReturnType.SpecialType == SpecialType.System_Int32 && method.Parameters.Length == 1)
            {
                // Substitute the type of the first parameter of Equals in the generic interface and then check if that
                // interface method is implemented by the given method.
                INamedTypeSymbol? iEqualityComparer = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsGenericIEqualityComparer1);
                if (method.IsImplementationOfInterfaceMethod(method.Parameters.First().Type, iEqualityComparer, WellKnownMemberNames.ObjectGetHashCode))
                {
                    return true;
                }

                INamedTypeSymbol? iHashCodeProvider = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsIHashCodeProvider);
                if (method.IsImplementationOfInterfaceMethod(null, iHashCodeProvider, WellKnownMemberNames.ObjectGetHashCode))
                {
                    return true;
                }
            }

            return false;
        }

        private static bool IsGetHashCodeOverride(IMethodSymbol method, Compilation compilation)
        {
            return method.IsGetHashCodeOverride();
        }

        private static bool IsToString(IMethodSymbol method, Compilation compilation)
        {
            return method.IsToStringOverride();
        }

        private static bool IsStaticConstructor(IMethodSymbol method, Compilation compilation)
        {
            return method.MethodKind == MethodKind.StaticConstructor;
        }

        private static bool IsFinalizer(IMethodSymbol method, Compilation compilation)
        {
            return method.IsFinalizer();
        }

        private static bool IsComparisonOperator(IMethodSymbol method, Compilation compilation)
        {
            if (!method.IsStatic || !method.IsPublic())
                return false;

            return method.Name switch
            {
                WellKnownMemberNames.EqualityOperatorName
                or WellKnownMemberNames.InequalityOperatorName
                or WellKnownMemberNames.LessThanOperatorName
                or WellKnownMemberNames.GreaterThanOperatorName
                or WellKnownMemberNames.LessThanOrEqualOperatorName
                or WellKnownMemberNames.GreaterThanOrEqualOperatorName => true,
                _ => false,
            };
        }

        private static bool IsImplicitCastOperator(IMethodSymbol method, Compilation compilation)
        {
            if (!method.IsStatic || !method.IsPublic())
                return false;
            return method.Name == WellKnownMemberNames.ImplicitConversionName;
        }
    }
}