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); } }