Skip to content

Commit afc0e8a

Browse files
Fix issue where we were recommending removing the GetEnumerator method used in a foreach (#75903)
Fixes #57470
2 parents e7cd138 + 13cdd19 commit afc0e8a

File tree

5 files changed

+70
-28
lines changed

5 files changed

+70
-28
lines changed

src/Analyzers/CSharp/Analyzers/RemoveUnusedMembers/CSharpRemoveUnusedMembersDiagnosticAnalyzer.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Threading;
88
using Microsoft.CodeAnalysis.CSharp.Syntax;
99
using Microsoft.CodeAnalysis.Diagnostics;
10+
using Microsoft.CodeAnalysis.LanguageService;
1011
using Microsoft.CodeAnalysis.RemoveUnusedMembers;
1112
using Microsoft.CodeAnalysis.Shared.Extensions;
1213

@@ -20,6 +21,8 @@ internal sealed class CSharpRemoveUnusedMembersDiagnosticAnalyzer
2021
TypeDeclarationSyntax,
2122
MemberDeclarationSyntax>
2223
{
24+
protected override ISemanticFacts SemanticFacts => CSharpSemanticFacts.Instance;
25+
2326
protected override IEnumerable<TypeDeclarationSyntax> GetTypeDeclarations(INamedTypeSymbol namedType, CancellationToken cancellationToken)
2427
{
2528
return namedType.DeclaringSyntaxReferences

src/Analyzers/CSharp/Tests/RemoveUnusedMembers/RemoveUnusedMembersTests.cs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.RemoveUnusedMembers;
1717

18-
using static Microsoft.CodeAnalysis.CSharp.UsePatternCombinators.AnalyzedPattern;
1918
using VerifyCS = CSharpCodeFixVerifier<
2019
CSharpRemoveUnusedMembersDiagnosticAnalyzer,
2120
CSharpRemoveUnusedMembersCodeFixProvider>;
@@ -3243,4 +3242,33 @@ class C
32433242

32443243
await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
32453244
}
3245+
3246+
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/57470")]
3247+
public async Task TestForeach()
3248+
{
3249+
await new VerifyCS.Test
3250+
{
3251+
TestCode = """
3252+
using System;
3253+
using System.Collections;
3254+
3255+
static class Program
3256+
{
3257+
static void Main(string[] args)
3258+
{
3259+
foreach (var i in 1..10)
3260+
Console.WriteLine(i);
3261+
}
3262+
3263+
static IEnumerator GetEnumerator(this Range range)
3264+
{
3265+
for (int i = range.Start.Value; i < range.End.Value; i++)
3266+
yield return i;
3267+
}
3268+
}
3269+
""",
3270+
LanguageVersion = LanguageVersion.CSharp13,
3271+
ReferenceAssemblies = ReferenceAssemblies.Net.Net90,
3272+
}.RunAsync();
3273+
}
32463274
}

src/Analyzers/Core/Analyzers/RemoveUnusedMembers/AbstractRemoveUnusedMembersDiagnosticAnalyzer.cs

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Microsoft.CodeAnalysis.CodeQuality;
1414
using Microsoft.CodeAnalysis.CodeStyle;
1515
using Microsoft.CodeAnalysis.Diagnostics;
16+
using Microsoft.CodeAnalysis.LanguageService;
1617
using Microsoft.CodeAnalysis.Operations;
1718
using Microsoft.CodeAnalysis.PooledObjects;
1819
using Microsoft.CodeAnalysis.Shared.Extensions;
@@ -25,8 +26,11 @@ internal abstract class AbstractRemoveUnusedMembersDiagnosticAnalyzer<
2526
TDocumentationCommentTriviaSyntax,
2627
TIdentifierNameSyntax,
2728
TTypeDeclarationSyntax,
28-
TMemberDeclarationSyntax>
29-
: AbstractCodeQualityDiagnosticAnalyzer
29+
TMemberDeclarationSyntax>()
30+
: AbstractCodeQualityDiagnosticAnalyzer(
31+
[s_removeUnusedMembersRule, s_removeUnreadMembersRule],
32+
// We want to analyze references in generated code, but not report unused members in generated code.
33+
GeneratedCodeAnalysisFlags.Analyze)
3034
where TDocumentationCommentTriviaSyntax : SyntaxNode
3135
where TIdentifierNameSyntax : SyntaxNode
3236
where TTypeDeclarationSyntax : TMemberDeclarationSyntax
@@ -56,11 +60,7 @@ internal abstract class AbstractRemoveUnusedMembersDiagnosticAnalyzer<
5660
new LocalizableResourceString(nameof(AnalyzersResources.Private_member_0_can_be_removed_as_the_value_assigned_to_it_is_never_read), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)),
5761
hasAnyCodeStyleOption: false, isUnnecessary: true);
5862

