Skip to content

Commit 74a5eae

Browse files
committed
IDISP024 don't call GC.SuppressFinalize if sealed and no finalizer.
Close #182
1 parent 93475ee commit 74a5eae

9 files changed

Lines changed: 278 additions & 1 deletion

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
namespace IDisposableAnalyzers.Test.IDISP024DoNotCallSuppressFinalizeIfSealedAndNoFinalizerTests
2+
{
3+
using Gu.Roslyn.Asserts;
4+
using Microsoft.CodeAnalysis.CodeFixes;
5+
using Microsoft.CodeAnalysis.Diagnostics;
6+
using NUnit.Framework;
7+
8+
public static class CodeFix
9+
{
10+
private static readonly DiagnosticAnalyzer Analyzer = new SuppressFinalizeAnalyzer();
11+
private static readonly CodeFixProvider Fix = new RemoveCallFix();
12+
13+
[Test]
14+
public static void SealedSimple()
15+
{
16+
var before = @"
17+
namespace N
18+
{
19+
using System;
20+
21+
public sealed class C : IDisposable
22+
{
23+
public void Dispose()
24+
{
25+
↓GC.SuppressFinalize(this);
26+
}
27+
}
28+
}";
29+
30+
var after = @"
31+
namespace N
32+
{
33+
using System;
34+
35+
public sealed class C : IDisposable
36+
{
37+
public void Dispose()
38+
{
39+
}
40+
}
41+
}";
42+
RoslynAssert.CodeFix(Analyzer, Fix, before, after);
43+
}
44+
}
45+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
namespace IDisposableAnalyzers.Test.IDISP024DoNotCallSuppressFinalizeIfSealedAndNoFinalizerTests
2+
{
3+
using Gu.Roslyn.Asserts;
4+
using Microsoft.CodeAnalysis.Diagnostics;
5+
using NUnit.Framework;
6+
7+
public static class Valid
8+
{
9+
private static readonly DiagnosticAnalyzer Analyzer = new SuppressFinalizeAnalyzer();
10+
11+
[Test]
12+
public static void SealedSimple()
13+
{
14+
var code = @"
15+
namespace N
16+
{
17+
using System;
18+
19+
public sealed class C : IDisposable
20+
{
21+
public void Dispose()
22+
{
23+
}
24+
}
25+
}";
26+
RoslynAssert.Valid(Analyzer, code);
27+
}
28+
29+
[Test]
30+
public static void SealedNoFinalizer()
31+
{
32+
var code = @"
33+
namespace N
34+
{
35+
using System;
36+
37+
public sealed class C : IDisposable
38+
{
39+
private bool isDisposed = false;
40+
41+
public void Dispose()
42+
{
43+
this.Dispose(true);
44+
}
45+
46+
private void Dispose(bool disposing)
47+
{
48+
if (!this.isDisposed)
49+
{
50+
this.isDisposed = true;
51+
}
52+
}
53+
}
54+
}";
55+
RoslynAssert.Valid(Analyzer, code);
56+
}
57+
58+
[Test]
59+
public static void SealedWithFinalizer()
60+
{
61+
var code = @"
62+
namespace N
63+
{
64+
using System;
65+
66+
public sealed class C : IDisposable
67+
{
68+
private bool isDisposed = false;
69+
70+
~C()
71+
{
72+
this.Dispose(false);
73+
}
74+
75+
public void Dispose()
76+
{
77+
this.Dispose(true);
78+
GC.SuppressFinalize(this);
79+
}
80+
81+
void Dispose(bool disposing)
82+
{
83+
if (!this.isDisposed)
84+
{
85+
this.isDisposed = true;
86+
}
87+
}
88+
}
89+
}";
90+
RoslynAssert.Valid(Analyzer, code);
91+
}
92+
}
93+
}

IDisposableAnalyzers.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{BAD2A4EE-6
3737
documentation\IDISP021.md = documentation\IDISP021.md
3838
documentation\IDISP022.md = documentation\IDISP022.md
3939
documentation\IDISP023.md = documentation\IDISP023.md
40+
documentation\IDISP024.md = documentation\IDISP024.md
4041
README.md = README.md
4142
RELEASE_NOTES.md = RELEASE_NOTES.md
4243
documentation\SyntaxTreeCacheAnalyzer.md = documentation\SyntaxTreeCacheAnalyzer.md

IDisposableAnalyzers/Analyzers/ReturnValueAnalyzer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ private static bool IsIgnored(ISymbol symbol)
223223

224224
private static ITypeSymbol? ReturnType(SyntaxNodeAnalysisContext context)
225225
{
226-
if (context.Node.FirstAncestorOrSelf<AnonymousFunctionExpressionSyntax>() is {} lambda)
226+
if (context.Node.FirstAncestorOrSelf<AnonymousFunctionExpressionSyntax>() is { } lambda)
227227
{
228228
var method = context.SemanticModel.GetSymbolSafe(lambda, context.CancellationToken) as IMethodSymbol;
229229
return method?.ReturnType;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
namespace IDisposableAnalyzers
2+
{
3+
using System.Collections.Immutable;
4+
using Gu.Roslyn.AnalyzerExtensions;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp;
7+
using Microsoft.CodeAnalysis.CSharp.Syntax;
8+
using Microsoft.CodeAnalysis.Diagnostics;
9+
10+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
11+
internal class SuppressFinalizeAnalyzer : DiagnosticAnalyzer
12+
{
13+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(
14+
Descriptors.IDISP024DoNotCallSuppressFinalizeIfSealedAndNoFinalizer);
15+
16+
public override void Initialize(AnalysisContext context)
17+
{
18+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
19+
context.EnableConcurrentExecution();
20+
context.RegisterSyntaxNodeAction(c => Handle(c), SyntaxKind.InvocationExpression);
21+
}
22+
23+
private static void Handle(SyntaxNodeAnalysisContext context)
24+
{
25+
if (context.Node is InvocationExpressionSyntax { ArgumentList: { Arguments: { Count: 1 } arguments } } invocation &&
26+
invocation.IsSymbol(KnownSymbol.GC.SuppressFinalize, context.SemanticModel, context.CancellationToken) &&
27+
context.SemanticModel.TryGetNamedType(arguments[0].Expression, context.CancellationToken, out var type) &&
28+
type.IsSealed &&
29+
!type.TryFindFirstMethod(x => x.MethodKind == MethodKind.Destructor, out _))
30+
{
31+
context.ReportDiagnostic(
32+
Diagnostic.Create(
33+
Descriptors.IDISP024DoNotCallSuppressFinalizeIfSealedAndNoFinalizer,
34+
invocation.GetLocation()));
35+
}
36+
}
37+
}
38+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
namespace IDisposableAnalyzers
2+
{
3+
using System.Collections.Immutable;
4+
using System.Composition;
5+
using System.Threading.Tasks;
6+
using Gu.Roslyn.CodeFixExtensions;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.CodeFixes;
9+
using Microsoft.CodeAnalysis.CSharp.Syntax;
10+
11+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(RemoveCallFix))]
12+
[Shared]
13+
internal class RemoveCallFix : DocumentEditorCodeFixProvider
14+
{
15+
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(
16+
Descriptors.IDISP024DoNotCallSuppressFinalizeIfSealedAndNoFinalizer.Id);
17+
18+
protected override DocumentEditorFixAllProvider? FixAllProvider() => null;
19+
20+
protected override async Task RegisterCodeFixesAsync(DocumentEditorCodeFixContext context)
21+
{
22+
var syntaxRoot = await context.Document.GetSyntaxRootAsync(context.CancellationToken)
23+
.ConfigureAwait(false);
24+
25+
foreach (var diagnostic in context.Diagnostics)
26+
{
27+
if (syntaxRoot.TryFindNodeOrAncestor(diagnostic, out ExpressionStatementSyntax? statement))
28+
{
29+
context.RegisterCodeFix(
30+
"Remove",
31+
(e, _) => e.RemoveNode(statement),
32+
equivalenceKey: nameof(SuppressFinalizeFix),
33+
diagnostic);
34+
break;
35+
}
36+
}
37+
}
38+
}
39+
}

IDisposableAnalyzers/Descriptors.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,15 @@ internal static class Descriptors
211211
isEnabledByDefault: true,
212212
description: "Don't use reference types in finalizer context.");
213213

214+
internal static readonly DiagnosticDescriptor IDISP024DoNotCallSuppressFinalizeIfSealedAndNoFinalizer = Descriptors.Create(
215+
id: "IDISP024",
216+
title: "Don't call GC.SuppressFinalize(this) when the type is sealed and has no finalizer.",
217+
messageFormat: "Don't call GC.SuppressFinalize(this) when the type is sealed and has no finalizer.",
218+
category: AnalyzerCategory.Correctness,
219+
defaultSeverity: DiagnosticSeverity.Warning,
220+
isEnabledByDefault: true,
221+
description: "Don't call GC.SuppressFinalize(this) when the type is sealed and has no finalizer.");
222+
214223
/// <summary>
215224
/// Create a DiagnosticDescriptor, which provides description about a <see cref="Diagnostic" />.
216225
/// </summary>

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Roslyn analyzers for IDisposable
3838
| [IDISP021](https://github.com/DotNetAnalyzers/IDisposableAnalyzers/blob/master/documentation/IDISP021.md)| Call this.Dispose(true).
3939
| [IDISP022](https://github.com/DotNetAnalyzers/IDisposableAnalyzers/blob/master/documentation/IDISP022.md)| Call this.Dispose(false).
4040
| [IDISP023](https://github.com/DotNetAnalyzers/IDisposableAnalyzers/blob/master/documentation/IDISP023.md)| Don't use reference types in finalizer context.
41+
| [IDISP024](https://github.com/DotNetAnalyzers/IDisposableAnalyzers/blob/master/documentation/IDISP024.md)| Don't call GC.SuppressFinalize(this) when the type is sealed and has no finalizer.
4142
| [SyntaxTreeCacheAnalyzer]()| Controls caching of for example semantic models for syntax trees.
4243

4344
## Using IDisposableAnalyzers

documentation/IDISP024.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# IDISP024
2+
## Don't call GC.SuppressFinalize(this) when the type is sealed and has no finalizer.
3+
4+
| Topic | Value
5+
| :-- | :--
6+
| Id | IDISP024
7+
| Severity | Warning
8+
| Enabled | True
9+
| Category | IDisposableAnalyzers.Correctness
10+
| Code | [SuppressFinalizeAnalyzer](https://github.com/DotNetAnalyzers/IDisposableAnalyzers/blob/master/IDisposableAnalyzers/Analyzers/SuppressFinalizeAnalyzer.cs)
11+
12+
13+
## Description
14+
15+
Don't call GC.SuppressFinalize(this) when the type is sealed and has no finalizer.
16+
17+
## Motivation
18+
19+
ADD MOTIVATION HERE
20+
21+
## How to fix violations
22+
23+
ADD HOW TO FIX VIOLATIONS HERE
24+
25+
<!-- start generated config severity -->
26+
## Configure severity
27+
28+
### Via ruleset file.
29+
30+
Configure the severity per project, for more info see [MSDN](https://msdn.microsoft.com/en-us/library/dd264949.aspx).
31+
32+
### Via #pragma directive.
33+
```C#
34+
#pragma warning disable IDISP024 // Don't call GC.SuppressFinalize(this) when the type is sealed and has no finalizer.
35+
Code violating the rule here
36+
#pragma warning restore IDISP024 // Don't call GC.SuppressFinalize(this) when the type is sealed and has no finalizer.
37+
```
38+
39+
Or put this at the top of the file to disable all instances.
40+
```C#
41+
#pragma warning disable IDISP024 // Don't call GC.SuppressFinalize(this) when the type is sealed and has no finalizer.
42+
```
43+
44+
### Via attribute `[SuppressMessage]`.
45+
46+
```C#
47+
[System.Diagnostics.CodeAnalysis.SuppressMessage("IDisposableAnalyzers.Correctness",
48+
"IDISP024:Don't call GC.SuppressFinalize(this) when the type is sealed and has no finalizer.",
49+
Justification = "Reason...")]
50+
```
51+
<!-- end generated config severity -->

0 commit comments

Comments
 (0)