Skip to content

Commit 572a41f

Browse files
Copilot333fred
andcommitted
Add support for await using and await foreach preview feature detection
Co-authored-by: 333fred <[email protected]>
1 parent 53a7a8a commit 572a41f

File tree

7 files changed

+293
-9
lines changed

7 files changed

+293
-9
lines changed

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.NetCore.Analyzers/Runtime/CSharpDetectPreviewFeatureAnalyzer.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,40 @@ public class CSharpDetectPreviewFeatureAnalyzer : DetectPreviewFeatureAnalyzer
2626
return awaitableInfo.RuntimeAwaitMethod;
2727
}
2828

29+
protected override ISymbol? SymbolFromUsingOperation(IUsingOperation operation)
30+
{
31+
// Only handle await using, not regular using
32+
var syntax = operation.Syntax;
33+
if (syntax is LocalDeclarationStatementSyntax localDeclaration &&
34+
localDeclaration.UsingKeyword.Kind() != SyntaxKind.None &&
35+
localDeclaration.AwaitKeyword.Kind() != SyntaxKind.None)
36+
{
37+
var awaitableInfo = operation.SemanticModel!.GetAwaitExpressionInfo(localDeclaration);
38+
return awaitableInfo.RuntimeAwaitMethod;
39+
}
40+
else if (syntax is UsingStatementSyntax usingStatement &&
41+
usingStatement.AwaitKeyword.Kind() != SyntaxKind.None)
42+
{
43+
var awaitableInfo = operation.SemanticModel!.GetAwaitExpressionInfo(usingStatement);
44+
return awaitableInfo.RuntimeAwaitMethod;
45+
}
46+
47+
return null;
48+
}
49+
50+
protected override ISymbol? SymbolFromForEachOperation(IForEachLoopOperation operation)
51+
{
52+
// Only handle await foreach, not regular foreach
53+
if (operation.Syntax is not CommonForEachStatementSyntax forEachSyntax ||
54+
forEachSyntax is not ForEachStatementSyntax { AwaitKeyword.RawKind: not 0 })
55+
{
56+
return null;
57+
}
58+
59+
var forEachInfo = operation.SemanticModel.GetForEachStatementInfo(forEachSyntax);
60+
return forEachInfo.MoveNextAwaitableInfo.RuntimeAwaitMethod;
61+
}
62+
2963
protected override SyntaxNode? GetPreviewSyntaxNodeForFieldsOrEvents(ISymbol fieldOrEventSymbol, ISymbol previewSymbol)
3064
{
3165
ImmutableArray<SyntaxReference> fieldOrEventReferences = fieldOrEventSymbol.DeclaringSyntaxReferences;

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/Runtime/DetectPreviewFeatureAnalyzer.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,8 +270,9 @@ public override void Initialize(AnalysisContext context)
270270
OperationKind.CatchClause,
271271
OperationKind.TypeOf,
272272
OperationKind.EventAssignment,
273-
OperationKind.Await
274-
);
273+
OperationKind.Await,
274+
OperationKind.Using,
275+
OperationKind.Loop);
275276

276277
// Handle preview symbol definitions
277278
context.RegisterSymbolAction(context => AnalyzeSymbol(context, requiresPreviewFeaturesSymbols, virtualStaticsInInterfaces, previewFeaturesAttribute), s_symbols);
@@ -826,6 +827,8 @@ private bool OperationUsesPreviewFeatures(OperationAnalysisContext context,
826827
ITypeOfOperation typeOfOperation => typeOfOperation.TypeOperand,
827828
IEventAssignmentOperation eventAssignment => GetOperationSymbol(eventAssignment.EventReference),
828829
IAwaitOperation awaitOperation => SymbolFromAwaitOperation(awaitOperation),
830+
IUsingOperation usingOperation => SymbolFromUsingOperation(usingOperation),
831+
IForEachLoopOperation forEachOperation => SymbolFromForEachOperation(forEachOperation),
829832
_ => null,
830833
};
831834

