diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 30ca45be..04e1fe9a 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -62,3 +62,8 @@ jobs: if: ${{ success() || failure() }} run: ./build.sh test-command-line shell: bash + + - name: smoke-test-aot + if: ${{ success() || failure() }} + run: ./build.sh smoke-test-aot + shell: bash diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index 1bf08491..5c6665e6 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -29,6 +29,7 @@ "NugetPack", "NugetPush", "Restore", + "SmokeTestAot", "SmokeTestCommands", "Test", "TestCodegen", diff --git a/build/Build.cs b/build/Build.cs index e485db06..084a57d8 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -50,7 +50,7 @@ partial class Build : NukeBuild .EnableNoRestore()); }); - Target Test => _ => _.DependsOn(TestCore, TestCodegen, TestCommandLine, TestEvents); + Target Test => _ => _.DependsOn(TestCore, TestCodegen, TestCommandLine, TestEvents, SmokeTestAot); Target TestCore => _ => _ .DependsOn(Compile) @@ -107,6 +107,27 @@ partial class Build : NukeBuild DotNet("run --framework net9.0 -- codegen preview --start", Solution.TestHarnesses.GeneratorTarget.Directory); }); + /// + /// AOT-clean consumer smoke test (jasperfx#213). The JasperFx.AotSmoke + /// project sets IsAotCompatible=true + promotes IL2026 / IL3050 / IL2046 + /// / IL2070 / IL2075 (the full AOT analyzer set) to errors and exercises + /// a representative slice of the AOT-clean JasperFx + JasperFx.Events + /// surface. The build fails if a previously-AOT-clean API gains an + /// annotation, or if Program.cs is changed to call into a reflective + /// surface. Also runs the program to confirm runtime behavior is intact. + /// + Target SmokeTestAot => _ => _ + .DependsOn(Compile) + .Executes(() => + { + DotNetBuild(s => s + .SetProjectFile(Solution.TestHarnesses.JasperFx_AotSmoke) + .SetConfiguration(Configuration) + .EnableNoRestore()); + + DotNet("run --framework net10.0 --no-build", Solution.TestHarnesses.JasperFx_AotSmoke.Directory); + }); + AbsolutePath ArtifactsDirectory => RootDirectory / "artifacts"; Target NugetPack => _ => _ diff --git a/jasperfx.sln b/jasperfx.sln index 772efdb8..f18d4ee8 100644 --- a/jasperfx.sln +++ b/jasperfx.sln @@ -73,6 +73,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JasperFx.SourceGeneration", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandLineBenchmarks", "src\CommandLineBenchmarks\CommandLineBenchmarks.csproj", "{8D7BF9A9-0345-4FBA-9972-1F8413006DC2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JasperFx.AotSmoke", "src\JasperFx.AotSmoke\JasperFx.AotSmoke.csproj", "{8FB8216F-216F-480F-9519-A5893F7F3151}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -389,6 +391,18 @@ Global {8D7BF9A9-0345-4FBA-9972-1F8413006DC2}.Release|x64.Build.0 = Release|Any CPU {8D7BF9A9-0345-4FBA-9972-1F8413006DC2}.Release|x86.ActiveCfg = Release|Any CPU {8D7BF9A9-0345-4FBA-9972-1F8413006DC2}.Release|x86.Build.0 = Release|Any CPU + {8FB8216F-216F-480F-9519-A5893F7F3151}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8FB8216F-216F-480F-9519-A5893F7F3151}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8FB8216F-216F-480F-9519-A5893F7F3151}.Debug|x64.ActiveCfg = Debug|Any CPU + {8FB8216F-216F-480F-9519-A5893F7F3151}.Debug|x64.Build.0 = Debug|Any CPU + {8FB8216F-216F-480F-9519-A5893F7F3151}.Debug|x86.ActiveCfg = Debug|Any CPU + {8FB8216F-216F-480F-9519-A5893F7F3151}.Debug|x86.Build.0 = Debug|Any CPU + {8FB8216F-216F-480F-9519-A5893F7F3151}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8FB8216F-216F-480F-9519-A5893F7F3151}.Release|Any CPU.Build.0 = Release|Any CPU + {8FB8216F-216F-480F-9519-A5893F7F3151}.Release|x64.ActiveCfg = Release|Any CPU + {8FB8216F-216F-480F-9519-A5893F7F3151}.Release|x64.Build.0 = Release|Any CPU + {8FB8216F-216F-480F-9519-A5893F7F3151}.Release|x86.ActiveCfg = Release|Any CPU + {8FB8216F-216F-480F-9519-A5893F7F3151}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -408,5 +422,6 @@ Global {F0759B24-D9E2-4E42-B352-6C6B8138FD8C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {37E2EECE-FF24-4D39-96B6-BED9BCE8D1D4} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {8D7BF9A9-0345-4FBA-9972-1F8413006DC2} = {A1BBEFB2-1FDB-4A32-8D00-DD5AA8E1E43A} + {8FB8216F-216F-480F-9519-A5893F7F3151} = {A1BBEFB2-1FDB-4A32-8D00-DD5AA8E1E43A} EndGlobalSection EndGlobal diff --git a/src/JasperFx.AotSmoke/JasperFx.AotSmoke.csproj b/src/JasperFx.AotSmoke/JasperFx.AotSmoke.csproj new file mode 100644 index 00000000..c380d156 --- /dev/null +++ b/src/JasperFx.AotSmoke/JasperFx.AotSmoke.csproj @@ -0,0 +1,34 @@ + + + + Exe + enable + enable + false + + + true + full + IL2026;IL2046;IL2055;IL2065;IL2067;IL2070;IL2072;IL2075;IL2090;IL2091;IL2111;IL3050;IL3051 + + + + + + + + diff --git a/src/JasperFx.AotSmoke/Program.cs b/src/JasperFx.AotSmoke/Program.cs new file mode 100644 index 00000000..0f58ddc3 --- /dev/null +++ b/src/JasperFx.AotSmoke/Program.cs @@ -0,0 +1,74 @@ +// AOT smoke test (jasperfx#213). +// +// This program touches a representative cross-section of the AOT-clean +// JasperFx + JasperFx.Events surface. The csproj sets IsAotCompatible=true +// and promotes the AOT analyzer warning codes to errors, so any change that +// adds [RequiresDynamicCode] / [RequiresUnreferencedCode] to an API exercised +// here — or any change to this file that calls into a reflective JasperFx +// surface — fails the build in CI. +// +// Intentionally *not* exercised here (those carry AOT annotations by design): +// - CommandFactory / CommandExecutor / CommandLineHostingExtensions +// (reflective command discovery; AOT-clean path is the source-generated +// DiscoveredCommands manifest, which is itself the smoke test for the +// JasperFx.SourceGenerator's CommandLine output) +// - GenericFactoryCache.BuildAs (the delegate-factory overloads are +// annotated [RequiresDynamicCode] because the default factory calls +// MakeGenericType; an AOT consumer supplies its own AOT-safe factory) +// - SnapshotGate.Read / Write (System.Text.Json without a generation +// context — Marten and Wolverine consumers wrap these with their own +// STJ context or pre-serialized strings) + +using JasperFx.CodeGeneration.Snapshots; +using JasperFx.Events; + +// --- SnapshotGate.ComputeHash / Verify ---------------------------------- +// Pure functions that compute SHA-256 over a canonical-input string and +// compare fingerprints. The AOT-clean substrate the codegen-snapshot +// contract (#243) is built on. + +const string sampleInput = "marten-version=9.0.0␞store-name=AppDb"; + +string hashA = SnapshotGate.ComputeHash(sampleInput); +string hashB = SnapshotGate.ComputeHash(sampleInput); +if (hashA != hashB) +{ + Console.Error.WriteLine($"SnapshotGate.ComputeHash is non-deterministic: '{hashA}' vs '{hashB}'."); + return 1; +} + +var live = new SnapshotFingerprint( + ProductName: "marten", + ProductVersion: "9.0.0-alpha.1", + JasperFxVersion: "2.0.0-alpha.10", + ConfigHash: hashA, + SchemaVersion: SnapshotGate.CurrentSchemaVersion); + +SnapshotVerdict firstBoot = SnapshotGate.Verify(live, persisted: null); +SnapshotVerdict accept = SnapshotGate.Verify(live, persisted: live); +SnapshotVerdict reject = SnapshotGate.Verify(live, persisted: live with { ConfigHash = "deadbeef" }); + +if (firstBoot != SnapshotVerdict.FirstBoot || + accept != SnapshotVerdict.Accept || + reject != SnapshotVerdict.RejectAndRegenerate) +{ + Console.Error.WriteLine( + $"SnapshotGate.Verify regression: firstBoot={firstBoot}, accept={accept}, reject={reject}."); + return 1; +} + +// --- JasperFx.Events.Event.For --------------------------------------- +// Generic factory for IEvent; AOT-clean for closed-over T. + +IEvent evt = Event.For(new SampleEvent("hello")); +IEvent tenantEvt = Event.For("tenant-a", new SampleEvent("hello")); +if (evt.Data.Message != tenantEvt.Data.Message) +{ + Console.Error.WriteLine("Event.For regression."); + return 1; +} + +Console.WriteLine($"JasperFx AOT smoke OK — ConfigHash={hashA[..16]}…"); +return 0; + +internal readonly record struct SampleEvent(string Message); diff --git a/src/JasperFx/CommandLine/ActivatorCommandCreator.cs b/src/JasperFx/CommandLine/ActivatorCommandCreator.cs index e74e02e4..d308918c 100644 --- a/src/JasperFx/CommandLine/ActivatorCommandCreator.cs +++ b/src/JasperFx/CommandLine/ActivatorCommandCreator.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using JasperFx.Core.Reflection; using Spectre.Console; @@ -11,6 +12,7 @@ public ActivatorCommandCreator() Debug.WriteLine("What?"); } + [RequiresUnreferencedCode("Activator.CreateInstance(Type) requires the public parameterless constructor of commandType to survive trimming.")] public IJasperFxCommand CreateCommand(Type commandType) { try @@ -25,6 +27,7 @@ public IJasperFxCommand CreateCommand(Type commandType) } } + [RequiresUnreferencedCode("Activator.CreateInstance(Type) requires the public parameterless constructor of modelType to survive trimming.")] public object CreateModel(Type modelType) { return Activator.CreateInstance(modelType)!; diff --git a/src/JasperFx/CommandLine/CommandExecutor.cs b/src/JasperFx/CommandLine/CommandExecutor.cs index 473dd9a8..8050aac9 100644 --- a/src/JasperFx/CommandLine/CommandExecutor.cs +++ b/src/JasperFx/CommandLine/CommandExecutor.cs @@ -1,4 +1,5 @@ -using JasperFx.CommandLine.Parsing; +using System.Diagnostics.CodeAnalysis; +using JasperFx.CommandLine.Parsing; using JasperFx.Core; using Spectre.Console; @@ -26,6 +27,8 @@ public CommandExecutor() : this(new CommandFactory()) public ICommandFactory Factory { get; } + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", + Justification = "Spectre.Console.AnsiConsole.WriteException is only invoked on the error-display path; the outer try/catch falls back to Console.Write on Exception, so AOT consumers degrade gracefully.")] private static async Task execute(CommandRun run) { bool success; @@ -70,6 +73,8 @@ private static async Task execute(CommandRun run) /// line arguments /// /// + [RequiresUnreferencedCode("Registers T via reflection and dispatches through CommandFactory.BuildRun, which depends on the command type's public constructor + input-type properties surviving trimming.")] + [RequiresDynamicCode("Enumerable command-argument parsing closes generic List via MakeGenericType.")] public static int ExecuteCommand(string[] args, string? optsFile = null) where T : IJasperFxCommand { var factory = new CommandFactory(); @@ -94,6 +99,8 @@ public static int ExecuteCommand(string[] args, string? optsFile = null) wher /// line arguments /// /// + [RequiresUnreferencedCode("Registers T via reflection and dispatches through CommandFactory.BuildRun, which depends on the command type's public constructor + input-type properties surviving trimming.")] + [RequiresDynamicCode("Enumerable command-argument parsing closes generic List via MakeGenericType.")] public static Task ExecuteCommandAsync(string[] args, string? optsFile = null) where T : IJasperFxCommand { var factory = new CommandFactory(); @@ -172,6 +179,8 @@ internal static IEnumerable ReadOptions(string? optionsFile) /// /// /// + [RequiresUnreferencedCode("Dispatches through ICommandFactory.BuildRun and reflective command instantiation.")] + [RequiresDynamicCode("Enumerable command-argument parsing closes generic List via MakeGenericType.")] public int Execute(string commandLine) { return ExecuteAsync(commandLine).GetAwaiter().GetResult(); @@ -182,6 +191,8 @@ public int Execute(string commandLine) /// /// /// + [RequiresUnreferencedCode("Dispatches through ICommandFactory.BuildRun and reflective command instantiation.")] + [RequiresDynamicCode("Enumerable command-argument parsing closes generic List via MakeGenericType.")] public int Execute(string[] args) { return ExecuteAsync(args).GetAwaiter().GetResult(); @@ -192,6 +203,8 @@ public int Execute(string[] args) /// /// /// + [RequiresUnreferencedCode("Dispatches through ICommandFactory.BuildRun and reflective command instantiation.")] + [RequiresDynamicCode("Enumerable command-argument parsing closes generic List via MakeGenericType.")] public Task ExecuteAsync(string commandLine) { commandLine = applyOptions(commandLine); @@ -205,6 +218,8 @@ public Task ExecuteAsync(string commandLine) /// /// /// + [RequiresUnreferencedCode("Dispatches through ICommandFactory.BuildRun and reflective command instantiation.")] + [RequiresDynamicCode("Enumerable command-argument parsing closes generic List via MakeGenericType.")] public Task ExecuteAsync(string[] args) { var run = Factory.BuildRun(ReadOptions(OptionsFile).Concat(args)); diff --git a/src/JasperFx/CommandLine/CommandFactory.cs b/src/JasperFx/CommandLine/CommandFactory.cs index 02c87b53..05c4261c 100644 --- a/src/JasperFx/CommandLine/CommandFactory.cs +++ b/src/JasperFx/CommandLine/CommandFactory.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; using System.Text.RegularExpressions; using JasperFx.CommandLine.Help; using JasperFx.CommandLine.Parsing; @@ -64,12 +65,16 @@ public Type? DefaultCommand } } + [RequiresUnreferencedCode("CommandFactory dispatches to commands and inputs via reflection; their public constructors and properties must survive trimming. AOT-publishing apps should consume commands through the source-generated manifest.")] + [RequiresDynamicCode("Command input parsing closes generic List via MakeGenericType for enumerable arguments / flags.")] public CommandRun BuildRun(string commandLine) { var args = StringTokenizer.Tokenize(commandLine); return BuildRun(args); } + [RequiresUnreferencedCode("CommandFactory dispatches to commands and inputs via reflection; their public constructors and properties must survive trimming. AOT-publishing apps should consume commands through the source-generated manifest.")] + [RequiresDynamicCode("Command input parsing closes generic List via MakeGenericType for enumerable arguments / flags.")] public CommandRun BuildRun(IEnumerable args) { if (!args.Any()) @@ -116,6 +121,7 @@ public CommandRun BuildRun(IEnumerable args) /// Add all the IJasperFxCommand classes in the given assembly to the command runner /// /// + [RequiresUnreferencedCode("Scans assembly.GetExportedTypes() for IJasperFxCommand. AOT-publishing apps should rely on the source-generated command manifest emitted by JasperFx.SourceGenerator.")] public void RegisterCommands(Assembly assembly) { foreach (var type in assembly @@ -129,15 +135,17 @@ public void RegisterCommands(Assembly assembly) { _extensionTypes.Add(attribute.ExtensionType); } - + } } + [RequiresUnreferencedCode("BuildAllCommands dispatches each registered command type through ICommandCreator.CreateCommand, which instantiates the type reflectively.")] public IEnumerable BuildAllCommands() { return _commandTypes.Select(x => _commandCreator.CreateCommand(x)); } + [RequiresUnreferencedCode("Activator.CreateInstance(extensionType) requires public parameterless constructor of each extension to survive trimming.")] public void ApplyExtensions(IHostBuilder builder) { if (builder is PreBuiltHostBuilder) @@ -151,6 +159,7 @@ public void ApplyExtensions(IHostBuilder builder) } } + [RequiresUnreferencedCode("Activator.CreateInstance(extensionType) requires public parameterless constructor of each extension to survive trimming.")] public void ApplyExtensions(IServiceCollection services) { try @@ -196,6 +205,8 @@ public CommandRun InvalidCommandRun(string commandName) }; } + [RequiresUnreferencedCode("Resolves the command type reflectively via ICommandCreator and walks its UsageGraph (which reads MemberInfo via reflection).")] + [RequiresDynamicCode("Enumerable argument / flag parsing closes generic List via MakeGenericType.")] private CommandRun buildRun(Queue queue, string commandName) { try @@ -242,6 +253,8 @@ private CommandRun buildRun(Queue queue, string commandName) return HelpRun(commandName); } + [RequiresUnreferencedCode("Builds a temporary command instance to pre-populate input. Inherits trim requirements from ActivatorCommandCreator.CreateCommand.")] + [RequiresDynamicCode("UsageGraph input building closes generic List for enumerable arguments.")] private object? tryBeforeBuild(Queue queue, string commandName) { var commandType = _commandTypes[commandName]; @@ -266,6 +279,7 @@ private CommandRun buildRun(Queue queue, string commandName) /// Add a single command type to the command runner /// /// + [RequiresUnreferencedCode("Registers T for later reflective instantiation via ICommandCreator; T's public constructors and input-type properties must survive trimming.")] public void RegisterCommand() { RegisterCommand(typeof(T)); @@ -274,6 +288,7 @@ public void RegisterCommand() /// /// Add a single command type to the command runner /// + [RequiresUnreferencedCode("Registers a command type for later reflective instantiation via ICommandCreator; its public constructors and input-type properties must survive trimming.")] public void RegisterCommand(Type type) { if (!IsJasperFxCommandType(type)) @@ -296,17 +311,22 @@ public static bool IsJasperFxCommandType(Type type) } + [RequiresUnreferencedCode("Resolves the command type reflectively via ICommandCreator. The type's public constructor must survive trimming.")] public IJasperFxCommand Build(string commandName) { return _commandCreator.CreateCommand(_commandTypes[commandName.ToLower()]); } + [RequiresUnreferencedCode("Resolves the command and its UsageGraph reflectively via ICommandCreator. The command type's public constructor and input properties must survive trimming.")] + [RequiresDynamicCode("UsageGraph input building closes generic List for enumerable arguments.")] public CommandRun HelpRun(string commandName) { return HelpRun(new Queue(new[] { commandName })); } + [RequiresUnreferencedCode("Resolves the command and its UsageGraph reflectively via ICommandCreator. The command type's public constructor and input properties must survive trimming.")] + [RequiresDynamicCode("UsageGraph input building closes generic List for enumerable arguments.")] public virtual CommandRun HelpRun(Queue queue) { var input = (HelpInput)new HelpCommand().Usages.BuildInput(queue, _commandCreator); @@ -372,6 +392,13 @@ public void SetAppName(string appName) /// Automatically discover any JasperFx commands in assemblies marked as /// [assembly: JasperFxCommandAssembly]. Also /// + /// + /// Tries the source-generated DiscoveredCommands manifest first + /// (AOT/trim-clean path); falls back to + + /// if no manifest is found. The trim/AOT + /// warnings are attached because the fallback path scans assemblies. + /// + [RequiresUnreferencedCode("Falls back to AssemblyFinder + assembly.GetExportedTypes() scanning if no source-generated command manifest is present. AOT-publishing apps should emit the manifest via JasperFx.SourceGenerator.")] public void RegisterCommandsFromExtensionAssemblies() { // Check for source-generated command manifest first to avoid assembly scanning @@ -404,6 +431,22 @@ public void RegisterCommandsFromExtensionAssemblies() /// Attempt to use a source-generated command manifest to register commands /// without runtime assembly scanning. Returns true if a manifest was found and used. /// + /// + /// The lookup uses well-known string identifiers + /// (JasperFx.Generated.DiscoveredCommands + CommandTypes) that + /// the trimmer cannot statically prove are reachable. The + /// attributes document that + /// consuming apps emit the manifest via the JasperFx.SourceGenerator + /// analyzer — when that source generator runs, the type and property are + /// produced as ordinary code in the consuming assembly and survive + /// trimming naturally. Apps that do not include the generator simply + /// fall through to , which carries its own + /// [RequiresUnreferencedCode]. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "JasperFx.Generated.DiscoveredCommands is emitted by JasperFx.SourceGenerator into the consuming app as ordinary code; the lookup degrades safely to the reflective fallback if the generator is not enabled.")] + [UnconditionalSuppressMessage("Trimming", "IL2075:DynamicallyAccessedMembers", + Justification = "Same as IL2026 — DiscoveredCommands.CommandTypes is generated source code in the consuming assembly.")] internal bool TryRegisterFromGeneratedManifest() { // Look for the generated DiscoveredCommands class in all loaded assemblies diff --git a/src/JasperFx/CommandLine/DependencyInjectionCommandCreator.cs b/src/JasperFx/CommandLine/DependencyInjectionCommandCreator.cs index 5b12a5fb..d3f387ad 100644 --- a/src/JasperFx/CommandLine/DependencyInjectionCommandCreator.cs +++ b/src/JasperFx/CommandLine/DependencyInjectionCommandCreator.cs @@ -1,4 +1,5 @@ -using JasperFx.CommandLine.Help; +using System.Diagnostics.CodeAnalysis; +using JasperFx.CommandLine.Help; using JasperFx.Core; using JasperFx.Core.Reflection; using Microsoft.Extensions.DependencyInjection; @@ -13,16 +14,18 @@ public DependencyInjectionCommandCreator(IServiceProvider serviceProvider) _serviceProvider = serviceProvider; } + [RequiresUnreferencedCode("Inspects commandType.GetProperties() for [InjectService] and uses ActivatorUtilities.CreateInstance; public constructors and properties of commandType must survive trimming.")] public IJasperFxCommand CreateCommand(Type commandType) { if (commandType.GetProperties().Any(x => x.HasAttribute())) { return new WrappedJasperFxCommand(_serviceProvider, commandType); } - + return (ActivatorUtilities.CreateInstance(_serviceProvider, commandType) as IJasperFxCommand)!; } + [RequiresUnreferencedCode("Activator.CreateInstance(Type) requires the public parameterless constructor of modelType to survive trimming.")] public object CreateModel(Type modelType) { return Activator.CreateInstance(modelType)!; diff --git a/src/JasperFx/CommandLine/Descriptions/DescribeCommand.cs b/src/JasperFx/CommandLine/Descriptions/DescribeCommand.cs index 4040df8f..1f2b414b 100644 --- a/src/JasperFx/CommandLine/Descriptions/DescribeCommand.cs +++ b/src/JasperFx/CommandLine/Descriptions/DescribeCommand.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Reflection; using JasperFx.CommandLine.TextualDisplays; using JasperFx.Core; @@ -148,7 +149,9 @@ public class ReferencedAssemblies : SystemPartBase public ReferencedAssemblies() : base("Referenced Assemblies", new Uri("system://assemblies")) { } - + + [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "Diagnostic-only describe command. Trimmed assemblies simply drop from the listed output; nothing branches on the value.")] public override Task WriteToConsole() { var description = new TextualDisplay("Referenced Assemblies"); diff --git a/src/JasperFx/CommandLine/Descriptions/DescriptionExtensions.cs b/src/JasperFx/CommandLine/Descriptions/DescriptionExtensions.cs index 335d641a..b182a2a7 100644 --- a/src/JasperFx/CommandLine/Descriptions/DescriptionExtensions.cs +++ b/src/JasperFx/CommandLine/Descriptions/DescriptionExtensions.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.DependencyInjection; using Spectre.Console; @@ -20,7 +21,7 @@ public static void AddSystemPart(this IServiceCollection services, ISystemPart d /// /// /// - public static void AddSystemPart(this IServiceCollection services) where T : class, ISystemPart + public static void AddSystemPart<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>(this IServiceCollection services) where T : class, ISystemPart { services.AddSingleton(); } diff --git a/src/JasperFx/CommandLine/Help/UsageGraph.cs b/src/JasperFx/CommandLine/Help/UsageGraph.cs index dbddceff..dfba5447 100644 --- a/src/JasperFx/CommandLine/Help/UsageGraph.cs +++ b/src/JasperFx/CommandLine/Help/UsageGraph.cs @@ -1,4 +1,5 @@ -using System.Linq.Expressions; +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; using System.Reflection; using JasperFx.CommandLine.Parsing; using JasperFx.Core; @@ -14,6 +15,8 @@ public class UsageGraph private readonly IList _usages = new List(); private readonly Lazy> _validUsages; + [RequiresUnreferencedCode("Walks commandType to derive its input type and reads input-type properties / fields via InputParser.GetHandlers. The source-generated handler registry (GeneratedParserRegistry) is used when present.")] + [RequiresDynamicCode("InputParser.BuildHandler closes generic List for enumerable arguments / flags when falling back from the generated registry.")] public UsageGraph(Type commandType) { _inputType = commandType.FindInterfaceThatCloses(typeof(IJasperFxCommand<>))!.GetTypeInfo() @@ -63,6 +66,7 @@ public IEnumerable Flags public string Description { get; private set; } + [RequiresUnreferencedCode("Delegates to ICommandCreator.CreateModel which instantiates _inputType via reflection.")] public object BuildInput(Queue tokens, ICommandCreator creator) { var model = creator.CreateModel(_inputType); diff --git a/src/JasperFx/CommandLine/ICommandCreator.cs b/src/JasperFx/CommandLine/ICommandCreator.cs index 754e3227..0894d47a 100644 --- a/src/JasperFx/CommandLine/ICommandCreator.cs +++ b/src/JasperFx/CommandLine/ICommandCreator.cs @@ -1,11 +1,26 @@ -namespace JasperFx.CommandLine; +using System.Diagnostics.CodeAnalysis; + +namespace JasperFx.CommandLine; /// /// Service locator for command types. The default just uses Activator.CreateInstance(). -/// Can be used to plug in IoC construction in JasperFx applications +/// Can be used to plug in IoC construction in JasperFx applications. /// +/// +/// Implementations resolve concrete and input model +/// types reflectively. The annotations propagate the requirement that callers +/// either supply types whose constructors / properties survive trimming, or +/// opt in via the published [RequiresUnreferencedCode] annotation. +/// AOT/trim-clean apps consume commands through the source-generated manifest +/// (see ) rather +/// than reflective scanning, so this interface's annotations are the precise +/// punch-list AOT consumers see when they call into the reflective path. +/// public interface ICommandCreator { + [RequiresUnreferencedCode("CreateCommand instantiates commandType via reflection (Activator.CreateInstance or DI). Public constructors and InjectService properties must survive trimming.")] IJasperFxCommand CreateCommand(Type commandType); + + [RequiresUnreferencedCode("CreateModel instantiates modelType via reflection (Activator.CreateInstance). Public parameterless constructor must survive trimming.")] object CreateModel(Type modelType); } \ No newline at end of file diff --git a/src/JasperFx/CommandLine/ICommandFactory.cs b/src/JasperFx/CommandLine/ICommandFactory.cs index dc30b2dc..fd2a92d7 100644 --- a/src/JasperFx/CommandLine/ICommandFactory.cs +++ b/src/JasperFx/CommandLine/ICommandFactory.cs @@ -1,19 +1,36 @@ -using System.Reflection; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; using Microsoft.Extensions.Hosting; namespace JasperFx.CommandLine; /// /// Interface that JasperFx uses to build command runs during execution. Can be used for custom -/// command activation +/// command activation. /// +/// +/// The annotations propagate the reflective surface to implementations (default +/// ) and to consumers (, +/// ). AOT/trim-clean apps short-circuit +/// the reflective discovery path via +/// and the JasperFx.SourceGenerator-emitted DiscoveredCommands manifest. +/// public interface ICommandFactory { + [RequiresUnreferencedCode("BuildRun resolves command types reflectively via ICommandCreator; their public constructors and input-type properties must survive trimming.")] + [RequiresDynamicCode("Command input parsing closes generic List via MakeGenericType for enumerable arguments / flags.")] CommandRun BuildRun(string commandLine); + + [RequiresUnreferencedCode("BuildRun resolves command types reflectively via ICommandCreator; their public constructors and input-type properties must survive trimming.")] + [RequiresDynamicCode("Command input parsing closes generic List via MakeGenericType for enumerable arguments / flags.")] CommandRun BuildRun(IEnumerable args); + + [RequiresUnreferencedCode("Scans assembly.GetExportedTypes() for IJasperFxCommand. AOT-publishing apps should rely on the source-generated DiscoveredCommands manifest emitted by JasperFx.SourceGenerator.")] void RegisterCommands(Assembly assembly); + [RequiresUnreferencedCode("BuildAllCommands dispatches each registered command type through ICommandCreator.CreateCommand, which instantiates the type reflectively.")] IEnumerable BuildAllCommands(); + [RequiresUnreferencedCode("Activator.CreateInstance(extensionType) requires public parameterless constructor of each extension to survive trimming.")] void ApplyExtensions(IHostBuilder builder); } \ No newline at end of file diff --git a/src/JasperFx/CommandLine/Internal/Conversion/ArrayConversion.cs b/src/JasperFx/CommandLine/Internal/Conversion/ArrayConversion.cs index ebcc91aa..0fee02c9 100644 --- a/src/JasperFx/CommandLine/Internal/Conversion/ArrayConversion.cs +++ b/src/JasperFx/CommandLine/Internal/Conversion/ArrayConversion.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using JasperFx.Core; namespace JasperFx.CommandLine.Internal.Conversion; @@ -11,6 +12,8 @@ public ArrayConversion(Conversions conversions) _conversions = conversions; } + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", + Justification = "Array.CreateInstance is only invoked through Conversions, which is itself reached only from annotated CommandLine entry points (CommandFactory.BuildRun, InputParser.GetHandlers / BuildHandler). AOT consumers see the annotation at those entry points.")] public Func? ConverterFor(Type type) { if (!type.IsArray) diff --git a/src/JasperFx/CommandLine/Internal/Conversion/StringConverterProvider.cs b/src/JasperFx/CommandLine/Internal/Conversion/StringConverterProvider.cs index 85e1f4d4..a654a7fd 100644 --- a/src/JasperFx/CommandLine/Internal/Conversion/StringConverterProvider.cs +++ b/src/JasperFx/CommandLine/Internal/Conversion/StringConverterProvider.cs @@ -1,10 +1,13 @@ -using System.Linq.Expressions; +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; using JasperFx.Core.Reflection; namespace JasperFx.CommandLine.Internal.Conversion; public class StringConverterProvider : IConversionProvider { + [UnconditionalSuppressMessage("Trimming", "IL2070:DynamicallyAccessedMembers", + Justification = "Looks up a public constructor that takes a single string. Only invoked through Conversions, reached from annotated CommandLine entry points (CommandFactory.BuildRun, InputParser.GetHandlers / BuildHandler). AOT consumers see the annotation at those entry points; types reached through input-model property scanning are preserved via the input type itself.")] public Func? ConverterFor(Type type) { if (!type.IsConcrete()) diff --git a/src/JasperFx/CommandLine/JasperFxAsyncCommand.cs b/src/JasperFx/CommandLine/JasperFxAsyncCommand.cs index 54cd2fb0..bc663de8 100644 --- a/src/JasperFx/CommandLine/JasperFxAsyncCommand.cs +++ b/src/JasperFx/CommandLine/JasperFxAsyncCommand.cs @@ -1,4 +1,5 @@ -using JasperFx.CommandLine.Help; +using System.Diagnostics.CodeAnalysis; +using JasperFx.CommandLine.Help; namespace JasperFx.CommandLine; @@ -8,6 +9,10 @@ namespace JasperFx.CommandLine; /// public abstract class JasperFxAsyncCommand : IJasperFxCommand { + [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "JasperFxAsyncCommand instances are constructed by ICommandCreator (annotated) from a Type that survives the trim graph because the command was reachable through CommandFactory.RegisterCommand[s]. The UsageGraph reads members of T (the user input type) — if T is reachable as the command's input, its members are preserved by the entry-point annotations.")] + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", + Justification = "Same as IL2026 — the UsageGraph closes List for enumerable arguments via InputParser.BuildHandler, reached only through annotated CommandFactory entry points.")] protected JasperFxAsyncCommand() { Usages = new UsageGraph(GetType()); diff --git a/src/JasperFx/CommandLine/JasperFxCommand.cs b/src/JasperFx/CommandLine/JasperFxCommand.cs index 79b31582..e8ef65c5 100644 --- a/src/JasperFx/CommandLine/JasperFxCommand.cs +++ b/src/JasperFx/CommandLine/JasperFxCommand.cs @@ -1,4 +1,5 @@ -using JasperFx.CommandLine.Help; +using System.Diagnostics.CodeAnalysis; +using JasperFx.CommandLine.Help; namespace JasperFx.CommandLine; @@ -8,6 +9,10 @@ namespace JasperFx.CommandLine; /// public abstract class JasperFxCommand : IJasperFxCommand { + [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "JasperFxCommand instances are constructed by ICommandCreator (annotated) from a Type that survives the trim graph because the command was reachable through CommandFactory.RegisterCommand[s]. The UsageGraph reads members of T (the user input type) — if T is reachable as the command's input, its members are preserved by the entry-point annotations.")] + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", + Justification = "Same as IL2026 — the UsageGraph closes List for enumerable arguments via InputParser.BuildHandler, reached only through annotated CommandFactory entry points.")] protected JasperFxCommand() { Usages = new UsageGraph(GetType()); diff --git a/src/JasperFx/CommandLine/OaktonShims.cs b/src/JasperFx/CommandLine/OaktonShims.cs index 45dc1733..cd2480ec 100644 --- a/src/JasperFx/CommandLine/OaktonShims.cs +++ b/src/JasperFx/CommandLine/OaktonShims.cs @@ -1,6 +1,7 @@ +using System.Diagnostics.CodeAnalysis; using System.Reflection; using JasperFx; using JasperFx.CommandLine.Commands; @@ -47,6 +48,7 @@ public static class CommandLineHostingExtensions /// /// [Obsolete("Prefer ApplyJasperFxExtensions")] + [RequiresUnreferencedCode("Scans extension assemblies for JasperFx commands. Apps targeting trim/AOT should pre-register commands via the source-generated DiscoveredCommands manifest.")] public static IHostBuilder ApplyOaktonExtensions(this IHostBuilder builder) { return builder.ApplyJasperFxExtensions(); @@ -62,6 +64,8 @@ public static IHostBuilder ApplyOaktonExtensions(this IHostBuilder builder) /// Optionally configure an expected "opts" file /// [Obsolete("Prefer RunJasperFxCommands")] + [RequiresUnreferencedCode("Dispatches to commands resolved reflectively from the entry/extension assemblies. Apps targeting trim/AOT should pre-register commands via the source-generated DiscoveredCommands manifest.")] + [RequiresDynamicCode("Command input parsing closes generic List via MakeGenericType for enumerable arguments / flags.")] public static Task RunOaktonCommands(this IHostBuilder builder, string[] args, string? optionsFile = null) { return builder.RunJasperFxCommands(args, optionsFile); @@ -77,6 +81,8 @@ public static Task RunOaktonCommands(this IHostBuilder builder, string[] ar /// Optionally configure an expected "opts" file /// [Obsolete("Prefer RunJasperFxCommands")] + [RequiresUnreferencedCode("Dispatches to commands resolved reflectively from the entry/extension assemblies. Apps targeting trim/AOT should pre-register commands via the source-generated DiscoveredCommands manifest.")] + [RequiresDynamicCode("Command input parsing closes generic List via MakeGenericType for enumerable arguments / flags.")] public static Task RunOaktonCommands(this IHost host, string[] args, string? optionsFile = null) { return host.RunJasperFxCommands(args, optionsFile); diff --git a/src/JasperFx/CommandLine/Parsing/EnumerableArgument.cs b/src/JasperFx/CommandLine/Parsing/EnumerableArgument.cs index b6a61b2a..0ee34e4d 100644 --- a/src/JasperFx/CommandLine/Parsing/EnumerableArgument.cs +++ b/src/JasperFx/CommandLine/Parsing/EnumerableArgument.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using JasperFx.CommandLine.Internal.Conversion; using JasperFx.Core.Reflection; @@ -16,6 +17,10 @@ public EnumerableArgument(MemberInfo member, Conversions conversions) : base(mem _converter = conversions.FindConverter(member.GetMemberType()!.DetermineElementType()!)!; } + [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "Reachable only through InputParser.BuildHandler, which carries RequiresUnreferencedCode + RequiresDynamicCode and is itself reached from the annotated CommandFactory.BuildRun / RegisterCommand entry points. The List closure is intrinsic and the trimmer keeps the closed generic when List is preserved.")] + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", + Justification = "Same as IL2026 — the CloseAndBuildAs call closes List via MakeGenericType, which AOT consumers see through the annotated entry points.")] public override bool Handle(object input, Queue tokens) { var elementType = _member.GetMemberType()!.GetGenericArguments().First(); diff --git a/src/JasperFx/CommandLine/Parsing/EnumerableFlag.cs b/src/JasperFx/CommandLine/Parsing/EnumerableFlag.cs index c3483185..666d1fa5 100644 --- a/src/JasperFx/CommandLine/Parsing/EnumerableFlag.cs +++ b/src/JasperFx/CommandLine/Parsing/EnumerableFlag.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using JasperFx.CommandLine.Internal.Conversion; using JasperFx.Core.Reflection; @@ -15,6 +16,10 @@ public EnumerableFlag(MemberInfo member, Conversions conversions) _member = member; } + [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "Reachable only through InputParser.BuildHandler, which carries RequiresUnreferencedCode + RequiresDynamicCode and is itself reached from the annotated CommandFactory.BuildRun / RegisterCommand entry points. The List closure is intrinsic and the trimmer keeps the closed generic when List is preserved.")] + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", + Justification = "Same as IL2026 — the CloseAndBuildAs call closes List via MakeGenericType, which AOT consumers see through the annotated entry points.")] public override bool Handle(object input, Queue tokens) { var elementType = _member.GetMemberType()!.DetermineElementType()!; diff --git a/src/JasperFx/CommandLine/Parsing/InputParser.cs b/src/JasperFx/CommandLine/Parsing/InputParser.cs index 02759a96..ec1e4333 100644 --- a/src/JasperFx/CommandLine/Parsing/InputParser.cs +++ b/src/JasperFx/CommandLine/Parsing/InputParser.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.RegularExpressions; using JasperFx.CommandLine.Internal.Conversion; @@ -18,6 +19,8 @@ public static class InputParser private static readonly Conversions _converter = new(); + [RequiresUnreferencedCode("Reads inputType.GetProperties() + GetFields(); public properties (writable) and instance fields of inputType must survive trimming.")] + [RequiresDynamicCode("BuildHandler closes generic List for enumerable arguments / flags.")] public static List GetHandlers(Type inputType) { var properties = inputType.GetProperties().Where(prop => prop.CanWrite); @@ -30,6 +33,8 @@ public static List GetHandlers(Type inputType) .Select(BuildHandler).ToList(); } + [RequiresUnreferencedCode("Reflects over member's declaring type to build a token handler; trimmer may remove members the handler depends on.")] + [RequiresDynamicCode("Closes generic List via MakeGenericType for enumerable arguments and flags.")] public static ITokenHandler BuildHandler(MemberInfo member) { var memberType = member.GetMemberType(); diff --git a/src/JasperFx/CommandLineHostingExtensions.cs b/src/JasperFx/CommandLineHostingExtensions.cs index a36ecbf2..6211868e 100644 --- a/src/JasperFx/CommandLineHostingExtensions.cs +++ b/src/JasperFx/CommandLineHostingExtensions.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Reflection; using JasperFx.CommandLine; using JasperFx.CommandLine.Commands; @@ -19,6 +20,7 @@ public static class CommandLineHostingExtensions /// /// /// + [RequiresUnreferencedCode("Scans extension assemblies for JasperFx commands and instantiates extension types via Activator.CreateInstance. Apps targeting trim/AOT should emit the JasperFx.Generated.DiscoveredCommands manifest via JasperFx.SourceGenerator.")] public static IHostBuilder ApplyJasperFxExtensions(this IHostBuilder builder) { var factory = new CommandFactory(); @@ -37,6 +39,8 @@ public static IHostBuilder ApplyJasperFxExtensions(this IHostBuilder builder) /// /// Optionally configure an expected "opts" file /// + [RequiresUnreferencedCode("Dispatches to commands resolved reflectively from the entry/extension assemblies. Apps targeting trim/AOT should pre-register commands via the source-generated DiscoveredCommands manifest.")] + [RequiresDynamicCode("Command input parsing closes generic List via MakeGenericType for enumerable arguments / flags.")] public static Task RunJasperFxCommands(this IHostBuilder builder, string[] args, string? optionsFile = null) { return execute(builder, Assembly.GetEntryAssembly(), args, optionsFile); @@ -51,6 +55,8 @@ public static Task RunJasperFxCommands(this IHostBuilder builder, string[] /// /// Optionally configure an expected "opts" file /// + [RequiresUnreferencedCode("Dispatches to commands resolved reflectively from the entry/extension assemblies. Apps targeting trim/AOT should pre-register commands via the source-generated DiscoveredCommands manifest.")] + [RequiresDynamicCode("Command input parsing closes generic List via MakeGenericType for enumerable arguments / flags.")] public static int RunJasperFxCommandsSynchronously(this IHostBuilder builder, string[] args, string? optionsFile = null) { return execute(builder, Assembly.GetEntryAssembly(), args, optionsFile).GetAwaiter().GetResult(); @@ -65,6 +71,8 @@ public static int RunJasperFxCommandsSynchronously(this IHostBuilder builder, st /// /// Optionally configure an expected "opts" file /// + [RequiresUnreferencedCode("Dispatches to commands resolved reflectively from the entry/extension assemblies. Apps targeting trim/AOT should pre-register commands via the source-generated DiscoveredCommands manifest.")] + [RequiresDynamicCode("Command input parsing closes generic List via MakeGenericType for enumerable arguments / flags.")] public static Task RunJasperFxCommands(this IHost host, string[] args, string? optionsFile = null) { return execute(new PreBuiltHostBuilder(host), Assembly.GetEntryAssembly(), args, optionsFile); @@ -79,6 +87,8 @@ public static Task RunJasperFxCommands(this IHost host, string[] args, stri /// /// Optionally configure an expected "opts" file /// + [RequiresUnreferencedCode("Dispatches to commands resolved reflectively from the entry/extension assemblies. Apps targeting trim/AOT should pre-register commands via the source-generated DiscoveredCommands manifest.")] + [RequiresDynamicCode("Command input parsing closes generic List via MakeGenericType for enumerable arguments / flags.")] public static int RunJasperFxCommandsSynchronously(this IHost host, string[] args, string? optionsFile = null) { return execute(new PreBuiltHostBuilder(host), Assembly.GetEntryAssembly(), args, optionsFile).GetAwaiter().GetResult(); @@ -103,6 +113,7 @@ internal static string[] ApplyArgumentDefaults(this string[] args, string? optio return args; } + [RequiresUnreferencedCode("Scans the entry/extension assemblies for IJasperFxCommand types via Assembly.GetExportedTypes().")] internal static void ApplyFactoryDefaults(this CommandFactory factory, Assembly? applicationAssembly) { factory.RegisterCommands(typeof(RunCommand).GetTypeInfo().Assembly); @@ -115,6 +126,8 @@ internal static void ApplyFactoryDefaults(this CommandFactory factory, Assembly? factory.RegisterCommandsFromExtensionAssemblies(); } + [RequiresUnreferencedCode("Builds a CommandExecutor that resolves commands reflectively.")] + [RequiresDynamicCode("CommandExecutor.ExecuteAsync closes generic List via MakeGenericType for enumerable arguments.")] private static Task execute(IHostBuilder runtimeSource, Assembly? applicationAssembly, string[] args, string? optionsFile) { @@ -124,6 +137,7 @@ private static Task execute(IHostBuilder runtimeSource, Assembly? applicati return commandExecutor.ExecuteAsync(args); } + [RequiresUnreferencedCode("Builds the default ActivatorCommandCreator-backed factory and walks command-instance properties via reflection.")] private static CommandExecutor buildExecutor(IHostBuilder source, Assembly? applicationAssembly) { if (JasperFxEnvironment.AutoStartHost && source is PreBuiltHostBuilder b) @@ -169,6 +183,8 @@ private static CommandExecutor buildExecutor(IHostBuilder source, Assembly? appl /// An already built IHost /// /// + [RequiresUnreferencedCode("Dispatches to commands resolved reflectively from the entry/extension assemblies. Apps targeting trim/AOT should pre-register commands via the source-generated DiscoveredCommands manifest.")] + [RequiresDynamicCode("Command input parsing closes generic List via MakeGenericType for enumerable arguments / flags.")] public static Task RunJasperFxCommands(this IHost host, string[] args) { // Workaround for IISExpress / VS2019 erroneously putting crap arguments