diff --git a/sonaranalyzer-dotnet/its/expected/Nancy/Nancy.Tests-{776D244D-BC4D-4BBB-A478-CAF2D04520E1}-S3966.json b/sonaranalyzer-dotnet/its/expected/Nancy/Nancy.Tests-{776D244D-BC4D-4BBB-A478-CAF2D04520E1}-S3966.json deleted file mode 100644 index 2c7eff47e6f..00000000000 --- a/sonaranalyzer-dotnet/its/expected/Nancy/Nancy.Tests-{776D244D-BC4D-4BBB-A478-CAF2D04520E1}-S3966.json +++ /dev/null @@ -1,17 +0,0 @@ -{ -"issues": [ -{ -"id": "S3966", -"message": "Refactor this code to make sure 'innerStream' is disposed only once.", -"location": { -"uri": "sources\Nancy\src\Nancy.Tests\Unit\Extensions\RequestStreamExtensionsFixture.cs", -"region": { -"startLine": 15, -"startColumn": 24, -"endLine": 15, -"endColumn": 56 -} -} -} -] -} diff --git a/sonaranalyzer-dotnet/rspec/cs/S3966_c#.html b/sonaranalyzer-dotnet/rspec/cs/S3966_c#.html index 4269dbe92bc..14ef0d195c6 100644 --- a/sonaranalyzer-dotnet/rspec/cs/S3966_c#.html +++ b/sonaranalyzer-dotnet/rspec/cs/S3966_c#.html @@ -1,33 +1,24 @@ -

A proper implementation of IDisposable.Dispose should allow for it to be called multiple times on the same object, however this is not -guaranteed and could result in an exception being thrown.

-

It is best not to rely on this behaviour and therefore make sure an object is disposed only once on all execution paths. This is particularly true -when dealing with nested using statements.

-

Noncompliant Code Example

+

Disposing an object twice, either with the using keyword or by calling Dispose directly, in the same method is at best +confusing and at worst error-prone. The next developer might see only one of the Dispose/using and try to use an +already-disposed object.

+

In addition, even if the documentation of +Disposable explicitly states that calling the Dispose method multiple times should not throw an exception, some +implementation still do it. Thus it is safer to not dispose an object twice when possible.

+

This rule raises an issue when, in the same method, the Dispose method is explicitly called twice on the same object, or when +using is used with a direct call to Dispose().

+

Noncompliant Code Examples

-using (Stream stream = new FileStream("file.txt", FileMode.OpenOrCreate))
+using (var d = new Disposable()) // Noncompliant
 {
-    using (StreamWriter writer = new StreamWriter(stream))  // Noncompliant: 'stream' will be disposed twice
-    {
-        // Use the writer object...
-    }
+    d.Dispose();
 }
 
+
+using var d = new Disposable();
+d.Dispose(); // Noncompliant {{Refactor this code to make sure 'd' is disposed only once.}}
+

Compliant Solution

-Stream stream = null;
-try
-{
-    stream = new FileStream("file.txt", FileMode.OpenOrCreate);
-    using (StreamWriter writer = new StreamWriter(stream))
-    {
-        stream = null;
-        // Use the writer object...
-    }
-}
-finally
-{
-    if(stream != null)
-        stream.Dispose();
-}
+using var d = new Disposable();
 
