Skip to content

Commit 58abbec

Browse files
committed
Support AsyncMethodBuilderAttribute
1 parent ad438f4 commit 58abbec

File tree

2 files changed

+68
-17
lines changed

2 files changed

+68
-17
lines changed

src/Menees.Analyzers/Men019SupportAsyncCancellationToken.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,16 @@ private sealed class NestedAnalyzer(
6262
Analyzer caller,
6363
INamedTypeSymbol cancellationTokenType,
6464
INamedTypeSymbol[] fixedTaskTypes,
65-
INamedTypeSymbol[] genericTaskTypes)
65+
INamedTypeSymbol[] genericTaskTypes,
66+
INamedTypeSymbol asyncMethodBuilderAttributeType)
6667
{
6768
#region Private Data Members
6869

6970
private readonly Analyzer callingAnalyzer = caller;
7071
private readonly INamedTypeSymbol cancellationTokenType = cancellationTokenType;
7172
private readonly INamedTypeSymbol[] fixedTaskTypes = fixedTaskTypes;
7273
private readonly INamedTypeSymbol[] genericTaskTypes = genericTaskTypes;
74+
private readonly INamedTypeSymbol asyncMethodBuilderAttributeType = asyncMethodBuilderAttributeType;
7375

7476
#endregion
7577

@@ -82,9 +84,11 @@ private sealed class NestedAnalyzer(
8284
INamedTypeSymbol? task1Type = compilation.GetTypeByMetadataName(typeof(Task<>).FullName);
8385
INamedTypeSymbol? valueTaskType = compilation.GetTypeByMetadataName(typeof(ValueTask).FullName);
8486
INamedTypeSymbol? valueTask1Type = compilation.GetTypeByMetadataName(typeof(ValueTask<>).FullName);
87+
INamedTypeSymbol? asyncMethodBuilderAttributeType = compilation.GetTypeByMetadataName(typeof(AsyncMethodBuilderAttribute).FullName);
8588

86-
NestedAnalyzer? result = cancellationTokenType != null && taskType != null && task1Type != null && valueTaskType != null && valueTask1Type != null
87-
? new(caller, cancellationTokenType, [taskType, valueTaskType], [task1Type, valueTask1Type])
89+
NestedAnalyzer? result = cancellationTokenType != null && taskType != null && task1Type != null
90+
&& valueTaskType != null && valueTask1Type != null && asyncMethodBuilderAttributeType != null
91+
? new(caller, cancellationTokenType, [taskType, valueTaskType], [task1Type, valueTask1Type], asyncMethodBuilderAttributeType)
8892
: null;
8993
return result;
9094
}
@@ -207,7 +211,8 @@ private bool IsAwaitable(ITypeSymbol type)
207211

208212
if (this.fixedTaskTypes.Contains(type, SymbolComparer)
209213
|| (type is INamedTypeSymbol { IsGenericType: true } namedType
210-
&& this.genericTaskTypes.Contains(namedType.OriginalDefinition, SymbolComparer)))
214+
&& this.genericTaskTypes.Contains(namedType.OriginalDefinition, SymbolComparer))
215+
|| type.GetAttributes().Any(attr => SymbolComparer.Equals(attr.AttributeClass, this.asyncMethodBuilderAttributeType)))
211216
{
212217
result = true;
213218
}

tests/Menees.Analyzers.Test/Men019UnitTests.cs

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,28 @@
77
[TestClass]
88
public class Men019UnitTests : CodeFixVerifier
99
{
10+
private const string SharedCode = """
11+
public sealed class Awaitable { public Awaiter GetAwaiter() => new(); }
12+
public sealed class Awaiter { public bool GetResult() => true; }
13+
14+
[AsyncMethodBuilder(typeof(MyTaskBuilder))]
15+
public readonly struct MyTask
16+
{
17+
}
18+
19+
class MyTaskBuilder
20+
{
21+
public static MyTaskBuilder Create() => new();
22+
public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine { }
23+
public void SetStateMachine(IAsyncStateMachine stateMachine) { }
24+
public void SetResult() { }
25+
public void SetException(Exception exception) { }
26+
public MyTask Task => default(MyTask);
27+
public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine { }
28+
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine { }
29+
}
30+
""";
31+
1032
#region Protected Properties
1133

1234
protected override DiagnosticAnalyzer CSharpDiagnosticAnalyzer => new Men019SupportAsyncCancellationToken();
@@ -22,8 +44,9 @@ public void ValidCodeTest()
2244
{
2345
this.VerifyCSharpDiagnostic(string.Empty);
2446

25-
const string test = @"#nullable enable
47+
string test = @"#nullable enable
2648
using System;
49+
using System.Runtime.CompilerServices;
2750
using System.Threading;
2851
using System.Threading.Tasks;
2952
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -42,8 +65,11 @@ public class TestBase : IThink1
4265
public int Ineligible(int i) => i;
4366
}
4467
45-
public sealed class Awaitable { public Awaiter GetAwaiter() => new(); }
46-
public sealed class Awaiter { public bool GetResult() => true; }
68+
interface InterfaceName
69+
{
70+
Task MethodAsync();
71+
Task MethodAsync(CancellationToken cancellationToken);
72+
}
4773
4874
public class Test : TestBase, IFormattable, IThink2
4975
{
@@ -53,6 +79,8 @@ public class Test : TestBase, IFormattable, IThink2
5379
5480
public Awaitable TryAwaitable(CancellationToken c = default) { return new Awaitable(); }
5581
82+
public MyTask UseMyTask(CancellationToken c) => new();
83+
5684
// Interface implementations
5785
Task IThink2.Think2() => Task.CompletedTask;
5886
public string ToString(string? format, IFormatProvider? formatProvider) => ""test"";
@@ -73,7 +101,8 @@ public void SyncCancel(CancellationToken c = default) { }
73101
74102
public Task SplitMethodGroup() => Task.CompletedTask; // Base class's overload is cancellable.
75103
public int Ineligible(int i, int multiplier) => i * multiplier;
76-
}";
104+
}"
105+
+ Environment.NewLine + SharedCode;
77106