@@ -842,6 +845,9 @@ private bool OperationUsesPreviewFeatures(OperationAnalysisContext context,
842845

843846
protected abstract ISymbol? SymbolFromAwaitOperation(IAwaitOperation operation);
844847

848+
protected abstract ISymbol? SymbolFromUsingOperation(IUsingOperation operation);
849+
850+
protected abstract ISymbol? SymbolFromForEachOperation(IForEachLoopOperation operation);
845851
private bool TypeParametersHavePreviewAttribute(ISymbol namedTypeSymbolOrMethodSymbol,
846852
ImmutableArray<ITypeParameterSymbol> typeParameters,
847853
ConcurrentDictionary<ISymbol, (bool isPreview, string? message, string? url)> requiresPreviewFeaturesSymbols,

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.VisualBasic.NetAnalyzers/Microsoft.NetCore.Analyzers/Runtime/BasicDetectPreviewFeatureAnalyzer.vb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ Namespace Microsoft.NetCore.VisualBasic.Analyzers.Runtime
2121
Return Nothing
2222
End Function
2323

24+
Protected Overrides Function SymbolFromUsingOperation(operation As IUsingOperation) As ISymbol
25+
Return Nothing
26+
End Function
27+
28+
Protected Overrides Function SymbolFromForEachOperation(operation As IForEachLoopOperation) As ISymbol
29+
Return Nothing
30+
End Function
31+
2432
Private Shared Function GetElementTypeForNullableAndArrayTypeNodes(parameterType As TypeSyntax) As TypeSyntax
2533
Dim ret As TypeSyntax = parameterType
2634
Dim loopVariable = TryCast(parameterType, NullableTypeSyntax)

src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler.CSharp/Analyzer.CSharp.Utilities.projitems

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
<Import_RootNamespace>Analyzer.CSharp.Utilities</Import_RootNamespace>
99
</PropertyGroup>
1010
<ItemGroup>
11+
<Compile Include="$(MSBuildThisFileDirectory)Extensions\SemanticModelExtensions.cs" />
1112
<Compile Include="$(MSBuildThisFileDirectory)Extensions\SyntaxGeneratorExtensions.cs" />
1213
<Compile Include="$(MSBuildThisFileDirectory)Extensions\SyntaxNodeExtensions.cs" />
1314
<Compile Include="$(MSBuildThisFileDirectory)Lightup\AwaitExpressionInfoWrapper.cs" />
15+
<Compile Include="$(MSBuildThisFileDirectory)Lightup\ForEachStatementInfoWrapper.cs" />
1416
<Compile Include="$(MSBuildThisFileDirectory)Lightup\SyntaxKindEx.cs" />
1517
</ItemGroup>
1618
</Project>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.
2+
3+
using System;
4+
using System.Threading;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp;
7+
using Microsoft.CodeAnalysis.CSharp.Syntax;
8+
9+
namespace Analyzer.Utilities.Lightup
10+
{
11+
internal static class SemanticModelExtensions
12+
{
13+
private static Func<SemanticModel, LocalDeclarationStatementSyntax, AwaitExpressionInfo>? s_GetAwaitExpressionInfoForLocalDeclaration;
14+
private static Func<SemanticModel, UsingStatementSyntax, AwaitExpressionInfo>? s_GetAwaitExpressionInfoForUsingStatement;
15+
16+
public static AwaitExpressionInfo GetAwaitExpressionInfo(this SemanticModel semanticModel, LocalDeclarationStatementSyntax awaitUsingDeclaration)
17+
{
18+
LazyInitializer.EnsureInitialized(ref s_GetAwaitExpressionInfoForLocalDeclaration, () =>
19+
{
20+
// Try to get the method from CSharpExtensions
21+
var csharpExtensionsType = typeof(Microsoft.CodeAnalysis.CSharpExtensions);
22+
var method = csharpExtensionsType.GetMethod(
23+
"GetAwaitExpressionInfo",
24+
new[] { typeof(SemanticModel), typeof(LocalDeclarationStatementSyntax) });
25+
26+
if (method != null)
27+
{
28+
return (model, syntax) => (AwaitExpressionInfo)method.Invoke(null, new object?[] { model, syntax })!;
29+
}
30+
31+
// Fallback if method doesn't exist
32+
return (model, syntax) => default;
33+
});
34+
35+
return s_GetAwaitExpressionInfoForLocalDeclaration!(semanticModel, awaitUsingDeclaration);
36+
}
37+
38+
public static AwaitExpressionInfo GetAwaitExpressionInfo(this SemanticModel semanticModel, UsingStatementSyntax awaitUsingStatement)
39+
{
40+
LazyInitializer.EnsureInitialized(ref s_GetAwaitExpressionInfoForUsingStatement, () =>
41+
{
42+
// Try to get the method from CSharpExtensions
43+
var csharpExtensionsType = typeof(Microsoft.CodeAnalysis.CSharpExtensions);
44+
var method = csharpExtensionsType.GetMethod(
45+
"GetAwaitExpressionInfo",
46+
new[] { typeof(SemanticModel), typeof(UsingStatementSyntax) });
47+
48+
if (method != null)
49+
{
50+
return (model, syntax) => (AwaitExpressionInfo)method.Invoke(null, new object?[] { model, syntax })!;
51+
}
52+
53+
// Fallback if method doesn't exist
54+
return (model, syntax) => default;
55+
});
56+
57+
return s_GetAwaitExpressionInfoForUsingStatement!(semanticModel, awaitUsingStatement);
58+
}
59+
}
60+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.
2+
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CSharp;
5+
6+
namespace Analyzer.Utilities.Lightup
7+
{
8+
internal static class ForEachStatementInfoWrapper
9+
{
10+
private static Func<ForEachStatementInfo, AwaitExpressionInfo>? s_MoveNextAwaitableInfoAccessor;
11+
private static Func<ForEachStatementInfo, AwaitExpressionInfo>? s_DisposeAwaitableInfoAccessor;
12+
13+
extension(ForEachStatementInfo info)
14+
{
15+
public AwaitExpressionInfo MoveNextAwaitableInfo
16+
{
17+
get
18+
{
19+
LazyInitializer.EnsureInitialized(ref s_MoveNextAwaitableInfoAccessor, () =>
20+
{
21+
return LightupHelpers.CreatePropertyAccessor<ForEachStatementInfo, AwaitExpressionInfo>(
22+
typeof(ForEachStatementInfo),
23+
"info",
24+
"MoveNextAwaitableInfo",
25+
fallbackResult: default);
26+
});
27+
28+
RoslynDebug.Assert(s_MoveNextAwaitableInfoAccessor is not null);
29+
return s_MoveNextAwaitableInfoAccessor(info);
30+
}
31+
}
32+
33+
public AwaitExpressionInfo DisposeAwaitableInfo
34+
{
35+
get
36+
{
37+
LazyInitializer.EnsureInitialized(ref s_DisposeAwaitableInfoAccessor, () =>
38+
{
39+
return LightupHelpers.CreatePropertyAccessor<ForEachStatementInfo, AwaitExpressionInfo>(
40+
typeof(ForEachStatementInfo),
41+
"info",
42+
"DisposeAwaitableInfo",
43+
fallbackResult: default);
44+
});
45+
46+
RoslynDebug.Assert(s_DisposeAwaitableInfoAccessor is not null);
47+
return s_DisposeAwaitableInfoAccessor(info);
48+
}
49+
}
50+
}
51+
}
52+
}

src/Microsoft.CodeAnalysis.NetAnalyzers/tests/Microsoft.CodeAnalysis.NetAnalyzers.UnitTests/Microsoft.NetCore.Analyzers/Runtime/DetectPreviewFeatureUnitTests.Misc.cs

Lines changed: 129 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -805,7 +805,7 @@ End Module
805805
}
806806

807807
[Fact]
808-
public async Task VerifyRuntimeAsyncReportsDiagnostic()
808+
public async Task VerifyRuntimeAsyncAwaitReportsDiagnostic()
809809
{
810810
var csInput = """
811811
using System.Threading.Tasks;
@@ -818,7 +818,7 @@ async Task M()
818818
}
819819
""";
820820

821-
var test = new RuntimeAsyncFixVerifier
821+
var test = new RuntimeAsyncTestVerifier
822822
{
823823
TestState =
824824
{
@@ -838,7 +838,7 @@ async Task M()
838838
}
839839

840840
[Fact]
841-
public async Task VerifyRuntimeAsyncReportsDiagnostic_CustomAwaiter()
841+
public async Task VerifyRuntimeAsyncAwaitCustomAwaiterReportsDiagnostic()
842842
{
843843
var csInput = """
844844
using System.Threading.Tasks;
@@ -851,7 +851,7 @@ async Task M()
851851
}
852852
""";
853853

854-
var test = new RuntimeAsyncFixVerifier
854+
var test = new RuntimeAsyncTestVerifier
855855
{
856856
TestState =
857857
{
@@ -869,11 +869,133 @@ async Task M()
869869
await test.RunAsync();
870870
}
871871

872-
private class RuntimeAsyncFixVerifier : VerifyCS.Test
872+
[Fact]
873+
public async Task VerifyRuntimeAsyncAwaitUsingDeclarationReportsDiagnostic()
874+
{
875+
var csInput = """
876+
using System.Threading.Tasks;
877+
using System;
878+
class C
879+
{
880+
async Task M()
881+
{
882+
await using var stream = new MemoryStream();
883+
}
884+
}
885+
886+
class MemoryStream : IAsyncDisposable
887+
{
888+
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
889+
}
890+
""";
891+
892+
var test = new RuntimeAsyncTestVerifier
893+
{
894+
TestState =
895+
{
896+
Sources =
897+
{
898+
csInput
899+
}
900+
},
901+
ExpectedDiagnostics =
902+
{
903+
// /0/Test0.cs(7,9): error CA2252: Using 'UnsafeAwaitAwaiter' requires opting into preview features. See https://aka.ms/dotnet-warnings/preview-features for more information.
904+
VerifyCS.Diagnostic(DetectPreviewFeatureAnalyzer.GeneralPreviewFeatureAttributeRule).WithSpan(7, 9, 7, 52).WithArguments("UnsafeAwaitAwaiter", DetectPreviewFeatureAnalyzer.DefaultURL)
905+
}
906+
};
907+
908+
await test.RunAsync();
909+
}
910+
911+
[Fact]
912+
public async Task VerifyRuntimeAsyncAwaitUsingStatementReportsDiagnostic()
913+
{
914+
var csInput = """
915+
using System.Threading.Tasks;
916+
using System;
917+
class C
918+
{
919+
async Task M()
920+
{
921+
await using (var stream = new MemoryStream())
922+
{
923+
}
924+
}
925+
}
926+
927+
class MemoryStream : IAsyncDisposable
928+
{
929+
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
930+
}
931+
""";
932+
933+
var test = new RuntimeAsyncTestVerifier
934+
{
935+
TestState =
936+
{
937+
Sources =
938+
{
939+
csInput
940+
}
941+
},
942+
ExpectedDiagnostics =
943+
{
944+
// /0/Test0.cs(7,9): error CA2252: Using 'UnsafeAwaitAwaiter' requires opting into preview features. See https://aka.ms/dotnet-warnings/preview-features for more information.
945+
VerifyCS.Diagnostic(DetectPreviewFeatureAnalyzer.GeneralPreviewFeatureAttributeRule).WithSpan(7, 9, 7, 54).WithArguments("UnsafeAwaitAwaiter", DetectPreviewFeatureAnalyzer.DefaultURL)
946+
}
947+
};
948+
949+
await test.RunAsync();
950+
}
951+
952+
[Fact]
953+
public async Task VerifyRuntimeAsyncAwaitForeachReportsDiagnostic()
954+
{
955+
var csInput = """
956+
using System.Collections.Generic;
957+
using System.Threading.Tasks;
958+
class C
959+
{
960+
async Task M()
961+
{
962+
await foreach (var item in GetItemsAsync())
963+
{
964+
}
965+
}
966+
967+
async IAsyncEnumerable<int> GetItemsAsync()
968+
{
969+
yield return 1;
970+
await Task.CompletedTask;
971+
}
972+
}
973+
""";
974+
975+
var test = new RuntimeAsyncTestVerifier
976+
{
977+
TestState =
978+
{
979+
Sources =
980+
{
981+
csInput
982+
}
983+
},
984+
ExpectedDiagnostics =
985+
{
986+
// /0/Test0.cs(7,9): error CA2252: Using 'UnsafeAwaitAwaiter' requires opting into preview features. See https://aka.ms/dotnet-warnings/preview-features for more information.
987+
VerifyCS.Diagnostic(DetectPreviewFeatureAnalyzer.GeneralPreviewFeatureAttributeRule).WithSpan(7, 9, 7, 56).WithArguments("UnsafeAwaitAwaiter", DetectPreviewFeatureAnalyzer.DefaultURL)
988+
}
989+
};
990+
991+
await test.RunAsync();
992+
}
993+
994+
private class RuntimeAsyncTestVerifier : VerifyCS.Test
873995
{
874-
public static readonly ReferenceAssemblies Net100 = new("net10.0", new PackageIdentity("Microsoft.NETCore.App.Ref", "10.0.0-rc.1.25451.107"), Path.Combine("ref", "net10.0"));
996+
public static readonly ReferenceAssemblies Net100 = new("net10.0", new PackageIdentity("Microsoft.NETCore.App.Ref", "10.0.0-rc.1.25451.107"), System.IO.Path.Combine("ref", "net10.0"));
875997

876-
public RuntimeAsyncFixVerifier()
998+
public RuntimeAsyncTestVerifier()
877999
{
8781000
ReferenceAssemblies = Net100;
8791001
LanguageVersion = CodeAnalysis.CSharp.LanguageVersion.CSharp10;

0 commit comments

Comments
 (0)