// 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.Concurrent;
using System.Collections.Immutable;
using System.Linq;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Analyzer.Utilities.PooledObjects;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.FlowAnalysis;
using Microsoft.CodeAnalysis.FlowAnalysis.DataFlow;
using Microsoft.CodeAnalysis.FlowAnalysis.DataFlow.DisposeAnalysis;
using Microsoft.CodeAnalysis.FlowAnalysis.DataFlow.PointsToAnalysis;
namespace Microsoft.NetCore.Analyzers.Runtime
{
using static MicrosoftNetCoreAnalyzersResources;
/// <summary>
/// CA2000: <inheritdoc cref="DisposeObjectsBeforeLosingScopeTitle"/>
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public sealed class DisposeObjectsBeforeLosingScope : DiagnosticAnalyzer
{
internal const string RuleId = "CA2000";
private static readonly LocalizableString s_localizableTitle = CreateLocalizableResourceString(nameof(DisposeObjectsBeforeLosingScopeTitle));
private static readonly LocalizableString s_localizableDescription = CreateLocalizableResourceString(nameof(DisposeObjectsBeforeLosingScopeDescription));
internal static readonly DiagnosticDescriptor NotDisposedRule = DiagnosticDescriptorHelper.Create(
RuleId,
s_localizableTitle,
CreateLocalizableResourceString(nameof(DisposeObjectsBeforeLosingScopeNotDisposedMessage)),
DiagnosticCategory.Reliability,
RuleLevel.Disabled,
description: s_localizableDescription,
isPortedFxCopRule: true,
isDataflowRule: true);
internal static readonly DiagnosticDescriptor MayBeDisposedRule = DiagnosticDescriptorHelper.Create(
RuleId,
s_localizableTitle,
CreateLocalizableResourceString(nameof(DisposeObjectsBeforeLosingScopeMayBeDisposedMessage)),
DiagnosticCategory.Reliability,
RuleLevel.Disabled,
description: s_localizableDescription,
isPortedFxCopRule: true,
isDataflowRule: true);
internal static readonly DiagnosticDescriptor NotDisposedOnExceptionPathsRule = DiagnosticDescriptorHelper.Create(
RuleId,
s_localizableTitle,
CreateLocalizableResourceString(nameof(DisposeObjectsBeforeLosingScopeNotDisposedOnExceptionPathsMessage)),
DiagnosticCategory.Reliability,
RuleLevel.Disabled,
description: s_localizableDescription,
isPortedFxCopRule: true,
isDataflowRule: true);
internal static readonly DiagnosticDescriptor MayBeDisposedOnExceptionPathsRule = DiagnosticDescriptorHelper.Create(
RuleId,
s_localizableTitle,
CreateLocalizableResourceString(nameof(DisposeObjectsBeforeLosingScopeMayBeDisposedOnExceptionPathsMessage)),
DiagnosticCategory.Reliability,
RuleLevel.Disabled,
description: s_localizableDescription,
isPortedFxCopRule: true,
isDataflowRule: true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
ImmutableArray.Create(NotDisposedRule, MayBeDisposedRule, NotDisposedOnExceptionPathsRule, MayBeDisposedOnExceptionPathsRule);
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterCompilationStartAction(compilationContext =>
{
if (!DisposeAnalysisHelper.TryGetOrCreate(compilationContext.Compilation, out var disposeAnalysisHelper))
{
return;
}
var reportedLocations = new ConcurrentDictionary<Location, bool>();
compilationContext.RegisterOperationBlockAction(operationBlockContext =>
{
if (operationBlockContext.OwningSymbol is not IMethodSymbol containingMethod ||
!disposeAnalysisHelper.HasAnyDisposableCreationDescendant(operationBlockContext.OperationBlocks, containingMethod) ||
operationBlockContext.Options.IsConfiguredToSkipAnalysis(NotDisposedRule, containingMethod, operationBlockContext.Compilation))
{
return;
}
var disposeAnalysisKind = operationBlockContext.Options.GetDisposeAnalysisKindOption(NotDisposedOnExceptionPathsRule, containingMethod,
operationBlockContext.Compilation, DisposeAnalysisKind.NonExceptionPaths);
var trackExceptionPaths = disposeAnalysisKind.AreExceptionPathsEnabled();
// For non-exception paths analysis, we can skip interprocedural analysis for certain invocations.
var interproceduralAnalysisPredicate = !trackExceptionPaths ?
new InterproceduralAnalysisPredicate(
skipAnalysisForInvokedMethodPredicate: SkipInterproceduralAnalysis,
skipAnalysisForInvokedLambdaOrLocalFunctionPredicate: null,
skipAnalysisForInvokedContextPredicate: null) :
null;
if (disposeAnalysisHelper.TryGetOrComputeResult(operationBlockContext.OperationBlocks, containingMethod,
operationBlockContext.Options, NotDisposedRule, PointsToAnalysisKind.PartialWithoutTrackingFieldsAndProperties, trackInstanceFields: false, trackExceptionPaths: trackExceptionPaths,
disposeAnalysisResult: out var disposeAnalysisResult, pointsToAnalysisResult: out var pointsToAnalysisResult, interproceduralAnalysisPredicate: interproceduralAnalysisPredicate))
{
using var notDisposedDiagnostics = ArrayBuilder<Diagnostic>.GetInstance();
using var mayBeNotDisposedDiagnostics = ArrayBuilder<Diagnostic>.GetInstance();
// Compute diagnostics for undisposed objects at exit block for non-exceptional exit paths.
var exitBlock = disposeAnalysisResult.ControlFlowGraph.GetExit();
var disposeDataAtExit = disposeAnalysisResult.ExitBlockOutput.Data;
ComputeDiagnostics(disposeDataAtExit,
notDisposedDiagnostics, mayBeNotDisposedDiagnostics, disposeAnalysisResult, pointsToAnalysisResult,
disposeAnalysisKind, isDisposeDataForExceptionPaths: false);
if (trackExceptionPaths)
{
// Compute diagnostics for undisposed objects at handled exception exit paths.
var disposeDataAtHandledExceptionPaths = disposeAnalysisResult.ExceptionPathsExitBlockOutput!.Data;
ComputeDiagnostics(disposeDataAtHandledExceptionPaths,
notDisposedDiagnostics, mayBeNotDisposedDiagnostics, disposeAnalysisResult, pointsToAnalysisResult,
disposeAnalysisKind, isDisposeDataForExceptionPaths: true);
// Compute diagnostics for undisposed objects at unhandled exception exit paths, if any.
var disposeDataAtUnhandledExceptionPaths = disposeAnalysisResult.MergedStateForUnhandledThrowOperations?.Data;
if (disposeDataAtUnhandledExceptionPaths != null)
{
ComputeDiagnostics(disposeDataAtUnhandledExceptionPaths,
notDisposedDiagnostics, mayBeNotDisposedDiagnostics, disposeAnalysisResult, pointsToAnalysisResult,
disposeAnalysisKind, isDisposeDataForExceptionPaths: true);
}
}
if (!notDisposedDiagnostics.Any() && !mayBeNotDisposedDiagnostics.Any())
{
return;
}
// Report diagnostics preferring *not* disposed diagnostics over may be not disposed diagnostics
// and avoiding duplicates.
foreach (var diagnostic in notDisposedDiagnostics.Concat(mayBeNotDisposedDiagnostics))
{
if (reportedLocations.TryAdd(diagnostic.Location, true))
{
operationBlockContext.ReportDiagnostic(diagnostic);
}
}
}
});
return;
// Local functions.
bool SkipInterproceduralAnalysis(IMethodSymbol invokedMethod)
{
// Skip interprocedural analysis if we are invoking a method and not passing any disposable object as an argument
// and not receiving a disposable object as a return value.
// We also check that we are not passing any object type argument which might hold disposable object
// and also check that we are not passing delegate type argument which can
// be a lambda or local function that has access to disposable object in current method's scope.
if (CanBeDisposable(invokedMethod.ReturnType))
{
return false;
}
foreach (var p in invokedMethod.Parameters)
{
if (CanBeDisposable(p.Type))
{
return false;
}
}
return true;
bool CanBeDisposable(ITypeSymbol type)
=> type.SpecialType == SpecialType.System_Object ||
type.DerivesFrom(disposeAnalysisHelper!.IDisposable) ||
type.TypeKind == TypeKind.Delegate;
}
});
}
private static void ComputeDiagnostics(
ImmutableDictionary<AbstractLocation, DisposeAbstractValue> disposeData,
ArrayBuilder<Diagnostic> notDisposedDiagnostics,
ArrayBuilder<Diagnostic> mayBeNotDisposedDiagnostics,
DisposeAnalysisResult disposeAnalysisResult,
PointsToAnalysisResult pointsToAnalysisResult,
DisposeAnalysisKind disposeAnalysisKind,
bool isDisposeDataForExceptionPaths)
{
foreach (var kvp in disposeData)
{
AbstractLocation location = kvp.Key;
DisposeAbstractValue disposeValue = kvp.Value;
if (disposeValue.Kind == DisposeAbstractValueKind.NotDisposable ||
location.Creation == null)
{
continue;
}
var isNotDisposed = disposeValue.Kind == DisposeAbstractValueKind.NotDisposed ||
(!disposeValue.DisposingOrEscapingOperations.IsEmpty &&
disposeValue.DisposingOrEscapingOperations.All(d => d.IsInsideCatchRegion(disposeAnalysisResult.ControlFlowGraph) && !location.GetTopOfCreationCallStackOrCreation().IsInsideCatchRegion(disposeAnalysisResult.ControlFlowGraph)));
var isMayBeNotDisposed = !isNotDisposed && (disposeValue.Kind == DisposeAbstractValueKind.MaybeDisposed || disposeValue.Kind == DisposeAbstractValueKind.NotDisposedOrEscaped);
if (isNotDisposed ||
(isMayBeNotDisposed && disposeAnalysisKind.AreMayBeNotDisposedViolationsEnabled()))
{
var syntax = location.TryGetNodeToReportDiagnostic(pointsToAnalysisResult);
if (syntax == null)
{
continue;
}
// CA2000: Call System.IDisposable.Dispose on object created by '{0}' before all references to it are out of scope.
var rule = GetRule(isNotDisposed);
// Ensure that we do not include multiple lines for the object creation expression in the diagnostic message.
var argument = syntax.ToString();
var indexOfNewLine = argument.IndexOf(Environment.NewLine, StringComparison.Ordinal);
if (indexOfNewLine > 0)
{
argument = argument[..indexOfNewLine];
}
var diagnostic = syntax.CreateDiagnostic(rule, argument);
if (isNotDisposed)
{
notDisposedDiagnostics.Add(diagnostic);
}
else
{
mayBeNotDisposedDiagnostics.Add(diagnostic);
}
}
}
DiagnosticDescriptor GetRule(bool isNotDisposed)
{
if (isNotDisposed)
{
return isDisposeDataForExceptionPaths ? NotDisposedOnExceptionPathsRule : NotDisposedRule;
}
else
{
return isDisposeDataForExceptionPaths ? MayBeDisposedOnExceptionPathsRule : MayBeDisposedRule;
}
}
}
}
}