diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/UnusedPrivateMember.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/UnusedPrivateMember.cs index eadf289d229..2efe588a1a0 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/Rules/UnusedPrivateMember.cs +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/UnusedPrivateMember.cs @@ -320,7 +320,7 @@ public CSharpRemovableSymbolWalker(Func getSema this.containingTypeAccessibility = containingTypeAccessibility; } - // This override is needed because VisitRecordDeclaration is not available due to the Roslyn version. + // This override is needed because VisitRecordDeclaration and LocalFunctionStatementSyntax are not available due to the Roslyn version. public override void Visit(SyntaxNode node) { if (node.IsAnyKind(SyntaxKindEx.RecordClassDeclaration, SyntaxKindEx.RecordStructDeclaration)) @@ -328,6 +328,11 @@ public override void Visit(SyntaxNode node) VisitBaseTypeDeclaration(node); } + if (node.IsKind(SyntaxKindEx.LocalFunctionStatement)) + { + ConditionalStore((IMethodSymbol)GetDeclaredSymbol(node), IsRemovableMethod); + } + base.Visit(node); } @@ -488,7 +493,7 @@ static bool IsPartial(TypeDeclarationSyntax typeDeclaration) => private static bool IsRemovableMethod(IMethodSymbol methodSymbol) => IsRemovableMember(methodSymbol) - && (methodSymbol.MethodKind == MethodKind.Ordinary || methodSymbol.MethodKind == MethodKind.Constructor) + && (methodSymbol.MethodKind is MethodKind.Ordinary or MethodKind.Constructor or MethodKindEx.LocalFunction) && !methodSymbol.IsMainMethod() && (!methodSymbol.IsEventHandler() || !IsDeclaredInPartialClass(methodSymbol)) // Event handlers could be added in XAML and no method reference will be generated in the .g.cs file. && !methodSymbol.IsSerializationConstructor() diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/SymbolHelper.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/SymbolHelper.cs index 77b6c38e9a0..92edd49dd36 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/SymbolHelper.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/SymbolHelper.cs @@ -315,6 +315,7 @@ public static string GetClassification(this ISymbol symbol) => { MethodKind: MethodKind.Destructor } => "destructor", { MethodKind: MethodKind.PropertyGet } => "getter", { MethodKind: MethodKind.PropertySet } => "setter", + { MethodKind: MethodKindEx.LocalFunction } => "local function", _ => "method", }, INamedTypeSymbol namedTypeSymbol => namedTypeSymbol switch diff --git a/analyzers/tests/SonarAnalyzer.Test/Helpers/SymbolHelperTest.cs b/analyzers/tests/SonarAnalyzer.Test/Helpers/SymbolHelperTest.cs index d74bbbab90d..98e24ec59cd 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Helpers/SymbolHelperTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Helpers/SymbolHelperTest.cs @@ -398,7 +398,7 @@ public void GetClassification_Record(TypeKind typeKind, string expected) [DataRow(MethodKind.ExplicitInterfaceImplementation, "method")] [DataRow(MethodKind.FunctionPointerSignature, "method")] [DataRow(MethodKind.LambdaMethod, "method")] - [DataRow(MethodKind.LocalFunction, "method")] + [DataRow(MethodKind.LocalFunction, "local function")] [DataRow(MethodKind.Ordinary, "method")] [DataRow(MethodKind.PropertyGet, "getter")] [DataRow(MethodKind.PropertySet, "setter")] diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.CSharp10.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.CSharp10.cs index 7e1cee0fc40..20970032551 100644 --- a/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.CSharp10.cs +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.CSharp10.cs @@ -70,7 +70,7 @@ static void Quix() { } [Obsolete] static void ForCoverage() { } - static void NoAttribute() { } + static void NoAttribute() { } // Noncompliant } } diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.CSharp7.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.CSharp7.cs index dc4fa168871..31319f69da1 100644 --- a/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.CSharp7.cs +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.CSharp7.cs @@ -198,15 +198,6 @@ private enum Y } } -// https://github.com/SonarSource/sonar-dotnet/issues/6699 -public class Repro_6699 -{ - public void MethodUsingLocalMethod() - { - void LocalMethod() { } // FN - } -} - // https://github.com/SonarSource/sonar-dotnet/issues/6724 public class Repro_6724 { diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.CSharp9.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.CSharp9.cs index eb65156b832..1ccd42adf73 100644 --- a/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.CSharp9.cs +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.CSharp9.cs @@ -69,7 +69,7 @@ static void Quix() { } [Obsolete] static void ForCoverage() { } - static void NoAttribute() { } + static void NoAttribute() { } // Noncompliant } } @@ -158,7 +158,7 @@ public bool MethodWithLocalFunction() { return false; - bool PrintMembers(StringBuilder builder) => true; // FN - local functions are not handled + bool PrintMembers(StringBuilder builder) => true; // Noncompliant } } diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.Fixed.Batch.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.Fixed.Batch.cs index f27dbf66f6a..05503920818 100644 --- a/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.Fixed.Batch.cs +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.Fixed.Batch.cs @@ -94,11 +94,13 @@ private interface MyInterface void Method(); } + // https://github.com/SonarSource/sonar-dotnet/issues/6699 public void MethodUsingLocalMethod() { - void LocalMethod() // FN: local function is never used - { + LocalMethod(); + void LocalMethod() + { } } } diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.Fixed.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.Fixed.cs index c45b707c95b..7deb5606f78 100644 --- a/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.Fixed.cs +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.Fixed.cs @@ -78,11 +78,13 @@ private int MyProperty [My] private class Class1 { } + // https://github.com/SonarSource/sonar-dotnet/issues/6699 public void MethodUsingLocalMethod() { - void LocalMethod() // FN: local function is never used - { + LocalMethod(); + void LocalMethod() + { } } } diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.cs index dad4f7fdd86..310299baa71 100644 --- a/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.cs +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UnusedPrivateMember.cs @@ -144,11 +144,17 @@ internal class Class4 : MyInterface // Noncompliant {{Remove the unused internal public void Method() { } } + // https://github.com/SonarSource/sonar-dotnet/issues/6699 public void MethodUsingLocalMethod() { - void LocalMethod() // FN: local function is never used + LocalMethod(); + + void LocalMethod() { + } + void UnusedLocalMethod() // Noncompliant {{Remove the unused private local function 'UnusedLocalMethod'.}} + { } } }