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