@@ -73,11 +73,8 @@ private sealed class NestedAnalyzer(
7373 private readonly INamedTypeSymbol [ ] fixedTaskTypes = fixedTaskTypes ;
7474 private readonly INamedTypeSymbol [ ] genericTaskTypes = genericTaskTypes ;
7575 private readonly INamedTypeSymbol asyncMethodBuilderAttributeType = asyncMethodBuilderAttributeType ;
76- #pragma warning disable IDE0079 // Remove unnecessary suppression. False positive.
77- #pragma warning disable RS1024 // Compare symbols correctly. False positive; this is using a SymbolEqualityComparer.
78- private readonly ConcurrentDictionary < ITypeSymbol , bool > typeHasCancellationTokenProperty = new ( SymbolComparer ) ;
79- #pragma warning restore RS1024 // Compare symbols correctly
80- #pragma warning restore IDE0079 // Remove unnecessary suppression
76+ private readonly ConcurrentDictionary < ( ITypeSymbol Source , ITypeSymbol Target ) , bool > canAccessCancellationTokenProperty
77+ = new ( TypeAccessComparer . Instance ) ;
8178
8279 #endregion
8380
@@ -101,16 +98,20 @@ private sealed class NestedAnalyzer(
10198
10299 public void HandleNamedTypeSymbol ( SymbolAnalysisContext context )
103100 {
104- // Look at each overload method group (including inherited methods).
105- // If a method group has any eligible async/awaitable methods and none
106- // of those are cancellable, then we'll report each eligible method.
107101 INamedTypeSymbol namedType = ( INamedTypeSymbol ) context . Symbol ;
108102
103+ // If the namedType has an instance-level CancellationToken property, then instance methods don't need a parameter.
104+ bool checkInstanceMethods = ! CanAccessCancellationTokenProperty ( source : namedType , target : namedType ) ;
105+
106+ // Look at each overload method group (including inherited methods).
107+ // If a method group has any eligible async/awaitable methods and none
108+ // of those are cancellable, then we'll report the "most eligible" method.
109109#pragma warning disable IDE0079 // Remove unnecessary suppression. False positive!
110110#pragma warning disable RS1024 // Compare symbols correctly. False positive! We're grouping by the Name not IMethodSymbol.
111111 IEnumerable < IGrouping < string , IMethodSymbol > > methodGroups = GetTypeAndBaseTypes ( namedType )
112112 . SelectMany ( type => type . GetMembers ( ) )
113113 . OfType < IMethodSymbol > ( )
114+ . Where ( method => checkInstanceMethods || method . IsStatic )
114115 . GroupBy ( m => m . Name , m => m , StringComparer . Ordinal ) ;
115116#pragma warning restore RS1024 // Compare symbols correctly
116117#pragma warning restore IDE0079 // Remove unnecessary suppression
@@ -138,7 +139,7 @@ public void HandleNamedTypeSymbol(SymbolAnalysisContext context)
138139
139140 List < IParameterSymbol > parameters = GetEligibleParameters ( method ) ;
140141 if ( HasCancellationTokenParameter ( parameters )
141- || parameters . Any ( ParameterHasCancellationTokenProperty ) )
142+ || parameters . Any ( p => CanAccessCancellationTokenProperty ( source : namedType , target : p . Type ) ) )
142143 {
143144 isAtLeastOneOverloadCancellable = true ;
144145 }
@@ -286,17 +287,22 @@ private bool HasCancellationTokenParameter(List<IParameterSymbol> parameters)
286287 return result ;
287288 }
288289
289- private bool ParameterHasCancellationTokenProperty ( IParameterSymbol parameterSymbol )
290+ private bool CanAccessCancellationTokenProperty ( ITypeSymbol source , ITypeSymbol target )
290291 {
291292 ISet < string > propertyNamesToCheck = this . callingAnalyzer . Settings . PropertyNamesForCancellation ;
292293 bool result = propertyNamesToCheck . Count > 0
293- && typeHasCancellationTokenProperty . GetOrAdd ( parameterSymbol . Type , type =>
294+ && canAccessCancellationTokenProperty . GetOrAdd ( ( source , target ) , tuple =>
294295 {
295296 bool hasCancellationTokenProperty = false ;
296- ITypeSymbol [ ] typeHierarchy = [ .. GetTypeAndBaseTypes ( type ) ] ;
297+ ITypeSymbol [ ] targetTypeHierarchy = [ .. GetTypeAndBaseTypes ( tuple . Target ) ] ;
297298 IEnumerable < IPropertySymbol > publicCancellationProperties = propertyNamesToCheck
298- . SelectMany ( propertyName => typeHierarchy . SelectMany ( t => t . GetMembers ( propertyName ) ) )
299- . Where ( m => m . Kind == SymbolKind . Property && m . DeclaredAccessibility == Accessibility . Public )
299+ . SelectMany ( propertyName => targetTypeHierarchy . SelectMany ( t => t . GetMembers ( propertyName ) ) )
300+ . Where ( m => m . Kind == SymbolKind . Property
301+ // Static CancellationToken properties are rare and don't go well with their
302+ // per-operation intent. https://devblogs.microsoft.com/dotnet/net-4-cancellation-framework/
303+ && ! m . IsStatic
304+ // If m is an inherited property, we have to use its containing type not target.
305+ && m . DeclaredAccessibility >= GetMinimumAccessibility ( tuple . Source , m . ContainingType ) )
300306 . Cast < IPropertySymbol > ( ) ;
301307 foreach ( IPropertySymbol propertySymbol in publicCancellationProperties )
302308 {
@@ -313,8 +319,59 @@ private bool ParameterHasCancellationTokenProperty(IParameterSymbol parameterSym
313319 return result ;
314320 }
315321
322+ private Accessibility GetMinimumAccessibility ( ITypeSymbol source , ITypeSymbol target )
323+ {
324+ Accessibility result = Accessibility . Public ;
325+
326+ if ( SymbolComparer . Equals ( source , target ) )
327+ {
328+ result = Accessibility . NotApplicable ;
329+ }
330+ else
331+ {
332+ bool useProtected = GetTypeAndBaseTypes ( source ) . Contains ( target , SymbolComparer ) ;
333+
334+ // This doesn't handle InternalsVisibleTo because Roslyn doesn't expose public members for those checks.
335+ // https://github.com/dotnet/roslyn/blob/main/src/Compilers/CSharp/Portable/Binder/Semantics/AccessCheck.cs
336+ bool useInternal = SymbolComparer . Equals ( source . ContainingAssembly , target . ContainingAssembly ) ;
337+
338+ if ( useProtected && useInternal )
339+ {
340+ result = Accessibility . ProtectedAndInternal ;
341+ }
342+ else if ( useProtected )
343+ {
344+ result = Accessibility . Protected ;
345+ }
346+ else if ( useInternal )
347+ {
348+ result = Accessibility . Internal ;
349+ }
350+ }
351+
352+ return result ;
353+ }
354+
316355 #endregion
317356 }
318357
358+ private sealed class TypeAccessComparer : IEqualityComparer < ( ITypeSymbol Source , ITypeSymbol Target ) >
359+ {
360+ private TypeAccessComparer ( )
361+ {
362+ }
363+
364+ public static TypeAccessComparer Instance { get ; } = new ( ) ;
365+
366+ public bool Equals (
367+ ( ITypeSymbol Source , ITypeSymbol Target ) x ,
368+ ( ITypeSymbol Source , ITypeSymbol Target ) y )
369+ => SymbolComparer . Equals ( x . Source , y . Source )
370+ && SymbolComparer . Equals ( x . Target , y . Target ) ;
371+
372+ public int GetHashCode ( ( ITypeSymbol Source , ITypeSymbol Target ) obj )
373+ => HashCode . Combine ( SymbolComparer . GetHashCode ( obj . Source ) , SymbolComparer . GetHashCode ( obj . Target ) ) ;
374+ }
375+
319376 #endregion
320377}
0 commit comments