From b89dd7aeb41e48bd58b82b0157897283f34662a6 Mon Sep 17 00:00:00 2001 From: Zsolt Kolbay <121798625+zsolt-kolbay-sonarsource@users.noreply.github.com> Date: Tue, 28 Feb 2023 15:25:12 +0100 Subject: [PATCH] New Rule S3398: Private static methods called only by inner class (#6781) Co-authored-by: Andrei Epure --- analyzers/rspec/cs/S3398_c#.html | 43 ++++ analyzers/rspec/cs/S3398_c#.json | 17 ++ analyzers/rspec/cs/Sonar_way_profile.json | 1 + ...rivateStaticMethodUsedOnlyByNestedClass.cs | 206 ++++++++++++++++ .../PackagingTests/RuleTypeMappingCS.cs | 2 +- ...teStaticMethodUsedOnlyByNestedClassTest.cs | 60 +++++ ...ticMethodUsedOnlyByNestedClass.CSharp10.cs | 43 ++++ ...aticMethodUsedOnlyByNestedClass.CSharp8.cs | 27 +++ ...aticMethodUsedOnlyByNestedClass.CSharp9.cs | 21 ++ ...rivateStaticMethodUsedOnlyByNestedClass.cs | 221 ++++++++++++++++++ 10 files changed, 640 insertions(+), 1 deletion(-) create mode 100644 analyzers/rspec/cs/S3398_c#.html create mode 100644 analyzers/rspec/cs/S3398_c#.json create mode 100644 analyzers/src/SonarAnalyzer.CSharp/Rules/PrivateStaticMethodUsedOnlyByNestedClass.cs create mode 100644 analyzers/tests/SonarAnalyzer.UnitTest/Rules/PrivateStaticMethodUsedOnlyByNestedClassTest.cs create mode 100644 analyzers/tests/SonarAnalyzer.UnitTest/TestCases/PrivateStaticMethodUsedOnlyByNestedClass.CSharp10.cs create mode 100644 analyzers/tests/SonarAnalyzer.UnitTest/TestCases/PrivateStaticMethodUsedOnlyByNestedClass.CSharp8.cs create mode 100644 analyzers/tests/SonarAnalyzer.UnitTest/TestCases/PrivateStaticMethodUsedOnlyByNestedClass.CSharp9.cs create mode 100644 analyzers/tests/SonarAnalyzer.UnitTest/TestCases/PrivateStaticMethodUsedOnlyByNestedClass.cs diff --git a/analyzers/rspec/cs/S3398_c#.html b/analyzers/rspec/cs/S3398_c#.html new file mode 100644 index 00000000000..11768b0b7ea --- /dev/null +++ b/analyzers/rspec/cs/S3398_c#.html @@ -0,0 +1,43 @@ +

When a private static method is only invoked by a nested class, there’s no reason not to move it into that class. It will still have +the same access to the outer class' static members, but the outer class will be clearer and less cluttered.

+

Noncompliant Code Example

+
+public class Outer
+{
+    private const int base = 42;
+
+    private static void Print(int num)  // Noncompliant - static method is only used by the nested class, should be moved there
+    {
+        Console.WriteLine(num + base);
+    }
+
+    public class Nested
+    {
+        public void SomeMethod()
+        {
+            Outer.Print(1);
+        }
+    }
+}
+
+

Compliant Solution

+
+public class Outer
+{
+    private const int base = 42;
+
+    public class Nested
+    {
+        public void SomeMethod()
+        {
+            Print(1);
+        }
+
+        private static void Print(int num)
+        {
+            Console.WriteLine(num + base);
+        }
+    }
+}
+
+ diff --git a/analyzers/rspec/cs/S3398_c#.json b/analyzers/rspec/cs/S3398_c#.json new file mode 100644 index 00000000000..ef177d698f6 --- /dev/null +++ b/analyzers/rspec/cs/S3398_c#.json @@ -0,0 +1,17 @@ +{ + "title": "\"private\" methods called only by inner classes should be moved to those classes", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5min" + }, + "tags": [ + "confusing" + ], + "defaultSeverity": "Minor", + "ruleSpecification": "RSPEC-3398", + "sqKey": "S3398", + "scope": "All", + "quickfix": "unknown" +} diff --git a/analyzers/rspec/cs/Sonar_way_profile.json b/analyzers/rspec/cs/Sonar_way_profile.json index d84e4e17b49..8957b5e69df 100644 --- a/analyzers/rspec/cs/Sonar_way_profile.json +++ b/analyzers/rspec/cs/Sonar_way_profile.json @@ -159,6 +159,7 @@ "S3358", "S3376", "S3397", + "S3398", "S3400", "S3415", "S3427", diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/PrivateStaticMethodUsedOnlyByNestedClass.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/PrivateStaticMethodUsedOnlyByNestedClass.cs new file mode 100644 index 00000000000..44998f457b3 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/PrivateStaticMethodUsedOnlyByNestedClass.cs @@ -0,0 +1,206 @@ +/* + * 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 SonarAnalyzer.CFG.Helpers; + +namespace SonarAnalyzer.Rules.CSharp; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class PrivateStaticMethodUsedOnlyByNestedClass : SonarDiagnosticAnalyzer +{ + private const string DiagnosticId = "S3398"; + private const string MessageFormat = "Move this method inside '{0}'."; + + private static readonly SyntaxKind[] AnalyzedSyntaxKinds = new[] + { + SyntaxKind.ClassDeclaration, + SyntaxKind.StructDeclaration, + SyntaxKind.InterfaceDeclaration, + SyntaxKindEx.RecordClassDeclaration, + SyntaxKindEx.RecordStructDeclaration + }; + + private static readonly DiagnosticDescriptor Rule = DescriptorFactory.Create(DiagnosticId, MessageFormat); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + protected override void Initialize(SonarAnalysisContext context) => + context.RegisterNodeAction(c => + { + var outerType = (TypeDeclarationSyntax)c.Node; + + if (!IsPartial(outerType) + && HasNestedTypeDeclarations(outerType) + && PrivateStaticMethodsOf(outerType) is { Length: > 0 } candidates) + { + var methodReferences = TypesWhichUseTheMethods(candidates, outerType, c.SemanticModel); + + foreach (var reference in methodReferences) + { + var typeToMoveInto = LowestCommonAncestorOrSelf(reference.Types); + if (typeToMoveInto != outerType) + { + var nestedTypeName = typeToMoveInto.Identifier.ValueText; + c.ReportIssue(Diagnostic.Create(Rule, reference.Method.Identifier.GetLocation(), nestedTypeName)); + } + } + } + }, + AnalyzedSyntaxKinds); + + private static bool IsPartial(TypeDeclarationSyntax type) => + type.Modifiers.Any(x => x.IsKind(SyntaxKind.PartialKeyword)); + + private static bool HasNestedTypeDeclarations(TypeDeclarationSyntax type) => + type.Members + .OfType() + .Any(); + + private static MethodDeclarationSyntax[] PrivateStaticMethodsOf(TypeDeclarationSyntax type) => + type.Members + .OfType() + .Where(x => IsPrivateAndStatic(x, type)) + .ToArray(); + + private static bool IsPrivateAndStatic(MethodDeclarationSyntax method, TypeDeclarationSyntax containingType) + { + return method.Modifiers.Any(x => x.IsKind(SyntaxKind.StaticKeyword)) + && (IsExplicitlyPrivate() || IsImplicityPrivate()); + + bool IsExplicitlyPrivate() => + HasAnyModifier(method, SyntaxKind.PrivateKeyword) && !HasAnyModifier(method, SyntaxKind.ProtectedKeyword); + + // The default accessibility for record class members is private, but for record structs (like all structs) it's internal. + bool IsImplicityPrivate() => + !HasAnyModifier(method, SyntaxKind.PublicKeyword, SyntaxKind.ProtectedKeyword, SyntaxKind.InternalKeyword) + && IsClassOrRecordClassOrInterfaceDeclaration(containingType); + + static bool IsClassOrRecordClassOrInterfaceDeclaration(TypeDeclarationSyntax type) => + type is ClassDeclarationSyntax or InterfaceDeclarationSyntax + || (RecordDeclarationSyntaxWrapper.IsInstance(type) && !((RecordDeclarationSyntaxWrapper)type).ClassOrStructKeyword.IsKind(SyntaxKind.StructKeyword)); + + static bool HasAnyModifier(MethodDeclarationSyntax method, params SyntaxKind[] modifiers) => + method.Modifiers.Any(x => x.IsAnyKind(modifiers)); + } + + private static TypeDeclarationSyntax LowestCommonAncestorOrSelf(IEnumerable declaredTypes) + { + var typeHierarchyFromTopToBottom = declaredTypes.Select(PathFromTop); + var minPathLength = typeHierarchyFromTopToBottom.Select(x => x.Length).Min(); + var firstPath = typeHierarchyFromTopToBottom.First(); + + var lastCommonPathIndex = 0; + for (int i = 0; i < minPathLength; i++) + { + var isPartOfCommonPath = typeHierarchyFromTopToBottom.All(x => x[i] == firstPath[i]); + if (isPartOfCommonPath) + { + lastCommonPathIndex = i; + } + else + { + break; + } + } + + return firstPath[lastCommonPathIndex]; + + static TypeDeclarationSyntax[] PathFromTop(SyntaxNode node) => + node.AncestorsAndSelf() + .OfType() + .Distinct() + .Reverse() + .ToArray(); + } + + private static IEnumerable TypesWhichUseTheMethods( + IEnumerable methods, TypeDeclarationSyntax outerType, SemanticModel model) + { + var collector = new PotentialMethodReferenceCollector(methods); + collector.Visit(outerType); + + return collector.PotentialMethodReferences + .Where(x => !OnlyUsedByOuterType(x)) + .Select(DeclaredTypesWhichActuallyUseTheMethod) + .Where(x => x.Types.Any()) + .ToArray(); + + MethodUsedByTypes DeclaredTypesWhichActuallyUseTheMethod(MethodWithPotentialReferences m) + { + var methodSymbol = model.GetDeclaredSymbol(m.Method); + + var typesWhichUseTheMethod = m.PotentialReferences + .Where(x => + !IsRecursiveMethodCall(x, m.Method) + && model.GetSymbolOrCandidateSymbol(x) is IMethodSymbol { } methodReference + && (methodReference.Equals(methodSymbol) || methodReference.ConstructedFrom.Equals(methodSymbol))) + .Select(ContainingTypeDeclaration) + .Distinct() + .ToArray(); + + return new MethodUsedByTypes(m.Method, typesWhichUseTheMethod); + } + + bool IsRecursiveMethodCall(IdentifierNameSyntax methodCall, MethodDeclarationSyntax methodDeclaration) => + methodCall.Ancestors().OfType().FirstOrDefault() == methodDeclaration; + + bool OnlyUsedByOuterType(MethodWithPotentialReferences m) => + m.PotentialReferences.All(x => ContainingTypeDeclaration(x) == outerType); + + static TypeDeclarationSyntax ContainingTypeDeclaration(IdentifierNameSyntax identifier) => + identifier + .Ancestors() + .OfType() + .First(); + } + + private sealed record MethodWithPotentialReferences(MethodDeclarationSyntax Method, IdentifierNameSyntax[] PotentialReferences); + + private sealed record MethodUsedByTypes(MethodDeclarationSyntax Method, TypeDeclarationSyntax[] Types); + + /// + /// Collects all the potential references to a set of methods inside the given syntax node. + /// The collector looks for identifiers which match any of the methods' names, but does not try to resolve them to symbols with the semantic model. + /// Performance gains: by only using the syntax tree to find matches we can eliminate certain methods (which are only used by the type which has declared it) without using the more costly symbolic lookup. + /// + private sealed class PotentialMethodReferenceCollector : CSharpSyntaxWalker + { + private readonly ISet methodsToFind; + private readonly Dictionary> potentialMethodReferences; + + public IEnumerable PotentialMethodReferences => + potentialMethodReferences.Select(x => new MethodWithPotentialReferences(x.Key, x.Value.ToArray())); + + public PotentialMethodReferenceCollector(IEnumerable methodsToFind) + { + this.methodsToFind = new HashSet(methodsToFind); + potentialMethodReferences = new(); + } + + public override void VisitIdentifierName(IdentifierNameSyntax node) + { + if (methodsToFind.FirstOrDefault(x => x.Identifier.ValueText == node.Identifier.ValueText) is { } method) + { + var referenceList = potentialMethodReferences.GetOrAdd(method, _ => new()); + referenceList.Add(node); + } + } + } +} diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs b/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs index 052bc77b511..62d4789d822 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/PackagingTests/RuleTypeMappingCS.cs @@ -3322,7 +3322,7 @@ internal static class RuleTypeMappingCS // ["S3395"], // ["S3396"], ["S3397"] = "BUG", - // ["S3398"], + ["S3398"] = "CODE_SMELL", // ["S3399"], ["S3400"] = "CODE_SMELL", // ["S3401"], diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/Rules/PrivateStaticMethodUsedOnlyByNestedClassTest.cs b/analyzers/tests/SonarAnalyzer.UnitTest/Rules/PrivateStaticMethodUsedOnlyByNestedClassTest.cs new file mode 100644 index 00000000000..26f32cea041 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/Rules/PrivateStaticMethodUsedOnlyByNestedClassTest.cs @@ -0,0 +1,60 @@ +/* + * 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 SonarAnalyzer.Rules.CSharp; + +namespace SonarAnalyzer.UnitTest.Rules; + +[TestClass] +public class PrivateStaticMethodUsedOnlyByNestedClassTest +{ + private readonly VerifierBuilder builder = new VerifierBuilder(); + + [TestMethod] + public void PrivateStaticMethodUsedOnlyByNestedClass_CS() => + builder + .AddPaths("PrivateStaticMethodUsedOnlyByNestedClass.cs") + .Verify(); + +#if NET + + [TestMethod] + public void PrivateStaticMethodUsedOnlyByNestedClass_CSharp8() => + builder + .AddPaths("PrivateStaticMethodUsedOnlyByNestedClass.CSharp8.cs") + .WithOptions(ParseOptionsHelper.FromCSharp8) + .Verify(); + +#endif + + [TestMethod] + public void PrivateStaticMethodUsedOnlyByNestedClass_CSharp9() => + builder + .AddPaths("PrivateStaticMethodUsedOnlyByNestedClass.CSharp9.cs") + .WithOptions(ParseOptionsHelper.FromCSharp9) + .Verify(); + + [TestMethod] + public void PrivateStaticMethodUsedOnlyByNestedClass_CSharp10() => + builder + .AddPaths("PrivateStaticMethodUsedOnlyByNestedClass.CSharp10.cs") + .WithOptions(ParseOptionsHelper.FromCSharp10) + .Verify(); +} diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/PrivateStaticMethodUsedOnlyByNestedClass.CSharp10.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/PrivateStaticMethodUsedOnlyByNestedClass.CSharp10.cs new file mode 100644 index 00000000000..3c9aa3e079f --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/PrivateStaticMethodUsedOnlyByNestedClass.CSharp10.cs @@ -0,0 +1,43 @@ +record class OuterRecordClass +{ + static void UsedOnlyByNestedClass() { } // Noncompliant + static void UsedOnlyByNestedRecord() { } // Noncompliant + + class NestedClass + { + void Foo() + { + UsedOnlyByNestedClass(); + } + } + + record class NestedRecord + { + void Foo() + { + UsedOnlyByNestedRecord(); + } + } +} + +record struct OuterRecordStruct +{ + private static void UsedOnlyByNestedClass() { } // Noncompliant + private static void UsedOnlyByNestedRecord() { } // Noncompliant + + class NestedClass + { + void Foo() + { + UsedOnlyByNestedClass(); + } + } + + record struct NestedRecord + { + void Foo() + { + UsedOnlyByNestedRecord(); + } + } +} diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/PrivateStaticMethodUsedOnlyByNestedClass.CSharp8.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/PrivateStaticMethodUsedOnlyByNestedClass.CSharp8.cs new file mode 100644 index 00000000000..53c6b418f0c --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/PrivateStaticMethodUsedOnlyByNestedClass.CSharp8.cs @@ -0,0 +1,27 @@ +class OuterClass +{ + static void OnlyUsedByNestedInterface() { } // Noncompliant + private protected static void PrivateProtectedMethod() { } // Compliant - method is not private + + interface INestedInterface + { + void Foo() + { + OnlyUsedByNestedInterface(); + PrivateProtectedMethod(); + } + } +} + +interface IOuterInterface +{ + static void OnlyUsedByNestedClass() { } // Noncompliant + + class NestedClass + { + void Foo() + { + OnlyUsedByNestedClass(); + } + } +} diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/PrivateStaticMethodUsedOnlyByNestedClass.CSharp9.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/PrivateStaticMethodUsedOnlyByNestedClass.CSharp9.cs new file mode 100644 index 00000000000..177ecbf9f69 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/PrivateStaticMethodUsedOnlyByNestedClass.CSharp9.cs @@ -0,0 +1,21 @@ +record OuterRecord +{ + static void UsedOnlyByNestedClass() { } // Noncompliant + static void UsedOnlyByNestedRecord() { } // Noncompliant + + class NestedClass + { + void Foo() + { + UsedOnlyByNestedClass(); + } + } + + record NestedRecord + { + void Foo() + { + UsedOnlyByNestedRecord(); + } + } +} diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/PrivateStaticMethodUsedOnlyByNestedClass.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/PrivateStaticMethodUsedOnlyByNestedClass.cs new file mode 100644 index 00000000000..6210a94aa03 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/PrivateStaticMethodUsedOnlyByNestedClass.cs @@ -0,0 +1,221 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +class OuterClass +{ + static void OnlyUsedOnceByNestedClass() { } // Noncompliant {{Move this method inside 'NestedClass'.}} + // ^^^^^^^^^^^^^^^^^^^^^^^^^ + static void OnlyUsedByNestedClassMultipleTimes() { } // Noncompliant + static void OnlyUsedByNestedClassWithClassName() { } // Noncompliant + static void UsedByMultipleSiblingNestedClasses() { } // Compliant - it needs to stay in the outer class + static void UsedByOuterAndNestedClasses() { } // Compliant - it's used by the outer class, so it needs to stay there + static void UsedBySiblingAndDeeperNestedClasses() { } // Compliant - SiblingNestedClass and DeeperNestedClass both need access to the method, so it must stay in the outer class + static void OnlyUsedByDeeperNestedClass() { } // Noncompliant {{Move this method inside 'DeeperNestedClass'.}} + static void UsedByNestedClassAndDeeperNestedClass() { } // Noncompliant {{Move this method inside 'NestedClass'.}} + static void UsedByDeeperNestedClassesOnTheSameLevel() { } // Noncompliant {{Move this method inside 'NestedClass'.}} + static void UnusedMethod() { } // Compliant - no need to move unused method anywhere + + void NotStatic() { } // Compliant - method is not static + static int _outerField; // Compliant - not a method + static int OuterProp { get; set; } // Compliant - not a method + + public static void PublicMethod() { } // Compliant - method is not private + protected static void ProtectedMethod() { } // Compliant - method is not private + internal static void InternalMethod() { } // Compliant - method is not private + protected internal static void ProtectedInternalMethod() { } // Compliant - method is not private + private static void PrivateMethod() { } // Noncompliant + private static void PrivateMethod(int arg) { } // Compliant - overloaded version of the previous method, not used anywhere + + static T GenericMethod(T arg) => arg; // Noncompliant + // ^^^^^^^^^^^^^ + + static int Recursive(int n) => Recursive(n - 1); // Noncompliant + static void MutuallyRecursive1() => MutuallyRecursive2(); // FN - both methods could be moved inisde the nested class + static void MutuallyRecursive2() => MutuallyRecursive1(); + + [DllImport("SomeLibrary.dll")] + private static extern void ExternalMethod(); // Noncompliant + + static int UsedOnlyByPropertyInNestedClass() => 42; // Noncompliant + static int UsedOnlyByFieldInitializerInNestedClass() => 42; // Noncompliant + static void UsedOnlyByConstructorInNestedClass() { } // Noncompliant + static void AssignedToDelegateInNestedClass() { } // Noncompliant + static void UsedInNameOfExpressionInNestedClass() { } // Noncompliant + + void Foo() + { + UsedByOuterAndNestedClasses(); + } + + class NestedClass + { + int _nestedField = UsedOnlyByFieldInitializerInNestedClass(); + int NestedProp => UsedOnlyByPropertyInNestedClass(); + + public NestedClass() + { + UsedOnlyByConstructorInNestedClass(); + } + + static void NestedClassMethodUsedByDeeperNestedClass() { } // Noncompliant {{Move this method inside 'DeeperNestedClass'.}} + + void Bar() + { + OnlyUsedOnceByNestedClass(); + OnlyUsedByNestedClassMultipleTimes(); + OuterClass.OnlyUsedByNestedClassWithClassName(); + UsedByMultipleSiblingNestedClasses(); + UsedByOuterAndNestedClasses(); + UsedByNestedClassAndDeeperNestedClass(); + + new OuterClass().NotStatic(); + _outerField = 42; + OuterProp = 42; + + PublicMethod(); + ProtectedMethod(); + InternalMethod(); + ProtectedInternalMethod(); + PrivateMethod(); + + GenericMethod(42); + + Recursive(42); + MutuallyRecursive1(); + + ExternalMethod(); + + Action methodDelegate = AssignedToDelegateInNestedClass; + string methodName = nameof(UsedInNameOfExpressionInNestedClass); + } + + void FooBaz() + { + OnlyUsedByNestedClassMultipleTimes(); + } + + class DeeperNestedClass + { + void FooBar() + { + OnlyUsedByDeeperNestedClass(); + UsedByNestedClassAndDeeperNestedClass(); + UsedByDeeperNestedClassesOnTheSameLevel(); + UsedBySiblingAndDeeperNestedClasses(); + NestedClassMethodUsedByDeeperNestedClass(); + } + } + + class AnotherDeeperNestedClass + { + void Foo() + { + UsedByDeeperNestedClassesOnTheSameLevel(); + } + } + } + + class SiblingNestedClass + { + void Baz() + { + UsedByMultipleSiblingNestedClasses(); + UsedBySiblingAndDeeperNestedClasses(); + } + } +} + +class ClassContainsStruct +{ + static void OnlyUsedByNestedStruct() { } // Noncompliant + + struct NestedStruct + { + void Foo() + { + OnlyUsedByNestedStruct(); + } + } +} + +struct StructContainsClass +{ + private static void OnlyUsedByNestedClass() { } // Noncompliant + + class NestedClass + { + void Foo() + { + OnlyUsedByNestedClass(); + } + } +} + +partial class PartialOuterClass +{ + static void OnlyUsedByNestedClass() { } // Compliant - partial classes are often a result of code generation, so their methods shouldn't be moved + static partial void PartialOnlyUsedByNestedClass() { } // Compliant +} + +partial class PartialOuterClass +{ + static partial void PartialOnlyUsedByNestedClass(); + + class NestedClass + { + void Foo() + { + OnlyUsedByNestedClass(); + PartialOnlyUsedByNestedClass(); + } + } +} + +[DebuggerDisplay("{UsedByDebuggerDisplay()}")] +class DebugViewClass +{ + static string UsedByDebuggerDisplay() => ""; // Noncompliant - FP: should not be moved to nested class, because it's also used by the attribute + + class NestedClass + { + void Foo() + { + UsedByDebuggerDisplay(); + } + } +} + +public class EdgeCaseWithLongCommonPaths +{ + private static void StaticMethod() { } // Noncompliant {{Move this method inside 'InsideMiddleOne'.}} + + public class MiddleOne + { + public class InsideMiddleOne + { + public class Foo + { + public class FooLeaf + { + public void Method() => StaticMethod(); + } + } + + public class Bar + { + public void Method() => StaticMethod(); + public class BarLeaf + { + public void Method() => StaticMethod(); + } + } + } + } + public class MiddleTwo { + public void StaticMethod() + { + } + public void Method() => StaticMethod(); + } +}