diff --git a/sonaranalyzer-dotnet/rspec/cs/S3966_c#.json b/sonaranalyzer-dotnet/rspec/cs/S3966_c#.json index 9456348a8fe..6f93f74b81d 100644 --- a/sonaranalyzer-dotnet/rspec/cs/S3966_c#.json +++ b/sonaranalyzer-dotnet/rspec/cs/S3966_c#.json @@ -7,6 +7,7 @@ "constantCost": "10min" }, "tags": [ + "confusing", "pitfall" ], "defaultSeverity": "Major", diff --git a/sonaranalyzer-dotnet/rspec/cs/Sonar_way_profile.json b/sonaranalyzer-dotnet/rspec/cs/Sonar_way_profile.json index 4d4afe1e51d..78719307dee 100644 --- a/sonaranalyzer-dotnet/rspec/cs/Sonar_way_profile.json +++ b/sonaranalyzer-dotnet/rspec/cs/Sonar_way_profile.json @@ -196,6 +196,7 @@ "S3928", "S3949", "S3963", + "S3966", "S3971", "S3972", "S3973", diff --git a/sonaranalyzer-dotnet/src/SonarAnalyzer.CSharp/RspecStrings.resx b/sonaranalyzer-dotnet/src/SonarAnalyzer.CSharp/RspecStrings.resx index debe7f0f32c..82aedf421d6 100644 --- a/sonaranalyzer-dotnet/src/SonarAnalyzer.CSharp/RspecStrings.resx +++ b/sonaranalyzer-dotnet/src/SonarAnalyzer.CSharp/RspecStrings.resx @@ -8534,10 +8534,10 @@ Major Code Smell - A proper implementation of IDisposable.Dispose should allow for it to be called multiple times on the same object, however this is not guaranteed and could result in an exception being thrown. + Disposing an object twice, either with the using keyword or by calling Dispose directly, in the same method is at best confusing and at worst error-prone. The next developer might see only one of the Dispose/using and try to use an already-disposed object. - False + True Constant/Issue @@ -8552,7 +8552,7 @@ Major - pitfall + confusing,pitfall Objects should not be disposed more than once diff --git a/sonaranalyzer-dotnet/src/SonarAnalyzer.CSharp/Rules/SymbolicExecution/ObjectsShouldNotBeDisposedMoreThanOnce.cs b/sonaranalyzer-dotnet/src/SonarAnalyzer.CSharp/Rules/SymbolicExecution/ObjectsShouldNotBeDisposedMoreThanOnce.cs index 5cb013408a6..d68fd8a2dae 100644 --- a/sonaranalyzer-dotnet/src/SonarAnalyzer.CSharp/Rules/SymbolicExecution/ObjectsShouldNotBeDisposedMoreThanOnce.cs +++ b/sonaranalyzer-dotnet/src/SonarAnalyzer.CSharp/Rules/SymbolicExecution/ObjectsShouldNotBeDisposedMoreThanOnce.cs @@ -27,7 +27,6 @@ using Microsoft.CodeAnalysis.Diagnostics; using SonarAnalyzer.ControlFlowGraph.CSharp; using SonarAnalyzer.Helpers; -using SonarAnalyzer.Helpers.CSharp; using SonarAnalyzer.Rules.SymbolicExecution; using SonarAnalyzer.ShimLayer.CSharp; using SonarAnalyzer.SymbolicExecution; @@ -39,13 +38,6 @@ internal sealed class ObjectsShouldNotBeDisposedMoreThanOnce : ISymbolicExecutio { internal const string DiagnosticId = "S3966"; private const string MessageFormat = "Refactor this code to make sure '{0}' is disposed only once."; - private const string LeaveOpenParameterName = "leaveOpen"; - private const string StreamParameterName = "stream"; - - private static readonly ImmutableDictionary typesDisposingUnderlyingStreamWithLeaveOpenArgumentIndex = - ImmutableDictionary.Empty - .Add(KnownType.System_IO_StreamReader, 4) - .Add(KnownType.System_IO_StreamWriter, 3); private static readonly DiagnosticDescriptor rule = DiagnosticDescriptorBuilder.GetDescriptor(DiagnosticId, MessageFormat, RspecStrings.ResourceManager); @@ -105,7 +97,7 @@ public override ProgramState PreProcessUsingStatement(ProgramPoint programPoint, newProgramState = ProcessDisposableSymbol(newProgramState, disposable.SyntaxNode, disposable.Symbol); } - return ProcessStreamDisposingTypes(newProgramState, (SyntaxNode)usingFinalizer.UsingStatement.Expression ?? usingFinalizer.UsingStatement.Declaration); + return newProgramState; } public override ProgramState PreProcessInstruction(ProgramPoint programPoint, ProgramState programState) @@ -138,8 +130,7 @@ private ProgramState PreProcessVariableDeclarator(ProgramState programState, Var var disposableSymbol = semanticModel.GetDeclaredSymbol(declarator); var newProgramState = programState.StoreSymbolicValue(disposableSymbol, programState.PeekValue()); - newProgramState = ProcessDisposableSymbol(newProgramState, declarator, disposableSymbol); - return ProcessStreamDisposingTypes(newProgramState, declarator); + return ProcessDisposableSymbol(newProgramState, declarator, disposableSymbol); } private ProgramState VisitInvocationExpression(InvocationExpressionSyntax instruction, ProgramState programState) @@ -171,46 +162,6 @@ instruction.Expression as IdentifierNameSyntax return newProgramState; } - private ProgramState ProcessStreamDisposingTypes(ProgramState programState, SyntaxNode usingExpression) - { - var newProgramState = programState; - var objectCreationExpressions = usingExpression.DescendantNodes().OfType(); - - foreach (var objectCreationExpression in objectCreationExpressions) - { - if (semanticModel.GetSymbolInfo(objectCreationExpression.Type).Symbol is ISymbol symbol - && symbol.GetSymbolType() is ITypeSymbol objectType - && objectType.DerivesOrImplementsAny(typesDisposingUnderlyingStreamWithLeaveOpenArgumentIndex.Keys.ToImmutableArray()) - && objectCreationExpression.ArgumentList?.Arguments is IEnumerable argumentList - && argumentList.Any()) - { - // Checks for the 'leaveOpen' argument. It is at position 4 if present for StreamReader constructor, while it is at position 3 for StreamWriter. - // See #2491 for more details - var leaveOpenArgumentIndex = typesDisposingUnderlyingStreamWithLeaveOpenArgumentIndex - .Where(entry => objectType.DerivesOrImplements(entry.Key)) - .Select(entry => entry.Value) - .First(); - if (!HasLeaveOpenTrueArgument(argumentList, leaveOpenArgumentIndex) - && GetArgument(argumentList, StreamParameterName, 0) is ArgumentSyntax argument) - { - var streamSymbol = semanticModel.GetSymbolInfo(argument.Expression).Symbol; - newProgramState = ProcessDisposableSymbol(newProgramState, argument.Expression, streamSymbol); - } - } - } - - return newProgramState; - } - - private static bool HasLeaveOpenTrueArgument(IEnumerable argumentList, int leaveOpenArgumentIndex) => - GetArgument(argumentList, LeaveOpenParameterName, leaveOpenArgumentIndex) is ArgumentSyntax leaveOpenArgument - && CSharpEquivalenceChecker.AreEquivalent(leaveOpenArgument.Expression, CSharpSyntaxHelper.TrueLiteralExpression); - - private static ArgumentSyntax GetArgument(IEnumerable argumentList, string argumentName, int defaultArgumentIndex) => - argumentList.FirstOrDefault(arg => argumentName.Equals(arg.NameColon?.Name.Identifier.ValueText)) is { } argument - ? argument - : argumentList.ElementAtOrDefault(defaultArgumentIndex); - private ProgramState ProcessDisposableSymbol(ProgramState programState, SyntaxNode disposeInstruction, ISymbol disposableSymbol) { if (disposableSymbol == null) // DisposableSymbol is null when we invoke an array element diff --git a/sonaranalyzer-dotnet/src/SonarAnalyzer.Utilities/Rules.Description/S3966.html b/sonaranalyzer-dotnet/src/SonarAnalyzer.Utilities/Rules.Description/S3966.html index 4269dbe92bc..14ef0d195c6 100644 --- a/sonaranalyzer-dotnet/src/SonarAnalyzer.Utilities/Rules.Description/S3966.html +++ b/sonaranalyzer-dotnet/src/SonarAnalyzer.Utilities/Rules.Description/S3966.html @@ -1,33 +1,24 @@ -

