diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.NetCore.Analyzers/Runtime/CSharpDetectPreviewFeatureAnalyzer.cs b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.NetCore.Analyzers/Runtime/CSharpDetectPreviewFeatureAnalyzer.cs index 2ab9ff663012..02986fcb2d9e 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.NetCore.Analyzers/Runtime/CSharpDetectPreviewFeatureAnalyzer.cs +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.CSharp.NetAnalyzers/Microsoft.NetCore.Analyzers/Runtime/CSharpDetectPreviewFeatureAnalyzer.cs @@ -26,6 +26,40 @@ public class CSharpDetectPreviewFeatureAnalyzer : DetectPreviewFeatureAnalyzer return awaitableInfo.RuntimeAwaitMethod; } + protected override ISymbol? SymbolFromUsingOperation(IUsingOperation operation) + { + // Only handle await using, not regular using + var syntax = operation.Syntax; + if (syntax is LocalDeclarationStatementSyntax localDeclaration && + localDeclaration.UsingKeyword.Kind() != SyntaxKind.None && + localDeclaration.AwaitKeyword.Kind() != SyntaxKind.None) + { + var awaitableInfo = operation.SemanticModel!.GetAwaitExpressionInfo(localDeclaration); + return awaitableInfo.RuntimeAwaitMethod; + } + else if (syntax is UsingStatementSyntax usingStatement && + usingStatement.AwaitKeyword.Kind() != SyntaxKind.None) + { + var awaitableInfo = operation.SemanticModel!.GetAwaitExpressionInfo(usingStatement); + return awaitableInfo.RuntimeAwaitMethod; + } + + return null; + } + + protected override ISymbol? SymbolFromForEachOperation(IForEachLoopOperation operation) + { + // Only handle await foreach, not regular foreach + if (operation.Syntax is not CommonForEachStatementSyntax forEachSyntax || + forEachSyntax is not ForEachStatementSyntax { AwaitKeyword.RawKind: not 0 }) + { + return null; + } + + var forEachInfo = operation.SemanticModel.GetForEachStatementInfo(forEachSyntax); + return forEachInfo.MoveNextAwaitableInfo.RuntimeAwaitMethod; + } + protected override SyntaxNode? GetPreviewSyntaxNodeForFieldsOrEvents(ISymbol fieldOrEventSymbol, ISymbol previewSymbol) { ImmutableArray fieldOrEventReferences = fieldOrEventSymbol.DeclaringSyntaxReferences; diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/Runtime/DetectPreviewFeatureAnalyzer.cs b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/Runtime/DetectPreviewFeatureAnalyzer.cs index 1df63a6f732d..ee73106cf969 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/Runtime/DetectPreviewFeatureAnalyzer.cs +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/Runtime/DetectPreviewFeatureAnalyzer.cs @@ -270,8 +270,9 @@ public override void Initialize(AnalysisContext context) OperationKind.CatchClause, OperationKind.TypeOf, OperationKind.EventAssignment, - OperationKind.Await - ); + OperationKind.Await, + OperationKind.Using, + OperationKind.Loop); // Handle preview symbol definitions context.RegisterSymbolAction(context => AnalyzeSymbol(context, requiresPreviewFeaturesSymbols, virtualStaticsInInterfaces, previewFeaturesAttribute), s_symbols); @@ -826,6 +827,8 @@ private bool OperationUsesPreviewFeatures(OperationAnalysisContext context, ITypeOfOperation typeOfOperation => typeOfOperation.TypeOperand, IEventAssignmentOperation eventAssignment => GetOperationSymbol(eventAssignment.EventReference), IAwaitOperation awaitOperation => SymbolFromAwaitOperation(awaitOperation), + IUsingOperation usingOperation => SymbolFromUsingOperation(usingOperation), + IForEachLoopOperation forEachOperation => SymbolFromForEachOperation(forEachOperation), _ => null, }; @@ -842,6 +845,9 @@ private bool OperationUsesPreviewFeatures(OperationAnalysisContext context, protected abstract ISymbol? SymbolFromAwaitOperation(IAwaitOperation operation); + protected abstract ISymbol? SymbolFromUsingOperation(IUsingOperation operation); + + protected abstract ISymbol? SymbolFromForEachOperation(IForEachLoopOperation operation); private bool TypeParametersHavePreviewAttribute(ISymbol namedTypeSymbolOrMethodSymbol, ImmutableArray typeParameters, ConcurrentDictionary requiresPreviewFeaturesSymbols, diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.VisualBasic.NetAnalyzers/Microsoft.NetCore.Analyzers/Runtime/BasicDetectPreviewFeatureAnalyzer.vb b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.VisualBasic.NetAnalyzers/Microsoft.NetCore.Analyzers/Runtime/BasicDetectPreviewFeatureAnalyzer.vb index be0214120c69..7a2740afa261 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.VisualBasic.NetAnalyzers/Microsoft.NetCore.Analyzers/Runtime/BasicDetectPreviewFeatureAnalyzer.vb +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.VisualBasic.NetAnalyzers/Microsoft.NetCore.Analyzers/Runtime/BasicDetectPreviewFeatureAnalyzer.vb @@ -21,6 +21,14 @@ Namespace Microsoft.NetCore.VisualBasic.Analyzers.Runtime Return Nothing End Function + Protected Overrides Function SymbolFromUsingOperation(operation As IUsingOperation) As ISymbol + Return Nothing + End Function + + Protected Overrides Function SymbolFromForEachOperation(operation As IForEachLoopOperation) As ISymbol + Return Nothing + End Function + Private Shared Function GetElementTypeForNullableAndArrayTypeNodes(parameterType As TypeSyntax) As TypeSyntax Dim ret As TypeSyntax = parameterType Dim loopVariable = TryCast(parameterType, NullableTypeSyntax) diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler.CSharp/Analyzer.CSharp.Utilities.projitems b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler.CSharp/Analyzer.CSharp.Utilities.projitems index cad6a604c501..575bee15fbfb 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler.CSharp/Analyzer.CSharp.Utilities.projitems +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler.CSharp/Analyzer.CSharp.Utilities.projitems @@ -8,9 +8,11 @@ Analyzer.CSharp.Utilities + + diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler.CSharp/Extensions/SemanticModelExtensions.cs b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler.CSharp/Extensions/SemanticModelExtensions.cs new file mode 100644 index 000000000000..c6b300592679 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler.CSharp/Extensions/SemanticModelExtensions.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Analyzer.Utilities.Lightup +{ + internal static class SemanticModelExtensions + { + private static Func? s_GetAwaitExpressionInfoForLocalDeclaration; + private static Func? s_GetAwaitExpressionInfoForUsingStatement; + + public static AwaitExpressionInfo GetAwaitExpressionInfo(this SemanticModel semanticModel, LocalDeclarationStatementSyntax awaitUsingDeclaration) + { + LazyInitializer.EnsureInitialized(ref s_GetAwaitExpressionInfoForLocalDeclaration, () => + { + // Try to get the method from CSharpExtensions + var csharpExtensionsType = typeof(Microsoft.CodeAnalysis.CSharp.CSharpExtensions); + var method = csharpExtensionsType.GetMethod( + "GetAwaitExpressionInfo", + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static, + null, + new[] { typeof(SemanticModel), typeof(LocalDeclarationStatementSyntax) }, + null); + + if (method != null) + { + return (model, syntax) => (AwaitExpressionInfo)method.Invoke(null, new object?[] { model, syntax })!; + } + + // Fallback if method doesn't exist + return (model, syntax) => default; + }); + + return s_GetAwaitExpressionInfoForLocalDeclaration!(semanticModel, awaitUsingDeclaration); + } + + public static AwaitExpressionInfo GetAwaitExpressionInfo(this SemanticModel semanticModel, UsingStatementSyntax awaitUsingStatement) + { + LazyInitializer.EnsureInitialized(ref s_GetAwaitExpressionInfoForUsingStatement, () => + { + // Try to get the method from CSharpExtensions + var csharpExtensionsType = typeof(Microsoft.CodeAnalysis.CSharp.CSharpExtensions); + var method = csharpExtensionsType.GetMethod( + "GetAwaitExpressionInfo", + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static, + null, + new[] { typeof(SemanticModel), typeof(UsingStatementSyntax) }, + null); + + if (method != null) + { + return (model, syntax) => (AwaitExpressionInfo)method.Invoke(null, new object?[] { model, syntax })!; + } + + // Fallback if method doesn't exist + return (model, syntax) => default; + }); + + return s_GetAwaitExpressionInfoForUsingStatement!(semanticModel, awaitUsingStatement); + } + } +} diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler.CSharp/Lightup/ForEachStatementInfoWrapper.cs b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler.CSharp/Lightup/ForEachStatementInfoWrapper.cs new file mode 100644 index 000000000000..16793b61c488 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler.CSharp/Lightup/ForEachStatementInfoWrapper.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Analyzer.Utilities.Lightup +{ + internal static class ForEachStatementInfoWrapper + { + private static Func? s_MoveNextAwaitableInfoAccessor; + private static Func? s_DisposeAwaitableInfoAccessor; + + extension(ForEachStatementInfo info) + { + public AwaitExpressionInfo MoveNextAwaitableInfo + { + get + { + LazyInitializer.EnsureInitialized(ref s_MoveNextAwaitableInfoAccessor, () => + { + return LightupHelpers.CreatePropertyAccessor( + typeof(ForEachStatementInfo), + "info", + "MoveNextAwaitableInfo", + fallbackResult: default); + }); + + RoslynDebug.Assert(s_MoveNextAwaitableInfoAccessor is not null); + return s_MoveNextAwaitableInfoAccessor(info); + } + } + + public AwaitExpressionInfo DisposeAwaitableInfo + { + get + { + LazyInitializer.EnsureInitialized(ref s_DisposeAwaitableInfoAccessor, () => + { + return LightupHelpers.CreatePropertyAccessor( + typeof(ForEachStatementInfo), + "info", + "DisposeAwaitableInfo", + fallbackResult: default); + }); + + RoslynDebug.Assert(s_DisposeAwaitableInfoAccessor is not null); + return s_DisposeAwaitableInfoAccessor(info); + } + } + } + } +} diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/tests/Microsoft.CodeAnalysis.NetAnalyzers.UnitTests/Microsoft.NetCore.Analyzers/Runtime/DetectPreviewFeatureUnitTests.Misc.cs b/src/Microsoft.CodeAnalysis.NetAnalyzers/tests/Microsoft.CodeAnalysis.NetAnalyzers.UnitTests/Microsoft.NetCore.Analyzers/Runtime/DetectPreviewFeatureUnitTests.Misc.cs index b3b84fdf3516..8fceb8050910 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/tests/Microsoft.CodeAnalysis.NetAnalyzers.UnitTests/Microsoft.NetCore.Analyzers/Runtime/DetectPreviewFeatureUnitTests.Misc.cs +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/tests/Microsoft.CodeAnalysis.NetAnalyzers.UnitTests/Microsoft.NetCore.Analyzers/Runtime/DetectPreviewFeatureUnitTests.Misc.cs @@ -805,7 +805,7 @@ End Module } [Fact] - public async Task VerifyRuntimeAsyncReportsDiagnostic() + public async Task VerifyRuntimeAsyncAwaitReportsDiagnostic() { var csInput = """ using System.Threading.Tasks; @@ -818,7 +818,7 @@ async Task M() } """; - var test = new RuntimeAsyncFixVerifier + var test = new RuntimeAsyncTestVerifier { TestState = { @@ -838,7 +838,7 @@ async Task M() } [Fact] - public async Task VerifyRuntimeAsyncReportsDiagnostic_CustomAwaiter() + public async Task VerifyRuntimeAsyncAwaitCustomAwaiterReportsDiagnostic() { var csInput = """ using System.Threading.Tasks; @@ -851,7 +851,7 @@ async Task M() } """; - var test = new RuntimeAsyncFixVerifier + var test = new RuntimeAsyncTestVerifier { TestState = { @@ -869,11 +869,133 @@ async Task M() await test.RunAsync(); } - private class RuntimeAsyncFixVerifier : VerifyCS.Test + [Fact] + public async Task VerifyRuntimeAsyncAwaitUsingDeclarationReportsDiagnostic() + { + var csInput = """ + using System.Threading.Tasks; + using System; + class C + { + async Task M() + { + await using var stream = new MemoryStream(); + } + } + + class MemoryStream : IAsyncDisposable + { + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + } + """; + + var test = new RuntimeAsyncTestVerifier + { + TestState = + { + Sources = + { + csInput + } + }, + ExpectedDiagnostics = + { + // /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. + VerifyCS.Diagnostic(DetectPreviewFeatureAnalyzer.GeneralPreviewFeatureAttributeRule).WithSpan(7, 9, 7, 52).WithArguments("UnsafeAwaitAwaiter", DetectPreviewFeatureAnalyzer.DefaultURL) + } + }; + + await test.RunAsync(); + } + + [Fact] + public async Task VerifyRuntimeAsyncAwaitUsingStatementReportsDiagnostic() + { + var csInput = """ + using System.Threading.Tasks; + using System; + class C + { + async Task M() + { + await using (var stream = new MemoryStream()) + { + } + } + } + + class MemoryStream : IAsyncDisposable + { + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + } + """; + + var test = new RuntimeAsyncTestVerifier + { + TestState = + { + Sources = + { + csInput + } + }, + ExpectedDiagnostics = + { + // /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. + VerifyCS.Diagnostic(DetectPreviewFeatureAnalyzer.GeneralPreviewFeatureAttributeRule).WithSpan(7, 9, 7, 54).WithArguments("UnsafeAwaitAwaiter", DetectPreviewFeatureAnalyzer.DefaultURL) + } + }; + + await test.RunAsync(); + } + + [Fact] + public async Task VerifyRuntimeAsyncAwaitForeachReportsDiagnostic() + { + var csInput = """ + using System.Collections.Generic; + using System.Threading.Tasks; + class C + { + async Task M() + { + await foreach (var item in GetItemsAsync()) + { + } + } + + async IAsyncEnumerable GetItemsAsync() + { + yield return 1; + await Task.CompletedTask; + } + } + """; + + var test = new RuntimeAsyncTestVerifier + { + TestState = + { + Sources = + { + csInput + } + }, + ExpectedDiagnostics = + { + // /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. + VerifyCS.Diagnostic(DetectPreviewFeatureAnalyzer.GeneralPreviewFeatureAttributeRule).WithSpan(7, 9, 7, 56).WithArguments("UnsafeAwaitAwaiter", DetectPreviewFeatureAnalyzer.DefaultURL) + } + }; + + await test.RunAsync(); + } + + private class RuntimeAsyncTestVerifier : VerifyCS.Test { - 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")); + 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")); - public RuntimeAsyncFixVerifier() + public RuntimeAsyncTestVerifier() { ReferenceAssemblies = Net100; LanguageVersion = CodeAnalysis.CSharp.LanguageVersion.CSharp10;