Skip to content

Commit b7e891b

Browse files
Always use .OriginalDefinition uniformly in the unread-members analyzer (#76698)
2 parents 6287206 + 9c37d3d commit b7e891b

File tree

1 file changed

+80
-47
lines changed

1 file changed

+80
-47
lines changed

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

Lines changed: 80 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,38 @@ protected virtual void HandleNamedTypeSymbolStart(SymbolStartAnalysisContext con
9090
{
9191
}
9292

93+
/// <summary>
94+
/// We always want to do our processing, considering the original symbol corresponding to the user's declared
95+
/// symbols. As such, we use an instance of this comparer with all the dictionaries and sets we create while
96+
/// processing so that reference to non-original definitions (like references to members from an instantiate generic
97+
/// type) still count as a use of the original user definition.
98+
/// </summary>
99+
internal sealed class OriginalDefinitionSymbolEqualityComparer : IEqualityComparer<ISymbol>
100+
{
101+
public static readonly OriginalDefinitionSymbolEqualityComparer Instance = new();
102+
103+
private OriginalDefinitionSymbolEqualityComparer()
104+
{
105+
}
106+
107+
bool IEqualityComparer<ISymbol>.Equals(ISymbol? x, ISymbol? y)
108+
=> Equals(x?.OriginalDefinition, y?.OriginalDefinition);
109+
110+
int IEqualityComparer<ISymbol>.GetHashCode(ISymbol obj)
111+
=> obj is null ? 0 : obj.OriginalDefinition.GetHashCode();
112+
}
113+
93114
private sealed class CompilationAnalyzer
94115
{
95116
private readonly object _gate = new();
96117

118+
private static readonly ObjectPool<HashSet<ISymbol>> s_originalDefinitionSymbolHashSetPool = new(() => new(OriginalDefinitionSymbolEqualityComparer.Instance));
119+
97120
/// <summary>
98121
/// State map for candidate member symbols, with the value indicating how each symbol is used in executable code.
99122
/// </summary>
100-
private readonly Dictionary<ISymbol, ValueUsageInfo> _symbolValueUsageStateMap = [];
123+
private readonly Dictionary<ISymbol, ValueUsageInfo> _symbolValueUsageStateMap_doNotAccessDirectly = new(OriginalDefinitionSymbolEqualityComparer.Instance);
124+
101125
/// <summary>
102126
/// List of properties that have a 'get' accessor usage, while the value itself is not used, e.g.:
103127
/// <code>
@@ -109,7 +133,7 @@ private sealed class CompilationAnalyzer
109133
/// </code>
110134
/// Here, 'get' accessor is used in an increment operation, but the result of the increment operation isn't used and 'P' itself is not used anywhere else, so it can be safely removed
111135
/// </summary>
112-
private readonly HashSet<IPropertySymbol> _propertiesWithShadowGetAccessorUsages = [];
136+
private readonly HashSet<IPropertySymbol> _propertiesWithShadowGetAccessorUsages = new(OriginalDefinitionSymbolEqualityComparer.Instance);
113137
private readonly INamedTypeSymbol? _taskType;
114138
private readonly INamedTypeSymbol? _genericTaskType;
115139
private readonly INamedTypeSymbol? _debuggerDisplayAttributeType;
@@ -258,7 +282,7 @@ bool ShouldAnalyze(SymbolStartAnalysisContext context, INamedTypeSymbol namedTyp
258282
// Check if we have at least one candidate symbol in analysis scope.
259283
foreach (var member in namedType.GetMembers())
260284
{
261-
if (IsCandidateSymbol(member.OriginalDefinition)
285+
if (IsCandidateSymbol(member)
262286
&& context.ShouldAnalyzeLocation(GetDiagnosticLocation(member)))
263287
{
264288
return true;
@@ -275,19 +299,55 @@ bool ShouldAnalyze(SymbolStartAnalysisContext context, INamedTypeSymbol namedTyp
275299

276300
private void AnalyzeSymbolDeclaration(SymbolAnalysisContext symbolContext)
277301
{
278-
var symbol = symbolContext.Symbol.OriginalDefinition;
302+
var symbol = symbolContext.Symbol;
279303
if (IsCandidateSymbol(symbol)
280304
&& symbolContext.ShouldAnalyzeLocation(GetDiagnosticLocation(symbol)))
281305
{
282-
lock (_gate)
306+
// Initialize unused state to 'ValueUsageInfo.None' to indicate that
307+
// no read/write references have been encountered yet for this symbol.
308+
// Note that we might receive a symbol reference (AnalyzeMemberOperation) callback before
309+
// this symbol declaration callback, so even though we cannot receive duplicate callbacks for a symbol,
310+
// an entry might already be present of the declared symbol here.
311+
AddSymbolUsage(symbol, ValueUsageInfo.None);
312+
}
313+
}
314+
315+
private void AddSymbolUsage(ISymbol? symbol, ValueUsageInfo info)
316+
{
317+
if (symbol is null)
318+
return;
319+
320+
lock (_gate)
321+
{
322+
_symbolValueUsageStateMap_doNotAccessDirectly.TryAdd(symbol, info);
323+
}
324+
}
325+
326+
private void UpdateSymbolUsage(ISymbol? symbol, ValueUsageInfo info)
327+
{
328+
if (symbol is null)
329+
return;
330+
331+
lock (_gate)
332+
{
333+
if (_symbolValueUsageStateMap_doNotAccessDirectly.TryGetValue(symbol, out var currentUsageInfo))
334+
info = currentUsageInfo | info;
335+
336+
_symbolValueUsageStateMap_doNotAccessDirectly[symbol] = info;
337+
}
338+
}
339+
340+
private bool TryGetAndRemoveSymbolUsage(ISymbol memberSymbol, out ValueUsageInfo valueUsageInfo)
341+
{
342+
lock (_gate)
343+
{
344+
if (_symbolValueUsageStateMap_doNotAccessDirectly.TryGetValue(memberSymbol, out valueUsageInfo))
283345
{
284-
// Initialize unused state to 'ValueUsageInfo.None' to indicate that
285-
// no read/write references have been encountered yet for this symbol.
286-
// Note that we might receive a symbol reference (AnalyzeMemberOperation) callback before
287-
// this symbol declaration callback, so even though we cannot receive duplicate callbacks for a symbol,
288-
// an entry might already be present of the declared symbol here.
289-
_symbolValueUsageStateMap.TryAdd(symbol, ValueUsageInfo.None);
346+
_symbolValueUsageStateMap_doNotAccessDirectly.Remove(memberSymbol);
347+
return true;
290348
}
349+
350+
return false;
291351
}
292352
}
293353

@@ -320,40 +380,15 @@ private void AnalyzeFieldInitializer(OperationAnalysisContext operationContext)
320380
private void OnSymbolUsage(ISymbol? memberSymbol, ValueUsageInfo usageInfo)
321381
{
322382
if (!IsCandidateSymbol(memberSymbol))
323-
{
324383
return;
325-
}
326384

327-
lock (_gate)
328-
{
329-
// Update the usage info for the memberSymbol
330-
if (_symbolValueUsageStateMap.TryGetValue(memberSymbol, out var currentUsageInfo))
331-
{
332-
usageInfo = currentUsageInfo | usageInfo;
333-
}
334-
335-
_symbolValueUsageStateMap[memberSymbol] = usageInfo;
336-
}
337-
}
338-
339-
private bool TryRemove(ISymbol memberSymbol, out ValueUsageInfo valueUsageInfo)
340-
{
341-
lock (_gate)
342-
{
343-
if (_symbolValueUsageStateMap.TryGetValue(memberSymbol, out valueUsageInfo))
344-
{
345-
_symbolValueUsageStateMap.Remove(memberSymbol);
346-
return true;
347-
}
348-
349-
return false;
350-
}
385+
UpdateSymbolUsage(memberSymbol, usageInfo);
351386
}
352387

353388
private void AnalyzeMemberReferenceOperation(OperationAnalysisContext operationContext)
354389
{
355390
var memberReference = (IMemberReferenceOperation)operationContext.Operation;
356-
var memberSymbol = memberReference.Member.OriginalDefinition;
391+
var memberSymbol = memberReference.Member;
357392
if (IsCandidateSymbol(memberSymbol))
358393
{
359394
// Get the value usage info.
@@ -421,7 +456,7 @@ private void AnalyzeLoopOperation(OperationAnalysisContext operationContext)
421456

422457
private void AnalyzeInvocationOperation(OperationAnalysisContext operationContext)
423458
{
424-
var targetMethod = ((IInvocationOperation)operationContext.Operation).TargetMethod.OriginalDefinition;
459+
var targetMethod = ((IInvocationOperation)operationContext.Operation).TargetMethod;
425460

426461
// A method invocation is considered as a read reference to the symbol
427462
// to ensure that we consider the method as "used".
@@ -444,7 +479,7 @@ private void AnalyzeNameOfOperation(OperationAnalysisContext operationContext)
444479

445480
if (nameofArgument is IMemberReferenceOperation memberReference)
446481
{
447-
OnSymbolUsage(memberReference.Member.OriginalDefinition, ValueUsageInfo.ReadWrite);
482+
OnSymbolUsage(memberReference.Member, ValueUsageInfo.ReadWrite);
448483
return;
449484
}
450485

@@ -453,12 +488,12 @@ private void AnalyzeNameOfOperation(OperationAnalysisContext operationContext)
453488
// a bound method group/property group.
454489
var symbolInfo = nameofArgument.SemanticModel!.GetSymbolInfo(nameofArgument.Syntax, operationContext.CancellationToken);
455490
foreach (var symbol in symbolInfo.GetAllSymbols())
456-
OnSymbolUsage(symbol.OriginalDefinition, ValueUsageInfo.ReadWrite);
491+
OnSymbolUsage(symbol, ValueUsageInfo.ReadWrite);
457492
}
458493

459494
private void AnalyzeObjectCreationOperation(OperationAnalysisContext operationContext)
460495
{
461-
var constructor = ((IObjectCreationOperation)operationContext.Operation).Constructor?.OriginalDefinition;
496+
var constructor = ((IObjectCreationOperation)operationContext.Operation).Constructor;
462497

463498
// An object creation is considered as a read reference to the constructor
464499
// to ensure that we consider the constructor as "used".
@@ -481,7 +516,7 @@ private void OnSymbolEnd(SymbolAnalysisContext symbolEndContext, bool hasUnsuppo
481516
// Report diagnostics for unused candidate members.
482517
var first = true;
483518

484-
using var _1 = PooledHashSet<ISymbol>.GetInstance(out var symbolsReferencedInDocComments);
519+
using var _1 = s_originalDefinitionSymbolHashSetPool.GetPooledObject(out var symbolsReferencedInDocComments);
485520
using var _2 = ArrayBuilder<string>.GetInstance(out var debuggerDisplayAttributeArguments);
486521

487522
var entryPoint = symbolEndContext.Compilation.GetEntryPoint(cancellationToken);
@@ -499,7 +534,7 @@ private void OnSymbolEnd(SymbolAnalysisContext symbolEndContext, bool hasUnsuppo
499534

500535
// Check if the underlying member is neither read nor a readable reference to the member is taken.
501536
// If so, we flag the member as either unused (never written) or unread (written but not read).
502-
if (TryRemove(member, out var valueUsageInfo) && !valueUsageInfo.IsReadFrom())
537+
if (TryGetAndRemoveSymbolUsage(member, out var valueUsageInfo) && !valueUsageInfo.IsReadFrom())
503538
{
504539
Debug.Assert(IsCandidateSymbol(member));
505540
Debug.Assert(!member.IsImplicitlyDeclared);
@@ -649,7 +684,7 @@ private void AddCandidateSymbolsReferencedInDocComments(
649684
foreach (var node in docComment.DescendantNodes().OfType<TIdentifierNameSyntax>())
650685
{
651686
lazyModel ??= compilation.GetSemanticModel(syntaxTree);
652-
var symbol = lazyModel.GetSymbolInfo(node, cancellationToken).Symbol?.OriginalDefinition;
687+
var symbol = lazyModel.GetSymbolInfo(node, cancellationToken).Symbol;
653688

654689
if (IsCandidateSymbol(symbol))
655690
builder.Add(symbol);
@@ -757,8 +792,6 @@ private bool IsCandidateSymbol([NotNullWhen(true)] ISymbol? memberSymbol)
757792
if (memberSymbol is null)
758793
return false;
759794

760-
Debug.Assert(memberSymbol == memberSymbol.OriginalDefinition);
761-
762795
if (memberSymbol.DeclaredAccessibility == Accessibility.Private &&
763796
!memberSymbol.IsImplicitlyDeclared)
764797
{

0 commit comments

Comments
 (0)