diff --git a/src/Analyzers/CSharp/Analyzers/RemoveUnusedMembers/CSharpRemoveUnusedMembersDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/RemoveUnusedMembers/CSharpRemoveUnusedMembersDiagnosticAnalyzer.cs index d0c61fa9d6eb2..6da6e2d4764ec 100644 --- a/src/Analyzers/CSharp/Analyzers/RemoveUnusedMembers/CSharpRemoveUnusedMembersDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/RemoveUnusedMembers/CSharpRemoveUnusedMembersDiagnosticAnalyzer.cs @@ -30,8 +30,21 @@ protected override IEnumerable GetTypeDeclarations(INamed .OfType(); } - protected override SyntaxList GetMembers(TypeDeclarationSyntax typeDeclaration) - => typeDeclaration.Members; + protected override IEnumerable GetMembersIncludingExtensionBlockMembers(TypeDeclarationSyntax typeDeclaration) + { + foreach (var member in typeDeclaration.Members) + { + if (member is ExtensionBlockDeclarationSyntax extensionBlock) + { + foreach (var extensionMember in extensionBlock.Members) + yield return extensionMember; + } + else + { + yield return member; + } + } + } protected override SyntaxNode GetParentIfSoleDeclarator(SyntaxNode node) { diff --git a/src/Analyzers/CSharp/Tests/RemoveUnusedMembers/RemoveUnusedMembersTests.cs b/src/Analyzers/CSharp/Tests/RemoveUnusedMembers/RemoveUnusedMembersTests.cs index 2a9fea523a670..40ca3d4dabca1 100644 --- a/src/Analyzers/CSharp/Tests/RemoveUnusedMembers/RemoveUnusedMembersTests.cs +++ b/src/Analyzers/CSharp/Tests/RemoveUnusedMembers/RemoveUnusedMembersTests.cs @@ -3607,4 +3607,393 @@ public void Dispose() LanguageVersion = LanguageVersion.CSharp13, ReferenceAssemblies = ReferenceAssemblies.Net.Net90, }.RunAsync(); + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/80645")] + public Task PrivateExtensionBlockMethod_01() + => new VerifyCS.Test + { + // method used as extension + TestCode = """ + public static class C + { + public static void Test() + { + 42.M(); + } + + extension(int i) + { + private void M() { } + } + } + """, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + + [Fact] + public Task PrivateExtensionBlockMethod_02() + => new VerifyCS.Test + { + // method unused + TestCode = """ + public static class C + { + extension(int i) + { + private void [|M|]() { } + } + } + """, + FixedCode = """ + public static class C + { + extension(int i) + { + } + } + """, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + + [Fact] + public Task PrivateExtensionBlockMethod_03() + => new VerifyCS.Test + { + // method used via disambiguation syntax + TestCode = """ + public static class C + { + public static void Test() + { + C.M(42); + } + + extension(int i) + { + private void M() { } + } + } + """, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + + [Fact] + public Task PrivateExtensionBlockMethod_04() + => new VerifyCS.Test + { + // method used in doc comment + TestCode = """ + /// + static class E + { + extension(int i) + { + private static void {|IDE0052:M|}() { } + } + } + """, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + + [Fact] + public Task PrivateExtensionBlockMethod_05() + => new VerifyCS.Test + { + // implementation method used in doc comment, static + TestCode = """ + /// + static class E + { + extension(int) + { + private static void {|IDE0052:M|}() { } + } + } + """, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + + [Fact] + public Task PrivateExtensionBlockMethod_06() + => new VerifyCS.Test + { + // implementation method used in doc comment, instance + TestCode = """ + /// + static class E + { + extension(int i) + { + private void {|IDE0052:M|}() { } + } + } + """, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + + [Fact] + public Task PrivateExtensionBlockMethod_07() + => new VerifyCS.Test + { + // implementation method used in nameof + TestCode = """ + static class E + { + public static void Test() + { + _ = nameof(E.M); + } + + extension(int i) + { + private void M() { } + } + } + """, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + + [Fact] + public Task PrivateExtensionBlockMethod_08() + => new VerifyCS.Test + { + // implementation method used in DebuggerDisplay attribute + TestCode = """ + [System.Diagnostics.DebuggerDisplayAttribute("{E.M(42)}")] + static class E + { + extension(int i) + { + private void M() { } + } + } + """, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + + [Fact] + public Task PrivateExtensionBlockMethod_09() + => new VerifyCS.Test + { + // method used in deconstruction + TestCode = """ + public static class C + { + public static void Test() + { + var (j, k) = 42; + } + + extension(int i) + { + private void Deconstruct(out int j, out int k) => throw null; + } + } + """, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + + [Fact] + public Task PrivateExtensionBlockMethod_10() + => new VerifyCS.Test + { + // GetEnumerator method used in foreach + TestCode = """ + public static class C + { + public static void Test() + { + foreach (var item in 42) + { + } + } + + extension(int i) + { + private System.Collections.IEnumerator GetEnumerator() => throw null; + } + } + """, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + + [Fact] + public Task PrivateExtensionBlockProperty_01() + => new VerifyCS.Test + { + // property used as extension + TestCode = """ + public static class C + { + public static void Test() + { + _ = 42.Property; + } + + extension(int i) + { + private int Property => 0; + } + } + """, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + + [Fact] + public Task PrivateExtensionBlockProperty_02() + => new VerifyCS.Test + { + // property unused + TestCode = """ + public static class C + { + extension(int i) + { + private int [|Property|] => 0; + } + } + """, + FixedCode = """ + public static class C + { + extension(int i) + { + } + } + """, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + + [Fact] + public Task PrivateExtensionBlockProperty_03() + => new VerifyCS.Test + { + // setter of private property unused + TestCode = """ + public static class C + { + public static void Test() + { + _ = 42.Property; + } + + extension(int i) + { + private int Property + { + get => 0; + set { } + } + } + } + """, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + + [Fact] + public Task PrivateExtensionBlockProperty_04() + => new VerifyCS.Test + { + // private setter unused + TestCode = """ + public static class C + { + public static void Test() + { + _ = 42.Property; + } + + extension(int i) + { + public int Property + { + get => 0; + private set { } + } + } + } + """, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + + [Fact] + public Task PrivateExtensionBlockProperty_05() + => new VerifyCS.Test + { + // property used via disambiguation syntax + TestCode = """ + public static class C + { + public static void Test() + { + C.get_Property(42); + } + + extension(int i) + { + private int Property + { + get => 0; + } + } + } + """, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + + [Fact] + public Task PrivateExtensionBlockProperty_06() + => new VerifyCS.Test + { + // property used in doc comment + TestCode = """ + /// + public static class E + { + extension(int i) + { + private int {|IDE0052:Property|} => 0; + } + } + """, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + + [Fact] + public Task PrivateExtensionBlockProperty_07() + => new VerifyCS.Test + { + // getter implementation method used in doc comment + TestCode = """ + /// + public static class E + { + extension(int i) + { + private int {|IDE0052:Property|} => 0; + } + } + """, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + + [Fact] + public Task PrivateExtensionBlockProperty_08() + => new VerifyCS.Test + { + // helper method used in DebuggerDisplay attribute + TestCode = """ + static class E + { + extension(int i) + { + [System.Diagnostics.DebuggerDisplayAttribute("{M2()}")] + public int Property => 0; + } + + private static string M2() => null; + } + """, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); } diff --git a/src/Analyzers/Core/Analyzers/RemoveUnusedMembers/AbstractRemoveUnusedMembersDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/RemoveUnusedMembers/AbstractRemoveUnusedMembersDiagnosticAnalyzer.cs index e48834a67bdce..864b0c21e66ad 100644 --- a/src/Analyzers/Core/Analyzers/RemoveUnusedMembers/AbstractRemoveUnusedMembersDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/RemoveUnusedMembers/AbstractRemoveUnusedMembersDiagnosticAnalyzer.cs @@ -62,7 +62,9 @@ internal abstract class AbstractRemoveUnusedMembersDiagnosticAnalyzer< protected abstract ISemanticFacts SemanticFacts { get; } protected abstract IEnumerable GetTypeDeclarations(INamedTypeSymbol namedType, CancellationToken cancellationToken); - protected abstract SyntaxList GetMembers(TTypeDeclarationSyntax typeDeclaration); + + // We analyze extension block members as part of the enclosing static class. + protected abstract IEnumerable GetMembersIncludingExtensionBlockMembers(TTypeDeclarationSyntax typeDeclaration); protected abstract SyntaxNode GetParentIfSoleDeclarator(SyntaxNode declaration); /// @@ -272,7 +274,11 @@ private void RegisterActions(CompilationStartAnalysisContext compilationStartCon OperationKind.DynamicMemberReference, OperationKind.DynamicObjectCreation); - symbolStartContext.RegisterSymbolEndAction(symbolEndContext => OnSymbolEnd(symbolEndContext, hasUnsupportedOperation)); + // We analyze extension block members as part of the enclosing static class. + if (symbolStartContext.Symbol is not INamedTypeSymbol { IsExtension: true }) + { + symbolStartContext.RegisterSymbolEndAction(symbolEndContext => OnSymbolEnd(symbolEndContext, hasUnsupportedOperation)); + } // Register custom language-specific actions, if any. _analyzer.HandleNamedTypeSymbolStart(symbolStartContext, onSymbolUsageFound); @@ -280,8 +286,13 @@ private void RegisterActions(CompilationStartAnalysisContext compilationStartCon bool ShouldAnalyze(SymbolStartAnalysisContext context, INamedTypeSymbol namedType) { + // Extension members are analyzed as part of the enclosing static class. + // When we enter the scope of the enclosing static class, we'll analyze extension members there. + if (namedType.IsExtension) + return false; + // Check if we have at least one candidate symbol in analysis scope. - foreach (var member in namedType.GetMembers()) + foreach (var member in GetMembersIncludingExtensionBlockMembers(namedType)) { if (IsCandidateSymbol(member) && context.ShouldAnalyzeLocation(GetDiagnosticLocation(member))) @@ -548,6 +559,19 @@ private void AnalyzeInvocationOperation(OperationAnalysisContext operationContex // method from which it was reduced as "used". if (targetMethod.ReducedFrom != null) OnSymbolUsage(targetMethod.ReducedFrom, ValueUsageInfo.Read); + + // If the invoked method is an implementation method for an extension member, + // also mark that extension member as "used". + // If the extension member is an accessor, also mark its associated property as "used". + if (targetMethod.TryGetCorrespondingExtensionBlockMethod() is { } extensionBlockMethod) + { + OnSymbolUsage(extensionBlockMethod, ValueUsageInfo.Read); + + if (extensionBlockMethod.AssociatedSymbol is { } associatedSymbol) + { + OnSymbolUsage(associatedSymbol, ValueUsageInfo.Read); + } + } } private void AnalyzeNameOfOperation(OperationAnalysisContext operationContext) @@ -605,7 +629,7 @@ private void OnSymbolEnd(SymbolAnalysisContext symbolEndContext, bool hasUnsuppo var isInlineArray = namedType.HasAttribute(_inlineArrayAttributeType); - foreach (var member in namedType.GetMembers()) + foreach (var member in GetMembersIncludingExtensionBlockMembers(namedType)) { if (SymbolEqualityComparer.Default.Equals(entryPoint, member)) continue; @@ -710,6 +734,24 @@ private void OnSymbolEnd(SymbolAnalysisContext symbolEndContext, bool hasUnsuppo } } + // We analyze extension block members as part of the enclosing static class. + private static IEnumerable GetMembersIncludingExtensionBlockMembers(INamedTypeSymbol namedType) + { + Debug.Assert(!namedType.IsExtension); + foreach (var member in namedType.GetMembers()) + { + if (member is INamedTypeSymbol { IsExtension: true } extensionBlock) + { + foreach (var extensionMember in extensionBlock.GetMembers()) + yield return extensionMember; + } + else + { + yield return member; + } + } + } + private static LocalizableString GetMessage( DiagnosticDescriptor rule, ISymbol member, @@ -776,8 +818,14 @@ private void AddCandidateSymbolsReferencedInDocComments( lazyModel ??= compilation.GetSemanticModel(syntaxTree); var symbol = lazyModel.GetSymbolInfo(node, cancellationToken).Symbol; - if (IsCandidateSymbol(symbol)) - builder.Add(symbol); + AddIfCandidateSymbol(builder, symbol); + + if (symbol is IMethodSymbol methodSymbol + && methodSymbol.TryGetCorrespondingExtensionBlockMethod() is { } extensionBlockMethod) + { + AddIfCandidateSymbol(builder, extensionBlockMethod); + AddIfCandidateSymbol(builder, extensionBlockMethod.AssociatedSymbol); + } } } } @@ -800,7 +848,7 @@ void AddAllDocumentationComments() AddDocumentationComments(currentType, documentationComments); // Walk each member - foreach (var member in _analyzer.GetMembers(currentType)) + foreach (var member in _analyzer.GetMembersIncludingExtensionBlockMembers(currentType)) { if (member is TTypeDeclarationSyntax childType) { @@ -830,13 +878,19 @@ static void AddDocumentationComments( documentationComments.AddIfNotNull(trivia.GetStructure() as TDocumentationCommentTriviaSyntax); } } + + void AddIfCandidateSymbol(HashSet builder, ISymbol? symbol) + { + if (IsCandidateSymbol(symbol)) + builder.Add(symbol); + } } private void AddDebuggerDisplayAttributeArguments(INamedTypeSymbol namedTypeSymbol, ArrayBuilder builder) { AddDebuggerDisplayAttributeArgumentsCore(namedTypeSymbol, builder); - foreach (var member in namedTypeSymbol.GetMembers()) + foreach (var member in GetMembersIncludingExtensionBlockMembers(namedTypeSymbol)) { switch (member) { diff --git a/src/Analyzers/VisualBasic/Analyzers/RemoveUnusedMembers/VisualBasicRemoveUnusedMembersDiagnosticAnalyzer.vb b/src/Analyzers/VisualBasic/Analyzers/RemoveUnusedMembers/VisualBasicRemoveUnusedMembersDiagnosticAnalyzer.vb index fdb5b329bf038..13890c1096d92 100644 --- a/src/Analyzers/VisualBasic/Analyzers/RemoveUnusedMembers/VisualBasicRemoveUnusedMembersDiagnosticAnalyzer.vb +++ b/src/Analyzers/VisualBasic/Analyzers/RemoveUnusedMembers/VisualBasicRemoveUnusedMembersDiagnosticAnalyzer.vb @@ -59,7 +59,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.RemoveUnusedMembers OfType(Of TypeBlockSyntax) End Function - Protected Overrides Function GetMembers(typeDeclaration As TypeBlockSyntax) As SyntaxList(Of StatementSyntax) + Protected Overrides Function GetMembersIncludingExtensionBlockMembers(typeDeclaration As TypeBlockSyntax) As IEnumerable(Of StatementSyntax) Return typeDeclaration.Members End Function diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/OrdinaryMethodReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/OrdinaryMethodReferenceFinder.cs index eb3ae5d47aa1a..6c65d0a37a15d 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/OrdinaryMethodReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/OrdinaryMethodReferenceFinder.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Extensions; namespace Microsoft.CodeAnalysis.FindSymbols.Finders; @@ -38,41 +39,11 @@ protected override ValueTask> DetermineCascadedSymbolsAs // If the given symbol is an extension member, cascade to its implementation method result.AddIfNotNull(symbol.AssociatedExtensionImplementation); - CascadeFromExtensionImplementation(symbol, result); - - return new(result.ToImmutableAndClear()); - } - - private static void CascadeFromExtensionImplementation(IMethodSymbol symbol, ArrayBuilder result) - { // If the given symbol is an implementation method of an extension member, cascade to the extension member itself - var containingType = symbol.ContainingType; - if (symbol is not { IsStatic: true, IsImplicitlyDeclared: true, ContainingType.MightContainExtensionMethods: true }) - return; - - // Having a compiler API to go from implementation method back to its corresponding extension member would be useful - // Tracked by https://github.com/dotnet/roslyn/issues/81686 + if (symbol.TryGetCorrespondingExtensionBlockMethod() is IMethodSymbol method) + result.Add(method); - foreach (var nestedType in containingType.GetTypeMembers()) - { - if (!nestedType.IsExtension || nestedType.ExtensionParameter is null) - continue; - - foreach (var member in nestedType.GetMembers()) - { - if (member is IMethodSymbol method) - { - var associated = method.AssociatedExtensionImplementation; - if (associated is null) - continue; - if (!Equals(associated, symbol)) - continue; - - result.Add(method); - return; - } - } - } + return new(result.ToImmutableAndClear()); } private static ImmutableArray GetOtherPartsOfPartial(IMethodSymbol symbol) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Extensions/Symbols/IMethodSymbolExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Extensions/Symbols/IMethodSymbolExtensions.cs index e3a8d463de519..86be9e8eba2e5 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Extensions/Symbols/IMethodSymbolExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Extensions/Symbols/IMethodSymbolExtensions.cs @@ -15,6 +15,37 @@ namespace Microsoft.CodeAnalysis.Shared.Extensions; internal static partial class IMethodSymbolExtensions { +#if !ROSLYN_4_12_OR_LOWER + internal static IMethodSymbol? TryGetCorrespondingExtensionBlockMethod(this IMethodSymbol methodSymbol) + { + if (methodSymbol is not { IsStatic: true, IsImplicitlyDeclared: true, ContainingType.MightContainExtensionMethods: true }) + return null; + + // Having a compiler API to go from implementation method back to its corresponding extension member would be useful + // Tracked by https://github.com/dotnet/roslyn/issues/81686 + + foreach (var nestedType in methodSymbol.ContainingType.GetTypeMembers()) + { + if (!nestedType.IsExtension || nestedType.ExtensionParameter is null) + continue; + + foreach (var member in nestedType.GetMembers()) + { + if (member is IMethodSymbol method) + { + var associated = method.AssociatedExtensionImplementation; + if (!Equals(associated, methodSymbol)) + continue; + + return method; + } + } + } + + return null; + } +#endif + /// /// Returns the methodSymbol and any partial parts. ///