A proper implementation of IDisposable.Dispose should allow for it to be called multiple times on the same object, however this is not -guaranteed and could result in an exception being thrown.

-

It is best not to rely on this behaviour and therefore make sure an object is disposed only once on all execution paths. This is particularly true -when dealing with nested using statements.

-

Noncompliant Code Example

+

Disposing an object twice, either with the using keyword or by calling Dispose directly, in the same method is at best +confusing and at worst error-prone. The next developer might see only one of the Dispose/using and try to use an +already-disposed object.

+

In addition, even if the documentation of +Disposable explicitly states that calling the Dispose method multiple times should not throw an exception, some +implementation still do it. Thus it is safer to not dispose an object twice when possible.

+

This rule raises an issue when, in the same method, the Dispose method is explicitly called twice on the same object, or when +using is used with a direct call to Dispose().

+

Noncompliant Code Examples

-using (Stream stream = new FileStream("file.txt", FileMode.OpenOrCreate))
+using (var d = new Disposable()) // Noncompliant
 {
-    using (StreamWriter writer = new StreamWriter(stream))  // Noncompliant: 'stream' will be disposed twice
-    {
-        // Use the writer object...
-    }
+    d.Dispose();
 }
 
+
+using var d = new Disposable();
+d.Dispose(); // Noncompliant {{Refactor this code to make sure 'd' is disposed only once.}}
+

