@@ -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