From ef1bb2a4dec3033dbed2577f1b05d0281a613ada Mon Sep 17 00:00:00 2001 From: Fred Silberberg Date: Fri, 19 Sep 2025 11:05:33 -0700 Subject: [PATCH 1/2] Update DetectPreviewFeatureAnalyzer for runtime async We need to be giving warnings around enabling preview features for runtime async. This updates the analyzer to do this. --- .../CSharpDetectPreviewFeatureAnalyzer.cs | 14 ++ .../Runtime/DetectPreviewFeatureAnalyzer.cs | 8 +- .../BasicDetectPreviewFeatureAnalyzer.vb | 7 +- .../Analyzer.CSharp.Utilities.projitems | 3 +- .../Lightup/AwaitExpressionInfoWrapper.cs | 32 +++++ .../Compiler/Lightup/LightupHelpers.cs | 2 +- .../DetectPreviewFeatureUnitTests.Misc.cs | 131 +++++++++++++++--- 7 files changed, 171 insertions(+), 26 deletions(-) create mode 100644 src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler.CSharp/Lightup/AwaitExpressionInfoWrapper.cs 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 849b35e3b4a4..26de7196caf0 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 @@ -3,15 +3,29 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; using Microsoft.NetCore.Analyzers.Runtime; +using Analyzer.Utilities.Lightup; namespace Microsoft.NetCore.CSharp.Analyzers.Runtime { [DiagnosticAnalyzer(LanguageNames.CSharp)] public class CSharpDetectPreviewFeatureAnalyzer : DetectPreviewFeatureAnalyzer { + protected override ISymbol? SymbolFromAwaitOperation(IAwaitOperation operation) + { + if (operation.Syntax is not AwaitExpressionSyntax awaitSyntax) + { + return null; + } + + var awaitableInfo = operation.SemanticModel.GetAwaitExpressionInfo(awaitSyntax); + return awaitableInfo.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 52f86e756313..1df63a6f732d 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 @@ -269,7 +269,8 @@ public override void Initialize(AnalysisContext context) OperationKind.ArrayCreation, OperationKind.CatchClause, OperationKind.TypeOf, - OperationKind.EventAssignment + OperationKind.EventAssignment, + OperationKind.Await ); // Handle preview symbol definitions @@ -809,7 +810,7 @@ private bool OperationUsesPreviewFeatures(OperationAnalysisContext context, return false; } - private static ISymbol? GetOperationSymbol(IOperation operation) + private ISymbol? GetOperationSymbol(IOperation operation) => operation switch { IInvocationOperation iOperation => iOperation.TargetMethod, @@ -824,6 +825,7 @@ private bool OperationUsesPreviewFeatures(OperationAnalysisContext context, ICatchClauseOperation catchClauseOperation => catchClauseOperation.ExceptionType, ITypeOfOperation typeOfOperation => typeOfOperation.TypeOperand, IEventAssignmentOperation eventAssignment => GetOperationSymbol(eventAssignment.EventReference), + IAwaitOperation awaitOperation => SymbolFromAwaitOperation(awaitOperation), _ => null, }; @@ -838,6 +840,8 @@ private bool OperationUsesPreviewFeatures(OperationAnalysisContext context, return ret; } + protected abstract ISymbol? SymbolFromAwaitOperation(IAwaitOperation 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 e357049f7d52..be0214120c69 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 @@ -2,6 +2,7 @@ Imports Microsoft.CodeAnalysis Imports Microsoft.CodeAnalysis.Diagnostics +Imports Microsoft.CodeAnalysis.Operations Imports Microsoft.NetCore.Analyzers.Runtime Imports Microsoft.CodeAnalysis.VisualBasic.Syntax @@ -16,6 +17,10 @@ Namespace Microsoft.NetCore.VisualBasic.Analyzers.Runtime Return identifier.ValueText.Equals(previewInterfaceSymbol.Name, StringComparison.OrdinalIgnoreCase) End Function + Protected Overrides Function SymbolFromAwaitOperation(operation As IAwaitOperation) 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) @@ -334,4 +339,4 @@ Namespace Microsoft.NetCore.VisualBasic.Analyzers.Runtime End Function End Class -End Namespace \ No newline at end of file +End Namespace 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 c6f9820f49f9..cad6a604c501 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 @@ -10,6 +10,7 @@ + - \ No newline at end of file + diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler.CSharp/Lightup/AwaitExpressionInfoWrapper.cs b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler.CSharp/Lightup/AwaitExpressionInfoWrapper.cs new file mode 100644 index 000000000000..8e977a2d3903 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler.CSharp/Lightup/AwaitExpressionInfoWrapper.cs @@ -0,0 +1,32 @@ +// 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 AwaitExpressionInfoWrapper + { + private static Func? s_RuntimeAwaitMethodAccessor; + + extension(AwaitExpressionInfo info) + { + public IMethodSymbol? RuntimeAwaitMethod + { + get + { + LazyInitializer.EnsureInitialized(ref s_RuntimeAwaitMethodAccessor, () => + { + return LightupHelpers.CreatePropertyAccessor( + typeof(AwaitExpressionInfo), + "info", + "RuntimeAwaitMethod", + fallbackResult: null); + }); + + return s_RuntimeAwaitMethodAccessor!(info); + } + } + } + } +} diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler/Lightup/LightupHelpers.cs b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler/Lightup/LightupHelpers.cs index 39b63ec4ad03..8c714a20a376 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler/Lightup/LightupHelpers.cs +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler/Lightup/LightupHelpers.cs @@ -53,7 +53,7 @@ internal static Func CreateSymbolPropertyAccessor CreatePropertyAccessor(type, "symbol", propertyName, fallbackResult); - private static Func CreatePropertyAccessor(Type? type, string parameterName, string propertyName, TProperty fallbackResult) + internal static Func CreatePropertyAccessor(Type? type, string parameterName, string propertyName, TProperty fallbackResult) { if (!TryGetProperty(type, propertyName, out var property)) { 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 a0fe473ba9ab..b3b84fdf3516 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 @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. using System.Threading.Tasks; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Testing; using Xunit; using VerifyCS = Test.Utilities.CSharpCodeFixVerifier< @@ -95,7 +96,7 @@ private static VerifyCS.Test TestCSPreview(string csInput) [Fact] public async Task TestCatchPreviewException() { - var csInput = @" + var csInput = @" using System.Runtime.Versioning; using System; namespace Preview_Feature_Scratch { @@ -131,7 +132,7 @@ static void Main(string[] args) [Fact] public async Task TestCustomMessageCustomURL() { - var csInput = @" + var csInput = @" using System.Runtime.Versioning; using System; namespace Preview_Feature_Scratch { @@ -160,7 +161,7 @@ public class Lib [Fact] public async Task TestCustomMessageDefaultURL() { - var csInput = @" + var csInput = @" using System.Runtime.Versioning; using System; namespace Preview_Feature_Scratch { @@ -187,7 +188,7 @@ public class Lib [Fact] public async Task TestDefaultMessageCustomURL() { - var csInput = @" + var csInput = @" using System.Runtime.Versioning; using System; namespace Preview_Feature_Scratch { @@ -214,7 +215,7 @@ public class Lib [Fact] public async Task TestArrayOfPreviewTypes() { - var csInput = @" + var csInput = @" using System.Runtime.Versioning; using System; namespace Preview_Feature_Scratch { @@ -243,7 +244,7 @@ public class Lib [Fact] public async Task TestArrayOfArraysOfPreviewTypes() { - var csInput = @" + var csInput = @" using System.Runtime.Versioning; using System; namespace Preview_Feature_Scratch { @@ -270,7 +271,7 @@ public class Lib [Fact] public async Task TestPreviewLanguageFeaturesHeirarchy() { - var csInput = @" + var csInput = @" using System.Runtime.Versioning; using System; namespace Preview_Feature_Scratch { @@ -304,7 +305,7 @@ public interface IProgram [Fact] public async Task TestPreviewLanguageFeatures() { - var csInput = @" + var csInput = @" using System.Runtime.Versioning; using System; namespace Preview_Feature_Scratch { @@ -339,7 +340,7 @@ public interface IProgram [Fact] public async Task TestInterfaceMethodInvocation() { - var csInput = @" + var csInput = @" using System.Runtime.Versioning; using System; namespace Preview_Feature_Scratch { @@ -401,7 +402,7 @@ public void Foo() [Fact] public async Task TestDelegate() { - var csInput = @" + var csInput = @" using System.Runtime.Versioning; using System; namespace Preview_Feature_Scratch { @@ -426,7 +427,7 @@ static void Main(string[] args) [Fact] public async Task TestTypeOf() { - var csInput = @" + var csInput = @" using System.Runtime.Versioning; using System; namespace Preview_Feature_Scratch { @@ -438,7 +439,7 @@ static void Main(string[] args) Console.WriteLine({|#0:typeof(IFoo)|}); } } - + [RequiresPreviewFeatures] interface IFoo { } }"; @@ -451,7 +452,7 @@ interface IFoo { } [Fact] public async Task TestSimpleCustomAttributeOnPreviewClass() { - var csInput = @" + var csInput = @" using System.Runtime.Versioning; using System; namespace Preview_Feature_Scratch { @@ -485,7 +486,7 @@ class MyAttribute : Attribute [Fact] public async Task TestSimpleCustomAttribute() { - var csInput = @" + var csInput = @" using System.Runtime.Versioning; using System; namespace Preview_Feature_Scratch { @@ -518,7 +519,7 @@ class MyAttribute : Attribute [Fact(Skip = "https://github.com/dotnet/roslyn-analyzers/issues/6134")] public async Task TestCustomAttribute() { - var csInput = @" + var csInput = @" using System.Runtime.Versioning; using System; namespace Preview_Feature_Scratch { @@ -593,7 +594,7 @@ public MyAttribute(bool foo) {} [Fact] public async Task TestDeepNesting() { - var csInput = @" + var csInput = @" using System.Runtime.Versioning; using System; namespace Preview_Feature_Scratch { @@ -638,7 +639,7 @@ public class NestedClass3 [Fact] public async Task TestNestedInvocation() { - var csInput = @" + var csInput = @" using System.Runtime.Versioning; using System; namespace Preview_Feature_Scratch { @@ -666,7 +667,7 @@ class A [Fact] public async Task TestNestedClass() { - var csInput = @" + var csInput = @" using System.Runtime.Versioning; using System; namespace Preview_Feature_Scratch { @@ -694,7 +695,7 @@ static void Main(string[] args) [Fact] public async Task TestCallback() { - var csInput = @" + var csInput = @" using System.Runtime.Versioning; using System; namespace Preview_Feature_Scratch {" + @@ -740,7 +741,7 @@ public class Foo [Fact] public async Task TestVbCaseInsensitiveCsharpSensitive() { - var csInput = @" + var csInput = @" using System.Runtime.Versioning; using System; namespace Preview_Feature_Scratch { @@ -775,7 +776,7 @@ public void UnmarkedMethodInUnMarkedInterface() { } test.ExpectedDiagnostics.Add(VerifyCS.Diagnostic(DetectPreviewFeatureAnalyzer.ImplementsPreviewInterfaceRule).WithLocation(1).WithArguments("Program", "IProgram", DetectPreviewFeatureAnalyzer.DefaultURL)); await test.RunAsync(); - var vbInput = @" + var vbInput = @" Imports System Imports System.Runtime.Versioning Module Preview_Feature_Scratch @@ -802,5 +803,93 @@ End Module testVb.ExpectedDiagnostics.Add(VerifyVB.Diagnostic(DetectPreviewFeatureAnalyzer.ImplementsPreviewInterfaceRule).WithLocation(1).WithArguments("Program", "Iprogram", DetectPreviewFeatureAnalyzer.DefaultURL)); await testVb.RunAsync(); } + + [Fact] + public async Task VerifyRuntimeAsyncReportsDiagnostic() + { + var csInput = """ + using System.Threading.Tasks; + class C + { + async Task M() + { + await Task.CompletedTask; + } + } + """; + + var test = new RuntimeAsyncFixVerifier + { + TestState = + { + Sources = + { + csInput + } + }, + ExpectedDiagnostics = + { + // /0/Test0.cs(6,9): error CA2252: Using 'Await' requires opting into preview features. See https://aka.ms/dotnet-warnings/preview-features for more information. + VerifyCS.Diagnostic(DetectPreviewFeatureAnalyzer.GeneralPreviewFeatureAttributeRule).WithSpan(6, 9, 6, 33).WithArguments("Await", DetectPreviewFeatureAnalyzer.DefaultURL) + } + }; + + await test.RunAsync(); + } + + [Fact] + public async Task VerifyRuntimeAsyncReportsDiagnostic_CustomAwaiter() + { + var csInput = """ + using System.Threading.Tasks; + class C + { + async Task M() + { + await Task.Yield(); + } + } + """; + + var test = new RuntimeAsyncFixVerifier + { + TestState = + { + Sources = + { + csInput + } + }, + ExpectedDiagnostics = + { + // /0/Test0.cs(6,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(6, 9, 6, 27).WithArguments("UnsafeAwaitAwaiter", DetectPreviewFeatureAnalyzer.DefaultURL) + } + }; + await test.RunAsync(); + } + + private class RuntimeAsyncFixVerifier : 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 RuntimeAsyncFixVerifier() + { + ReferenceAssemblies = Net100; + LanguageVersion = CodeAnalysis.CSharp.LanguageVersion.CSharp10; + } + + protected override ParseOptions CreateParseOptions() + { + var options = base.CreateParseOptions(); + return options.WithFeatures([new("runtime-async", "on")]); + } + + protected override CompilationOptions CreateCompilationOptions() + { + var options = base.CreateCompilationOptions(); + return options.WithSpecificDiagnosticOptions([new("SYSLIB5007", ReportDiagnostic.Suppress)]); + } + } } } From 3cec935429ec66fed6f90ad8cc3d205f09cf3c7f Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Mon, 22 Sep 2025 11:14:36 -0700 Subject: [PATCH 2/2] Minor feedback --- .../Runtime/CSharpDetectPreviewFeatureAnalyzer.cs | 2 +- .../Compiler.CSharp/Lightup/AwaitExpressionInfoWrapper.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) 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 26de7196caf0..2ab9ff663012 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 @@ -2,13 +2,13 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; +using Analyzer.Utilities.Lightup; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; using Microsoft.NetCore.Analyzers.Runtime; -using Analyzer.Utilities.Lightup; namespace Microsoft.NetCore.CSharp.Analyzers.Runtime { diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler.CSharp/Lightup/AwaitExpressionInfoWrapper.cs b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler.CSharp/Lightup/AwaitExpressionInfoWrapper.cs index 8e977a2d3903..719a13858a87 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler.CSharp/Lightup/AwaitExpressionInfoWrapper.cs +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler.CSharp/Lightup/AwaitExpressionInfoWrapper.cs @@ -24,7 +24,8 @@ public IMethodSymbol? RuntimeAwaitMethod fallbackResult: null); }); - return s_RuntimeAwaitMethodAccessor!(info); + RoslynDebug.Assert(s_RuntimeAwaitMethodAccessor is not null); + return s_RuntimeAwaitMethodAccessor(info); } } }