From f11c68ed63a4912dbfdf3b3acc1e8b66decedac4 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Mon, 13 Oct 2025 17:23:37 -0700 Subject: [PATCH 1/3] Add test for runtime async and update linker handling of method body --- .../Linker.Dataflow/MethodBodyScanner.cs | 13 ++- .../Linker/MethodDefinitionExtensions.cs | 5 + .../DataFlow/RuntimeAsyncMethods.cs | 107 ++++++++++++++++++ .../TestCasesRunner/TestCaseCompiler.cs | 19 +++- 4 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/RuntimeAsyncMethods.cs diff --git a/src/tools/illink/src/linker/Linker.Dataflow/MethodBodyScanner.cs b/src/tools/illink/src/linker/Linker.Dataflow/MethodBodyScanner.cs index ae0d2de743cdd9..8326be1caf557d 100644 --- a/src/tools/illink/src/linker/Linker.Dataflow/MethodBodyScanner.cs +++ b/src/tools/illink/src/linker/Linker.Dataflow/MethodBodyScanner.cs @@ -694,14 +694,15 @@ protected virtual void Scan(MethodIL methodIL, ref InterproceduralState interpro case Code.Ret: { - - bool hasReturnValue = !methodBody.Method.ReturnsVoid(); - - if (currentStack.Count != (hasReturnValue ? 1 : 0)) + if (!methodBody.Method.IsRuntimeAsync()) { - WarnAboutInvalidILInMethod(methodIL, operation.Offset); + bool ilHasReturnValue = !methodBody.Method.ReturnsVoid(); + if (currentStack.Count != (ilHasReturnValue ? 1 : 0)) + { + WarnAboutInvalidILInMethod(methodIL, operation.Offset); + } } - if (hasReturnValue) + if (currentStack.Count == 1) { StackSlot retStackSlot = PopUnknown(currentStack, 1, methodIL, operation.Offset); // If the return value is a reference, treat it as the value itself for now diff --git a/src/tools/illink/src/linker/Linker/MethodDefinitionExtensions.cs b/src/tools/illink/src/linker/Linker/MethodDefinitionExtensions.cs index 352f5443213822..857eab502b0149 100644 --- a/src/tools/illink/src/linker/Linker/MethodDefinitionExtensions.cs +++ b/src/tools/illink/src/linker/Linker/MethodDefinitionExtensions.cs @@ -54,6 +54,11 @@ public static bool IsEventMethod(this MethodDefinition md) (md.SemanticsAttributes & MethodSemanticsAttributes.RemoveOn) != 0; } + public static bool IsRuntimeAsync(this MethodDefinition md) + { + return (md.ImplAttributes & (MethodImplAttributes)0x2000) == (MethodImplAttributes)0x2000; + } + public static bool TryGetProperty(this MethodDefinition md, [NotNullWhen(true)] out PropertyDefinition? property) { property = null; diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/RuntimeAsyncMethods.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/RuntimeAsyncMethods.cs new file mode 100644 index 00000000000000..d1fde0efc7cd12 --- /dev/null +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/RuntimeAsyncMethods.cs @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Mono.Linker.Tests.Cases.Expectations.Assertions; +using Mono.Linker.Tests.Cases.Expectations.Helpers; +using Mono.Linker.Tests.Cases.Expectations.Metadata; + +namespace Mono.Linker.Tests.Cases.DataFlow +{ + [SkipKeptItemsValidation] + [SetupCompileArgument("/features:runtime-async=on")] + [SetupCompileArgument("/nowarn:SYSLIB5007")] + public class RuntimeAsyncMethods + { + public static async Task Main() + { + await BasicRuntimeAsyncMethod(); + await RuntimeAsyncWithDataFlowAnnotations(null); + await RuntimeAsyncWithCapturedLocalDataFlow(); + await RuntimeAsyncWithMultipleAwaits(); + await RuntimeAsyncWithReassignment(true); + await RuntimeAsyncReturningAnnotatedType(); + await RuntimeAsyncWithCorrectParameter(null); + await RuntimeAsyncWithLocalAll(); + } + + static async Task BasicRuntimeAsyncMethod() + { + await Task.Delay(1); + Console.WriteLine("Basic runtime async"); + } + + [ExpectedWarning("IL2067", "type", nameof(DataFlowTypeExtensions.RequiresAll), nameof(RuntimeAsyncWithDataFlowAnnotations))] + static async Task RuntimeAsyncWithDataFlowAnnotations([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type) + { + await Task.Delay(1); + type.RequiresAll(); + } + + [ExpectedWarning("IL2072", nameof(DataFlowTypeExtensions.RequiresAll), nameof(GetWithPublicMethods))] + static async Task RuntimeAsyncWithCapturedLocalDataFlow() + { + Type t = GetWithPublicMethods(); + await Task.Delay(1); + t.RequiresAll(); + } + + [ExpectedWarning("IL2072", nameof(DataFlowTypeExtensions.RequiresAll), nameof(GetWithPublicMethods))] + static async Task RuntimeAsyncWithMultipleAwaits() + { + Type t = GetWithPublicMethods(); + await Task.Delay(1); + t.RequiresPublicMethods(); + await Task.Delay(1); + t.RequiresAll(); + } + + [ExpectedWarning("IL2072", nameof(DataFlowTypeExtensions.RequiresAll), nameof(GetWithPublicMethods))] + [ExpectedWarning("IL2072", nameof(DataFlowTypeExtensions.RequiresAll), nameof(GetWithPublicFields))] + static async Task RuntimeAsyncWithReassignment(bool condition) + { + Type t = GetWithPublicMethods(); + await Task.Delay(1); + if (condition) + { + t = GetWithPublicFields(); + } + await Task.Delay(1); + t.RequiresAll(); + } + + [ExpectedWarning("IL2106", nameof(RuntimeAsyncReturningAnnotatedType))] + [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] + static async Task RuntimeAsyncReturningAnnotatedType() + { + await Task.Delay(1); + return GetWithPublicMethods(); + } + + [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] + static Type GetWithPublicMethods() => null; + + [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] + static Type GetWithPublicFields() => null; + + [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] + static Type GetWithAllMembers() => null; + + static async Task RuntimeAsyncWithCorrectParameter([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type) + { + type ??= GetWithAllMembers(); + await Task.Delay(1); + type.RequiresAll(); + } + + static async Task RuntimeAsyncWithLocalAll() + { + Type t = GetWithAllMembers(); + await Task.Delay(1); + t.RequiresAll(); + } + } +} diff --git a/src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/TestCaseCompiler.cs b/src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/TestCaseCompiler.cs index 0cdbb6c6b44299..2a571b64fa3ee2 100644 --- a/src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/TestCaseCompiler.cs +++ b/src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/TestCaseCompiler.cs @@ -267,6 +267,7 @@ protected virtual NPath CompileCSharpAssemblyWithRoslyn(CompilerOptions options) // Default debug info format for the current platform. DebugInformationFormat debugType = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? DebugInformationFormat.Pdb : DebugInformationFormat.PortablePdb; bool emitPdb = false; + Dictionary features = []; if (options.AdditionalArguments != null) { foreach (var option in options.AdditionalArguments) @@ -303,11 +304,27 @@ protected virtual NPath CompileCSharpAssemblyWithRoslyn(CompilerOptions options) compilationOptions = compilationOptions.WithMainTypeName(mainTypeName); break; } + else if (splitIndex != -1 && option[..splitIndex] == "/features") + { + var feature = option[(splitIndex + 1)..]; + var featureSplit = feature.IndexOf('='); + if (featureSplit == -1) + throw new InvalidOperationException($"Argument is malformed: '{option}'"); + features.Add(feature[..featureSplit], feature[(featureSplit + 1)..]); + break; + } + else if(splitIndex != -1 && option[..splitIndex] == "/nowarn") + { + var nowarn = option[(splitIndex + 1)..]; + var withNoWarn = compilationOptions.SpecificDiagnosticOptions.SetItem(nowarn, ReportDiagnostic.Suppress); + compilationOptions = compilationOptions.WithSpecificDiagnosticOptions(withNoWarn); + break; + } throw new NotImplementedException(option); } } } - var parseOptions = new CSharpParseOptions(preprocessorSymbols: options.Defines, languageVersion: languageVersion); + var parseOptions = new CSharpParseOptions(preprocessorSymbols: options.Defines, languageVersion: languageVersion).WithFeatures(features); var emitOptions = new EmitOptions(debugInformationFormat: debugType); var pdbPath = (!emitPdb || debugType == DebugInformationFormat.Embedded) ? null : options.OutputPath.ChangeExtension(".pdb").ToString(); From 9598a852467ddbd467a22af9f9e82fe784bc1e8d Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Wed, 15 Oct 2025 20:25:54 -0700 Subject: [PATCH 2/3] Update src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/TestCaseCompiler.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../test/Mono.Linker.Tests/TestCasesRunner/TestCaseCompiler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/TestCaseCompiler.cs b/src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/TestCaseCompiler.cs index 2a571b64fa3ee2..22c7f1fcf1d2e3 100644 --- a/src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/TestCaseCompiler.cs +++ b/src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/TestCaseCompiler.cs @@ -313,7 +313,7 @@ protected virtual NPath CompileCSharpAssemblyWithRoslyn(CompilerOptions options) features.Add(feature[..featureSplit], feature[(featureSplit + 1)..]); break; } - else if(splitIndex != -1 && option[..splitIndex] == "/nowarn") + else if (splitIndex != -1 && option[..splitIndex] == "/nowarn") { var nowarn = option[(splitIndex + 1)..]; var withNoWarn = compilationOptions.SpecificDiagnosticOptions.SetItem(nowarn, ReportDiagnostic.Suppress); From 3e564218dbabb7b73bd72bb4f94275801de9ca10 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Fri, 17 Oct 2025 14:43:59 -0700 Subject: [PATCH 3/3] Ignore test on NativeAOT --- .../DataFlowTests.g.cs | 6 ++++++ .../Mono.Linker.Tests.Cases/DataFlow/RuntimeAsyncMethods.cs | 1 + 2 files changed, 7 insertions(+) diff --git a/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/generated/ILLink.RoslynAnalyzer.Tests.Generator/ILLink.RoslynAnalyzer.Tests.TestCaseGenerator/DataFlowTests.g.cs b/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/generated/ILLink.RoslynAnalyzer.Tests.Generator/ILLink.RoslynAnalyzer.Tests.TestCaseGenerator/DataFlowTests.g.cs index 077a0c4af576ba..1c5d2c6d252e7c 100644 --- a/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/generated/ILLink.RoslynAnalyzer.Tests.Generator/ILLink.RoslynAnalyzer.Tests.TestCaseGenerator/DataFlowTests.g.cs +++ b/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/generated/ILLink.RoslynAnalyzer.Tests.Generator/ILLink.RoslynAnalyzer.Tests.TestCaseGenerator/DataFlowTests.g.cs @@ -43,6 +43,12 @@ public Task ModifierDataFlow() return RunTest(allowMissingWarnings: true); } + [Fact] + public Task RuntimeAsyncMethods() + { + return RunTest(allowMissingWarnings: true); + } + [Fact] public Task StaticInterfaceMethodDataflow() { diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/RuntimeAsyncMethods.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/RuntimeAsyncMethods.cs index d1fde0efc7cd12..f45a448e807098 100644 --- a/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/RuntimeAsyncMethods.cs +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/RuntimeAsyncMethods.cs @@ -12,6 +12,7 @@ namespace Mono.Linker.Tests.Cases.DataFlow { [SkipKeptItemsValidation] + [IgnoreTestCase("NativeAOT doesn't support runtime async yet", IgnoredBy = Tool.NativeAot)] [SetupCompileArgument("/features:runtime-async=on")] [SetupCompileArgument("/nowarn:SYSLIB5007")] public class RuntimeAsyncMethods