Skip to content

Commit 8d578ed

Browse files
committed
Support accessible protected and internal CancellationTokens
1 parent 7c3f73b commit 8d578ed

File tree

4 files changed

+83
-17
lines changed

4 files changed

+83
-17
lines changed

src/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<AssemblyOriginatorKeyFile>../Analyzers.snk</AssemblyOriginatorKeyFile>
2222

2323
<!-- NOTE: Change the version in Vsix\source.extension.vsixmanifest to match this! -->
24-
<Version>3.3.2</Version>
24+
<Version>3.3.3</Version>
2525

2626
<RoslynVersion>3.11.0</RoslynVersion>
2727
</PropertyGroup>

src/Menees.Analyzers.Vsix/source.extension.vsixmanifest

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011" xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011">
33
<Metadata>
4-
<Identity Id="Menees.Analyzers.Vsix" Version="3.3.2" Language="en-US" Publisher="Bill Menees"/>
4+
<Identity Id="Menees.Analyzers.Vsix" Version="3.3.3" Language="en-US" Publisher="Bill Menees"/>
55
<DisplayName>Menees.Analyzers.Vsix</DisplayName>
66
<Description xml:space="preserve">Provides analyzers for validating that tabs are used for indentation, that the lengths of lines, methods, properties, and files are acceptable, and that #regions are used within long files and files that contain multiple types.</Description>
77
<License>License.txt</License>

src/Menees.Analyzers/Men019SupportAsyncCancellationToken.cs

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

tests/Menees.Analyzers.Test/Men019UnitTests.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,16 @@ public void SyncCancel(CancellationToken c = default) { }
143143
144144
public Task SplitMethodGroup() => Task.CompletedTask; // Base class's overload is cancellable.
145145
public int Ineligible(int i, int multiplier) => i * multiplier;
146-
}"
146+
}
147+
148+
public abstract class Job { protected CancellationToken Cancellation {get;} }
149+
public sealed class DerivedJob : Job
150+
{
151+
public Task InheritsCancellationProperty() => Task.CompletedTask;
152+
public static Task TakesCancellableParameter(Job job) => Task.CompletedTask;
153+
public static Task TakesCancellableParameter2(Job job) => Task.CompletedTask; // Cache should already have (DerivedJob,Job) accessibility
154+
}
155+
"
147156
+ Environment.NewLine + SharedCode;
148157

149158
this.VerifyCSharpDiagnostic(test);

0 commit comments

Comments
 (0)