diff --git a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/INamedTypeSymbolExtensions.cs b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/INamedTypeSymbolExtensions.cs index afbb30853f2..8230e1b03e9 100644 --- a/analyzers/src/SonarAnalyzer.CFG/ShimLayer/INamedTypeSymbolExtensions.cs +++ b/analyzers/src/SonarAnalyzer.CFG/ShimLayer/INamedTypeSymbolExtensions.cs @@ -1,14 +1,19 @@ // Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; + namespace StyleCop.Analyzers.Lightup { - using System; - using System.Collections.Immutable; - using Microsoft.CodeAnalysis; - public static class INamedTypeSymbolExtensions { + private const string MainMethodImplicitName = "
$"; + + private static readonly HashSet ProgramClassImplicitName = new HashSet { "Program", "$" }; private static readonly Func TupleUnderlyingTypeAccessor; private static readonly Func> TupleElementsAccessor; private static readonly Func IsSerializableAccessor; @@ -20,19 +25,15 @@ static INamedTypeSymbolExtensions() IsSerializableAccessor = LightupHelpers.CreateSyntaxPropertyAccessor(typeof(INamedTypeSymbol), nameof(IsSerializable)); } - public static INamedTypeSymbol TupleUnderlyingType(this INamedTypeSymbol symbol) - { - return TupleUnderlyingTypeAccessor(symbol); - } + public static INamedTypeSymbol TupleUnderlyingType(this INamedTypeSymbol symbol) => TupleUnderlyingTypeAccessor(symbol); - public static ImmutableArray TupleElements(this INamedTypeSymbol symbol) - { - return TupleElementsAccessor(symbol); - } + public static ImmutableArray TupleElements(this INamedTypeSymbol symbol) => TupleElementsAccessor(symbol); - public static bool IsSerializable(this INamedTypeSymbol symbol) - { - return IsSerializableAccessor(symbol); - } + public static bool IsSerializable(this INamedTypeSymbol symbol) => IsSerializableAccessor(symbol); + + public static bool IsTopLevelProgram(this INamedTypeSymbol symbol) => + ProgramClassImplicitName.Contains(symbol.Name) + && symbol.ContainingNamespace.IsGlobalNamespace + && symbol.GetMembers(MainMethodImplicitName).Any(); } } diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/UnusedPrivateMember.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/UnusedPrivateMember.cs index 8d2510a1801..626627a0776 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/UnusedPrivateMember.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/UnusedPrivateMember.cs @@ -72,16 +72,10 @@ protected override void Initialize(SonarAnalysisContext context) => { var namedType = (INamedTypeSymbol)cc.Symbol; - if (namedType.TypeKind != TypeKind.Struct - && namedType.TypeKind != TypeKind.Class - && namedType.TypeKind != TypeKind.Delegate - && namedType.TypeKind != TypeKind.Enum - && namedType.TypeKind != TypeKind.Interface) - { - return; - } - - if (namedType.ContainingType != null || namedType.DerivesFromAny(IgnoredTypes)) + if (namedType.ContainingType != null + // We skip top level statements since they cannot have fields. Other declared types are analyzed separately. + || namedType.IsTopLevelProgram() + || namedType.DerivesFromAny(IgnoredTypes)) { return; } @@ -137,8 +131,10 @@ protected override void Initialize(SonarAnalysisContext context) => }); }); - private static IEnumerable GetDiagnosticsForUnusedPrivateMembers(CSharpSymbolUsageCollector usageCollector, ISet removableSymbols, string accessibility, - BidirectionalDictionary fieldLikeSymbols) + private static IEnumerable GetDiagnosticsForUnusedPrivateMembers(CSharpSymbolUsageCollector usageCollector, + ISet removableSymbols, + string accessibility, + BidirectionalDictionary fieldLikeSymbols) { var unusedSymbols = GetUnusedSymbols(usageCollector, removableSymbols); @@ -181,7 +177,9 @@ private static IEnumerable GetDiagnosticsForUnreadFields(IEnumerable private static string GetFieldAccessibilityForMessage(ISymbol symbol) => symbol.DeclaredAccessibility == Accessibility.Private ? "private" : "private class"; - private static IEnumerable GetDiagnosticsForMembers(ICollection unusedSymbols, string accessibility, BidirectionalDictionary fieldLikeSymbols) + private static IEnumerable GetDiagnosticsForMembers(ICollection unusedSymbols, + string accessibility, + BidirectionalDictionary fieldLikeSymbols) { var diagnostics = new List(); var alreadyReportedFieldLikeSymbols = new HashSet(); diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/Rules/UnusedPrivateMemberTest.cs b/analyzers/tests/SonarAnalyzer.UnitTest/Rules/UnusedPrivateMemberTest.cs index 9087a22ef92..de29ae8cdc9 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/Rules/UnusedPrivateMemberTest.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/Rules/UnusedPrivateMemberTest.cs @@ -203,26 +203,24 @@ public void UnusedPrivateMember_FromCSharp8() => [TestMethod] public void UnusedPrivateMember_FromCSharp9() => Verifier.VerifyAnalyzerFromCSharp9Library( - @"TestCases\UnusedPrivateMember.CSharp9.cs", + new[] + { + @"TestCases\UnusedPrivateMember.CSharp9.cs", + @"TestCases\UnusedPrivateMember.CSharp9.Second.cs" + }, new UnusedPrivateMember()); [TestMethod] public void UnusedPrivateMember_FromCSharp9_TopLevelStatements() => - Verifier.VerifyAnalyzerFromCSharp9Console( - @"TestCases\UnusedPrivateMember.CSharp9.TopLevelStatements.cs", - new UnusedPrivateMember()); + Verifier.VerifyAnalyzerFromCSharp9Console(@"TestCases\UnusedPrivateMember.CSharp9.TopLevelStatements.cs", new UnusedPrivateMember()); [TestMethod] public void UnusedPrivateMember_FromCSharp10() => - Verifier.VerifyAnalyzerFromCSharp10Library( - @"TestCases\UnusedPrivateMember.CSharp10.cs", - new UnusedPrivateMember()); + Verifier.VerifyAnalyzerFromCSharp10Library(@"TestCases\UnusedPrivateMember.CSharp10.cs", new UnusedPrivateMember()); [TestMethod] public void UnusedPrivateMember_FromCSharpPreview() => - Verifier.VerifyAnalyzerCSharpPreviewLibrary( - @"TestCases\UnusedPrivateMember.CSharpPreview.cs", - new UnusedPrivateMember()); + Verifier.VerifyAnalyzerCSharpPreviewLibrary(@"TestCases\UnusedPrivateMember.CSharpPreview.cs", new UnusedPrivateMember()); #endif [TestMethod] diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/UnusedPrivateMember.CSharp7.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/UnusedPrivateMember.CSharp7.cs index 84b7f814794..328ac268b56 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/UnusedPrivateMember.CSharp7.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/UnusedPrivateMember.CSharp7.cs @@ -159,4 +159,23 @@ public class EmptyCtor // That's invalid syntax, but it is still empty ctor and we should not raise for it, even if it is not used public EmptyCtor() => // Error [CS1525,CS1002] } + + public class WithEnums + { + private enum X // Noncompliant + { + A + } + + public void UseEnum() + { + var b = Y.B; + } + + private enum Y + { + A, + B + } + } } diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/UnusedPrivateMember.CSharp9.Second.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/UnusedPrivateMember.CSharp9.Second.cs new file mode 100644 index 00000000000..5fd8d0fab90 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/UnusedPrivateMember.CSharp9.Second.cs @@ -0,0 +1,7 @@ +namespace Tests.Diagnostics +{ + public partial class PartialMethods + { + partial void UnusedMethod(); // Noncompliant + } +} diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/UnusedPrivateMember.CSharp9.TopLevelStatements.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/UnusedPrivateMember.CSharp9.TopLevelStatements.cs index 11d0b0d394c..1c94da7bba1 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/UnusedPrivateMember.CSharp9.TopLevelStatements.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/UnusedPrivateMember.CSharp9.TopLevelStatements.cs @@ -13,19 +13,16 @@ public record Product(string Name, int CategoryId); public record Record { private int a; // Noncompliant {{Remove the unused private field 'a'.}} - // Noncompliant@-1 - duplicate issue reported private int b; public int B() => b; private nint Value { get; init; } private nint UnusedValue { get; init; } // Noncompliant - // Noncompliant@-1 - duplicate issue reported public Record Create() => new() { Value = 1 }; private interface IFoo // Noncompliant - // Noncompliant@-1 - duplicate issue reported { public void Bar() { } } @@ -45,7 +42,6 @@ public void UseNested2() } private record UnusedNested1(string Name, int CategoryId); // Noncompliant - // Noncompliant@-1 - duplicate issue reported internal record UnusedNested2(string Name, int CategoryId); // Noncompliant public record UnusedNested3(string Name, int CategoryId); @@ -76,7 +72,6 @@ private TargetTypedNew(int arg) } private TargetTypedNew(string arg) // Noncompliant - FP - // Noncompliant@-1 - duplicate issue reported { var x = arg; } diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/UnusedPrivateMember.CSharp9.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/UnusedPrivateMember.CSharp9.cs index f988aa6c89a..21037b3750a 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/UnusedPrivateMember.CSharp9.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/UnusedPrivateMember.CSharp9.cs @@ -100,4 +100,8 @@ public static void Foo() PositionalRecord @record = new PositionalRecord(""); } } + + public partial class PartialMethods + { + } } diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestFramework/Verifier.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestFramework/Verifier.cs index 5f67c4c13af..6f97c8b809f 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/TestFramework/Verifier.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestFramework/Verifier.cs @@ -112,11 +112,11 @@ public static void VerifyAnalyzerFromCSharp9Console(string path, DiagnosticAnaly public static void VerifyAnalyzerFromCSharp10Console(string path, DiagnosticAnalyzer diagnosticAnalyzer, IEnumerable additionalReferences = null) => VerifyNonConcurrentAnalyzer(new[] { path }, - new[] { diagnosticAnalyzer }, - ParseOptionsHelper.FromCSharp10, - CompilationErrorBehavior.Default, - OutputKind.ConsoleApplication, - additionalReferences); + new[] { diagnosticAnalyzer }, + ParseOptionsHelper.FromCSharp10, + CompilationErrorBehavior.Default, + OutputKind.ConsoleApplication, + additionalReferences); /// /// Verify analyzer from C# 9 with top level statements in non-concurrent execution mode.