78107
this.VerifyCSharpDiagnostic(test);
79108
}
@@ -85,7 +114,9 @@ public void SyncCancel(CancellationToken c = default) { }
85114
[TestMethod]
86115
public void InvalidCodeTest()
87116
{
88-
const string test = @"#nullable enable
117+
string test = @"#nullable enable
118+
using System;
119+
using System.Runtime.CompilerServices;
89120
using System.Threading;
90121
using System.Threading.Tasks;
91122
@@ -106,45 +137,60 @@ public class Test
106137
107138
public Task LocalMethodGroup(string name) => Task.CompletedTask;
108139
public Task LocalMethodGroup(string name, int i) => Task.CompletedTask;
109-
}";
140+
141+
public Awaitable TryAwaitable() { return new Awaitable(); }
142+
143+
public MyTask UseMyTask() => new();
144+
}"
145+
+ Environment.NewLine + SharedCode;
110146

111147
DiagnosticAnalyzer analyzer = this.CSharpDiagnosticAnalyzer;
112148
DiagnosticResult[] expected =
113149
[
114150
new DiagnosticResult(analyzer)
115151
{
116152
Message = "Async method Think1 should take a CancellationToken parameter.",
117-
Locations = [new DiagnosticResultLocation("Test0.cs", 5, 33)]
153+
Locations = [new DiagnosticResultLocation("Test0.cs", 7, 33)]
118154
},
119155
new DiagnosticResult(analyzer)
120156
{
121157
Message = "Async method CheckPrivate should take a CancellationToken parameter.",
122-
Locations = [new DiagnosticResultLocation("Test0.cs", 11, 21)]
158+
Locations = [new DiagnosticResultLocation("Test0.cs", 13, 21)]
123159
},
124160
new DiagnosticResult(analyzer)
125161
{
126162
Message = "Async method UseAsyncKeyword should take a CancellationToken parameter.",
127-
Locations = [new DiagnosticResultLocation("Test0.cs", 13, 20)]
163+
Locations = [new DiagnosticResultLocation("Test0.cs", 15, 20)]
128164
},
129165
new DiagnosticResult(analyzer)
130166
{
131167
Message = "Async method GetString should take a CancellationToken parameter.",
132-
Locations = [new DiagnosticResultLocation("Test0.cs", 15, 30)]
168+
Locations = [new DiagnosticResultLocation("Test0.cs", 17, 30)]
133169
},
134170
new DiagnosticResult(analyzer)
135171
{
136172
Message = "Async method GetsCancelledProperty should take a CancellationToken parameter.",
137-
Locations = [new DiagnosticResultLocation("Test0.cs", 18, 14)]
173+
Locations = [new DiagnosticResultLocation("Test0.cs", 20, 14)]
138174
},
139175
new DiagnosticResult(analyzer)
140176
{
141177
Message = "Async method LocalMethodGroup should take a CancellationToken parameter.",
142-
Locations = [new DiagnosticResultLocation("Test0.cs", 20, 14)]
178+
Locations = [new DiagnosticResultLocation("Test0.cs", 22, 14)]
143179
},
144180
new DiagnosticResult(analyzer)
145181
{
146182
Message = "Async method LocalMethodGroup should take a CancellationToken parameter.",
147-
Locations = [new DiagnosticResultLocation("Test0.cs", 21, 14)]
183+
Locations = [new DiagnosticResultLocation("Test0.cs", 23, 14)]
184+
},
185+
new DiagnosticResult(analyzer)
186+
{
187+
Message = "Async method TryAwaitable should take a CancellationToken parameter.",
188+
Locations = [new DiagnosticResultLocation("Test0.cs", 25, 19)]
189+
},
190+
new DiagnosticResult(analyzer)
191+
{
192+
Message = "Async method UseMyTask should take a CancellationToken parameter.",
193+
Locations = [new DiagnosticResultLocation("Test0.cs", 27, 16)]
148194
},
149195
];
150196

0 commit comments

Comments
 (0)