Skip to content

Commit d3f68e0

Browse files
committed
IDISP025 Class with no virtual dispose method should be sealed.
Fix #180
1 parent 74a5eae commit d3f68e0

20 files changed

Lines changed: 254 additions & 24 deletions

File tree

IDisposableAnalyzers.Test/IDISP024DoNotCallSuppressFinalizeIfSealedAndNoFinalizerTests/CodeFix.cs renamed to IDisposableAnalyzers.Test/IDISP024DoNotCallSuppressFinalizeTests/CodeFix.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace IDisposableAnalyzers.Test.IDISP024DoNotCallSuppressFinalizeIfSealedAndNoFinalizerTests
1+
namespace IDisposableAnalyzers.Test.IDISP024DoNotCallSuppressFinalizeTests
22
{
33
using Gu.Roslyn.Asserts;
44
using Microsoft.CodeAnalysis.CodeFixes;

IDisposableAnalyzers.Test/IDISP024DoNotCallSuppressFinalizeIfSealedAndNoFinalizerTests/Valid.cs renamed to IDisposableAnalyzers.Test/IDISP024DoNotCallSuppressFinalizeTests/Valid.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace IDisposableAnalyzers.Test.IDISP024DoNotCallSuppressFinalizeIfSealedAndNoFinalizerTests
1+
namespace IDisposableAnalyzers.Test.IDISP024DoNotCallSuppressFinalizeTests
22
{
33
using Gu.Roslyn.Asserts;
44
using Microsoft.CodeAnalysis.Diagnostics;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
namespace IDisposableAnalyzers.Test.IDISP025SealDisposableTests
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 ClassDeclarationAnalyzer();
11+
private static readonly CodeFixProvider Fix = new SealFix();
12+
13+
[Test]
14+
public static void Simple()
15+
{
16+
var before = @"
17+
namespace N
18+
{
19+
using System;
20+
21+
public class ↓C : IDisposable
22+
{
23+
public void Dispose()
24+
{
25+
}
26+
}
27+
}";
28+
29+
var after = @"
30+
namespace N
31+
{
32+
using System;
33+
34+
public sealed class C : IDisposable
35+
{
36+
public void Dispose()
37+
{
38+
}
39+
}
40+
}";
41+
RoslynAssert.CodeFix(Analyzer, Fix, before, after);
42+
}
43+
}
44+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
namespace IDisposableAnalyzers.Test.IDISP025SealDisposableTests
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 ClassDeclarationAnalyzer();
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 VrirtualSimple()
31+
{
32+
var code = @"
33+
namespace N
34+
{
35+
using System;
36+
37+
public class C : IDisposable
38+
{
39+
public virtual void Dispose()
40+
{
41+
}
42+
}
43+
}";
44+
RoslynAssert.Valid(Analyzer, code);
45+
}
46+
}
47+
}

IDisposableAnalyzers.Test/Recursion.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ namespace N
3131
{
3232
using System;
3333
34-
public class C : IDisposable
34+
public sealed class C : IDisposable
3535
{
3636
private IDisposable disposable;
3737
@@ -77,7 +77,7 @@ namespace N
7777
{
7878
using System;
7979
80-
public class C : IDisposable
80+
public sealed class C : IDisposable
8181
{
8282
private IDisposable disposable;
8383

IDisposableAnalyzers.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{BAD2A4EE-6
3838
documentation\IDISP022.md = documentation\IDISP022.md
3939
documentation\IDISP023.md = documentation\IDISP023.md
4040
documentation\IDISP024.md = documentation\IDISP024.md
41+
documentation\IDISP025.md = documentation\IDISP025.md
4142
README.md = README.md
4243
RELEASE_NOTES.md = RELEASE_NOTES.md
4344
documentation\SyntaxTreeCacheAnalyzer.md = documentation\SyntaxTreeCacheAnalyzer.md
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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 ClassDeclarationAnalyzer : DiagnosticAnalyzer
12+
{
13+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(
14+
Descriptors.IDISP025SealDisposable);
15+
16+
public override void Initialize(AnalysisContext context)
17+
{
18+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
19+
context.EnableConcurrentExecution();
20+
context.RegisterSyntaxNodeAction(c => Handle(c), SyntaxKind.ClassDeclaration);
21+
}
22+
23+
private static void Handle(SyntaxNodeAnalysisContext context)
24+
{
25+
if (context.Node is ClassDeclarationSyntax { } classDeclaration &&
26+
context.ContainingSymbol is INamedTypeSymbol { IsSealed: false } type &&
27+
type.IsAssignableTo(KnownSymbol.IDisposable, context.SemanticModel.Compilation) &&
28+
DisposeMethod.TryFindIDisposableDispose(type, context.Compilation, Search.TopLevel, out var disposeMethod) &&
29+
disposeMethod is { IsVirtual: false, IsOverride: false } &&
30+
!DisposeMethod.TryFindVirtualDispose(type, context.Compilation, Search.TopLevel, out _))
31+
{
32+
context.ReportDiagnostic(
33+
Diagnostic.Create(
34+
Descriptors.IDISP025SealDisposable,
35+
classDeclaration.Identifier.GetLocation(),
36+
additionalLocations: new[] { disposeMethod.Locations[0] }));
37+
}
38+
}
39+
}
40+
}

IDisposableAnalyzers/Analyzers/SuppressFinalizeAnalyzer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
internal class SuppressFinalizeAnalyzer : DiagnosticAnalyzer
1212
{
1313
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(
14-
Descriptors.IDISP024DoNotCallSuppressFinalizeIfSealedAndNoFinalizer);
14+
Descriptors.IDISP024DoNotCallSuppressFinalize);
1515

1616
public override void Initialize(AnalysisContext context)
1717
{
@@ -30,7 +30,7 @@ private static void Handle(SyntaxNodeAnalysisContext context)
3030
{
3131
context.ReportDiagnostic(
3232
Diagnostic.Create(
33-
Descriptors.IDISP024DoNotCallSuppressFinalizeIfSealedAndNoFinalizer,
33+
Descriptors.IDISP024DoNotCallSuppressFinalize,
3434
invocation.GetLocation()));
3535
}
3636
}

IDisposableAnalyzers/CodeFixes/RemoveCallFix.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
internal class RemoveCallFix : DocumentEditorCodeFixProvider
1414
{
1515
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(
16-
Descriptors.IDISP024DoNotCallSuppressFinalizeIfSealedAndNoFinalizer.Id);
16+
Descriptors.IDISP024DoNotCallSuppressFinalize.Id);
1717

1818
protected override DocumentEditorFixAllProvider? FixAllProvider() => null;
1919

@@ -29,9 +29,8 @@ protected override async Task RegisterCodeFixesAsync(DocumentEditorCodeFixContex
2929
context.RegisterCodeFix(
3030
"Remove",
3131
(e, _) => e.RemoveNode(statement),
32-
equivalenceKey: nameof(SuppressFinalizeFix),
32+
equivalenceKey: nameof(RemoveCallFix),
3333
diagnostic);
34-
break;
3534
}
3635
}
3736
}
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 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(SealFix))]
12+
[Shared]
13+
internal class SealFix : DocumentEditorCodeFixProvider
14+
{
15+
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(
16+
Descriptors.IDISP025SealDisposable.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 ClassDeclarationSyntax? classDeclaration))
28+
{
29+
context.RegisterCodeFix(
30+
"Seal",
31+
(e, _) => e.Seal(classDeclaration),
32+
equivalenceKey: nameof(SealFix),
33+
diagnostic);
34+
}
35+
}
36+
}
37+
}
38+
}

0 commit comments

Comments
 (0)