-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Report diagnostics from TryFindDisposePatternMethod
when binding foreach
loop
#75422
Changes from 1 commit
9bab481
ed0cb34
bbd8ba6
adfc9f2
c31ec4e
0139aac
75a746a
92328fe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
# This document lists known breaking changes in Roslyn after .NET 9 all the way to .NET 10. | ||
|
||
## Diagnostics now reported for improper use of pattern-based disposal method in `foreach` | ||
|
||
***Introduced in Visual Studio 2022 version 17.13*** | ||
|
||
For instance, an obsolete `DisposeAsync` method is now reported in `await foreach`. | ||
```csharp | ||
await foreach (var i in new C()) { } // 'C.AsyncEnumerator.DisposeAsync()' is obsolete | ||
|
||
class C | ||
{ | ||
public AsyncEnumerator GetAsyncEnumerator(System.Threading.CancellationToken token = default) | ||
{ | ||
throw null; | ||
} | ||
|
||
public sealed class AsyncEnumerator : System.IAsyncDisposable | ||
{ | ||
public int Current { get => throw null; } | ||
public Task<bool> MoveNextAsync() => throw null; | ||
|
||
[System.Obsolete] | ||
public ValueTask DisposeAsync() => throw null; | ||
} | ||
} | ||
``` | ||
|
||
Similarly, an `[UnmanagedCallersOnly]` `Dispose` method is now reported in `foreach` with a `ref struct` enumerator. | ||
```csharp | ||
public struct S | ||
{ | ||
public static void M2(S s) | ||
{ | ||
foreach (var i in s) { } // 'SEnumerator.Dispose()' is attributed with 'UnmanagedCallersOnly' and cannot be called directly. | ||
} | ||
public static SEnumerator GetEnumerator() => throw null; | ||
} | ||
public ref struct SEnumerator | ||
{ | ||
public bool MoveNext() => throw null; | ||
public int Current => throw null; | ||
[UnmanagedCallersOnly] | ||
public void Dispose() => throw null; | ||
} | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1193,15 +1193,16 @@ private void GetDisposalInfoForEnumerator(SyntaxNode syntax, ref ForEachEnumerat | |
|
||
if (enumeratorType.IsRefLikeType || isAsync) | ||
{ | ||
// we throw away any binding diagnostics, and assume it's not disposable if we encounter errors | ||
var receiver = new BoundDisposableValuePlaceholder(syntax, enumeratorType); | ||
MethodSymbol patternDisposeMethod = TryFindDisposePatternMethod(receiver, syntax, isAsync, BindingDiagnosticBag.Discarded, out bool expanded); | ||
BindingDiagnosticBag patternDiagnostics = BindingDiagnosticBag.GetInstance(diagnostics); | ||
MethodSymbol patternDisposeMethod = TryFindDisposePatternMethod(receiver, syntax, isAsync, patternDiagnostics, out bool expanded); | ||
if (patternDisposeMethod is object) | ||
{ | ||
Debug.Assert(!patternDisposeMethod.IsExtensionMethod); | ||
Debug.Assert(patternDisposeMethod.ParameterRefKinds.IsDefaultOrEmpty || | ||
patternDisposeMethod.ParameterRefKinds.All(static refKind => refKind is RefKind.None or RefKind.In or RefKind.RefReadOnlyParameter)); | ||
|
||
diagnostics.AddRangeAndFree(patternDiagnostics); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
var argsBuilder = ArrayBuilder<BoundExpression>.GetInstance(patternDisposeMethod.ParameterCount); | ||
var argsToParams = default(ImmutableArray<int>); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4821,8 +4821,8 @@ public static class Extensions | |
CompileAndVerify(comp, expectedOutput: "NextAsync(0) Current(1) Got(1,-1) NextAsync(1) Current(2) Got(2,-2) NextAsync(2) Dispose(3) Done"); | ||
} | ||
|
||
[Fact] | ||
public void TestWithPatternAndObsolete() | ||
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/30257")] | ||
public void TestWithPatternAndObsolete_WithDisposableInterface() | ||
{ | ||
string source = @" | ||
using System.Threading.Tasks; | ||
|
@@ -4852,6 +4852,9 @@ public sealed class AsyncEnumerator : System.IAsyncDisposable | |
}"; | ||
var comp = CreateCompilationWithTasksExtensions(source + s_IAsyncEnumerable, options: TestOptions.DebugExe); | ||
comp.VerifyDiagnostics( | ||
// (7,15): warning CS0612: 'C.AsyncEnumerator.DisposeAsync()' is obsolete | ||
// await foreach (var i in new C()) | ||
Diagnostic(ErrorCode.WRN_DeprecatedSymbol, "foreach").WithArguments("C.AsyncEnumerator.DisposeAsync()").WithLocation(7, 15), | ||
// (7,15): warning CS0612: 'C.GetAsyncEnumerator(CancellationToken)' is obsolete | ||
// await foreach (var i in new C()) | ||
Diagnostic(ErrorCode.WRN_DeprecatedSymbol, "foreach").WithArguments("C.GetAsyncEnumerator(System.Threading.CancellationToken)").WithLocation(7, 15), | ||
|
@@ -4860,9 +4863,52 @@ public sealed class AsyncEnumerator : System.IAsyncDisposable | |
Diagnostic(ErrorCode.WRN_DeprecatedSymbol, "foreach").WithArguments("C.AsyncEnumerator.MoveNextAsync()").WithLocation(7, 15), | ||
// (7,15): warning CS0612: 'C.AsyncEnumerator.Current' is obsolete | ||
// await foreach (var i in new C()) | ||
Diagnostic(ErrorCode.WRN_DeprecatedSymbol, "foreach").WithArguments("C.AsyncEnumerator.Current").WithLocation(7, 15) | ||
); | ||
// Note: Obsolete on DisposeAsync is not reported since always called through IAsyncDisposable interface | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So the comment was incorrect - we do not go through IAsyncDisposable interface? #Resolved There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this was changed in PR but this comment in test was missed |
||
Diagnostic(ErrorCode.WRN_DeprecatedSymbol, "foreach").WithArguments("C.AsyncEnumerator.Current").WithLocation(7, 15)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
} | ||
|
||
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/30257")] | ||
public void TestWithPatternAndObsolete_WithoutDisposableInterface() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider testing with explicit interface implementation of the DisposeAsync method. #Resolved There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you add this test? I couldn't find it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My mistake. Added now :-) |
||
{ | ||
string source = @" | ||
using System.Threading.Tasks; | ||
class C | ||
{ | ||
static async System.Threading.Tasks.Task Main() | ||
{ | ||
await foreach (var i in new C()) | ||
{ | ||
} | ||
} | ||
[System.Obsolete] | ||
public AsyncEnumerator GetAsyncEnumerator(System.Threading.CancellationToken token = default) | ||
{ | ||
throw null; | ||
} | ||
[System.Obsolete] | ||
public sealed class AsyncEnumerator | ||
{ | ||
[System.Obsolete] | ||
public int Current { get => throw null; } | ||
[System.Obsolete] | ||
public Task<bool> MoveNextAsync() => throw null; | ||
[System.Obsolete] | ||
public ValueTask DisposeAsync() => throw null; | ||
} | ||
}"; | ||
var comp = CreateCompilationWithTasksExtensions(source + s_IAsyncEnumerable, options: TestOptions.DebugExe); | ||
comp.VerifyDiagnostics( | ||
// (7,15): warning CS0612: 'C.AsyncEnumerator.DisposeAsync()' is obsolete | ||
// await foreach (var i in new C()) | ||
Diagnostic(ErrorCode.WRN_DeprecatedSymbol, "foreach").WithArguments("C.AsyncEnumerator.DisposeAsync()").WithLocation(7, 15), | ||
// (7,15): warning CS0612: 'C.GetAsyncEnumerator(CancellationToken)' is obsolete | ||
// await foreach (var i in new C()) | ||
Diagnostic(ErrorCode.WRN_DeprecatedSymbol, "foreach").WithArguments("C.GetAsyncEnumerator(System.Threading.CancellationToken)").WithLocation(7, 15), | ||
// (7,15): warning CS0612: 'C.AsyncEnumerator.MoveNextAsync()' is obsolete | ||
// await foreach (var i in new C()) | ||
Diagnostic(ErrorCode.WRN_DeprecatedSymbol, "foreach").WithArguments("C.AsyncEnumerator.MoveNextAsync()").WithLocation(7, 15), | ||
// (7,15): warning CS0612: 'C.AsyncEnumerator.Current' is obsolete | ||
// await foreach (var i in new C()) | ||
Diagnostic(ErrorCode.WRN_DeprecatedSymbol, "foreach").WithArguments("C.AsyncEnumerator.Current").WithLocation(7, 15)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
} | ||
|
||
[Fact] | ||
|
@@ -8495,6 +8541,9 @@ public static class Extensions | |
}"; | ||
var comp = CreateCompilationWithMscorlib46(source, options: TestOptions.DebugExe, parseOptions: TestOptions.Regular9); | ||
comp.VerifyDiagnostics( | ||
// (8,15): warning CS0612: 'C.Enumerator.DisposeAsync()' is obsolete | ||
// await foreach (var i in new C()) | ||
Diagnostic(ErrorCode.WRN_DeprecatedSymbol, "foreach").WithArguments("C.Enumerator.DisposeAsync()").WithLocation(8, 15), | ||
// (8,15): warning CS0612: 'Extensions.GetAsyncEnumerator(C)' is obsolete | ||
// await foreach (var i in new C()) | ||
Diagnostic(ErrorCode.WRN_DeprecatedSymbol, "foreach").WithArguments("Extensions.GetAsyncEnumerator(C)").WithLocation(8, 15), | ||
|
@@ -8503,8 +8552,7 @@ public static class Extensions | |
Diagnostic(ErrorCode.WRN_DeprecatedSymbol, "foreach").WithArguments("C.Enumerator.MoveNextAsync()").WithLocation(8, 15), | ||
// (8,15): warning CS0612: 'C.Enumerator.Current' is obsolete | ||
// await foreach (var i in new C()) | ||
Diagnostic(ErrorCode.WRN_DeprecatedSymbol, "foreach").WithArguments("C.Enumerator.Current").WithLocation(8, 15) | ||
); | ||
Diagnostic(ErrorCode.WRN_DeprecatedSymbol, "foreach").WithArguments("C.Enumerator.Current").WithLocation(8, 15)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
CompileAndVerify(comp, expectedOutput: "123Disposed"); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9398,7 +9398,7 @@ public static class CExt | |
); | ||
} | ||
|
||
[Fact] | ||
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/73934")] | ||
public void UnmanagedCallersOnlyDeclaredOnPatternDispose() | ||
{ | ||
var comp = CreateCompilation(new[] { @" | ||
|
@@ -9424,10 +9424,12 @@ public static class CExt | |
", UnmanagedCallersOnlyAttribute }); | ||
|
||
comp.VerifyDiagnostics( | ||
// (14,6): error CS8896: 'UnmanagedCallersOnly' can only be applied to ordinary static non-abstract, non-virtual methods or static local functions. | ||
// 0.cs(7,9): error CS8901: 'SEnumerator.Dispose()' is attributed with 'UnmanagedCallersOnly' and cannot be called directly. Obtain a function pointer to this method. | ||
// foreach (var i in s) {} | ||
Diagnostic(ErrorCode.ERR_UnmanagedCallersOnlyMethodsCannotBeCalledDirectly, "foreach (var i in s) {}").WithArguments("SEnumerator.Dispose()").WithLocation(7, 9), | ||
// 0.cs(14,6): error CS8896: 'UnmanagedCallersOnly' can only be applied to ordinary static non-abstract, non-virtual methods or static local functions. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
// [UnmanagedCallersOnly] | ||
Diagnostic(ErrorCode.ERR_UnmanagedCallersOnlyRequiresStatic, "UnmanagedCallersOnly").WithLocation(14, 6) | ||
); | ||
Diagnostic(ErrorCode.ERR_UnmanagedCallersOnlyRequiresStatic, "UnmanagedCallersOnly").WithLocation(14, 6)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
} | ||
|
||
[Fact] | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should be specific here. If we are talking about Obsolete diagnostics, we should say that. In general, I wouldn't call a usage of an obsolete API as an improper use. #Closed