diff --git a/src/Analyzers/CSharp/Analyzers/RemoveUnusedMembers/CSharpRemoveUnusedMembersDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/RemoveUnusedMembers/CSharpRemoveUnusedMembersDiagnosticAnalyzer.cs index 2bb616b174829..55ed47da767b2 100644 --- a/src/Analyzers/CSharp/Analyzers/RemoveUnusedMembers/CSharpRemoveUnusedMembersDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/RemoveUnusedMembers/CSharpRemoveUnusedMembersDiagnosticAnalyzer.cs @@ -8,11 +8,12 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.RemoveUnusedMembers; +using Microsoft.CodeAnalysis.Shared.Extensions; namespace Microsoft.CodeAnalysis.CSharp.RemoveUnusedMembers; [DiagnosticAnalyzer(LanguageNames.CSharp)] -internal class CSharpRemoveUnusedMembersDiagnosticAnalyzer +internal sealed class CSharpRemoveUnusedMembersDiagnosticAnalyzer : AbstractRemoveUnusedMembersDiagnosticAnalyzer< DocumentationCommentTriviaSyntax, IdentifierNameSyntax, @@ -28,4 +29,18 @@ protected override IEnumerable GetTypeDeclarations(INamed protected override SyntaxList GetMembers(TypeDeclarationSyntax typeDeclaration) => typeDeclaration.Members; + + protected override SyntaxNode GetParentIfSoleDeclarator(SyntaxNode node) + { + return node switch + { + VariableDeclaratorSyntax variableDeclarator + => variableDeclarator.Parent is VariableDeclarationSyntax + { + Parent: FieldDeclarationSyntax { Declaration.Variables.Count: 0 } or + EventFieldDeclarationSyntax { Declaration.Variables.Count: 0 } + } declaration ? declaration.GetRequiredParent() : node, + _ => node, + }; + } } diff --git a/src/Analyzers/CSharp/Tests/RemoveUnusedMembers/RemoveUnusedMembersTests.cs b/src/Analyzers/CSharp/Tests/RemoveUnusedMembers/RemoveUnusedMembersTests.cs index 2bcf767b9dfeb..c06801cfe9adc 100644 --- a/src/Analyzers/CSharp/Tests/RemoveUnusedMembers/RemoveUnusedMembersTests.cs +++ b/src/Analyzers/CSharp/Tests/RemoveUnusedMembers/RemoveUnusedMembersTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Linq; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CSharp; @@ -16,6 +15,7 @@ namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.RemoveUnusedMembers; +using static Microsoft.CodeAnalysis.CSharp.UsePatternCombinators.AnalyzedPattern; using VerifyCS = CSharpCodeFixVerifier< CSharpRemoveUnusedMembersDiagnosticAnalyzer, CSharpRemoveUnusedMembersCodeFixProvider>; @@ -30,19 +30,20 @@ public void TestStandardProperty(AnalyzerProperty property) [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/31582")] public async Task FieldReadViaSuppression() { - var code = """ - #nullable enable - class MyClass - { - string? _field = null; - public void M() + await new VerifyCS.Test + { + TestCode = """ + #nullable enable + class MyClass { - _field!.ToString(); + string? _field = null; + public void M() + { + _field!.ToString(); + } } - } - """; - - await VerifyCS.VerifyCodeFixAsync(code, code); + """ + }.RunAsync(); } [Theory] @@ -53,14 +54,16 @@ public void M() [InlineData("private protected")] public async Task NonPrivateField(string accessibility) { - var code = $$""" - class MyClass - { - {{accessibility}} int _goo; - } - """; + await new VerifyCS.Test + { + TestCode = $$""" + class MyClass + { + {{accessibility}} int _goo; + } + """, + }.RunAsync(); - await VerifyCS.VerifyCodeFixAsync(code, code); } [Theory] @@ -71,14 +74,15 @@ class MyClass [InlineData("private protected")] public async Task NonPrivateFieldWithConstantInitializer(string accessibility) { - var code = $$""" - class MyClass - { - {{accessibility}} int _goo = 0; - } - """; - - await VerifyCS.VerifyCodeFixAsync(code, code); + await new VerifyCS.Test + { + TestCode = $$""" + class MyClass + { + {{accessibility}} int _goo = 0; + } + """, + }.RunAsync(); } [Theory] @@ -89,15 +93,16 @@ class MyClass [InlineData("private protected")] public async Task NonPrivateFieldWithNonConstantInitializer(string accessibility) { - var code = $$""" - class MyClass - { - {{accessibility}} int _goo = _goo2; - private static readonly int _goo2 = 0; - } - """; - - await VerifyCS.VerifyCodeFixAsync(code, code); + await new VerifyCS.Test + { + TestCode = $$""" + class MyClass + { + {{accessibility}} int _goo = _goo2; + private static readonly int _goo2 = 0; + } + """, + }.RunAsync(); } [Theory] @@ -1462,30 +1467,26 @@ public void M() [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/33994")] public async Task PropertyIsOnlyWritten() { - var source = - """ - class MyClass - { - private int {|#0:P|} { get; set; } - public void M() - { - P = 0; - } - } - """; - - var descriptor = new CSharpRemoveUnusedMembersDiagnosticAnalyzer().SupportedDiagnostics.First(x => x.Id == "IDE0052"); - var expectedMessage = string.Format(AnalyzersResources.Private_property_0_can_be_converted_to_a_method_as_its_get_accessor_is_never_invoked, "MyClass.P"); - await new VerifyCS.Test { - TestCode = source, + TestCode = """ + class MyClass + { + private int {|#0:P|} { get; set; } + public void M() + { + P = 0; + } + } + """, ExpectedDiagnostics = { // Test0.cs(3,17): info IDE0052: Private property 'MyClass.P' can be converted to a method as its get accessor is never invoked. - VerifyCS.Diagnostic(descriptor).WithMessage(expectedMessage).WithLocation(0), + VerifyCS + .Diagnostic(new CSharpRemoveUnusedMembersDiagnosticAnalyzer().SupportedDiagnostics.First(x => x.Id == "IDE0052")) + .WithMessage(string.Format(AnalyzersResources.Private_property_0_can_be_converted_to_a_method_as_its_get_accessor_is_never_invoked, "MyClass.P")) + .WithLocation(0), }, - FixedCode = source, }.RunAsync(); } @@ -1745,20 +1746,21 @@ class MyClass } [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/43191")] - public async Task PropertyIsIncrementedAndValueDropped_VerifyAnalizerMessage() + public async Task PropertyIsIncrementedAndValueDropped_VerifyAnalyzerMessage() { var code = """ class MyClass { - private int P { get; set; } + private int {|#0:P|} { get; set; } public void M1() { ++P; } } """; await VerifyCS.VerifyAnalyzerAsync(code, new DiagnosticResult( CSharpRemoveUnusedMembersDiagnosticAnalyzer.s_removeUnreadMembersRule) - .WithSpan(3, 17, 3, 18) - .WithArguments("MyClass.P")); + .WithLocation(0) + .WithArguments("MyClass.P") + .WithOptions(DiagnosticOptions.IgnoreAdditionalLocations)); } [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/43191")] @@ -1805,20 +1807,19 @@ class MyClass } [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/43191")] - public async Task IndexerIsIncrementedAndValueDropped_VerifyAnalizerMessage() + public async Task IndexerIsIncrementedAndValueDropped_VerifyAnalyzerMessage() { - var code = """ + await VerifyCS.VerifyAnalyzerAsync(""" class MyClass { - private int this[int x] { get { return 0; } set { } } + private int {|#0:this|}[int x] { get { return 0; } set { } } public void M1(int x) => ++this[x]; } - """; - - await VerifyCS.VerifyAnalyzerAsync(code, new DiagnosticResult( - CSharpRemoveUnusedMembersDiagnosticAnalyzer.s_removeUnreadMembersRule) - .WithSpan(3, 17, 3, 21) - .WithArguments("MyClass.this")); + """, new DiagnosticResult( + CSharpRemoveUnusedMembersDiagnosticAnalyzer.s_removeUnreadMembersRule) + .WithLocation(0) + .WithArguments("MyClass.this") + .WithOptions(DiagnosticOptions.IgnoreAdditionalLocations)); } [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/43191")] @@ -1921,20 +1922,21 @@ class MyClass } [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/43191")] - public async Task PropertyIsTargetOfCompoundAssignmentAndValueDropped_VerifyAnalizerMessage() + public async Task PropertyIsTargetOfCompoundAssignmentAndValueDropped_VerifyAnalyzerMessage() { var code = """ class MyClass { - private int P { get; set; } + private int {|#0:P|} { get; set; } public void M1(int x) { P += x; } } """; await VerifyCS.VerifyAnalyzerAsync(code, new DiagnosticResult( CSharpRemoveUnusedMembersDiagnosticAnalyzer.s_removeUnreadMembersRule) - .WithSpan(3, 17, 3, 18) - .WithArguments("MyClass.P")); + .WithLocation(0) + .WithArguments("MyClass.P") + .WithOptions(DiagnosticOptions.IgnoreAdditionalLocations)); } [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/43191")] @@ -1986,15 +1988,16 @@ public async Task IndexerIsTargetOfCompoundAssignmentAndValueDropped_VerifyAnaly var code = """ class MyClass { - private int this[int x] { get { return 0; } set { } } + private int {|#0:this|}[int x] { get { return 0; } set { } } public void M1(int x, int y) => this[x] += y; } """; await VerifyCS.VerifyAnalyzerAsync(code, new DiagnosticResult( CSharpRemoveUnusedMembersDiagnosticAnalyzer.s_removeUnreadMembersRule) - .WithSpan(3, 17, 3, 21) - .WithArguments("MyClass.this")); + .WithLocation(0) + .WithArguments("MyClass.this") + .WithOptions(DiagnosticOptions.IgnoreAdditionalLocations)); } [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/43191")] @@ -3173,15 +3176,18 @@ await VerifyCS.VerifyAnalyzerAsync( """ class C { - private C(int i) { } + private {|#0:C|}(int i) { } } """, -// /0/Test0.cs(3,13): info IDE0051: Private member 'C.C' is unused -VerifyCS.Diagnostic("IDE0051").WithSpan(3, 13, 3, 14).WithArguments("C.C")); + // /0/Test0.cs(3,13): info IDE0051: Private member 'C.C' is unused + VerifyCS.Diagnostic("IDE0051") + .WithLocation(0) + .WithArguments("C.C") + .WithOptions(DiagnosticOptions.IgnoreAdditionalLocations)); } [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/62856")] - public async Task DontWarnForAwaiterMethods() + public async Task DoNotWarnForAwaiterMethods() { const string code = """ using System; diff --git a/src/Analyzers/Core/Analyzers/Helpers/DiagnosticHelper.cs b/src/Analyzers/Core/Analyzers/Helpers/DiagnosticHelper.cs index 151a02031558b..e108c76b24797 100644 --- a/src/Analyzers/Core/Analyzers/Helpers/DiagnosticHelper.cs +++ b/src/Analyzers/Core/Analyzers/Helpers/DiagnosticHelper.cs @@ -47,21 +47,22 @@ public static Diagnostic Create( params object[] messageArgs) { if (descriptor == null) - { throw new ArgumentNullException(nameof(descriptor)); - } - LocalizableString message; + var message = CreateMessage(descriptor, messageArgs); + return CreateWithMessage(descriptor, location, notificationOption, analyzerOptions, additionalLocations, properties, message); + } + + private static LocalizableString CreateMessage(DiagnosticDescriptor descriptor, object[] messageArgs) + { if (messageArgs == null || messageArgs.Length == 0) { - message = descriptor.MessageFormat; + return descriptor.MessageFormat; } else { - message = new LocalizableStringWithArguments(descriptor.MessageFormat, messageArgs); + return new LocalizableStringWithArguments(descriptor.MessageFormat, messageArgs); } - - return CreateWithMessage(descriptor, location, notificationOption, analyzerOptions, additionalLocations, properties, message); } /// @@ -92,11 +93,28 @@ public static Diagnostic CreateWithLocationTags( ImmutableArray additionalLocations, ImmutableArray additionalUnnecessaryLocations, params object[] messageArgs) + { + return CreateWithLocationTags( + descriptor, + location, + notificationOption, + analyzerOptions, + CreateMessage(descriptor, messageArgs), + additionalLocations, + additionalUnnecessaryLocations); + } + + private static Diagnostic CreateWithLocationTags( + DiagnosticDescriptor descriptor, + Location location, + NotificationOption2 notificationOption, + AnalyzerOptions analyzerOptions, + LocalizableString message, + ImmutableArray additionalLocations, + ImmutableArray additionalUnnecessaryLocations) { if (additionalUnnecessaryLocations.IsEmpty) - { - return Create(descriptor, location, notificationOption, analyzerOptions, additionalLocations, ImmutableDictionary.Empty, messageArgs); - } + return CreateWithMessage(descriptor, location, notificationOption, analyzerOptions, additionalLocations, ImmutableDictionary.Empty, message); var tagIndices = ImmutableDictionary>.Empty .Add(WellKnownDiagnosticTags.Unnecessary, Enumerable.Range(additionalLocations.Length, additionalUnnecessaryLocations.Length)); @@ -105,10 +123,10 @@ public static Diagnostic CreateWithLocationTags( location, notificationOption, analyzerOptions, + message, additionalLocations.AddRange(additionalUnnecessaryLocations), tagIndices, - ImmutableDictionary.Empty, - messageArgs); + ImmutableDictionary.Empty); } /// @@ -144,11 +162,30 @@ public static Diagnostic CreateWithLocationTags( ImmutableArray additionalUnnecessaryLocations, ImmutableDictionary? properties, params object[] messageArgs) + { + return CreateWithLocationTags( + descriptor, + location, + notificationOption, + analyzerOptions, + CreateMessage(descriptor, messageArgs), + additionalLocations, + additionalUnnecessaryLocations, + properties); + } + + public static Diagnostic CreateWithLocationTags( + DiagnosticDescriptor descriptor, + Location location, + NotificationOption2 notificationOption, + AnalyzerOptions analyzerOptions, + LocalizableString message, + ImmutableArray additionalLocations, + ImmutableArray additionalUnnecessaryLocations, + ImmutableDictionary? properties) { if (additionalUnnecessaryLocations.IsEmpty) - { - return Create(descriptor, location, notificationOption, analyzerOptions, additionalLocations, properties, messageArgs); - } + return CreateWithMessage(descriptor, location, notificationOption, analyzerOptions, additionalLocations, properties, message); var tagIndices = ImmutableDictionary>.Empty .Add(WellKnownDiagnosticTags.Unnecessary, Enumerable.Range(additionalLocations.Length, additionalUnnecessaryLocations.Length)); @@ -157,41 +194,21 @@ public static Diagnostic CreateWithLocationTags( location, notificationOption, analyzerOptions, + message, additionalLocations.AddRange(additionalUnnecessaryLocations), tagIndices, - properties, - messageArgs); + properties); } - /// - /// Create a diagnostic that adds properties specifying a tag for a set of locations. - /// - /// A describing the diagnostic. - /// An optional primary location of the diagnostic. If null, will return . - /// Notification option for the diagnostic. - /// - /// An optional set of additional locations related to the diagnostic. - /// Typically, these are locations of other items referenced in the message. - /// - /// - /// a map of location tag to index in additional locations. - /// "AbstractRemoveUnnecessaryParenthesesDiagnosticAnalyzer" for an example of usage. - /// - /// - /// An optional set of name-value pairs by means of which the analyzer that creates the diagnostic - /// can convey more detailed information to the fixer. - /// - /// Arguments to the message of the diagnostic. - /// The instance. private static Diagnostic CreateWithLocationTags( DiagnosticDescriptor descriptor, Location location, NotificationOption2 notificationOption, AnalyzerOptions analyzerOptions, + LocalizableString message, IEnumerable additionalLocations, IDictionary> tagIndices, - ImmutableDictionary? properties, - params object[] messageArgs) + ImmutableDictionary? properties) { Contract.ThrowIfTrue(additionalLocations.IsEmpty()); Contract.ThrowIfTrue(tagIndices.IsEmpty()); @@ -199,7 +216,7 @@ private static Diagnostic CreateWithLocationTags( properties ??= ImmutableDictionary.Empty; properties = properties.AddRange(tagIndices.Select(kvp => new KeyValuePair(kvp.Key, EncodeIndices(kvp.Value, additionalLocations.Count())))); - return Create(descriptor, location, notificationOption, analyzerOptions, additionalLocations, properties, messageArgs); + return CreateWithMessage(descriptor, location, notificationOption, analyzerOptions, additionalLocations, properties, message); static string EncodeIndices(IEnumerable indices, int additionalLocationsLength) { diff --git a/src/Analyzers/Core/Analyzers/RemoveUnusedMembers/AbstractRemoveUnusedMembersDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/RemoveUnusedMembers/AbstractRemoveUnusedMembersDiagnosticAnalyzer.cs index 7dcd15beb0c86..814efb24c087c 100644 --- a/src/Analyzers/Core/Analyzers/RemoveUnusedMembers/AbstractRemoveUnusedMembersDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/RemoveUnusedMembers/AbstractRemoveUnusedMembersDiagnosticAnalyzer.cs @@ -64,6 +64,7 @@ protected AbstractRemoveUnusedMembersDiagnosticAnalyzer() protected abstract IEnumerable GetTypeDeclarations(INamedTypeSymbol namedType, CancellationToken cancellationToken); protected abstract SyntaxList GetMembers(TTypeDeclarationSyntax typeDeclaration); + protected abstract SyntaxNode GetParentIfSoleDeclarator(SyntaxNode declaration); // We need to analyze the whole document even for edits within a method body, // because we might add or remove references to members in executable code. @@ -428,6 +429,7 @@ private void AnalyzeObjectCreationOperation(OperationAnalysisContext operationCo private void OnSymbolEnd(SymbolAnalysisContext symbolEndContext, bool hasUnsupportedOperation) { + var cancellationToken = symbolEndContext.CancellationToken; if (hasUnsupportedOperation) { return; @@ -445,7 +447,7 @@ private void OnSymbolEnd(SymbolAnalysisContext symbolEndContext, bool hasUnsuppo using var _1 = PooledHashSet.GetInstance(out var symbolsReferencedInDocComments); using var _2 = ArrayBuilder.GetInstance(out var debuggerDisplayAttributeArguments); - var entryPoint = symbolEndContext.Compilation.GetEntryPoint(symbolEndContext.CancellationToken); + var entryPoint = symbolEndContext.Compilation.GetEntryPoint(cancellationToken); var namedType = (INamedTypeSymbol)symbolEndContext.Symbol; foreach (var member in namedType.GetMembers()) @@ -467,7 +469,7 @@ private void OnSymbolEnd(SymbolAnalysisContext symbolEndContext, bool hasUnsuppo { // Bail out if there are syntax errors in any of the declarations of the containing type. // Note that we check this only for the first time that we report an unused or unread member for the containing type. - if (HasSyntaxErrors(namedType, symbolEndContext.CancellationToken)) + if (HasSyntaxErrors(namedType, cancellationToken)) { return; } @@ -475,7 +477,7 @@ private void OnSymbolEnd(SymbolAnalysisContext symbolEndContext, bool hasUnsuppo // Compute the set of candidate symbols referenced in all the documentation comments within the named type declarations. // This set is computed once and used for all the iterations of the loop. AddCandidateSymbolsReferencedInDocComments( - namedType, symbolEndContext.Compilation, symbolsReferencedInDocComments, symbolEndContext.CancellationToken); + namedType, symbolEndContext.Compilation, symbolsReferencedInDocComments, cancellationToken); // Compute the set of string arguments to DebuggerDisplay attributes applied to any symbol within the named type declaration. // These strings may have an embedded reference to the symbol. @@ -509,50 +511,64 @@ member is IPropertySymbol property && continue; } + // We change the message only if both 'get' and 'set' accessors are present and + // there are no shadow 'get' accessor usages. Otherwise the message will be confusing + var isConvertibleProperty = + member is IPropertySymbol { GetMethod: not null, SetMethod: not null } property2 && + !_propertiesWithShadowGetAccessorUsages.Contains(property2); + + var diagnosticLocation = GetDiagnosticLocation(member); + var fadingLocation = member.DeclaringSyntaxReferences.FirstOrDefault( + r => r.SyntaxTree == diagnosticLocation.SourceTree && r.Span.Contains(diagnosticLocation.SourceSpan)); + + var fadingNode = fadingLocation?.GetSyntax(cancellationToken) ?? diagnosticLocation.FindNode(cancellationToken); + fadingNode = fadingNode != null ? this._analyzer.GetParentIfSoleDeclarator(fadingNode) : null; + + var additionalUnnecessaryLocations = !isConvertibleProperty && fadingNode is not null + ? [fadingNode.GetLocation()] + : ImmutableArray.Empty; + // Most of the members should have a single location, except for partial methods. // We report the diagnostic on the first location of the member. - var diagnostic = DiagnosticHelper.CreateWithMessage( + symbolEndContext.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags( rule, - GetDiagnosticLocation(member), + diagnosticLocation, NotificationOption2.ForSeverity(rule.DefaultSeverity), symbolEndContext.Options, - additionalLocations: null, - properties: null, - GetMessage(rule, member)); - symbolEndContext.ReportDiagnostic(diagnostic); + message: GetMessage(rule, member, isConvertibleProperty), + additionalLocations: [], + additionalUnnecessaryLocations: additionalUnnecessaryLocations, + properties: null)); } } } - private LocalizableString GetMessage( + private static LocalizableString GetMessage( DiagnosticDescriptor rule, - ISymbol member) + ISymbol member, + bool isConvertibleProperty) { - var messageFormat = rule.MessageFormat; + var memberString = member.ToDisplayString(ContainingTypeAndNameOnlyFormat); + if (rule == s_removeUnreadMembersRule) { // IDE0052 has a different message for method and property symbols. switch (member) { case IMethodSymbol: - messageFormat = AnalyzersResources.Private_method_0_can_be_removed_as_it_is_never_invoked; - break; - - case IPropertySymbol property: - // We change the message only if both 'get' and 'set' accessors are present and - // there are no shadow 'get' accessor usages. Otherwise the message will be confusing - if (property.GetMethod != null && property.SetMethod != null && - !_propertiesWithShadowGetAccessorUsages.Contains(property)) - { - messageFormat = AnalyzersResources.Private_property_0_can_be_converted_to_a_method_as_its_get_accessor_is_never_invoked; - } - - break; + return new DiagnosticHelper.LocalizableStringWithArguments( + AnalyzersResources.Private_method_0_can_be_removed_as_it_is_never_invoked, + memberString); + + case IPropertySymbol when isConvertibleProperty: + return new DiagnosticHelper.LocalizableStringWithArguments( + AnalyzersResources.Private_property_0_can_be_converted_to_a_method_as_its_get_accessor_is_never_invoked, + memberString); } } return new DiagnosticHelper.LocalizableStringWithArguments( - messageFormat, member.ToDisplayString(ContainingTypeAndNameOnlyFormat)); + rule.MessageFormat, memberString); } private static bool HasSyntaxErrors(INamedTypeSymbol namedTypeSymbol, CancellationToken cancellationToken) diff --git a/src/Analyzers/VisualBasic/Analyzers/RemoveUnusedMembers/VisualBasicRemoveUnusedMembersDiagnosticAnalyzer.vb b/src/Analyzers/VisualBasic/Analyzers/RemoveUnusedMembers/VisualBasicRemoveUnusedMembersDiagnosticAnalyzer.vb index e5a7cff41e3c1..21a7da1fd0061 100644 --- a/src/Analyzers/VisualBasic/Analyzers/RemoveUnusedMembers/VisualBasicRemoveUnusedMembersDiagnosticAnalyzer.vb +++ b/src/Analyzers/VisualBasic/Analyzers/RemoveUnusedMembers/VisualBasicRemoveUnusedMembersDiagnosticAnalyzer.vb @@ -59,5 +59,17 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.RemoveUnusedMembers Protected Overrides Function GetMembers(typeDeclaration As TypeBlockSyntax) As SyntaxList(Of StatementSyntax) Return typeDeclaration.Members End Function + + Protected Overrides Function GetParentIfSoleDeclarator(node As SyntaxNode) As SyntaxNode + Dim modifiedIdentifier = TryCast(node, ModifiedIdentifierSyntax) + Dim declarator = TryCast(modifiedIdentifier?.Parent, VariableDeclaratorSyntax) + Dim field = TryCast(declarator?.Parent, FieldDeclarationSyntax) + + If declarator?.Names.Count = 1 AndAlso field?.Declarators.Count = 1 Then + Return field + End If + + Return node + End Function End Class End Namespace diff --git a/src/Features/DiagnosticsTestUtilities/CodeActions/CSharpCodeFixVerifier`2.cs b/src/Features/DiagnosticsTestUtilities/CodeActions/CSharpCodeFixVerifier`2.cs index f84a43aa103a1..d534c9087c602 100644 --- a/src/Features/DiagnosticsTestUtilities/CodeActions/CSharpCodeFixVerifier`2.cs +++ b/src/Features/DiagnosticsTestUtilities/CodeActions/CSharpCodeFixVerifier`2.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp.Testing; @@ -36,7 +37,8 @@ public static void VerifyStandardProperty(AnalyzerProperty property) => CodeFixVerifierHelper.VerifyStandardProperty(new TAnalyzer(), property); /// - public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) + public static async Task VerifyAnalyzerAsync( + [StringSyntax(PredefinedEmbeddedLanguageNames.CSharpTest)] string source, params DiagnosticResult[] expected) { var test = new Test { @@ -48,7 +50,9 @@ public static async Task VerifyAnalyzerAsync(string source, params DiagnosticRes } /// - public static async Task VerifyCodeFixAsync(string source, string fixedSource) + public static async Task VerifyCodeFixAsync( + [StringSyntax(PredefinedEmbeddedLanguageNames.CSharpTest)] string source, + [StringSyntax(PredefinedEmbeddedLanguageNames.CSharpTest)] string fixedSource) => await VerifyCodeFixAsync(source, DiagnosticResult.EmptyDiagnosticResults, fixedSource); ///