// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using Analyzer.Utilities; using Analyzer.Utilities.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; using static Microsoft.NetCore.Analyzers.MicrosoftNetCoreAnalyzersResources; namespace Microsoft.NetCore.Analyzers.Runtime { /// <summary> /// CA2250: <inheritdoc cref="UseCancellationTokenThrowIfCancellationRequestedTitle"/> /// </summary> [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] public sealed class UseCancellationTokenThrowIfCancellationRequested : DiagnosticAnalyzer { internal const string RuleId = "CA2250"; internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create( RuleId, CreateLocalizableResourceString(nameof(UseCancellationTokenThrowIfCancellationRequestedTitle)), CreateLocalizableResourceString(nameof(UseCancellationTokenThrowIfCancellationRequestedMessage)), DiagnosticCategory.Usage, RuleLevel.IdeSuggestion, CreateLocalizableResourceString(nameof(UseCancellationTokenThrowIfCancellationRequestedDescription)), isPortedFxCopRule: false, isDataflowRule: false); public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule); public override void Initialize(AnalysisContext context) { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(OnCompilationStart); } private static void OnCompilationStart(CompilationStartAnalysisContext context) { if (!RequiredSymbols.TryGetSymbols(context.Compilation, out var symbols)) return; context.RegisterOperationAction(AnalyzeOperation, OperationKind.Conditional); return; void AnalyzeOperation(OperationAnalysisContext context) { var conditional = (IConditionalOperation)context.Operation; if (symbols.IsSimpleAffirmativeCheck(conditional, out _) || symbols.IsNegatedCheckWithThrowingElseClause(conditional, out _)) { context.ReportDiagnostic(conditional.CreateDiagnostic(Rule)); } } } /// <summary> /// If <paramref name="singleOrBlock"/> is a block operation with one child, returns that child. /// If <paramref name="singleOrBlock"/> is a block operation with more than one child, returns <see langword="null"/>. /// If <paramref name="singleOrBlock"/> is not a block operation, returns <paramref name="singleOrBlock"/>. /// </summary> /// <param name="singleOrBlock">The operation to unwrap.</param> internal static IOperation? GetSingleStatementOrDefault(IOperation? singleOrBlock) { if (singleOrBlock is IBlockOperation blockOperation) { return blockOperation.Operations.Length is 1 ? blockOperation.Operations[0] : default; } return singleOrBlock; } // Use readonly struct to avoid allocations. #pragma warning disable CA1815 // Override equals and operator equals on value types internal readonly struct RequiredSymbols #pragma warning restore CA1815 // Override equals and operator equals on value types { public static bool TryGetSymbols(Compilation compilation, out RequiredSymbols symbols) { symbols = default; INamedTypeSymbol boolType = compilation.GetSpecialType(SpecialType.System_Boolean); if (boolType is null) return false; if (!compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingCancellationToken, out INamedTypeSymbol? cancellationTokenType)) return false; IMethodSymbol? throwIfCancellationRequestedMethod = cancellationTokenType.GetMembers(nameof(CancellationToken.ThrowIfCancellationRequested)) .OfType<IMethodSymbol>() .GetFirstOrDefaultMemberWithParameterInfos(); IPropertySymbol? isCancellationRequestedProperty = cancellationTokenType.GetMembers(nameof(CancellationToken.IsCancellationRequested)) .OfType<IPropertySymbol>() .FirstOrDefault(); if (throwIfCancellationRequestedMethod is null || isCancellationRequestedProperty is null) return false; if (!compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemOperationCanceledException, out INamedTypeSymbol? operationCanceledExceptionType)) return false; IMethodSymbol? operationCanceledExceptionDefaultCtor = operationCanceledExceptionType.InstanceConstructors .GetFirstOrDefaultMemberWithParameterInfos(); IMethodSymbol? operationCanceledExceptionTokenCtor = operationCanceledExceptionType.InstanceConstructors .GetFirstOrDefaultMemberWithParameterInfos(ParameterInfo.GetParameterInfo(cancellationTokenType)); if (operationCanceledExceptionDefaultCtor is null || operationCanceledExceptionTokenCtor is null) return false; symbols = new RequiredSymbols { IsCancellationRequestedProperty = isCancellationRequestedProperty, OperationCanceledExceptionDefaultCtor = operationCanceledExceptionDefaultCtor, OperationCanceledExceptionTokenCtor = operationCanceledExceptionTokenCtor }; return true; } public IPropertySymbol IsCancellationRequestedProperty { get; init; } public IMethodSymbol OperationCanceledExceptionDefaultCtor { get; init; } public IMethodSymbol OperationCanceledExceptionTokenCtor { get; init; } /// <summary> /// Indicates whether the specified operation is a conditional statement of the form /// <code> /// if (token.IsCancellationRequested) /// throw new OperationCanceledException(); /// </code> /// </summary> public bool IsSimpleAffirmativeCheck(IConditionalOperation conditional, [NotNullWhen(true)] out IPropertyReferenceOperation? isCancellationRequestedPropertyReference) { IOperation? whenTrueUnwrapped = GetSingleStatementOrDefault(conditional.WhenTrue); if (conditional.Condition is IPropertyReferenceOperation propertyReference && SymbolEqualityComparer.Default.Equals(propertyReference.Property, IsCancellationRequestedProperty) && whenTrueUnwrapped is IThrowOperation @throw && @throw.GetThrownException() is IObjectCreationOperation objectCreation && objectCreation.Constructor is { } constructor && IsDefaultOrTokenOperationCanceledExceptionCtor(constructor)) { isCancellationRequestedPropertyReference = propertyReference; return true; } isCancellationRequestedPropertyReference = default; return false; } /// <summary> /// Indicates whether the specified operation is a conditional statement of the form /// <code> /// if (!token.IsCancellationRequested) /// { /// // statements /// } /// else /// { /// throw new OperationCanceledException(); /// } /// </code> /// </summary> public bool IsNegatedCheckWithThrowingElseClause(IConditionalOperation conditional, [NotNullWhen(true)] out IPropertyReferenceOperation? isCancellationRequestedPropertyReference) { IOperation? whenFalseUnwrapped = GetSingleStatementOrDefault(conditional.WhenFalse); if (conditional.Condition is IUnaryOperation { OperatorKind: UnaryOperatorKind.Not } unary && unary.Operand is IPropertyReferenceOperation propertyReference && SymbolEqualityComparer.Default.Equals(propertyReference.Property, IsCancellationRequestedProperty) && whenFalseUnwrapped is IThrowOperation @throw && @throw.GetThrownException() is IObjectCreationOperation objectCreation && objectCreation.Constructor is { } constructor && IsDefaultOrTokenOperationCanceledExceptionCtor(constructor)) { isCancellationRequestedPropertyReference = propertyReference; return true; } isCancellationRequestedPropertyReference = default; return false; } private bool IsDefaultOrTokenOperationCanceledExceptionCtor(IMethodSymbol method) { return SymbolEqualityComparer.Default.Equals(method, OperationCanceledExceptionDefaultCtor) || SymbolEqualityComparer.Default.Equals(method, OperationCanceledExceptionTokenCtor); } } } }