59-
protected AbstractRemoveUnusedMembersDiagnosticAnalyzer()
60-
: base([s_removeUnusedMembersRule, s_removeUnreadMembersRule],
61-
GeneratedCodeAnalysisFlags.Analyze) // We want to analyze references in generated code, but not report unused members in generated code.
62-
{
63-
}
63+
protected abstract ISemanticFacts SemanticFacts { get; }
6464

6565
protected abstract IEnumerable<TTypeDeclarationSyntax> GetTypeDeclarations(INamedTypeSymbol namedType, CancellationToken cancellationToken);
6666
protected abstract SyntaxList<TMemberDeclarationSyntax> GetMembers(TTypeDeclarationSyntax typeDeclaration);
@@ -86,7 +86,8 @@ protected virtual void HandleNamedTypeSymbolStart(SymbolStartAnalysisContext con
8686

8787
private sealed class CompilationAnalyzer
8888
{
89-
private readonly object _gate;
89+
private readonly object _gate = new();
90+
9091
/// <summary>
9192
/// State map for candidate member symbols, with the value indicating how each symbol is used in executable code.
9293
/// </summary>
@@ -114,7 +115,6 @@ private CompilationAnalyzer(
114115
Compilation compilation,
115116
AbstractRemoveUnusedMembersDiagnosticAnalyzer<TDocumentationCommentTriviaSyntax, TIdentifierNameSyntax, TTypeDeclarationSyntax, TMemberDeclarationSyntax> analyzer)
116117
{
117-
_gate = new object();
118118
_analyzer = analyzer;
119119

120120
_taskType = compilation.TaskType();
@@ -204,15 +204,17 @@ private void RegisterActions(CompilationStartAnalysisContext compilationStartCon
204204
symbolStartContext.RegisterOperationAction(AnalyzeInvocationOperation, OperationKind.Invocation);
205205
symbolStartContext.RegisterOperationAction(AnalyzeNameOfOperation, OperationKind.NameOf);
206206
symbolStartContext.RegisterOperationAction(AnalyzeObjectCreationOperation, OperationKind.ObjectCreation);
207+
symbolStartContext.RegisterOperationAction(AnalyzeLoopOperation, OperationKind.Loop);
207208

208209
// We bail out reporting diagnostics for named types if it contains following kind of operations:
209210
// 1. Invalid operations, i.e. erroneous code:
210211
// We do so to ensure that we don't report false positives during editing scenarios in the IDE, where the user
211212
// is still editing code and fixing unresolved references to symbols, such as overload resolution errors.
212213
// 2. Dynamic operations, where we do not know the exact member being referenced at compile time.
213214
// 3. Operations with OperationKind.None.
214-
symbolStartContext.RegisterOperationAction(_ => hasUnsupportedOperation = true, OperationKind.Invalid, OperationKind.None,
215-
OperationKind.DynamicIndexerAccess, OperationKind.DynamicInvocation, OperationKind.DynamicMemberReference, OperationKind.DynamicObjectCreation);
215+
symbolStartContext.RegisterOperationAction(
216+
_ => hasUnsupportedOperation = true,
217+
OperationKind.Invalid, OperationKind.None, OperationKind.DynamicIndexerAccess, OperationKind.DynamicInvocation, OperationKind.DynamicMemberReference, OperationKind.DynamicObjectCreation);
216218

217219
symbolStartContext.RegisterSymbolEndAction(symbolEndContext => OnSymbolEnd(symbolEndContext, hasUnsupportedOperation));
218220

@@ -369,6 +371,18 @@ memberReference.Parent is IIncrementOrDecrementOperation ||
369371
}
370372
}
371373

374+
private void AnalyzeLoopOperation(OperationAnalysisContext operationContext)
375+
{
376+
var operation = operationContext.Operation;
377+
if (operation is not IForEachLoopOperation loopOperation)
378+
return;
379+
380+
var symbols = _analyzer.SemanticFacts.GetForEachSymbols(operation.SemanticModel!, loopOperation.Syntax);
381+
OnSymbolUsage(symbols.CurrentProperty, ValueUsageInfo.Read);
382+
OnSymbolUsage(symbols.GetEnumeratorMethod, ValueUsageInfo.Read);
383+
OnSymbolUsage(symbols.MoveNextMethod, ValueUsageInfo.Read);
384+
}
385+
372386
private void AnalyzeInvocationOperation(OperationAnalysisContext operationContext)
373387
{
374388
var targetMethod = ((IInvocationOperation)operationContext.Operation).TargetMethod.OriginalDefinition;
@@ -380,9 +394,7 @@ private void AnalyzeInvocationOperation(OperationAnalysisContext operationContex
380394
// If the invoked method is a reduced extension method, also mark the original
381395
// method from which it was reduced as "used".
382396
if (targetMethod.ReducedFrom != null)
383-
{
384397
OnSymbolUsage(targetMethod.ReducedFrom, ValueUsageInfo.Read);
385-
}
386398
}
387399

388400
private void AnalyzeNameOfOperation(OperationAnalysisContext operationContext)

src/Analyzers/VisualBasic/Analyzers/RemoveUnusedMembers/VisualBasicRemoveUnusedMembersDiagnosticAnalyzer.vb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
Imports System.Threading
66
Imports Microsoft.CodeAnalysis.Diagnostics
7+
Imports Microsoft.CodeAnalysis.LanguageService
78
Imports Microsoft.CodeAnalysis.RemoveUnusedMembers
89
Imports Microsoft.CodeAnalysis.VisualBasic.Syntax
910

@@ -16,6 +17,8 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.RemoveUnusedMembers
1617
TypeBlockSyntax,
1718
StatementSyntax)
1819

20+
Protected Overrides ReadOnly Property SemanticFacts As ISemanticFacts = VisualBasicSemanticFacts.Instance
21+
1922
Protected Overrides Sub HandleNamedTypeSymbolStart(context As SymbolStartAnalysisContext, onSymbolUsageFound As Action(Of ISymbol, ValueUsageInfo))
2023
' Mark all methods with handles clause as having a read reference
2124
' to ensure that we consider the method as "used".

src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SemanticFacts/CSharpSemanticFacts.cs

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -166,22 +166,18 @@ private static void AppendAliasNames(IEnumerable<BaseNamespaceDeclarationSyntax>
166166
}
167167
}
168168

169-
public ForEachSymbols GetForEachSymbols(SemanticModel semanticModel, SyntaxNode forEachStatement)
169+
public ForEachSymbols GetForEachSymbols(SemanticModel semanticModel, SyntaxNode node)
170170
{
171-
if (forEachStatement is CommonForEachStatementSyntax csforEachStatement)
172-
{
173-
var info = semanticModel.GetForEachStatementInfo(csforEachStatement);
174-
return new ForEachSymbols(
175-
info.GetEnumeratorMethod,
176-
info.MoveNextMethod,
177-
info.CurrentProperty,
178-
info.DisposeMethod,
179-
info.ElementType);
180-
}
181-
else
182-
{
171+
if (node is not CommonForEachStatementSyntax forEachStatement)
183172
return default;
184-
}
173+
174+
var info = semanticModel.GetForEachStatementInfo(forEachStatement);
175+
return new ForEachSymbols(
176+
info.GetEnumeratorMethod,
177+
info.MoveNextMethod,
178+
info.CurrentProperty,
179+
info.DisposeMethod,
180+
info.ElementType);
185181
}
186182

187183
public SymbolInfo GetCollectionInitializerSymbolInfo(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken)

0 commit comments

Comments
 (0)