From be60a3142662ad2190442811668026ecbadd1f72 Mon Sep 17 00:00:00 2001
From: Zsolt Kolbay
<121798625+zsolt-kolbay-sonarsource@users.noreply.github.com>
Date: Tue, 21 Feb 2023 17:21:40 +0100
Subject: [PATCH] New Rule S2094: Classes should not be empty (#6754)
---
...97-5AFD-4813-AEDC-AF33FACEADF0}-S2094.json | 17 ++
.../its/expected/Net5/Net5--net5.0-S2094.json | 95 +++++++++
.../its/expected/Net7/Net7--net7.0-S2094.json | 17 ++
.../akka.net/Akka--netstandard2.0-S2094.json | 199 ++++++++++++++++++
.../Akka.Benchmarks--netcoreapp3.1-S2094.json | 30 +++
.../Akka.Cluster--netstandard2.0-S2094.json | 30 +++
...Akka.DI.TestKit--netstandard2.0-S2094.json | 56 +++++
...kka.MultiNodeTestRunner--net471-S2094.json | 30 +++
...kka.MultiNodeTestRunner--net5.0-S2094.json | 30 +++
...tiNodeTestRunner--netcoreapp3.1-S2094.json | 30 +++
...stRunner.Shared--netstandard2.0-S2094.json | 121 +++++++++++
.../Akka.Remote--netstandard2.0-S2094.json | 147 +++++++++++++
...ests.Performance--netcoreapp3.1-S2094.json | 17 ++
.../Akka.Streams--netstandard2.0-S2094.json | 56 +++++
.../RemotePingPong--net471-S2094.json | 17 ++
.../RemotePingPong--net5.0-S2094.json | 17 ++
.../RemotePingPong--netcoreapp3.1-S2094.json | 17 ++
...hoService.Server--netcoreapp3.1-S2094.json | 17 ++
analyzers/rspec/cs/S2094_c#.html | 27 +++
analyzers/rspec/cs/S2094_c#.json | 17 ++
analyzers/rspec/cs/Sonar_way_profile.json | 1 +
analyzers/rspec/vbnet/S2094_vb.net.html | 27 +++
analyzers/rspec/vbnet/S2094_vb.net.json | 17 ++
analyzers/rspec/vbnet/Sonar_way_profile.json | 1 +
.../Facade/CSharpSyntaxKindFacade.cs | 2 +-
.../Rules/ClassShouldNotBeEmpty.cs | 40 ++++
.../Facade/ISyntaxKindFacade.cs | 2 +-
.../SonarAnalyzer.Common/Helpers/KnownType.cs | 1 +
.../Rules/ClassNotInstantiatableBase.cs | 6 +-
.../Rules/ClassShouldNotBeEmptyBase.cs | 58 +++++
.../Rules/ExceptionsShouldBePublicBase.cs | 2 +-
.../Facade/VisualBasicSyntaxKindFacade.cs | 2 +-
.../Rules/ClassShouldNotBeEmpty.cs | 35 +++
.../AspNetCoreMetadataReference.cs | 1 +
.../PackagingTests/RuleTypeMappingCS.cs | 2 +-
.../PackagingTests/RuleTypeMappingVB.cs | 2 +-
.../Rules/ClassShouldNotBeEmptyTest.cs | 80 +++++++
.../ClassShouldNotBeEmpty.CSharp10.cs | 13 ++
.../ClassShouldNotBeEmpty.CSharp9.cs | 44 ++++
.../ClassShouldNotBeEmpty.Inheritance.cs | 23 ++
.../ClassShouldNotBeEmpty.Inheritance.vb | 39 ++++
.../TestCases/ClassShouldNotBeEmpty.cs | 94 +++++++++
.../TestCases/ClassShouldNotBeEmpty.vb | 121 +++++++++++
its/projects/NoSonarTest/Class1.cs | 3 +-
its/projects/VbNoSonarTest/Class1.vb | 5 +
.../WebConfig.CSharp/Dummy.cs | 3 +-
.../WebConfig.VB/WebConfig.VB/Dummy.vb | 5 +
.../it/csharp/IncrementalAnalysisTest.java | 49 +++--
.../csharp/ProjectLevelDuplicationTest.java | 6 +-
49 files changed, 1639 insertions(+), 32 deletions(-)
create mode 100644 analyzers/its/expected/Ember-MM/Ember.Plugins-{9496C697-5AFD-4813-AEDC-AF33FACEADF0}-S2094.json
create mode 100644 analyzers/its/expected/Net5/Net5--net5.0-S2094.json
create mode 100644 analyzers/its/expected/Net7/Net7--net7.0-S2094.json
create mode 100644 analyzers/its/expected/akka.net/Akka--netstandard2.0-S2094.json
create mode 100644 analyzers/its/expected/akka.net/Akka.Benchmarks--netcoreapp3.1-S2094.json
create mode 100644 analyzers/its/expected/akka.net/Akka.Cluster--netstandard2.0-S2094.json
create mode 100644 analyzers/its/expected/akka.net/Akka.DI.TestKit--netstandard2.0-S2094.json
create mode 100644 analyzers/its/expected/akka.net/Akka.MultiNodeTestRunner--net471-S2094.json
create mode 100644 analyzers/its/expected/akka.net/Akka.MultiNodeTestRunner--net5.0-S2094.json
create mode 100644 analyzers/its/expected/akka.net/Akka.MultiNodeTestRunner--netcoreapp3.1-S2094.json
create mode 100644 analyzers/its/expected/akka.net/Akka.MultiNodeTestRunner.Shared--netstandard2.0-S2094.json
create mode 100644 analyzers/its/expected/akka.net/Akka.Remote--netstandard2.0-S2094.json
create mode 100644 analyzers/its/expected/akka.net/Akka.Remote.Tests.Performance--netcoreapp3.1-S2094.json
create mode 100644 analyzers/its/expected/akka.net/Akka.Streams--netstandard2.0-S2094.json
create mode 100644 analyzers/its/expected/akka.net/RemotePingPong--net471-S2094.json
create mode 100644 analyzers/its/expected/akka.net/RemotePingPong--net5.0-S2094.json
create mode 100644 analyzers/its/expected/akka.net/RemotePingPong--netcoreapp3.1-S2094.json
create mode 100644 analyzers/its/expected/akka.net/TcpEchoService.Server--netcoreapp3.1-S2094.json
create mode 100644 analyzers/rspec/cs/S2094_c#.html
create mode 100644 analyzers/rspec/cs/S2094_c#.json
create mode 100644 analyzers/rspec/vbnet/S2094_vb.net.html
create mode 100644 analyzers/rspec/vbnet/S2094_vb.net.json
create mode 100644 analyzers/src/SonarAnalyzer.CSharp/Rules/ClassShouldNotBeEmpty.cs
create mode 100644 analyzers/src/SonarAnalyzer.Common/Rules/ClassShouldNotBeEmptyBase.cs
create mode 100644 analyzers/src/SonarAnalyzer.VisualBasic/Rules/ClassShouldNotBeEmpty.cs
create mode 100644 analyzers/tests/SonarAnalyzer.UnitTest/Rules/ClassShouldNotBeEmptyTest.cs
create mode 100644 analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ClassShouldNotBeEmpty.CSharp10.cs
create mode 100644 analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ClassShouldNotBeEmpty.CSharp9.cs
create mode 100644 analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ClassShouldNotBeEmpty.Inheritance.cs
create mode 100644 analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ClassShouldNotBeEmpty.Inheritance.vb
create mode 100644 analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ClassShouldNotBeEmpty.cs
create mode 100644 analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ClassShouldNotBeEmpty.vb
diff --git a/analyzers/its/expected/Ember-MM/Ember.Plugins-{9496C697-5AFD-4813-AEDC-AF33FACEADF0}-S2094.json b/analyzers/its/expected/Ember-MM/Ember.Plugins-{9496C697-5AFD-4813-AEDC-AF33FACEADF0}-S2094.json
new file mode 100644
index 00000000000..0ac2683eee3
--- /dev/null
+++ b/analyzers/its/expected/Ember-MM/Ember.Plugins-{9496C697-5AFD-4813-AEDC-AF33FACEADF0}-S2094.json
@@ -0,0 +1,17 @@
+{
+"issues": [
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\Ember-MM\Ember.Plugins\PluginActionContext.cs",
+"region": {
+"startLine": 6,
+"startColumn": 18,
+"endLine": 6,
+"endColumn": 37
+}
+}
+}
+]
+}
diff --git a/analyzers/its/expected/Net5/Net5--net5.0-S2094.json b/analyzers/its/expected/Net5/Net5--net5.0-S2094.json
new file mode 100644
index 00000000000..808fb732c6d
--- /dev/null
+++ b/analyzers/its/expected/Net5/Net5--net5.0-S2094.json
@@ -0,0 +1,95 @@
+{
+"issues": [
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\Net5\Net5\ModuleInitializers.cs",
+"region": {
+"startLine": 3,
+"startColumn": 18,
+"endLine": 3,
+"endColumn": 36
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\Net5\Net5\ParameterValidation.cs",
+"region": {
+"startLine": 3,
+"startColumn": 18,
+"endLine": 3,
+"endColumn": 37
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\Net5\Net5\S2330.cs",
+"region": {
+"startLine": 5,
+"startColumn": 32,
+"endLine": 5,
+"endColumn": 37
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\Net5\Net5\S3240.cs",
+"region": {
+"startLine": 5,
+"startColumn": 24,
+"endLine": 5,
+"endColumn": 29
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\Net5\Net5\S3247.cs",
+"region": {
+"startLine": 8,
+"startColumn": 15,
+"endLine": 8,
+"endColumn": 24
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\Net5\Net5\S3453.cs",
+"region": {
+"startLine": 22,
+"startColumn": 26,
+"endLine": 22,
+"endColumn": 34
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\Net5\Net5\StaticLambdas.cs",
+"region": {
+"startLine": 3,
+"startColumn": 18,
+"endLine": 3,
+"endColumn": 31
+}
+}
+}
+]
+}
diff --git a/analyzers/its/expected/Net7/Net7--net7.0-S2094.json b/analyzers/its/expected/Net7/Net7--net7.0-S2094.json
new file mode 100644
index 00000000000..289b4af880f
--- /dev/null
+++ b/analyzers/its/expected/Net7/Net7--net7.0-S2094.json
@@ -0,0 +1,17 @@
+{
+"issues": [
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\Net7\Net7\features\WarningWave7.cs",
+"region": {
+"startLine": 5,
+"startColumn": 22,
+"endLine": 5,
+"endColumn": 35
+}
+}
+}
+]
+}
diff --git a/analyzers/its/expected/akka.net/Akka--netstandard2.0-S2094.json b/analyzers/its/expected/akka.net/Akka--netstandard2.0-S2094.json
new file mode 100644
index 00000000000..6370b12ad44
--- /dev/null
+++ b/analyzers/its/expected/akka.net/Akka--netstandard2.0-S2094.json
@@ -0,0 +1,199 @@
+{
+"issues": [
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka\Actor\ActorSelection.cs",
+"region": {
+"startLine": 388,
+"startColumn": 27,
+"endLine": 388,
+"endColumn": 47
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka\Actor\FSM.cs",
+"region": {
+"startLine": 202,
+"startColumn": 31,
+"endLine": 202,
+"endColumn": 37
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka\Event\LoggerInitialized.cs",
+"region": {
+"startLine": 15,
+"startColumn": 18,
+"endLine": 15,
+"endColumn": 35
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka\Event\Logging.cs",
+"region": {
+"startLine": 18,
+"startColumn": 18,
+"endLine": 18,
+"endColumn": 44
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka\IO\Dns.cs",
+"region": {
+"startLine": 62,
+"startColumn": 31,
+"endLine": 62,
+"endColumn": 38
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka\IO\Inet.cs",
+"region": {
+"startLine": 229,
+"startColumn": 31,
+"endLine": 229,
+"endColumn": 43
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka\IO\Tcp.cs",
+"region": {
+"startLine": 56,
+"startColumn": 33,
+"endLine": 56,
+"endColumn": 48
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka\IO\Tcp.cs",
+"region": {
+"startLine": 87,
+"startColumn": 22,
+"endLine": 87,
+"endColumn": 29
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka\IO\Tcp.cs",
+"region": {
+"startLine": 738,
+"startColumn": 22,
+"endLine": 738,
+"endColumn": 27
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka\IO\Udp.cs",
+"region": {
+"startLine": 37,
+"startColumn": 33,
+"endLine": 37,
+"endColumn": 48
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka\IO\Udp.cs",
+"region": {
+"startLine": 107,
+"startColumn": 31,
+"endLine": 107,
+"endColumn": 38
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka\IO\UdpConnected.cs",
+"region": {
+"startLine": 95,
+"startColumn": 31,
+"endLine": 95,
+"endColumn": 38
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka\Pattern\BackoffOptions.cs",
+"region": {
+"startLine": 243,
+"startColumn": 27,
+"endLine": 243,
+"endColumn": 38
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka\Routing\Listeners.cs",
+"region": {
+"startLine": 37,
+"startColumn": 27,
+"endLine": 37,
+"endColumn": 42
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka\Routing\RouterMsg.cs",
+"region": {
+"startLine": 31,
+"startColumn": 27,
+"endLine": 31,
+"endColumn": 50
+}
+}
+}
+]
+}
diff --git a/analyzers/its/expected/akka.net/Akka.Benchmarks--netcoreapp3.1-S2094.json b/analyzers/its/expected/akka.net/Akka.Benchmarks--netcoreapp3.1-S2094.json
new file mode 100644
index 00000000000..025ab6cbded
--- /dev/null
+++ b/analyzers/its/expected/akka.net/Akka.Benchmarks--netcoreapp3.1-S2094.json
@@ -0,0 +1,30 @@
+{
+"issues": [
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\benchmark\Akka.Benchmarks\IO\TcpOperationsBenchmarks.cs",
+"region": {
+"startLine": 76,
+"startColumn": 22,
+"endLine": 76,
+"endColumn": 43
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\benchmark\Akka.Benchmarks\IO\TcpOperationsBenchmarks.cs",
+"region": {
+"startLine": 77,
+"startColumn": 22,
+"endLine": 77,
+"endColumn": 48
+}
+}
+}
+]
+}
diff --git a/analyzers/its/expected/akka.net/Akka.Cluster--netstandard2.0-S2094.json b/analyzers/its/expected/akka.net/Akka.Cluster--netstandard2.0-S2094.json
new file mode 100644
index 00000000000..7e3af656072
--- /dev/null
+++ b/analyzers/its/expected/akka.net/Akka.Cluster--netstandard2.0-S2094.json
@@ -0,0 +1,30 @@
+{
+"issues": [
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.Cluster\ClusterDaemon.cs",
+"region": {
+"startLine": 281,
+"startColumn": 24,
+"endLine": 281,
+"endColumn": 36
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.Cluster\ClusterHeartbeat.cs",
+"region": {
+"startLine": 368,
+"startColumn": 23,
+"endLine": 368,
+"endColumn": 36
+}
+}
+}
+]
+}
diff --git a/analyzers/its/expected/akka.net/Akka.DI.TestKit--netstandard2.0-S2094.json b/analyzers/its/expected/akka.net/Akka.DI.TestKit--netstandard2.0-S2094.json
new file mode 100644
index 00000000000..8bbc0bb2e33
--- /dev/null
+++ b/analyzers/its/expected/akka.net/Akka.DI.TestKit--netstandard2.0-S2094.json
@@ -0,0 +1,56 @@
+{
+"issues": [
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\contrib\dependencyinjection\Akka.DI.TestKit\DiResolverSpec.cs",
+"region": {
+"startLine": 28,
+"startColumn": 15,
+"endLine": 28,
+"endColumn": 27
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\contrib\dependencyinjection\Akka.DI.TestKit\DiResolverSpec.cs",
+"region": {
+"startLine": 60,
+"startColumn": 26,
+"endLine": 60,
+"endColumn": 34
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\contrib\dependencyinjection\Akka.DI.TestKit\DiResolverSpec.cs",
+"region": {
+"startLine": 76,
+"startColumn": 26,
+"endLine": 76,
+"endColumn": 33
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\contrib\dependencyinjection\Akka.DI.TestKit\DiResolverSpec.cs",
+"region": {
+"startLine": 78,
+"startColumn": 26,
+"endLine": 78,
+"endColumn": 37
+}
+}
+}
+]
+}
diff --git a/analyzers/its/expected/akka.net/Akka.MultiNodeTestRunner--net471-S2094.json b/analyzers/its/expected/akka.net/Akka.MultiNodeTestRunner--net471-S2094.json
new file mode 100644
index 00000000000..3c7716e2695
--- /dev/null
+++ b/analyzers/its/expected/akka.net/Akka.MultiNodeTestRunner--net471-S2094.json
@@ -0,0 +1,30 @@
+{
+"issues": [
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.MultiNodeTestRunner\Program.cs",
+"region": {
+"startLine": 617,
+"startColumn": 22,
+"endLine": 617,
+"endColumn": 34
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.MultiNodeTestRunner\Program.cs",
+"region": {
+"startLine": 618,
+"startColumn": 22,
+"endLine": 618,
+"endColumn": 37
+}
+}
+}
+]
+}
diff --git a/analyzers/its/expected/akka.net/Akka.MultiNodeTestRunner--net5.0-S2094.json b/analyzers/its/expected/akka.net/Akka.MultiNodeTestRunner--net5.0-S2094.json
new file mode 100644
index 00000000000..3c7716e2695
--- /dev/null
+++ b/analyzers/its/expected/akka.net/Akka.MultiNodeTestRunner--net5.0-S2094.json
@@ -0,0 +1,30 @@
+{
+"issues": [
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.MultiNodeTestRunner\Program.cs",
+"region": {
+"startLine": 617,
+"startColumn": 22,
+"endLine": 617,
+"endColumn": 34
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.MultiNodeTestRunner\Program.cs",
+"region": {
+"startLine": 618,
+"startColumn": 22,
+"endLine": 618,
+"endColumn": 37
+}
+}
+}
+]
+}
diff --git a/analyzers/its/expected/akka.net/Akka.MultiNodeTestRunner--netcoreapp3.1-S2094.json b/analyzers/its/expected/akka.net/Akka.MultiNodeTestRunner--netcoreapp3.1-S2094.json
new file mode 100644
index 00000000000..3c7716e2695
--- /dev/null
+++ b/analyzers/its/expected/akka.net/Akka.MultiNodeTestRunner--netcoreapp3.1-S2094.json
@@ -0,0 +1,30 @@
+{
+"issues": [
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.MultiNodeTestRunner\Program.cs",
+"region": {
+"startLine": 617,
+"startColumn": 22,
+"endLine": 617,
+"endColumn": 34
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.MultiNodeTestRunner\Program.cs",
+"region": {
+"startLine": 618,
+"startColumn": 22,
+"endLine": 618,
+"endColumn": 37
+}
+}
+}
+]
+}
diff --git a/analyzers/its/expected/akka.net/Akka.MultiNodeTestRunner.Shared--netstandard2.0-S2094.json b/analyzers/its/expected/akka.net/Akka.MultiNodeTestRunner.Shared--netstandard2.0-S2094.json
new file mode 100644
index 00000000000..42a6a955478
--- /dev/null
+++ b/analyzers/its/expected/akka.net/Akka.MultiNodeTestRunner.Shared--netstandard2.0-S2094.json
@@ -0,0 +1,121 @@
+{
+"issues": [
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.MultiNodeTestRunner.Shared\CompilerErrorCollection.cs",
+"region": {
+"startLine": 14,
+"startColumn": 18,
+"endLine": 14,
+"endColumn": 41
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.MultiNodeTestRunner.Shared\Reporting\TestRunCoordinator.cs",
+"region": {
+"startLine": 27,
+"startColumn": 22,
+"endLine": 27,
+"endColumn": 41
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.MultiNodeTestRunner.Shared\Sinks\Messages.cs",
+"region": {
+"startLine": 156,
+"startColumn": 18,
+"endLine": 156,
+"endColumn": 28
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.MultiNodeTestRunner.Shared\Sinks\MessageSinkActor.cs",
+"region": {
+"startLine": 42,
+"startColumn": 22,
+"endLine": 42,
+"endColumn": 41
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.MultiNodeTestRunner.Shared\Sinks\SinkCoordinator.cs",
+"region": {
+"startLine": 43,
+"startColumn": 22,
+"endLine": 43,
+"endColumn": 35
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.MultiNodeTestRunner.Shared\Sinks\SinkCoordinator.cs",
+"region": {
+"startLine": 48,
+"startColumn": 22,
+"endLine": 48,
+"endColumn": 32
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.MultiNodeTestRunner.Shared\Sinks\SinkCoordinator.cs",
+"region": {
+"startLine": 66,
+"startColumn": 22,
+"endLine": 66,
+"endColumn": 37
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.MultiNodeTestRunner.Shared\Sinks\TimelineLogCollectorActor.cs",
+"region": {
+"startLine": 190,
+"startColumn": 22,
+"endLine": 190,
+"endColumn": 31
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.MultiNodeTestRunner.Shared\Sinks\TimelineLogCollectorActor.cs",
+"region": {
+"startLine": 202,
+"startColumn": 22,
+"endLine": 202,
+"endColumn": 32
+}
+}
+}
+]
+}
diff --git a/analyzers/its/expected/akka.net/Akka.Remote--netstandard2.0-S2094.json b/analyzers/its/expected/akka.net/Akka.Remote--netstandard2.0-S2094.json
new file mode 100644
index 00000000000..5ecaa3af7c5
--- /dev/null
+++ b/analyzers/its/expected/akka.net/Akka.Remote--netstandard2.0-S2094.json
@@ -0,0 +1,147 @@
+{
+"issues": [
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.Remote\Endpoint.cs",
+"region": {
+"startLine": 794,
+"startColumn": 22,
+"endLine": 794,
+"endColumn": 45
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.Remote\Endpoint.cs",
+"region": {
+"startLine": 799,
+"startColumn": 22,
+"endLine": 799,
+"endColumn": 28
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.Remote\EndpointManager.cs",
+"region": {
+"startLine": 115,
+"startColumn": 31,
+"endLine": 115,
+"endColumn": 46
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.Remote\EndpointManager.cs",
+"region": {
+"startLine": 289,
+"startColumn": 29,
+"endLine": 289,
+"endColumn": 34
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.Remote\Transport\AkkaPduCodec.cs",
+"region": {
+"startLine": 95,
+"startColumn": 27,
+"endLine": 95,
+"endColumn": 36
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.Remote\Transport\AkkaProtocolTransport.cs",
+"region": {
+"startLine": 491,
+"startColumn": 20,
+"endLine": 491,
+"endColumn": 34
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.Remote\Transport\AkkaProtocolTransport.cs",
+"region": {
+"startLine": 493,
+"startColumn": 20,
+"endLine": 493,
+"endColumn": 34
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.Remote\Transport\AkkaProtocolTransport.cs",
+"region": {
+"startLine": 538,
+"startColumn": 29,
+"endLine": 538,
+"endColumn": 46
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.Remote\Transport\AkkaProtocolTransport.cs",
+"region": {
+"startLine": 722,
+"startColumn": 20,
+"endLine": 722,
+"endColumn": 38
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.Remote\Transport\TestTransport.cs",
+"region": {
+"startLine": 297,
+"startColumn": 27,
+"endLine": 297,
+"endColumn": 35
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.Remote\Transport\ThrottleTransportAdapter.cs",
+"region": {
+"startLine": 947,
+"startColumn": 22,
+"endLine": 947,
+"endColumn": 29
+}
+}
+}
+]
+}
diff --git a/analyzers/its/expected/akka.net/Akka.Remote.Tests.Performance--netcoreapp3.1-S2094.json b/analyzers/its/expected/akka.net/Akka.Remote.Tests.Performance--netcoreapp3.1-S2094.json
new file mode 100644
index 00000000000..76790ed6bfc
--- /dev/null
+++ b/analyzers/its/expected/akka.net/Akka.Remote.Tests.Performance--netcoreapp3.1-S2094.json
@@ -0,0 +1,17 @@
+{
+"issues": [
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.Remote.Tests.Performance\Transports\RemoteMessagingThroughputSpecBase.cs",
+"region": {
+"startLine": 43,
+"startColumn": 26,
+"endLine": 43,
+"endColumn": 36
+}
+}
+}
+]
+}
diff --git a/analyzers/its/expected/akka.net/Akka.Streams--netstandard2.0-S2094.json b/analyzers/its/expected/akka.net/Akka.Streams--netstandard2.0-S2094.json
new file mode 100644
index 00000000000..cea40b0ba52
--- /dev/null
+++ b/analyzers/its/expected/akka.net/Akka.Streams--netstandard2.0-S2094.json
@@ -0,0 +1,56 @@
+{
+"issues": [
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.Streams\Attributes.cs",
+"region": {
+"startLine": 257,
+"startColumn": 26,
+"endLine": 257,
+"endColumn": 39
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.Streams\Attributes.cs",
+"region": {
+"startLine": 263,
+"startColumn": 26,
+"endLine": 263,
+"endColumn": 35
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.Streams\Attributes.cs",
+"region": {
+"startLine": 275,
+"startColumn": 26,
+"endLine": 275,
+"endColumn": 42
+}
+}
+},
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\core\Akka.Streams\Stage\Context.cs",
+"region": {
+"startLine": 53,
+"startColumn": 25,
+"endLine": 53,
+"endColumn": 38
+}
+}
+}
+]
+}
diff --git a/analyzers/its/expected/akka.net/RemotePingPong--net471-S2094.json b/analyzers/its/expected/akka.net/RemotePingPong--net471-S2094.json
new file mode 100644
index 00000000000..19fa046d379
--- /dev/null
+++ b/analyzers/its/expected/akka.net/RemotePingPong--net471-S2094.json
@@ -0,0 +1,17 @@
+{
+"issues": [
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\benchmark\RemotePingPong\Program.cs",
+"region": {
+"startLine": 226,
+"startColumn": 26,
+"endLine": 226,
+"endColumn": 36
+}
+}
+}
+]
+}
diff --git a/analyzers/its/expected/akka.net/RemotePingPong--net5.0-S2094.json b/analyzers/its/expected/akka.net/RemotePingPong--net5.0-S2094.json
new file mode 100644
index 00000000000..19fa046d379
--- /dev/null
+++ b/analyzers/its/expected/akka.net/RemotePingPong--net5.0-S2094.json
@@ -0,0 +1,17 @@
+{
+"issues": [
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\benchmark\RemotePingPong\Program.cs",
+"region": {
+"startLine": 226,
+"startColumn": 26,
+"endLine": 226,
+"endColumn": 36
+}
+}
+}
+]
+}
diff --git a/analyzers/its/expected/akka.net/RemotePingPong--netcoreapp3.1-S2094.json b/analyzers/its/expected/akka.net/RemotePingPong--netcoreapp3.1-S2094.json
new file mode 100644
index 00000000000..19fa046d379
--- /dev/null
+++ b/analyzers/its/expected/akka.net/RemotePingPong--netcoreapp3.1-S2094.json
@@ -0,0 +1,17 @@
+{
+"issues": [
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\benchmark\RemotePingPong\Program.cs",
+"region": {
+"startLine": 226,
+"startColumn": 26,
+"endLine": 226,
+"endColumn": 36
+}
+}
+}
+]
+}
diff --git a/analyzers/its/expected/akka.net/TcpEchoService.Server--netcoreapp3.1-S2094.json b/analyzers/its/expected/akka.net/TcpEchoService.Server--netcoreapp3.1-S2094.json
new file mode 100644
index 00000000000..0c7c2328513
--- /dev/null
+++ b/analyzers/its/expected/akka.net/TcpEchoService.Server--netcoreapp3.1-S2094.json
@@ -0,0 +1,17 @@
+{
+"issues": [
+{
+"id": "S2094",
+"message": "Remove this empty class, or add members to it.",
+"location": {
+"uri": "sources\akka.net\src\examples\TcpEchoService.Server\Actors.cs",
+"region": {
+"startLine": 47,
+"startColumn": 22,
+"endLine": 47,
+"endColumn": 32
+}
+}
+}
+]
+}
diff --git a/analyzers/rspec/cs/S2094_c#.html b/analyzers/rspec/cs/S2094_c#.html
new file mode 100644
index 00000000000..369f2453587
--- /dev/null
+++ b/analyzers/rspec/cs/S2094_c#.html
@@ -0,0 +1,27 @@
+
There is no good excuse for an empty class. If it’s being used simply as a common extension point, it should be replaced with an
+interface
. If it was stubbed in as a placeholder for future development it should be fleshed-out. In any other case, it should be
+eliminated.
+Noncompliant Code Example
+
+public class Empty // Noncompliant
+{
+}
+
+Compliant Solution
+
+public interface IEmpty
+{
+}
+
+Exceptions
+Partial classes are ignored entirely, as they are often used with Source Generators. Subclasses of System.Exception are be ignored, as even an
+empty Exception class can provide useful information by its type name alone. Subclasses of certain framework types - like the PageModel class used in
+ASP.NET Core Razor Pages - are also be ignored.
+
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+public class EmptyPageModel: PageModel // Compliant - an empty PageModel can be fully functional, the C# code can be in the cshtml file
+{
+}
+
+
diff --git a/analyzers/rspec/cs/S2094_c#.json b/analyzers/rspec/cs/S2094_c#.json
new file mode 100644
index 00000000000..78db5e8197d
--- /dev/null
+++ b/analyzers/rspec/cs/S2094_c#.json
@@ -0,0 +1,17 @@
+{
+ "title": "Classes should not be empty",
+ "type": "CODE_SMELL",
+ "status": "ready",
+ "remediation": {
+ "func": "Constant\/Issue",
+ "constantCost": "5min"
+ },
+ "tags": [
+ "clumsy"
+ ],
+ "defaultSeverity": "Minor",
+ "ruleSpecification": "RSPEC-2094",
+ "sqKey": "S2094",
+ "scope": "All",
+ "quickfix": "unknown"
+}
diff --git a/analyzers/rspec/cs/Sonar_way_profile.json b/analyzers/rspec/cs/Sonar_way_profile.json
index 2b7c15c5eb5..af947b58cfc 100644
--- a/analyzers/rspec/cs/Sonar_way_profile.json
+++ b/analyzers/rspec/cs/Sonar_way_profile.json
@@ -59,6 +59,7 @@
"S2068",
"S2077",
"S2092",
+ "S2094",
"S2114",
"S2115",
"S2123",
diff --git a/analyzers/rspec/vbnet/S2094_vb.net.html b/analyzers/rspec/vbnet/S2094_vb.net.html
new file mode 100644
index 00000000000..dd073431b49
--- /dev/null
+++ b/analyzers/rspec/vbnet/S2094_vb.net.html
@@ -0,0 +1,27 @@
+There is no good excuse for an empty class. If it’s being used simply as a common extension point, it should be replaced with an
+Interface
. If it was stubbed in as a placeholder for future development it should be fleshed-out. In any other case, it should be
+eliminated.
+Noncompliant Code Example
+
+Public Class Empty ' Noncompliant
+
+End Class
+
+Compliant Solution
+
+Public Interface IEmpty
+
+End Interface
+
+Exceptions
+Partial classes are ignored entirely, as they are often used with Source Generators. Subclasses of System.Exception will be ignored, as even an
+empty Exception class can provide useful information by its type name alone. Subclasses of certain framework types - like the PageModel class used in
+ASP.NET Core Razor Pages - will also be ignored.
+
+Imports Microsoft.AspNetCore.Mvc.RazorPages
+
+Public Class EmptyPageModel ' Compliant - an empty PageModel can be fully functional, the VB code can be in the vbhtml file
+ Inherits PageModel
+End Class
+
+
diff --git a/analyzers/rspec/vbnet/S2094_vb.net.json b/analyzers/rspec/vbnet/S2094_vb.net.json
new file mode 100644
index 00000000000..78db5e8197d
--- /dev/null
+++ b/analyzers/rspec/vbnet/S2094_vb.net.json
@@ -0,0 +1,17 @@
+{
+ "title": "Classes should not be empty",
+ "type": "CODE_SMELL",
+ "status": "ready",
+ "remediation": {
+ "func": "Constant\/Issue",
+ "constantCost": "5min"
+ },
+ "tags": [
+ "clumsy"
+ ],
+ "defaultSeverity": "Minor",
+ "ruleSpecification": "RSPEC-2094",
+ "sqKey": "S2094",
+ "scope": "All",
+ "quickfix": "unknown"
+}
diff --git a/analyzers/rspec/vbnet/Sonar_way_profile.json b/analyzers/rspec/vbnet/Sonar_way_profile.json
index d6da56534fc..a54c097cfa1 100644
--- a/analyzers/rspec/vbnet/Sonar_way_profile.json
+++ b/analyzers/rspec/vbnet/Sonar_way_profile.json
@@ -38,6 +38,7 @@
"S1940",
"S2068",
"S2077",
+ "S2094",
"S2166",
"S2178",
"S2222",
diff --git a/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxKindFacade.cs b/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxKindFacade.cs
index 3a60ed1408e..afcfab645ef 100644
--- a/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxKindFacade.cs
+++ b/analyzers/src/SonarAnalyzer.CSharp/Facade/CSharpSyntaxKindFacade.cs
@@ -24,7 +24,7 @@ internal sealed class CSharpSyntaxKindFacade : ISyntaxKindFacade
{
public SyntaxKind Attribute => SyntaxKind.Attribute;
public SyntaxKind ClassDeclaration => SyntaxKind.ClassDeclaration;
- public SyntaxKind[] ClassAndRecordDeclaration => new[]
+ public SyntaxKind[] ClassAndRecordClassDeclarations => new[]
{
SyntaxKind.ClassDeclaration,
SyntaxKindEx.RecordClassDeclaration,
diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/ClassShouldNotBeEmpty.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/ClassShouldNotBeEmpty.cs
new file mode 100644
index 00000000000..1dae0adc1d4
--- /dev/null
+++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/ClassShouldNotBeEmpty.cs
@@ -0,0 +1,40 @@
+/*
+ * SonarAnalyzer for .NET
+ * Copyright (C) 2015-2023 SonarSource SA
+ * mailto: contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+namespace SonarAnalyzer.Rules.CSharp;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class ClassShouldNotBeEmpty : ClassShouldNotBeEmptyBase
+{
+ protected override ILanguageFacade Language => CSharpFacade.Instance;
+
+ protected override bool IsEmptyAndNotPartial(SyntaxNode node) =>
+ node is TypeDeclarationSyntax { Members.Count: 0 } typeDeclaration
+ && !typeDeclaration.Modifiers.Any(x => x.IsKind(SyntaxKind.PartialKeyword))
+ && (node is ClassDeclarationSyntax || IsParameterlessRecord(node));
+
+ protected override bool IsClassWithDeclaredBaseClass(SyntaxNode node) => node is ClassDeclarationSyntax { BaseList: not null };
+
+ protected override string DeclarationTypeKeyword(SyntaxNode node) => ((TypeDeclarationSyntax)node).Keyword.ValueText;
+
+ private bool IsParameterlessRecord(SyntaxNode node) =>
+ RecordDeclarationSyntaxWrapper.IsInstance(node)
+ && (RecordDeclarationSyntaxWrapper)node is { ParameterList.Parameters.Count: 0 };
+}
diff --git a/analyzers/src/SonarAnalyzer.Common/Facade/ISyntaxKindFacade.cs b/analyzers/src/SonarAnalyzer.Common/Facade/ISyntaxKindFacade.cs
index d2828707e29..a9a2ddf5c12 100644
--- a/analyzers/src/SonarAnalyzer.Common/Facade/ISyntaxKindFacade.cs
+++ b/analyzers/src/SonarAnalyzer.Common/Facade/ISyntaxKindFacade.cs
@@ -25,7 +25,7 @@ public interface ISyntaxKindFacade
{
abstract TSyntaxKind Attribute { get; }
abstract TSyntaxKind ClassDeclaration { get; }
- abstract TSyntaxKind[] ClassAndRecordDeclaration { get; }
+ abstract TSyntaxKind[] ClassAndRecordClassDeclarations { get; }
abstract TSyntaxKind[] ClassAndModuleDeclarations { get; }
abstract TSyntaxKind[] CommentTrivia { get; }
abstract TSyntaxKind[] ComparisonKinds { get; }
diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs
index 7b0c7b39c21..b0b254a4c2a 100644
--- a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs
+++ b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs
@@ -63,6 +63,7 @@ public sealed partial class KnownType
public static readonly KnownType Microsoft_AspNetCore_Mvc_IgnoreAntiforgeryTokenAttribute = new("Microsoft.AspNetCore.Mvc.IgnoreAntiforgeryTokenAttribute");
public static readonly KnownType Microsoft_AspNetCore_Mvc_NonActionAttribute = new("Microsoft.AspNetCore.Mvc.NonActionAttribute");
public static readonly KnownType Microsoft_AspNetCore_Mvc_NonControllerAttribute = new("Microsoft.AspNetCore.Mvc.NonControllerAttribute");
+ public static readonly KnownType Microsoft_AspNetCore_Mvc_RazorPages_PageModel = new("Microsoft.AspNetCore.Mvc.RazorPages.PageModel");
public static readonly KnownType Microsoft_AspNetCore_Mvc_RequestFormLimitsAttribute = new("Microsoft.AspNetCore.Mvc.RequestFormLimitsAttribute");
public static readonly KnownType Microsoft_AspNetCore_Mvc_RequestSizeLimitAttribute = new("Microsoft.AspNetCore.Mvc.RequestSizeLimitAttribute");
public static readonly KnownType Microsoft_AspNetCore_Razor_Hosting_RazorCompiledItemAttribute = new("Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemAttribute");
diff --git a/analyzers/src/SonarAnalyzer.Common/Rules/ClassNotInstantiatableBase.cs b/analyzers/src/SonarAnalyzer.Common/Rules/ClassNotInstantiatableBase.cs
index 9ec20458fb6..fc6d0a8a846 100644
--- a/analyzers/src/SonarAnalyzer.Common/Rules/ClassNotInstantiatableBase.cs
+++ b/analyzers/src/SonarAnalyzer.Common/Rules/ClassNotInstantiatableBase.cs
@@ -35,8 +35,8 @@ public abstract class ClassNotInstantiatableBase :
protected override void Initialize(SonarAnalysisContext context) =>
context.RegisterSymbolAction(CheckClassWithOnlyUnusedPrivateConstructors, SymbolKind.NamedType);
- private bool IsTypeDeclaration(SyntaxNode node) =>
- Language.Syntax.IsAnyKind(node, Language.SyntaxKind.ClassAndRecordDeclaration);
+ private bool IsClassTypeDeclaration(SyntaxNode node) =>
+ Language.Syntax.IsAnyKind(node, Language.SyntaxKind.ClassAndRecordClassDeclarations);
private bool IsAnyConstructorCalled(INamedTypeSymbol namedType, IEnumerable typeDeclarations) =>
typeDeclarations
@@ -79,7 +79,7 @@ private void CheckClassWithOnlyUnusedPrivateConstructors(SonarSymbolReportingCon
private bool IsAnyNestedTypeExtendingCurrentType(IEnumerable descendantNodes, INamedTypeSymbol namedType, SemanticModel semanticModel) =>
descendantNodes
- .Where(IsTypeDeclaration)
+ .Where(IsClassTypeDeclaration)
.Select(x => (semanticModel.GetDeclaredSymbol(x) as ITypeSymbol)?.BaseType)
.WhereNotNull()
.Any(baseType => baseType.OriginalDefinition.DerivesFrom(namedType));
diff --git a/analyzers/src/SonarAnalyzer.Common/Rules/ClassShouldNotBeEmptyBase.cs b/analyzers/src/SonarAnalyzer.Common/Rules/ClassShouldNotBeEmptyBase.cs
new file mode 100644
index 00000000000..95de276a89d
--- /dev/null
+++ b/analyzers/src/SonarAnalyzer.Common/Rules/ClassShouldNotBeEmptyBase.cs
@@ -0,0 +1,58 @@
+/*
+ * SonarAnalyzer for .NET
+ * Copyright (C) 2015-2023 SonarSource SA
+ * mailto: contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+namespace SonarAnalyzer.Rules;
+
+public abstract class ClassShouldNotBeEmptyBase : SonarDiagnosticAnalyzer
+ where TSyntaxKind : struct
+{
+ private const string DiagnosticId = "S2094";
+
+ private static readonly ImmutableArray BaseClassesToIgnore = ImmutableArray.Create(
+ KnownType.Microsoft_AspNetCore_Mvc_RazorPages_PageModel,
+ KnownType.System_Exception);
+
+ protected abstract bool IsEmptyAndNotPartial(SyntaxNode node);
+ protected abstract bool IsClassWithDeclaredBaseClass(SyntaxNode node);
+ protected abstract string DeclarationTypeKeyword(SyntaxNode node);
+
+ protected override string MessageFormat => "Remove this empty {0}, or add members to it.";
+
+ protected ClassShouldNotBeEmptyBase() : base(DiagnosticId) { }
+
+ protected override void Initialize(SonarAnalysisContext context) =>
+ context.RegisterNodeAction(
+ Language.GeneratedCodeRecognizer,
+ c =>
+ {
+ if (Language.Syntax.NodeIdentifier(c.Node) is { IsMissing: false } identifier
+ && IsEmptyAndNotPartial(c.Node)
+ && !ShouldIgnoreBecauseOfBaseClass(c.Node, c.SemanticModel))
+ {
+ c.ReportIssue(Diagnostic.Create(Rule, identifier.GetLocation(), DeclarationTypeKeyword(c.Node)));
+ }
+ },
+ Language.SyntaxKind.ClassAndRecordClassDeclarations);
+
+ private bool ShouldIgnoreBecauseOfBaseClass(SyntaxNode node, SemanticModel model) =>
+ IsClassWithDeclaredBaseClass(node)
+ && model.GetDeclaredSymbol(node) is INamedTypeSymbol classSymbol
+ && (classSymbol.BaseType is { IsAbstract: true } || classSymbol.DerivesFromAny(BaseClassesToIgnore));
+}
diff --git a/analyzers/src/SonarAnalyzer.Common/Rules/ExceptionsShouldBePublicBase.cs b/analyzers/src/SonarAnalyzer.Common/Rules/ExceptionsShouldBePublicBase.cs
index 578533d7b3a..5abe025433b 100644
--- a/analyzers/src/SonarAnalyzer.Common/Rules/ExceptionsShouldBePublicBase.cs
+++ b/analyzers/src/SonarAnalyzer.Common/Rules/ExceptionsShouldBePublicBase.cs
@@ -46,5 +46,5 @@ public abstract class ExceptionsShouldBePublicBase : SonarDiagnosti
c.ReportIssue(Diagnostic.Create(Rule, Language.Syntax.NodeIdentifier(c.Node).Value.GetLocation()));
}
},
- Language.SyntaxKind.ClassAndRecordDeclaration);
+ Language.SyntaxKind.ClassAndRecordClassDeclarations);
}
diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicSyntaxKindFacade.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicSyntaxKindFacade.cs
index 42983cd7a8f..b6cca795fee 100644
--- a/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicSyntaxKindFacade.cs
+++ b/analyzers/src/SonarAnalyzer.VisualBasic/Facade/VisualBasicSyntaxKindFacade.cs
@@ -24,7 +24,7 @@ internal sealed class VisualBasicSyntaxKindFacade : ISyntaxKindFacade SyntaxKind.Attribute;
public SyntaxKind ClassDeclaration => SyntaxKind.ClassBlock;
- public SyntaxKind[] ClassAndRecordDeclaration => new[] { SyntaxKind.ClassBlock };
+ public SyntaxKind[] ClassAndRecordClassDeclarations => new[] { SyntaxKind.ClassBlock };
public SyntaxKind[] ClassAndModuleDeclarations => new[]
{
SyntaxKind.ClassBlock,
diff --git a/analyzers/src/SonarAnalyzer.VisualBasic/Rules/ClassShouldNotBeEmpty.cs b/analyzers/src/SonarAnalyzer.VisualBasic/Rules/ClassShouldNotBeEmpty.cs
new file mode 100644
index 00000000000..931ae271056
--- /dev/null
+++ b/analyzers/src/SonarAnalyzer.VisualBasic/Rules/ClassShouldNotBeEmpty.cs
@@ -0,0 +1,35 @@
+/*
+ * SonarAnalyzer for .NET
+ * Copyright (C) 2015-2023 SonarSource SA
+ * mailto: contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+namespace SonarAnalyzer.Rules.VisualBasic;
+
+[DiagnosticAnalyzer(LanguageNames.VisualBasic)]
+public sealed class ClassShouldNotBeEmpty : ClassShouldNotBeEmptyBase
+{
+ protected override ILanguageFacade Language => VisualBasicFacade.Instance;
+
+ protected override bool IsEmptyAndNotPartial(SyntaxNode node) =>
+ node is ClassBlockSyntax { Members.Count: 0 } classSyntax
+ && !classSyntax.ClassStatement.Modifiers.Any(x => x.IsKind(SyntaxKind.PartialKeyword));
+
+ protected override bool IsClassWithDeclaredBaseClass(SyntaxNode node) => node is ClassBlockSyntax { Inherits.Count: > 0 };
+
+ protected override string DeclarationTypeKeyword(SyntaxNode node) => ((TypeBlockSyntax)node).BlockStatement.DeclarationKeyword.ValueText.ToLower();
+}
diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/MetadataReferences/AspNetCoreMetadataReference.cs b/analyzers/tests/SonarAnalyzer.UnitTest/MetadataReferences/AspNetCoreMetadataReference.cs
index fdb8994b483..3cd9c1d5b99 100644
--- a/analyzers/tests/SonarAnalyzer.UnitTest/MetadataReferences/AspNetCoreMetadataReference.cs
+++ b/analyzers/tests/SonarAnalyzer.UnitTest/MetadataReferences/AspNetCoreMetadataReference.cs
@@ -37,6 +37,7 @@ internal static class AspNetCoreMetadataReference
internal static MetadataReference MicrosoftAspNetCoreMvcAbstractions { get; } = Create(typeof(Microsoft.AspNetCore.Mvc.IActionResult));
internal static MetadataReference MicrosoftAspNetCoreMvcCore { get; } = Create(typeof(Microsoft.AspNetCore.Mvc.ControllerBase));
internal static MetadataReference MicrosoftAspNetCoreMvcViewFeatures { get; } = Create(typeof(Microsoft.AspNetCore.Mvc.Controller));
+ internal static MetadataReference MicrosoftAspNetCoreRazorPages { get; } = Create(typeof(Microsoft.AspNetCore.Mvc.RazorPages.PageModel));
internal static MetadataReference MicrosoftAspNetCoreWebHost { get; } = Create(typeof(Microsoft.AspNetCore.WebHost));
internal static MetadataReference MicrosoftExtensionsHostingAbstractions { get; } = Create(typeof(Microsoft.Extensions.Hosting.IHost));
}
diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs b/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs
index 4dc49032b0f..65ea9f59d34 100644
--- a/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs
+++ b/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs
@@ -2018,7 +2018,7 @@ internal static class RuleTypeMappingCS
// ["S2091"],
["S2092"] = "SECURITY_HOTSPOT",
// ["S2093"],
- // ["S2094"],
+ ["S2094"] = "CODE_SMELL",
// ["S2095"],
// ["S2096"],
// ["S2097"],
diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingVB.cs b/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingVB.cs
index c4f42cdce08..626cb1e3a0a 100644
--- a/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingVB.cs
+++ b/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingVB.cs
@@ -2018,7 +2018,7 @@ internal static class RuleTypeMappingVB
// ["S2091"],
// ["S2092"],
// ["S2093"],
- // ["S2094"],
+ ["S2094"] = "CODE_SMELL",
// ["S2095"],
// ["S2096"],
// ["S2097"],
diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/Rules/ClassShouldNotBeEmptyTest.cs b/analyzers/tests/SonarAnalyzer.UnitTest/Rules/ClassShouldNotBeEmptyTest.cs
new file mode 100644
index 00000000000..e5c2643502a
--- /dev/null
+++ b/analyzers/tests/SonarAnalyzer.UnitTest/Rules/ClassShouldNotBeEmptyTest.cs
@@ -0,0 +1,80 @@
+/*
+ * SonarAnalyzer for .NET
+ * Copyright (C) 2015-2023 SonarSource SA
+ * mailto: contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+using CS = SonarAnalyzer.Rules.CSharp;
+using VB = SonarAnalyzer.Rules.VisualBasic;
+
+namespace SonarAnalyzer.UnitTest.Rules;
+
+[TestClass]
+public class ClassShouldNotBeEmptyTest
+{
+ private readonly VerifierBuilder builderCS = new VerifierBuilder();
+ private readonly VerifierBuilder builderVB = new VerifierBuilder();
+
+ [TestMethod]
+ public void ClassShouldNotBeEmpty_CS() =>
+ builderCS
+ .AddPaths("ClassShouldNotBeEmpty.cs")
+ .Verify();
+
+ [TestMethod]
+ public void ClassShouldNotBeEmpty_VB() =>
+ builderVB
+ .AddPaths("ClassShouldNotBeEmpty.vb")
+ .Verify();
+
+#if NET
+
+ private static readonly MetadataReference[] AdditionalReferences = new[]
+ {
+ AspNetCoreMetadataReference.MicrosoftAspNetCoreRazorPages
+ };
+
+ [TestMethod]
+ public void ClassShouldNotBeEmpty_CSharp9() =>
+ builderCS
+ .AddPaths("ClassShouldNotBeEmpty.CSharp9.cs")
+ .WithOptions(ParseOptionsHelper.FromCSharp9)
+ .Verify();
+
+ [TestMethod]
+ public void ClassShouldNotBeEmpty_CSharp10() =>
+ builderCS
+ .AddPaths("ClassShouldNotBeEmpty.CSharp10.cs")
+ .WithOptions(ParseOptionsHelper.FromCSharp10)
+ .Verify();
+
+ [TestMethod]
+ public void ClassShouldNotBeEmpty_Inheritance_CS() =>
+ builderCS
+ .AddPaths("ClassShouldNotBeEmpty.Inheritance.cs")
+ .AddReferences(AdditionalReferences)
+ .Verify();
+
+ [TestMethod]
+ public void ClassShouldNotBeEmpty_Inheritance_VB() =>
+ builderVB
+ .AddPaths("ClassShouldNotBeEmpty.Inheritance.vb")
+ .AddReferences(AdditionalReferences)
+ .Verify();
+
+#endif
+}
diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ClassShouldNotBeEmpty.CSharp10.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ClassShouldNotBeEmpty.CSharp10.cs
new file mode 100644
index 00000000000..1f2342b1612
--- /dev/null
+++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ClassShouldNotBeEmpty.CSharp10.cs
@@ -0,0 +1,13 @@
+
+record class EmptyRecordClass1(); // Noncompliant {{Remove this empty record, or add members to it.}}
+// ^^^^^^^^^^^^^^^^^
+record class EmptyRecordClass2() { }; // Noncompliant
+
+record struct EmptyRecordStruct1(); // Compliant - this rule only deals with classes
+record struct EmptyRecordStruct2() { };
+
+record class NotEmptyRecordClass1(int RecordMember);
+record class NotEmptyRecordClass2()
+{
+ int RecordMember => 42;
+};
diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ClassShouldNotBeEmpty.CSharp9.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ClassShouldNotBeEmpty.CSharp9.cs
new file mode 100644
index 00000000000..dbdd5228c73
--- /dev/null
+++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ClassShouldNotBeEmpty.CSharp9.cs
@@ -0,0 +1,44 @@
+using System;
+
+record EmptyRecord1(); // Noncompliant {{Remove this empty record, or add members to it.}}
+// ^^^^^^^^^^^^
+record EmptyRecord2() { }; // Noncompliant
+
+record RecordWithParameters(int RecordMember);
+
+record RecordWithProperty
+{
+ int SomeProperty => 42;
+}
+record RecordWithField
+{
+ int SomeField = 42;
+}
+record RecordWithMethod
+{
+ void Method() { }
+}
+record RecordWithMethodOverride
+{
+ public override string ToString() => "";
+}
+record RecordWithIndexer
+{
+ int this[int index] => 42;
+}
+record RecordWithDelegate
+{
+ delegate void MethodDelegate();
+}
+record RecordWithEvent
+{
+ event EventHandler CustomEvent;
+}
+
+partial record EmptyPartialRecord(); // Compliant - partial classes are ignored, so partial record classes are ignored as well
+
+record EmptyGenericRecord(); // Noncompliant
+// ^^^^^^^^^^^^^^^^^^
+record EmptyGenericRecordWithContraint() where T : class; // Noncompliant
+record NotEmptyGenericRecord(T RecordMember);
+record NotEmptyGenericRecordWithContraint(T RecordMember) where T : class;
diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ClassShouldNotBeEmpty.Inheritance.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ClassShouldNotBeEmpty.Inheritance.cs
new file mode 100644
index 00000000000..c1753d38de1
--- /dev/null
+++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ClassShouldNotBeEmpty.Inheritance.cs
@@ -0,0 +1,23 @@
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using System;
+
+class BaseClass
+{
+ int Prop => 42;
+}
+class SubClass: BaseClass { } // Noncompliant - not derived from any special base class
+
+abstract class AbstractBaseWithAbstractMethods
+{
+ public abstract void AbstractMethod();
+}
+abstract class AbstractBaseWithoutAbstractMethods
+{
+ public virtual void DefaultMethod() { }
+}
+class NoImplementation: AbstractBaseWithAbstractMethods { } // Error - abstract methods should be implemented
+class DefaultImplementation: AbstractBaseWithoutAbstractMethods { } // Compliant - the class will use the default implementation of DefaultMethod
+
+class EmptyPageModel: PageModel { } // Compliant - an empty PageModel can be fully functional, the C# code can be in the cshtml file
+class CustomException: Exception { } // Compliant - empty exception classes are allowed, the name of the class already provides information
+
diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ClassShouldNotBeEmpty.Inheritance.vb b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ClassShouldNotBeEmpty.Inheritance.vb
new file mode 100644
index 00000000000..60cf17b6482
--- /dev/null
+++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ClassShouldNotBeEmpty.Inheritance.vb
@@ -0,0 +1,39 @@
+Imports Microsoft.AspNetCore.Mvc.RazorPages
+Imports System
+
+Public Class BaseClass
+ Private ReadOnly Property Prop As Integer
+ Get
+ Return 42
+ End Get
+ End Property
+End Class
+
+Public Class SubClass ' Noncompliant - not derived from any special base class
+ Inherits BaseClass
+End Class
+
+MustInherit Class AbstractBaseWithAbstractMethods
+ Public MustOverride Sub AbstractMethod()
+End Class
+
+MustInherit Class AbstractBaseWithoutAbstractMethods
+ Public Overridable Sub DefaultMethod()
+ End Sub
+End Class
+
+Class NoImplementation ' Error - abstract methods should be implemented
+ Inherits AbstractBaseWithAbstractMethods
+End Class
+
+Class DefaultImplementation ' Compliant - the class will use the default implementation of DefaultMethod
+ Inherits AbstractBaseWithoutAbstractMethods
+End Class
+
+Public Class EmptyPageModel ' Compliant - an empty PageModel can be fully functional, the VB code can be in the vbhtml file
+ Inherits PageModel
+End Class
+
+Public Class CustomException ' Compliant - empty exception classes are allowed, the name of the class already provides information
+ Inherits Exception
+End Class
diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ClassShouldNotBeEmpty.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ClassShouldNotBeEmpty.cs
new file mode 100644
index 00000000000..493dbc28ec3
--- /dev/null
+++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ClassShouldNotBeEmpty.cs
@@ -0,0 +1,94 @@
+using System;
+
+class Empty { } // Noncompliant {{Remove this empty class, or add members to it.}}
+// ^^^^^
+
+public class PublicEmpty { } // Noncompliant
+internal class InternalEmpty { } // Noncompliant
+
+class EmptyWithComments // Noncompliant
+{
+ // Some comment
+}
+
+class ClassWithProperty
+{
+ int SomeProperty => 42;
+}
+class ClassWithField
+{
+ int SomeField = 42;
+}
+class ClassWithMethod
+{
+ void Method() { }
+}
+class ClassWithIndexer
+{
+ int this[int index] => 42;
+}
+class ClassWithDelegate
+{
+ delegate void MethodDelegate();
+}
+class ClassWithEvent
+{
+ event EventHandler CustomEvent;
+}
+
+class OuterClass
+{
+ class InnerEmpty1 { } // Noncompliant
+ private class InnerEmpty2 { } // Noncompliant
+ protected class InnerEmpty3 { } // Noncompliant
+ internal class InnerEmpty4 { } // Noncompliant
+ protected internal class InnerEmpty5 { } // Noncompliant
+ public class InnerEmpty6 { } // Noncompliant
+
+ public class InnerEmptyWithComments // Noncompliant
+ {
+ // Some comment
+ }
+
+ class InnerNonEmpty
+ {
+ public int SomeProperty => 42;
+ }
+}
+
+class GenericEmpty { } // Noncompliant
+// ^^^^^^^^^^^^
+class GenericEmptyWithConstraints // Noncompliant
+ where T : class
+{
+}
+
+class GenericNotEmpty
+{
+ void Method(T arg) { }
+}
+class GenericNotEmptyWithConstraints
+ where T : class
+{
+ void Method(T arg) { }
+}
+
+static class StaticEmpty { } // Noncompliant
+
+abstract class AbstractEmpty { } // Noncompliant
+
+partial class PartialEmpty { } // Compliant - Source Generators and some frameworks use empty partial classes as placeholders
+
+partial class PartialNotEmpty
+{
+ int Prop => 42;
+}
+
+interface IMarker { } // Compliant - this rule only deals with classes
+
+struct EmptyStruct { } // Compliant - this rule only deals with classes
+
+enum EmptyEnum { } // Compliant - this rule only deals with classes
+
+class { } // Error
+
diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ClassShouldNotBeEmpty.vb b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ClassShouldNotBeEmpty.vb
new file mode 100644
index 00000000000..d6221105cf5
--- /dev/null
+++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/ClassShouldNotBeEmpty.vb
@@ -0,0 +1,121 @@
+Imports System
+
+Class Empty ' Noncompliant {{Remove this empty class, or add members to it.}}
+ ' ^^^^^
+End Class
+
+
+Public Class PublicEmpty ' Noncompliant
+End Class
+
+Friend Class InternalEmpty ' Noncompliant
+End Class
+
+Class EmptyWithComments ' Noncompliant
+ ' Some comment
+End Class
+
+Class ClassWithProperty
+ Private ReadOnly Property SomeProperty As Integer
+ Get
+ Return 42
+ End Get
+ End Property
+End Class
+
+Class ClassWithField
+ Private SomeField As Integer = 42
+End Class
+
+Class ClassWithMethod
+ Private Sub Method()
+ End Sub
+End Class
+
+Class ClassWithIndexer
+ Private ReadOnly Property Item(index As Integer) As Integer
+ Get
+ Return 42
+ End Get
+ End Property
+End Class
+
+Class ClassWithDelegate
+ Delegate Sub MethodDelegate()
+End Class
+
+Class ClassWithEvent
+ Private Event CustomEvent As EventHandler
+End Class
+
+Class OuterClass
+ Class InnerEmpty1 ' Noncompliant
+ End Class
+
+ Private Class InnerEmpty2 ' Noncompliant
+ End Class
+
+ Protected Class InnerEmpty3 ' Noncompliant
+ End Class
+
+ Friend Class InnerEmpty4 ' Noncompliant
+ End Class
+
+ Protected Friend Class InnerEmpty5 ' Noncompliant
+ End Class
+
+ Public Class InnerEmpty6 ' Noncompliant
+ End Class
+
+ Public Class InnerEmptyWithComments ' Noncompliant
+ ' Some comment
+ End Class
+
+ Class InnerNonEmpty
+ Public ReadOnly Property SomeProperty As Integer
+ Get
+ Return 42
+ End Get
+ End Property
+ End Class
+End Class
+
+Class GenericEmpty(Of T) ' Noncompliant
+ ' ^^^^^^^^^^^^
+End Class
+
+Class GenericEmptyWithConstraints(Of T As Class) ' Noncompliant
+End Class
+
+Class GenericNotEmpty(Of T)
+ Private Sub Method(arg As T)
+ End Sub
+End Class
+
+Class GenericNotEmptyWithConstraints(Of T As Class)
+ Private Sub Method(arg As T)
+ End Sub
+End Class
+
+MustInherit Class AbstractEmpty ' Noncompliant
+End Class
+
+Partial Class PartialEmpty ' Compliant - Source Generators and some frameworks use empty partial classes as placeholders
+End Class
+
+Partial Class PartialNotEmpty
+ Public ReadOnly Property Prop As Integer
+ Get
+ Return 42
+ End Get
+ End Property
+End Class
+
+Interface IMarker ' Compliant - this rule only deals with classes
+End Interface
+
+Structure EmptyStruct ' Compliant - this rule only deals with classes
+End Structure
+
+Class ' Error
+End Class
diff --git a/its/projects/NoSonarTest/Class1.cs b/its/projects/NoSonarTest/Class1.cs
index 478cb4f4c4c..3c8694a6917 100644
--- a/its/projects/NoSonarTest/Class1.cs
+++ b/its/projects/NoSonarTest/Class1.cs
@@ -2,8 +2,9 @@
namespace CSLib.foo
{
- class IFoo
+ public class IFoo
{
+ public int Prop => 42;
}
class IBar // NOSONAR
diff --git a/its/projects/VbNoSonarTest/Class1.vb b/its/projects/VbNoSonarTest/Class1.vb
index f74b5360629..f4ceb44c546 100644
--- a/its/projects/VbNoSonarTest/Class1.vb
+++ b/its/projects/VbNoSonarTest/Class1.vb
@@ -17,4 +17,9 @@ Public Class IFoo ' NOSONAR
End Class
Public Class ABCDEFGHIJKLMNOPQRSTUVWXYZ
+ Public ReadOnly Property Prop As Integer
+ Get
+ Return 42
+ End Get
+ End Property
End Class
diff --git a/its/projects/WebConfig.CSharp/WebConfig.CSharp/Dummy.cs b/its/projects/WebConfig.CSharp/WebConfig.CSharp/Dummy.cs
index ee8e16dbee4..5b95f27d009 100644
--- a/its/projects/WebConfig.CSharp/WebConfig.CSharp/Dummy.cs
+++ b/its/projects/WebConfig.CSharp/WebConfig.CSharp/Dummy.cs
@@ -1,4 +1,5 @@
-public class Dummy
+public class Dummy
{
// This is present just to have a file to analyze.
+ public int Prop => 42;
}
diff --git a/its/projects/WebConfig.VB/WebConfig.VB/Dummy.vb b/its/projects/WebConfig.VB/WebConfig.VB/Dummy.vb
index 507f3246d11..7cc629b4999 100644
--- a/its/projects/WebConfig.VB/WebConfig.VB/Dummy.vb
+++ b/its/projects/WebConfig.VB/WebConfig.VB/Dummy.vb
@@ -1,3 +1,8 @@
Public Class Dummy
' This is present just to have a file to analyze.
+ Public ReadOnly Property Prop As Integer
+ Get
+ Return 42
+ End Get
+ End Property
End Class
diff --git a/its/src/test/java/com/sonar/it/csharp/IncrementalAnalysisTest.java b/its/src/test/java/com/sonar/it/csharp/IncrementalAnalysisTest.java
index b86be5883e0..a8e183d5fcc 100644
--- a/its/src/test/java/com/sonar/it/csharp/IncrementalAnalysisTest.java
+++ b/its/src/test/java/com/sonar/it/csharp/IncrementalAnalysisTest.java
@@ -29,6 +29,7 @@
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Path;
+import java.util.stream.Collectors;
import java.util.List;
import org.junit.Before;
import org.junit.Rule;
@@ -43,6 +44,7 @@
public class IncrementalAnalysisTest {
private static final String PROJECT = "IncrementalPRAnalysis";
+ private static final String PULL_REQUEST_KEY = "42";
@Rule
public TemporaryFolder temp = TestUtils.createTempFolder();
@@ -68,7 +70,7 @@ public void incrementalPrAnalysis_NoCache_FullAnalysisDone() throws IOException
assertThat(beginStepResults.getLogs()).contains("Processing pull request with base branch 'base-branch'.");
assertThat(beginStepResults.getLogs()).contains("Cache data is not available. Incremental PR analysis is disabled.");
assertAllFilesWereAnalysed(endStepResults, projectDir);
- List allIssues = TestUtils.getIssues(ORCHESTRATOR, PROJECT, "42");
+ List allIssues = TestUtils.getIssues(ORCHESTRATOR, PROJECT, PULL_REQUEST_KEY);
assertThat(allIssues).hasSize(1);
assertThat(allIssues.get(0).getRule()).isEqualTo("csharpsquid:S1134");
}
@@ -85,7 +87,7 @@ public void incrementalPrAnalysis_cacheAvailableNoChanges_nothingReported() thro
assertTrue(endStepResults.isSuccess());
assertCacheIsUsed(beginStepResults, PROJECT);
assertThat(endStepResults.getLogs()).doesNotContain("Adding normal issue S1134");
- List allIssues = TestUtils.getIssues(ORCHESTRATOR, PROJECT, "42");
+ List allIssues = TestUtils.getIssues(ORCHESTRATOR, PROJECT, PULL_REQUEST_KEY);
assertThat(allIssues).isEmpty();
}
@@ -110,12 +112,16 @@ public void incrementalPrAnalysis_cacheAvailableChangesDone_issuesReportedForCha
assertThat(endStepResults.getLogs()).doesNotContain("Adding normal issue S1134: " + unchanged2Path);
assertThat(endStepResults.getLogs()).contains("Adding normal issue S1134: " + withChangesPath);
assertThat(endStepResults.getLogs()).contains("Adding normal issue S1134: " + fileToBeAddedPath);
- List allIssues = TestUtils.getIssues(ORCHESTRATOR, PROJECT, "42");
- assertThat(allIssues).hasSize(2);
- assertThat(allIssues.get(0).getRule()).isEqualTo("csharpsquid:S1134");
- assertThat(allIssues.get(0).getComponent()).isEqualTo("IncrementalPRAnalysis:IncrementalPRAnalysis/AddedFile.cs");
- assertThat(allIssues.get(1).getRule()).isEqualTo("csharpsquid:S1134");
- assertThat(allIssues.get(1).getComponent()).isEqualTo("IncrementalPRAnalysis:IncrementalPRAnalysis/WithChanges.cs");
+ List fixMeIssues = TestUtils
+ .getIssues(ORCHESTRATOR, PROJECT, PULL_REQUEST_KEY)
+ .stream()
+ .filter(x -> x.getRule().equals("csharpsquid:S1134"))
+ .collect(Collectors.toList());
+ assertThat(fixMeIssues).hasSize(2);
+ assertThat(fixMeIssues.get(0).getRule()).isEqualTo("csharpsquid:S1134");
+ assertThat(fixMeIssues.get(0).getComponent()).isEqualTo("IncrementalPRAnalysis:IncrementalPRAnalysis/AddedFile.cs");
+ assertThat(fixMeIssues.get(1).getRule()).isEqualTo("csharpsquid:S1134");
+ assertThat(fixMeIssues.get(1).getComponent()).isEqualTo("IncrementalPRAnalysis:IncrementalPRAnalysis/WithChanges.cs");
}
@Test
@@ -132,14 +138,18 @@ public void incrementalPrAnalysis_cacheAvailableProjectBaseDirChanged_everything
assertTrue(endStepResults.isSuccess());
assertCacheIsUsed(beginStepResults, PROJECT);
assertAllFilesWereAnalysed(endStepResults, projectDir);
- List allIssues = TestUtils.getIssues(ORCHESTRATOR, PROJECT, "42");
- assertThat(allIssues).hasSize(3);
- assertThat(allIssues.get(0).getRule()).isEqualTo("csharpsquid:S1134");
- assertThat(allIssues.get(0).getComponent()).isEqualTo("IncrementalPRAnalysis:Unchanged1.cs");
- assertThat(allIssues.get(1).getRule()).isEqualTo("csharpsquid:S1134");
- assertThat(allIssues.get(1).getComponent()).isEqualTo("IncrementalPRAnalysis:Unchanged2.cs");
- assertThat(allIssues.get(2).getRule()).isEqualTo("csharpsquid:S1134");
- assertThat(allIssues.get(2).getComponent()).isEqualTo("IncrementalPRAnalysis:WithChanges.cs");
+ List fixMeIssues = TestUtils
+ .getIssues(ORCHESTRATOR, PROJECT, PULL_REQUEST_KEY)
+ .stream()
+ .filter(x -> x.getRule().equals("csharpsquid:S1134"))
+ .collect(Collectors.toList());
+ assertThat(fixMeIssues).hasSize(3);
+ assertThat(fixMeIssues.get(0).getRule()).isEqualTo("csharpsquid:S1134");
+ assertThat(fixMeIssues.get(0).getComponent()).isEqualTo("IncrementalPRAnalysis:Unchanged1.cs");
+ assertThat(fixMeIssues.get(1).getRule()).isEqualTo("csharpsquid:S1134");
+ assertThat(fixMeIssues.get(1).getComponent()).isEqualTo("IncrementalPRAnalysis:Unchanged2.cs");
+ assertThat(fixMeIssues.get(2).getRule()).isEqualTo("csharpsquid:S1134");
+ assertThat(fixMeIssues.get(2).getComponent()).isEqualTo("IncrementalPRAnalysis:WithChanges.cs");
}
@Test
@@ -157,7 +167,10 @@ public void incrementalPrAnalysis_cacheAvailableDuplicationIntroduced_duplicatio
assertTrue(endStepResults.isSuccess());
assertCacheIsUsed(beginStepResults, projectName);
- List duplications = TestUtils.getDuplication(ORCHESTRATOR, "IncrementalPRAnalysisDuplication:IncrementalPRAnalysisDuplication/CopyClass.cs", "42")
+ List duplications = TestUtils.getDuplication(
+ ORCHESTRATOR,
+ "IncrementalPRAnalysisDuplication:IncrementalPRAnalysisDuplication/CopyClass.cs",
+ PULL_REQUEST_KEY)
.getDuplicationsList();
assertThat(duplications).isNotEmpty();
}
@@ -221,7 +234,7 @@ private BeginAndEndStepResults executeAnalysisForPRBranch(String project, Path p
BuildResult beginStepResults = ORCHESTRATOR.executeBuild(beginStep
.setProperty("sonar.pullrequest.base", "base-branch")
- .setProperty("sonar.pullrequest.key", "42")
+ .setProperty("sonar.pullrequest.key", PULL_REQUEST_KEY)
.setProperty("sonar.pullrequest.branch", "pull-request")
.setProperty("sonar.verbose", "true"));
TestUtils.runMSBuild(ORCHESTRATOR, projectDir, "/t:Restore,Rebuild");
diff --git a/its/src/test/java/com/sonar/it/csharp/ProjectLevelDuplicationTest.java b/its/src/test/java/com/sonar/it/csharp/ProjectLevelDuplicationTest.java
index 569bec85251..2fa97ca1925 100644
--- a/its/src/test/java/com/sonar/it/csharp/ProjectLevelDuplicationTest.java
+++ b/its/src/test/java/com/sonar/it/csharp/ProjectLevelDuplicationTest.java
@@ -50,10 +50,10 @@ public void containsOnlyOneProjectLevelIssue() throws Exception {
assertThat(getComponent("ProjectLevelDuplication")).isNotNull();
- List issues = getIssues("ProjectLevelDuplication")
+ List projectLevelIssues = getIssues("ProjectLevelDuplication")
.stream()
- .filter(x -> x.getRule().startsWith("csharpsquid:"))
+ .filter(x -> x.getRule().equals("csharpsquid:S3904"))
.collect(Collectors.toList());
- assertThat(issues).hasSize(1);
+ assertThat(projectLevelIssues).hasSize(1);
}
}