Skip to content

Commit dcad84f

Browse files
authored
Merge pull request #2348 from mavasani/DisposeBugs
Fix false positives for Dispose rules
2 parents 278d518 + 5f3209a commit dcad84f

File tree

5 files changed

+192
-11
lines changed

5 files changed

+192
-11
lines changed

src/Microsoft.NetCore.Analyzers/Core/Runtime/DisposableFieldsShouldBeDisposed.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public sealed class DisposableFieldsShouldBeDisposed : DiagnosticAnalyzer
3939
public override void Initialize(AnalysisContext context)
4040
{
4141
context.EnableConcurrentExecution();
42-
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
42+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze);
4343

4444
context.RegisterCompilationStartAction(compilationContext =>
4545
{

src/Microsoft.NetCore.Analyzers/UnitTests/Runtime/DisposableFieldsShouldBeDisposedTests.cs

+53
Original file line numberDiff line numberDiff line change
@@ -2745,5 +2745,58 @@ public void Dispose()
27452745
}
27462746
");
27472747
}
2748+
2749+
[Fact, WorkItem(2306, "https://github.com/dotnet/roslyn-analyzers/issues/2306")]
2750+
public void DisposableAllocationInConstructor_DisposedInGeneratedCodeFile_NoDiagnostic()
2751+
{
2752+
VerifyCSharp(@"
2753+
using System;
2754+
2755+
class A : IDisposable
2756+
{
2757+
public void Dispose()
2758+
{
2759+
}
2760+
}
2761+
2762+
class B : IDisposable
2763+
{
2764+
private readonly A a;
2765+
public B()
2766+
{
2767+
a = new A();
2768+
}
2769+
2770+
[System.CodeDom.Compiler.GeneratedCodeAttribute("""", """")]
2771+
public void Dispose()
2772+
{
2773+
a.Dispose();
2774+
}
2775+
}
2776+
");
2777+
2778+
VerifyBasic(@"
2779+
Imports System
2780+
2781+
Class A
2782+
Implements IDisposable
2783+
Public Sub Dispose() Implements IDisposable.Dispose
2784+
End Sub
2785+
End Class
2786+
2787+
Class B
2788+
Implements IDisposable
2789+
2790+
Private ReadOnly a As A
2791+
Sub New()
2792+
a = New A()
2793+
End Sub
2794+
2795+
<System.CodeDom.Compiler.GeneratedCodeAttribute("""", """")> _
2796+
Public Sub Dispose() Implements IDisposable.Dispose
2797+
a.Dispose()
2798+
End Sub
2799+
End Class");
2800+
}
27482801
}
27492802
}

src/Microsoft.NetCore.Analyzers/UnitTests/Runtime/DisposeObjectsBeforeLosingScopeTests.cs

+91
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,97 @@ public bool ValueExists(int i)
486486
");
487487
}
488488

489+
[Fact, WorkItem(2245, "https://github.com/dotnet/roslyn-analyzers/issues/2245")]
490+
public void OutDisposableArgument_StoredIntoField_NoDiagnostic()
491+
{
492+
VerifyCSharp(@"
493+
using System;
494+
495+
class A : IDisposable
496+
{
497+
public void Dispose()
498+
{
499+
}
500+
}
501+
502+
class Test
503+
{
504+
private A _a;
505+
void M(out A param)
506+
{
507+
param = new A();
508+
}
509+
510+
void Method()
511+
{
512+
M(out _a); // This is considered as an escape of interprocedural disposable creation.
513+
}
514+
}
515+
");
516+
}
517+
518+
[Fact, WorkItem(2245, "https://github.com/dotnet/roslyn-analyzers/issues/2245")]
519+
public void OutDisposableArgument_WithinTryXXXInvocation_DisposedOnSuccessPath_NoDiagnostic()
520+
{
521+
VerifyCSharp(@"
522+
using System;
523+
using System.Collections.Concurrent;
524+
525+
public class C
526+
{
527+
private readonly ConcurrentDictionary<object, IDisposable> _dictionary;
528+
public C(ConcurrentDictionary<object, IDisposable> dictionary)
529+
{
530+
_dictionary = dictionary;
531+
}
532+
533+
public void Remove1(object key)
534+
{
535+
if (_dictionary.TryRemove(key, out IDisposable value))
536+
{
537+
value.Dispose();
538+
}
539+
}
540+
541+
public void Remove2(object key)
542+
{
543+
if (!_dictionary.TryRemove(key, out IDisposable value))
544+
{
545+
return;
546+
}
547+
548+
value.Dispose();
549+
}
550+
}");
551+
}
552+
553+
[Fact, WorkItem(2245, "https://github.com/dotnet/roslyn-analyzers/issues/2245")]
554+
public void OutDisposableArgument_WithinTryXXXInvocation_NotDisposed_Diagnostic()
555+
{
556+
VerifyCSharp(@"
557+
using System;
558+
using System.Collections.Concurrent;
559+
560+
public class C
561+
{
562+
private readonly ConcurrentDictionary<object, IDisposable> _dictionary;
563+
public C(ConcurrentDictionary<object, IDisposable> dictionary)
564+
{
565+
_dictionary = dictionary;
566+
}
567+
568+
public void Remove(object key)
569+
{
570+
if (_dictionary.TryRemove(key, out IDisposable value))
571+
{
572+
// value is not disposed.
573+
}
574+
}
575+
}",
576+
// Test0.cs(15,40): warning CA2000: Call System.IDisposable.Dispose on object created by 'out IDisposable value' before all references to it are out of scope.
577+
GetCSharpResultAt(15, 40, "out IDisposable value"));
578+
}
579+
489580
[Fact]
490581
public void LocalWithMultipleDisposableAssignment_DisposeCallOnSome_Diagnostic()
491582
{

src/Utilities/FlowAnalysis/FlowAnalysis/Analysis/DisposeAnalysis/DisposeAnalysis.DisposeDataFlowOperationVisitor.cs

+31-8
Original file line numberDiff line numberDiff line change
@@ -335,14 +335,6 @@ protected override void PostProcessArgument(IArgumentOperation operation, bool i
335335
{
336336
base.PostProcessArgument(operation, isEscaped);
337337
if (isEscaped)
338-
{
339-
PostProcessEscapedArgument();
340-
}
341-
342-
return;
343-
344-
// Local functions.
345-
void PostProcessEscapedArgument()
346338
{
347339
// Discover if a disposable object is being passed into the creation method for this new disposable object
348340
// and if the new disposable object assumes ownership of that passed in disposable object.
@@ -351,8 +343,39 @@ void PostProcessEscapedArgument()
351343
var pointsToValue = GetPointsToAbstractValue(operation.Value);
352344
HandlePossibleEscapingOperation(operation, pointsToValue.Locations);
353345
}
346+
else if (FlowBranchConditionKind == ControlFlowConditionKind.WhenFalse &&
347+
operation.Parameter.RefKind == RefKind.Out &&
348+
operation.Parent is IInvocationOperation invocation &&
349+
invocation.TargetMethod.ReturnType.SpecialType == SpecialType.System_Boolean)
350+
{
351+
// Case 1:
352+
// // Assume 'obj' is not a valid object on the 'else' path.
353+
// if (TryXXX(out IDisposable obj))
354+
// {
355+
// obj.Dispose();
356+
// }
357+
//
358+
// return;
359+
360+
// Case 2:
361+
// if (!TryXXX(out IDisposable obj))
362+
// {
363+
// return; // Assume 'obj' is not a valid object on this path.
364+
// }
365+
//
366+
// obj.Dispose();
367+
368+
HandlePossibleInvalidatingOperation(operation);
369+
}
370+
}
371+
else if (operation.Parameter.RefKind == RefKind.Out || operation.Parameter.RefKind == RefKind.Ref)
372+
{
373+
HandlePossibleEscapingOperation(operation, GetEscapedLocations(operation));
354374
}
355375

376+
return;
377+
378+
// Local functions.
356379
bool IsDisposeOwnershipTransfer()
357380
{
358381
if (!operation.Parameter.Type.IsDisposable(WellKnownTypeProvider.IDisposable))

src/Utilities/FlowAnalysis/FlowAnalysis/Analysis/PointsToAnalysis/PointsToAnalysis.PointsToDataFlowOperationVisitor.cs

+16-2
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,12 @@ protected override void PostProcessArgument(IArgumentOperation operation, bool i
319319
AnalysisEntityFactory.TryCreate(operation, out var analysisEntity))
320320
{
321321
CacheAbstractValue(operation, GetAbstractValue(analysisEntity));
322+
323+
if (analysisEntity.SymbolOpt?.Kind == SymbolKind.Field)
324+
{
325+
// Ref/Out field argument is considered escaped.
326+
HandleEscapingOperation(operation, operation.Value);
327+
}
322328
}
323329
}
324330
else if (operation.Parameter.RefKind == RefKind.Ref)
@@ -568,8 +574,16 @@ private void HandleEscapingOperation(IOperation escapingOperation, IOperation es
568574
Debug.Assert(escapingOperation != null);
569575
Debug.Assert(escapedInstance != null);
570576

571-
var escapedInstancePointsToValue = GetPointsToAbstractValue(escapedInstance);
572-
AnalysisEntityFactory.TryCreate(escapedInstance, out var escapedEntityOpt);
577+
PointsToAbstractValue escapedInstancePointsToValue;
578+
if (AnalysisEntityFactory.TryCreate(escapedInstance, out var escapedEntityOpt))
579+
{
580+
escapedInstancePointsToValue = GetAbstractValue(escapedEntityOpt);
581+
}
582+
else
583+
{
584+
escapedInstancePointsToValue = GetPointsToAbstractValue(escapedInstance);
585+
}
586+
573587
HandleEscapingLocations(escapingOperation, builder, escapedEntityOpt, escapedInstancePointsToValue);
574588
}
575589

0 commit comments

Comments
 (0)