Compliant Solution

-Stream stream = null;
-try
-{
-    stream = new FileStream("file.txt", FileMode.OpenOrCreate);
-    using (StreamWriter writer = new StreamWriter(stream))
-    {
-        stream = null;
-        // Use the writer object...
-    }
-}
-finally
-{
-    if(stream != null)
-        stream.Dispose();
-}
+using var d = new Disposable();
 
diff --git a/sonaranalyzer-dotnet/tests/SonarAnalyzer.UnitTest/TestCases/ObjectsShouldNotBeDisposedMoreThanOnce.cs b/sonaranalyzer-dotnet/tests/SonarAnalyzer.UnitTest/TestCases/ObjectsShouldNotBeDisposedMoreThanOnce.cs index b172979e58b..9b5fa499b28 100644 --- a/sonaranalyzer-dotnet/tests/SonarAnalyzer.UnitTest/TestCases/ObjectsShouldNotBeDisposedMoreThanOnce.cs +++ b/sonaranalyzer-dotnet/tests/SonarAnalyzer.UnitTest/TestCases/ObjectsShouldNotBeDisposedMoreThanOnce.cs @@ -7,14 +7,14 @@ public interface IInterface1 : IDisposable { } class Program { - public void DisposedTwise() + public void DisposedTwice() { var d = new Disposable(); d.Dispose(); d.Dispose(); // Noncompliant } - public void DisposedTwise_Conditional() + public void DisposedTwice_Conditional() { IDisposable d = null; d = new Disposable(); @@ -26,7 +26,7 @@ public void DisposedTwise_Conditional() // ^ } - public void DisposedTwise_Try() + public void DisposedTwice_Try() { IDisposable d = null; try @@ -41,74 +41,43 @@ public void DisposedTwise_Try() } } - public void DisposedTwise_Array() + public void DisposedTwice_Array() { var a = new[] { new Disposable() }; a[0].Dispose(); a[0].Dispose(); // Compliant, we don't handle arrays } - public void Disposed_Using_WithDeclaration() + public void Dispose_Stream_LeaveOpenFalse() { - using (var d = new Disposable()) // Noncompliant + using (MemoryStream memoryStream = new MemoryStream()) // Compliant + using (StreamWriter writer = new StreamWriter(memoryStream, new System.Text.UTF8Encoding(false), 1024, leaveOpen: false)) { - d.Dispose(); } } - public void Disposed_Using_WithExpressions() + public void Dispose_Stream_LeaveOpenTrue() { - var d = new Disposable(); - using (d) // Noncompliant + using (MemoryStream memoryStream = new MemoryStream()) // Compliant + using (StreamWriter writer = new StreamWriter(memoryStream, new System.Text.UTF8Encoding(false), 1024, leaveOpen: true)) { - d.Dispose(); } } - public void Disposed_Using3(Stream str) + public void Disposed_Using_WithDeclaration() { - using (var s = new FileStream("path", FileMode.Open)) // Noncompliant -// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - { - using (var sr = new StreamReader(s)) - { - } - } - - Stream stream; - using (stream = new FileStream("path", FileMode.Open)) // Noncompliant -// ^^^^^^ - { - using (var sr = new StreamReader(stream)) - { - } - } - - using (str) + using (var d = new Disposable()) // Noncompliant { - var sr = new StreamReader(str); - using (sr) // Compliant, we cannot detect if 'str' was argument of the 'sr' constructor or not - { - } + d.Dispose(); } } - public void Disposed_Using4() + public void Disposed_Using_WithExpressions() { - Stream s = new FileStream("path", FileMode.Open); - try - { - using (var sr = new StreamReader(s)) - { - s = null; - } - } - finally + var d = new Disposable(); + using (d) // Noncompliant { - if (s != null) - { - s.Dispose(); - } + d.Dispose(); } } @@ -118,14 +87,14 @@ public void Disposed_Using_Parameters(IDisposable param1) param1.Dispose(); // Noncompliant } - public void Close_ParametersOfDifferenceTypes(IInterface1 interface1, IDisposable interface2) + public void Close_ParametersOfDifferentTypes(IInterface1 interface1, IDisposable interface2) { // Regression test for https://github.com/SonarSource/sonar-csharp/issues/1038 interface1.Dispose(); // ok, only called once on each parameter interface2.Dispose(); } - public void Close_ParametersOfSameTypes(IInterface1 instance1, IInterface1 instance2) + public void Close_ParametersOfSameType(IInterface1 instance1, IInterface1 instance2) { // Regression test for https://github.com/SonarSource/sonar-csharp/issues/1038 instance1.Dispose(); @@ -140,7 +109,6 @@ public void Close_OneParameterDisposedTwice(IInterface1 instance1, IInterface1 i instance2.Dispose(); // ok - only disposed once } - } public class Disposable : IDisposable @@ -165,137 +133,6 @@ public void DoSomething() } } - public class UsingWithLeaveOpenArgument - { - public void LeaveOpenTrue1() - { - using (MemoryStream memoryStream = new MemoryStream()) - using (StreamWriter writer = new StreamWriter(memoryStream, new System.Text.UTF8Encoding(false), 1024, true)) - { - string str = null; - } - } - - public void LeaveOpenTrue2() - { - using (MemoryStream memoryStream = new MemoryStream()) - using (StreamWriter writer = new StreamWriter(memoryStream, new System.Text.UTF8Encoding(false), 1024, leaveOpen: true)) - { - string str = null; - } - } - - public void LeaveOpenTrue3() - { - using (MemoryStream memoryStream = new MemoryStream()) - using (StreamWriter writer = new StreamWriter(memoryStream, new System.Text.UTF8Encoding(false), leaveOpen: true, bufferSize: 1024)) - { - string str = null; - } - } - - public void LeaveOpenTrue4() - { - using (MemoryStream memoryStream = new MemoryStream()) - using (StreamReader reader = new StreamReader(memoryStream, new System.Text.UTF8Encoding(false), false, 1024, true)) - { - string str = null; - } - } - - public void LeaveOpenTrue5() - { - using (MemoryStream memoryStream = new MemoryStream()) - using (StreamReader reader = new StreamReader(memoryStream, new System.Text.UTF8Encoding(false), bufferSize: 2014, leaveOpen: true, detectEncodingFromByteOrderMarks: false)) - { - string str = null; - } - } - - public void LeaveOpenTrue6() - { - using (MemoryStream memoryStream = new MemoryStream()) - using (StreamWriter writer = new StreamWriter(memoryStream, new System.Text.UTF8Encoding(false), leaveOpen: true, bufferSize: 1024)) - using (StreamReader reader = new StreamReader(memoryStream, new System.Text.UTF8Encoding(false), bufferSize: 2014, leaveOpen: true, detectEncodingFromByteOrderMarks: false)) - { - string str = null; - } - } - - public void LeaveOpenTrue7() - { - using (MemoryStream memoryStream = new MemoryStream()) // Noncompliant - using (StreamWriter writer = new StreamWriter(memoryStream)) - using (StreamReader reader = new StreamReader(memoryStream, new System.Text.UTF8Encoding(false), bufferSize: 2014, leaveOpen: true, detectEncodingFromByteOrderMarks: false)) - { - string str = null; - } - } - - public void LeaveOpenTrue8() - { - using (MemoryStream memoryStream = new MemoryStream()) - using (StreamWriter writer = new StreamWriter(leaveOpen: true, stream: memoryStream, encoding: new System.Text.UTF8Encoding(false), bufferSize: 1024)) - { - string str = null; - } - } - - public void LeaveOpenFalse1() - { - using (MemoryStream memoryStream = new MemoryStream()) // Noncompliant - using (StreamWriter writer = new StreamWriter(memoryStream, new System.Text.UTF8Encoding(false), 1024, false)) - { - string str = null; - } - } - - public void LeaveOpenFalse2() - { - using (MemoryStream memoryStream = new MemoryStream()) // Noncompliant - using (StreamWriter writer = new StreamWriter(memoryStream, new System.Text.UTF8Encoding(false), 1024, leaveOpen: false)) - { - string str = null; - } - } - - public void LeaveOpenFalse3() - { - using (MemoryStream memoryStream = new MemoryStream()) // Noncompliant - using (StreamWriter writer = new StreamWriter(memoryStream, new System.Text.UTF8Encoding(false), leaveOpen: false, bufferSize: 1024)) - { - string str = null; - } - } - - public void LeaveOpenFalse4() - { - using (MemoryStream memoryStream = new MemoryStream()) // Noncompliant - using (StreamReader reader = new StreamReader(memoryStream, new System.Text.UTF8Encoding(false), true, 1024, false)) - { - string str = null; - } - } - - public void LeaveOpenFalse5() - { - using (MemoryStream memoryStream = new MemoryStream()) // Noncompliant - using (StreamReader reader = new StreamReader(memoryStream, new System.Text.UTF8Encoding(false), bufferSize: 2014, leaveOpen: false, detectEncodingFromByteOrderMarks: true)) - { - string str = null; - } - } - - public void LeaveOpenFalse6() - { - using (MemoryStream memoryStream = new MemoryStream()) // Noncompliant - using (StreamWriter writer = new StreamWriter(leaveOpen: false, stream: memoryStream, encoding: new System.Text.UTF8Encoding(false), bufferSize: 1024)) - { - string str = null; - } - } - } - class TestLoopWithBreak { public static void LoopWithBreak(System.Collections.Generic.IEnumerable list, bool condition, IInterface1 instance1) @@ -330,70 +167,21 @@ public void Disposed_UsingDeclaration() using var d = new Disposable(); d.Dispose(); // Noncompliant {{Refactor this code to make sure 'd' is disposed only once.}} } - - public void Implicit_Disposed_UsingDeclaration(Stream str) - { - using var s = new FileStream("path", FileMode.Open); - using var sr = new StreamReader(s); // Noncompliant {{Refactor this code to make sure 's' is disposed only once.}} - } - - public void Disposed_Using_Valid() - { - var s = new FileStream("path", FileMode.Open); - try - { - using var sr = new StreamReader(s); - s = null; - } - finally - { - if (s != null) - { - s.Dispose(); - } - } - } - - public void Mixed_Usings_Disposed(Stream str) - { - using var s = new FileStream("path", FileMode.Open); - using (var sr = new StreamReader(s)) // Noncompliant {{Refactor this code to make sure 's' is disposed only once.}} - { - } - } - - public void Mixed_Usings_ComplexCFG_Disposed(Stream str) - { - using var s = new FileStream("path1", FileMode.Open); - using (var sr1 = new FileStream("path2", FileMode.Open)) - { - if (str != null) - { - using (var sr2 = new StreamReader(s)) // Noncompliant - { - } - } - } - } - - public void FileStream_Using_Null(Stream str) - { - using FileStream s = null; - s.Dispose(); // Ok - s is null here, so it will raise a null pointer dereference instead - } } public class NullCoalescenceAssignment { - public void NullCoalescenceAssignment_NotNull(Stream s) + public void NullCoalescenceAssignment_Compliant(IDisposable s) + { + s ??= new Disposable(); + s.Dispose(); + } + + public void NullCoalescenceAssignment_NonCompliant(IDisposable s) { - if (s == null) + using (s ??= new Disposable()) // Noncompliant { - s ??= new FileStream("path", FileMode.Open); - using (var sr = new StreamReader(s)) - { - } - s.Dispose(); // Noncompliant + s.Dispose(); } } } @@ -447,7 +235,6 @@ public void Method(object myObject1, object myObject2) } } - public ref struct Struct { public void Dispose()