From ebb5525d5dbf824a2e5948c5ac6be0c13c329fbe Mon Sep 17 00:00:00 2001 From: Julien Couvreur Date: Tue, 16 Dec 2025 18:14:17 -0800 Subject: [PATCH 1/2] Extensions: fix RemoveUnusedMember scenarios --- ...rpRemoveUnusedMembersDiagnosticAnalyzer.cs | 19 +- .../RemoveUnusedMembersTests.cs | 481 ++++++++++++++++++ ...ctRemoveUnusedMembersDiagnosticAnalyzer.cs | 76 ++- ...icRemoveUnusedMembersDiagnosticAnalyzer.vb | 2 +- .../Finders/OrdinaryMethodReferenceFinder.cs | 37 +- .../Symbols/IMethodSymbolExtensions.cs | 33 ++ 6 files changed, 604 insertions(+), 44 deletions(-) diff --git a/src/Analyzers/CSharp/Analyzers/RemoveUnusedMembers/CSharpRemoveUnusedMembersDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/RemoveUnusedMembers/CSharpRemoveUnusedMembersDiagnosticAnalyzer.cs index d0c61fa9d6eb2..bc2bf7d099ac6 100644 --- a/src/Analyzers/CSharp/Analyzers/RemoveUnusedMembers/CSharpRemoveUnusedMembersDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/RemoveUnusedMembers/CSharpRemoveUnusedMembersDiagnosticAnalyzer.cs @@ -30,8 +30,23 @@ 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..08739b4b38d76 100644 --- a/src/Analyzers/CSharp/Tests/RemoveUnusedMembers/RemoveUnusedMembersTests.cs +++ b/src/Analyzers/CSharp/Tests/RemoveUnusedMembers/RemoveUnusedMembersTests.cs @@ -3607,4 +3607,485 @@ public void Dispose() LanguageVersion = LanguageVersion.CSharp13, ReferenceAssemblies = ReferenceAssemblies.Net.Net90, }.RunAsync(); + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/80645")] + public async Task PrivateExtensionBlockMethod_01() + { + // method used as extension + var code = """ + public static class C + { + public static void Test() + { + 42.M(); + } + + extension(int i) + { + private void M() { } + } + } + """; + + await new VerifyCS.Test + { + TestCode = code, + FixedCode = code, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + } + + [Fact] + public async Task PrivateExtensionBlockMethod_02() + { + // method unused + var code = """ + public static class C + { + extension(int i) + { + private void [|M|]() { } + } + } + """; + + var fixedCode = """ + public static class C + { + extension(int i) + { + } + } + """; + + await new VerifyCS.Test + { + TestCode = code, + FixedCode = fixedCode, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + } + + [Fact] + public async Task PrivateExtensionBlockMethod_03() + { + // method used via disambiguation syntax + var code = """ + public static class C + { + public static void Test() + { + C.M(42); + } + + extension(int i) + { + private void M() { } + } + } + """; + + await new VerifyCS.Test + { + TestCode = code, + FixedCode = code, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + } + + [Fact] + public async Task PrivateExtensionBlockMethod_04() + { + // method used in doc comment + var code = """ + /// + static class E + { + extension(int i) + { + private static void {|IDE0052:M|}() { } + } + } + """; + + await new VerifyCS.Test + { + TestCode = code, + FixedCode = code, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + } + + [Fact] + public async Task PrivateExtensionBlockMethod_05() + { + // implementation method used in doc comment, static + var code = """ + /// + static class E + { + extension(int) + { + private static void {|IDE0052:M|}() { } + } + } + """; + + await new VerifyCS.Test + { + TestCode = code, + FixedCode = code, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + } + + [Fact] + public async Task PrivateExtensionBlockMethod_06() + { + // implementation method used in doc comment, instance + var code = """ + /// + static class E + { + extension(int i) + { + private void {|IDE0052:M|}() { } + } + } + """; + + await new VerifyCS.Test + { + TestCode = code, + FixedCode = code, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + } + + [Fact] + public async Task PrivateExtensionBlockMethod_07() + { + // implementation method used in nameof + var code = """ + static class E + { + public static void Test() + { + _ = nameof(E.M); + } + + extension(int i) + { + private void M() { } + } + } + """; + + await new VerifyCS.Test + { + TestCode = code, + FixedCode = code, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + } + + [Fact] + public async Task PrivateExtensionBlockMethod_08() + { + // implementation method used in DebuggerDisplay attribute + var code = """ + [System.Diagnostics.DebuggerDisplayAttribute("{E.M(42)}")] + static class E + { + extension(int i) + { + private void M() { } + } + } + """; + + await new VerifyCS.Test + { + TestCode = code, + FixedCode = code, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + } + + [Fact] + public async Task PrivateExtensionBlockMethod_09() + { + // method used in deconstruction + var code = """ + 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; + } + } + """; + + await new VerifyCS.Test + { + TestCode = code, + FixedCode = code, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + } + + [Fact] + public async Task PrivateExtensionBlockMethod_10() + { + // GetEnumerator method used in foreach + var code = """ + public static class C + { + public static void Test() + { + foreach (var item in 42) + { + } + } + + extension(int i) + { + private System.Collections.IEnumerator GetEnumerator() => throw null; + } + } + """; + + await new VerifyCS.Test + { + TestCode = code, + FixedCode = code, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + } + + [Fact] + public async Task PrivateExtensionBlockProperty_01() + { + // property used as extension + var code = """ + public static class C + { + public static void Test() + { + _ = 42.Property; + } + + extension(int i) + { + private int Property => 0; + } + } + """; + + await new VerifyCS.Test + { + TestCode = code, + FixedCode = code, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + } + + [Fact] + public async Task PrivateExtensionBlockProperty_02() + { + // property unused + var code = """ + public static class C + { + extension(int i) + { + private int [|Property|] => 0; + } + } + """; + + var fixedCode = """ + public static class C + { + extension(int i) + { + } + } + """; + + await new VerifyCS.Test + { + TestCode = code, + FixedCode = fixedCode, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + } + + [Fact] + public async Task PrivateExtensionBlockProperty_03() + { + // setter of private property unused + var code = """ + public static class C + { + public static void Test() + { + _ = 42.Property; + } + + extension(int i) + { + private int Property + { + get => 0; + set { } + } + } + } + """; + + await new VerifyCS.Test + { + TestCode = code, + FixedCode = code, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + } + + [Fact] + public async Task PrivateExtensionBlockProperty_04() + { + // private setter unused + var code = """ + public static class C + { + public static void Test() + { + _ = 42.Property; + } + + extension(int i) + { + public int Property + { + get => 0; + private set { } + } + } + } + """; + + await new VerifyCS.Test + { + TestCode = code, + FixedCode = code, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + } + + [Fact] + public async Task PrivateExtensionBlockProperty_05() + { + // property used via disambiguation syntax + var code = """ + public static class C + { + public static void Test() + { + C.get_Property(42); + } + + extension(int i) + { + private int Property + { + get => 0; + } + } + } + """; + + await new VerifyCS.Test + { + TestCode = code, + FixedCode = code, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + } + + [Fact] + public async Task PrivateExtensionBlockProperty_06() + { + // property used in doc comment + var code = """ + /// + public static class E + { + extension(int i) + { + private int {|IDE0052:Property|} => 0; + } + } + """; + + await new VerifyCS.Test + { + TestCode = code, + FixedCode = code, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + } + + [Fact] + public async Task PrivateExtensionBlockProperty_07() + { + // getter implementation method used in doc comment + var code = """ + /// + public static class E + { + extension(int i) + { + private int {|IDE0052:Property|} => 0; + } + } + """; + + await new VerifyCS.Test + { + TestCode = code, + FixedCode = code, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + } + + [Fact] + public async Task PrivateExtensionBlockProperty_08() + { + // helper method used in DebuggerDisplay attribute + var code = """ + static class E + { + extension(int i) + { + [System.Diagnostics.DebuggerDisplayAttribute("{M2()}")] + public int Property => 0; + } + + private static string M2() => null; + } + """; + + await new VerifyCS.Test + { + TestCode = code, + FixedCode = code, + LanguageVersion = LanguageVersion.CSharp14, + }.RunAsync(); + } } diff --git a/src/Analyzers/Core/Analyzers/RemoveUnusedMembers/AbstractRemoveUnusedMembersDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/RemoveUnusedMembers/AbstractRemoveUnusedMembersDiagnosticAnalyzer.cs index e48834a67bdce..0e555bb809412 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) { + 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,26 @@ 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 +820,18 @@ 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); + + if (extensionBlockMethod.AssociatedSymbol is { } associatedSymbol) + { + AddIfCandidateSymbol(builder, associatedSymbol); + } + } } } } @@ -800,7 +854,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 +884,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..882619776eef2 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Extensions/Symbols/IMethodSymbolExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Extensions/Symbols/IMethodSymbolExtensions.cs @@ -15,6 +15,39 @@ 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 (associated is null) + continue; + if (!Equals(associated, methodSymbol)) + continue; + + return method; + } + } + } + + return null; + } +#endif + /// /// Returns the methodSymbol and any partial parts. /// From 930558c7a04d5c3b0089bdbe5256890fcf0560d4 Mon Sep 17 00:00:00 2001 From: Julien Couvreur Date: Thu, 18 Dec 2025 14:05:05 -0800 Subject: [PATCH 2/2] Address feedback --- ...rpRemoveUnusedMembersDiagnosticAnalyzer.cs | 2 - .../RemoveUnusedMembersTests.cs | 612 ++++++++---------- ...ctRemoveUnusedMembersDiagnosticAnalyzer.cs | 12 +- .../Symbols/IMethodSymbolExtensions.cs | 2 - 4 files changed, 263 insertions(+), 365 deletions(-) diff --git a/src/Analyzers/CSharp/Analyzers/RemoveUnusedMembers/CSharpRemoveUnusedMembersDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/RemoveUnusedMembers/CSharpRemoveUnusedMembersDiagnosticAnalyzer.cs index bc2bf7d099ac6..6da6e2d4764ec 100644 --- a/src/Analyzers/CSharp/Analyzers/RemoveUnusedMembers/CSharpRemoveUnusedMembersDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/RemoveUnusedMembers/CSharpRemoveUnusedMembersDiagnosticAnalyzer.cs @@ -37,9 +37,7 @@ protected override IEnumerable GetMembersIncludingExten if (member is ExtensionBlockDeclarationSyntax extensionBlock) { foreach (var extensionMember in extensionBlock.Members) - { yield return extensionMember; - } } else { diff --git a/src/Analyzers/CSharp/Tests/RemoveUnusedMembers/RemoveUnusedMembersTests.cs b/src/Analyzers/CSharp/Tests/RemoveUnusedMembers/RemoveUnusedMembersTests.cs index 08739b4b38d76..40ca3d4dabca1 100644 --- a/src/Analyzers/CSharp/Tests/RemoveUnusedMembers/RemoveUnusedMembersTests.cs +++ b/src/Analyzers/CSharp/Tests/RemoveUnusedMembers/RemoveUnusedMembersTests.cs @@ -3609,483 +3609,391 @@ public void Dispose() }.RunAsync(); [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/80645")] - public async Task PrivateExtensionBlockMethod_01() - { - // method used as extension - var code = """ - public static class C - { - public static void Test() + public Task PrivateExtensionBlockMethod_01() + => new VerifyCS.Test + { + // method used as extension + TestCode = """ + public static class C { - 42.M(); - } + public static void Test() + { + 42.M(); + } - extension(int i) - { - private void M() { } + extension(int i) + { + private void M() { } + } } - } - """; - - await new VerifyCS.Test - { - TestCode = code, - FixedCode = code, + """, LanguageVersion = LanguageVersion.CSharp14, }.RunAsync(); - } [Fact] - public async Task PrivateExtensionBlockMethod_02() - { - // method unused - var code = """ - public static class C - { - extension(int i) + public Task PrivateExtensionBlockMethod_02() + => new VerifyCS.Test + { + // method unused + TestCode = """ + public static class C { - private void [|M|]() { } + extension(int i) + { + private void [|M|]() { } + } } - } - """; - - var fixedCode = """ - public static class C - { - extension(int i) + """, + FixedCode = """ + public static class C { + extension(int i) + { + } } - } - """; - - await new VerifyCS.Test - { - TestCode = code, - FixedCode = fixedCode, + """, LanguageVersion = LanguageVersion.CSharp14, }.RunAsync(); - } [Fact] - public async Task PrivateExtensionBlockMethod_03() - { - // method used via disambiguation syntax - var code = """ - public static class C - { - public static void Test() + public Task PrivateExtensionBlockMethod_03() + => new VerifyCS.Test + { + // method used via disambiguation syntax + TestCode = """ + public static class C { - C.M(42); - } + public static void Test() + { + C.M(42); + } - extension(int i) - { - private void M() { } + extension(int i) + { + private void M() { } + } } - } - """; - - await new VerifyCS.Test - { - TestCode = code, - FixedCode = code, + """, LanguageVersion = LanguageVersion.CSharp14, }.RunAsync(); - } [Fact] - public async Task PrivateExtensionBlockMethod_04() - { - // method used in doc comment - var code = """ - /// - static class E - { - extension(int i) + public Task PrivateExtensionBlockMethod_04() + => new VerifyCS.Test + { + // method used in doc comment + TestCode = """ + /// + static class E { - private static void {|IDE0052:M|}() { } + extension(int i) + { + private static void {|IDE0052:M|}() { } + } } - } - """; - - await new VerifyCS.Test - { - TestCode = code, - FixedCode = code, + """, LanguageVersion = LanguageVersion.CSharp14, }.RunAsync(); - } [Fact] - public async Task PrivateExtensionBlockMethod_05() - { - // implementation method used in doc comment, static - var code = """ - /// - static class E - { - extension(int) + public Task PrivateExtensionBlockMethod_05() + => new VerifyCS.Test + { + // implementation method used in doc comment, static + TestCode = """ + /// + static class E { - private static void {|IDE0052:M|}() { } + extension(int) + { + private static void {|IDE0052:M|}() { } + } } - } - """; - - await new VerifyCS.Test - { - TestCode = code, - FixedCode = code, + """, LanguageVersion = LanguageVersion.CSharp14, }.RunAsync(); - } [Fact] - public async Task PrivateExtensionBlockMethod_06() - { - // implementation method used in doc comment, instance - var code = """ - /// - static class E - { - extension(int i) + public Task PrivateExtensionBlockMethod_06() + => new VerifyCS.Test + { + // implementation method used in doc comment, instance + TestCode = """ + /// + static class E { - private void {|IDE0052:M|}() { } + extension(int i) + { + private void {|IDE0052:M|}() { } + } } - } - """; - - await new VerifyCS.Test - { - TestCode = code, - FixedCode = code, + """, LanguageVersion = LanguageVersion.CSharp14, }.RunAsync(); - } [Fact] - public async Task PrivateExtensionBlockMethod_07() - { - // implementation method used in nameof - var code = """ - static class E - { - public static void Test() + public Task PrivateExtensionBlockMethod_07() + => new VerifyCS.Test + { + // implementation method used in nameof + TestCode = """ + static class E { - _ = nameof(E.M); - } + public static void Test() + { + _ = nameof(E.M); + } - extension(int i) - { - private void M() { } + extension(int i) + { + private void M() { } + } } - } - """; - - await new VerifyCS.Test - { - TestCode = code, - FixedCode = code, + """, LanguageVersion = LanguageVersion.CSharp14, }.RunAsync(); - } [Fact] - public async Task PrivateExtensionBlockMethod_08() - { - // implementation method used in DebuggerDisplay attribute - var code = """ - [System.Diagnostics.DebuggerDisplayAttribute("{E.M(42)}")] - static class E - { - extension(int i) + public Task PrivateExtensionBlockMethod_08() + => new VerifyCS.Test + { + // implementation method used in DebuggerDisplay attribute + TestCode = """ + [System.Diagnostics.DebuggerDisplayAttribute("{E.M(42)}")] + static class E { - private void M() { } + extension(int i) + { + private void M() { } + } } - } - """; - - await new VerifyCS.Test - { - TestCode = code, - FixedCode = code, + """, LanguageVersion = LanguageVersion.CSharp14, }.RunAsync(); - } [Fact] - public async Task PrivateExtensionBlockMethod_09() - { - // method used in deconstruction - var code = """ - public static class C - { - public static void Test() + public Task PrivateExtensionBlockMethod_09() + => new VerifyCS.Test + { + // method used in deconstruction + TestCode = """ + public static class C { - var (j, k) = 42; - } + public static void Test() + { + var (j, k) = 42; + } - extension(int i) - { - private void Deconstruct(out int j, out int k) => throw null; + extension(int i) + { + private void Deconstruct(out int j, out int k) => throw null; + } } - } - """; - - await new VerifyCS.Test - { - TestCode = code, - FixedCode = code, + """, LanguageVersion = LanguageVersion.CSharp14, }.RunAsync(); - } [Fact] - public async Task PrivateExtensionBlockMethod_10() - { - // GetEnumerator method used in foreach - var code = """ - public static class C - { - public static void Test() + public Task PrivateExtensionBlockMethod_10() + => new VerifyCS.Test + { + // GetEnumerator method used in foreach + TestCode = """ + public static class C { - foreach (var item in 42) + public static void Test() { + foreach (var item in 42) + { + } } - } - extension(int i) - { - private System.Collections.IEnumerator GetEnumerator() => throw null; + extension(int i) + { + private System.Collections.IEnumerator GetEnumerator() => throw null; + } } - } - """; - - await new VerifyCS.Test - { - TestCode = code, - FixedCode = code, + """, LanguageVersion = LanguageVersion.CSharp14, }.RunAsync(); - } [Fact] - public async Task PrivateExtensionBlockProperty_01() - { - // property used as extension - var code = """ - public static class C - { - public static void Test() + public Task PrivateExtensionBlockProperty_01() + => new VerifyCS.Test + { + // property used as extension + TestCode = """ + public static class C { - _ = 42.Property; - } + public static void Test() + { + _ = 42.Property; + } - extension(int i) - { - private int Property => 0; + extension(int i) + { + private int Property => 0; + } } - } - """; - - await new VerifyCS.Test - { - TestCode = code, - FixedCode = code, + """, LanguageVersion = LanguageVersion.CSharp14, }.RunAsync(); - } [Fact] - public async Task PrivateExtensionBlockProperty_02() - { - // property unused - var code = """ - public static class C - { - extension(int i) + public Task PrivateExtensionBlockProperty_02() + => new VerifyCS.Test + { + // property unused + TestCode = """ + public static class C { - private int [|Property|] => 0; + extension(int i) + { + private int [|Property|] => 0; + } } - } - """; - - var fixedCode = """ - public static class C - { - extension(int i) + """, + FixedCode = """ + public static class C { + extension(int i) + { + } } - } - """; - - await new VerifyCS.Test - { - TestCode = code, - FixedCode = fixedCode, + """, LanguageVersion = LanguageVersion.CSharp14, }.RunAsync(); - } [Fact] - public async Task PrivateExtensionBlockProperty_03() - { - // setter of private property unused - var code = """ - public static class C - { - public static void Test() + public Task PrivateExtensionBlockProperty_03() + => new VerifyCS.Test + { + // setter of private property unused + TestCode = """ + public static class C { - _ = 42.Property; - } + public static void Test() + { + _ = 42.Property; + } - extension(int i) - { - private int Property + extension(int i) { - get => 0; - set { } + private int Property + { + get => 0; + set { } + } } } - } - """; - - await new VerifyCS.Test - { - TestCode = code, - FixedCode = code, + """, LanguageVersion = LanguageVersion.CSharp14, }.RunAsync(); - } [Fact] - public async Task PrivateExtensionBlockProperty_04() - { - // private setter unused - var code = """ - public static class C - { - public static void Test() + public Task PrivateExtensionBlockProperty_04() + => new VerifyCS.Test + { + // private setter unused + TestCode = """ + public static class C { - _ = 42.Property; - } + public static void Test() + { + _ = 42.Property; + } - extension(int i) - { - public int Property + extension(int i) { - get => 0; - private set { } + public int Property + { + get => 0; + private set { } + } } } - } - """; - - await new VerifyCS.Test - { - TestCode = code, - FixedCode = code, + """, LanguageVersion = LanguageVersion.CSharp14, }.RunAsync(); - } [Fact] - public async Task PrivateExtensionBlockProperty_05() - { - // property used via disambiguation syntax - var code = """ - public static class C - { - public static void Test() + public Task PrivateExtensionBlockProperty_05() + => new VerifyCS.Test + { + // property used via disambiguation syntax + TestCode = """ + public static class C { - C.get_Property(42); - } + public static void Test() + { + C.get_Property(42); + } - extension(int i) - { - private int Property + extension(int i) { - get => 0; + private int Property + { + get => 0; + } } } - } - """; - - await new VerifyCS.Test - { - TestCode = code, - FixedCode = code, + """, LanguageVersion = LanguageVersion.CSharp14, }.RunAsync(); - } [Fact] - public async Task PrivateExtensionBlockProperty_06() - { - // property used in doc comment - var code = """ - /// - public static class E - { - extension(int i) + public Task PrivateExtensionBlockProperty_06() + => new VerifyCS.Test + { + // property used in doc comment + TestCode = """ + /// + public static class E { - private int {|IDE0052:Property|} => 0; + extension(int i) + { + private int {|IDE0052:Property|} => 0; + } } - } - """; - - await new VerifyCS.Test - { - TestCode = code, - FixedCode = code, + """, LanguageVersion = LanguageVersion.CSharp14, }.RunAsync(); - } [Fact] - public async Task PrivateExtensionBlockProperty_07() - { - // getter implementation method used in doc comment - var code = """ - /// - public static class E - { - extension(int i) + public Task PrivateExtensionBlockProperty_07() + => new VerifyCS.Test + { + // getter implementation method used in doc comment + TestCode = """ + /// + public static class E { - private int {|IDE0052:Property|} => 0; + extension(int i) + { + private int {|IDE0052:Property|} => 0; + } } - } - """; - - await new VerifyCS.Test - { - TestCode = code, - FixedCode = code, + """, LanguageVersion = LanguageVersion.CSharp14, }.RunAsync(); - } [Fact] - public async Task PrivateExtensionBlockProperty_08() - { - // helper method used in DebuggerDisplay attribute - var code = """ - static class E - { - extension(int i) + public Task PrivateExtensionBlockProperty_08() + => new VerifyCS.Test + { + // helper method used in DebuggerDisplay attribute + TestCode = """ + static class E { - [System.Diagnostics.DebuggerDisplayAttribute("{M2()}")] - public int Property => 0; - } - - private static string M2() => null; - } - """; + extension(int i) + { + [System.Diagnostics.DebuggerDisplayAttribute("{M2()}")] + public int Property => 0; + } - await new VerifyCS.Test - { - TestCode = code, - FixedCode = code, + 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 0e555bb809412..864b0c21e66ad 100644 --- a/src/Analyzers/Core/Analyzers/RemoveUnusedMembers/AbstractRemoveUnusedMembersDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/RemoveUnusedMembers/AbstractRemoveUnusedMembersDiagnosticAnalyzer.cs @@ -286,10 +286,10 @@ 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 GetMembersIncludingExtensionBlockMembers(namedType)) @@ -743,9 +743,7 @@ private static IEnumerable GetMembersIncludingExtensionBlockMembers(INa if (member is INamedTypeSymbol { IsExtension: true } extensionBlock) { foreach (var extensionMember in extensionBlock.GetMembers()) - { yield return extensionMember; - } } else { @@ -826,11 +824,7 @@ private void AddCandidateSymbolsReferencedInDocComments( && methodSymbol.TryGetCorrespondingExtensionBlockMethod() is { } extensionBlockMethod) { AddIfCandidateSymbol(builder, extensionBlockMethod); - - if (extensionBlockMethod.AssociatedSymbol is { } associatedSymbol) - { - AddIfCandidateSymbol(builder, associatedSymbol); - } + AddIfCandidateSymbol(builder, extensionBlockMethod.AssociatedSymbol); } } } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Extensions/Symbols/IMethodSymbolExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Extensions/Symbols/IMethodSymbolExtensions.cs index 882619776eef2..86be9e8eba2e5 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Extensions/Symbols/IMethodSymbolExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Extensions/Symbols/IMethodSymbolExtensions.cs @@ -34,8 +34,6 @@ internal static partial class IMethodSymbolExtensions if (member is IMethodSymbol method) { var associated = method.AssociatedExtensionImplementation; - if (associated is null) - continue; if (!Equals(associated, methodSymbol)) continue;